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

1

6

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.

Ammo answered 5/1, 2018 at 20:24 Comment(8)
No; enum variants are not their own types thus they cannot participate in the type system. See also Can struct-like enums be used as types?Gambit
@Gambit Is there another way without enums to accomplish what I've tried to do? I mentioned making EntryType a trait, and trying to use two structs which both implement EntryType. However I ran into issues there because I couldn't downcast Entry<EntryType> into EntryType<DirectoryEntryInfo> or EntryType<FileEntryInfo>. I'm not set on using enums, I'm open to anything that'll get the kind of behavior I mentioned in the question.Ammo
@Gambit I'm aware enum variants are not their own type, I should have been more specific. I'm just looking for a way to do something similar as a workaround, regardless of whether it involves enums or not.Ammo
I wrote an answer based on enum, but there's a downcasting solution too, using Box, if you make EntryType require Any. I wrote this too, which may help.Wymore
Thanks! I'm gonna try out both of those, but I have a feeling the enum based one will be best for my use case.Ammo
@trentcl it's probably worth putting that into your answer (or even a second answer if you feel they are different enough). Comments are ephemeral and useful solutions shouldn't be lost.Gambit
@Gambit well, I gave it a shot, but the solution I came up with using Box is such an abomination that I think, on the whole, I'd rather not! (Anyone reading this may feel free to use it in their own answer, or as an example of what not to do.)Wymore
@trentcl sometimes an answer showing a poor solution and explaining why it is poor is useful too ;-)Gambit
W
5

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.

Wymore answered 5/1, 2018 at 21:33 Comment(1)
Thanks, this worked great and is exactly what I needed!Ammo

© 2022 - 2025 — McMap. All rights reserved.