Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Defining a method for a struct only when a field is a certain enum variant?

I have the following struct:

#[derive(Debug)]
pub struct Entry {
    pub index: usize,
    pub name: String,
    pub filename_offset: u64,
    pub entry_type: EntryType,
}

#[derive(Debug)]
pub enum EntryType {
    File {
        file_offset: u64,
        length: usize,
    },

    Directory {
        parent_index: usize,
        next_index: usize,
    },
}

Entry is an entry in a GameCube ROM file system table which describes a file or directory. I defined various methods for Entry such as Entry::read_filename and Entry::write_to_disk. However, I have some methods that don't make sense to be available to both regular files and directories. For example, Entry::iter_contents iterates over all of a directory's child entries.

I want to be able to define certain methods such as Entry::iter_contents only for entries where entry_type is a certain variant.

I tried turning EntryType into a trait and made a DirectoryEntryInfo and FileEntryInfo struct, which both implemented EntryType.

Sadly, there were some problems with this approach. I have a Vec<Entry> elsewhere and with this change it would become Vec<Entry<EntryType>>. Using a trait like this, I have no way to downcast Entry<EntryList> to Entry<DirectoryEntryInfo>. I also tried doing something with Any, as that is the only way I am aware of to downcast in Rust, but I was only able to cast entry_type, not the entire Entry itself.

Ultimately, I'd like to end up with something similar to this:

impl<T: EntryType> Entry<T> {
    pub fn as_dir(&self) -> Option<Entry<DirectoryEntryInfo>> { ... }
    pub fn as_file(&self) -> Option<Entry<FileEntryInfo>> { ... }
    ...
}

impl Entry<DirectoryEntryInfo> {
    ...
}

impl Entry<FileEntryInfo> {
    ...
}

This way, I could access all of the entries fields without knowing whether or not it's a directory or file, as well as be able to cast it to a type that would provide me with all of the Entry fields in addition to methods based on the type parameter like Entry::iter_contents.

Is there a good way to do this without something like RFC 1450?

I'm aware that enum variants are not their own types and cannot be used as type parameters. I am just looking for an alternate way to conditionally define a method for a struct and still be able to have a way to store any variant of this struct in something like a Vec. This article is extremely close to what I am trying to do. However, using the example from it, there is no way to store a MyEnum<Bool> without knowing whether that Bool is True or False at compile time. Being able to downcast something like MyEnum<Box<Bool>> to MyEnum<False> would fix this, but I'm not aware of anything like that in Rust.

like image 971
Addison Avatar asked Jan 05 '18 20:01

Addison


1 Answers

Unfortunately, you can't do quite that, because (as mentioned in the question comments) enum variants are not types and information about the variant isn't available to the type system.

One possible approach is to "hoist" the enum to the outer layer and have each variant contain a struct that wraps the shared data:

struct EntryInfo {
    index: usize,
    name: String,
    filename_offset: u64,
}

pub struct FileEntry {
    info: EntryInfo,
    file_offset: u64,
    length: usize,
}

pub struct DirEntry {
    info: EntryInfo,
    parent_index: usize,
    next_index: usize,
}

pub enum Entry {
    File(FileEntry),
    Dir(DirEntry),
}

Then you can easily define as_file and as_dir along the following lines:

impl Entry {
    pub fn as_dir(&self) -> Option<&DirEntry> {
        match *self {
            Entry::Dir(ref d) => Some(d),
            _ => None,
        }
    }

    pub fn as_file(&self) -> Option<&FileEntry> {
        match *self {
            Entry::File(ref f) => Some(f),
            _ => None,
        }
    }
}

It's not ideal, because any code you would have written on Entry before now needs to defer to EntryInfo in the appropriate variant. One thing that can make things easier is writing a helper method to find the wrapped EntryInfo:

fn as_info(&self) -> &EntryInfo {
    match *self {
        Entry::Dir(ref d) => &d.info,
        Entry::File(ref f) => &f.info,
    }
}

Then you can use self.as_info() instead of self.info in the implementation of Entry.

like image 92
trent Avatar answered Nov 11 '22 11:11

trent