Noob question again: enum and struct?

I need to check my understanding again, if you don’t mind :slight_smile:

I am working through The Rust Programming Language, and I think I understand structs and enums, but as structs are very similar to structs in C (and I do understand C I think), but enums not so much, I wanted to see if I really do understand.

Would you say my explanation is correct if I say:

A struct is a user defined type that can/will have some named fields, that can be of different types, that all exists at the same time.

An enum is a user defined type that can have either elements defined by the user, or named fields, that can be of different types, and only one exists at the same time.

Is that more or less correct?

And if it is, is there a real advantage of using an enum if you could also use a struct, other than memory consumption?

I don't know C, but I can explain you why an enum is useful.

Enums represent what is known in functional programming lingo as a "sum” type, which means that the value of the enum can be any of its variants, but only one variant is possible at a given time.

Structs, on the other hand, represent the "product" type in functional programming lingo.

When should you use an enum over a struct? When your type can have more than one representation.

1 Like

Yeah, that sounds about right.

Often simulating enums with structs will involve wrapping everything in Option to make it nullable. That makes your code have a lot more unwrap calls, which makes it easier to make mistakes.

2 Likes

It's pretty rare that you actually have the question show up, because you either one "only one of them" or "all of them", and the choice is pretty clear.

Like the whole point of Result is that it's either Ok xor Err. Having a struct with both is just confusing -- what are you supposed to do if it's neither or both? That doesn't make sense.

(I deal with that in ActionResult<TValue> Class (Microsoft.AspNetCore.Mvc) | Microsoft Learn a bunch, and it's a real pain.)

If you're coming from C, you can think of where you might have used a union in C, just in Rust an enum has the extra metadata to track which one it is, so it can be used safely.

3 Likes

If you know C then you should know Unions. As a side comment - Rust enums also allow classic numbering.

struct F;

enum Classic {
    I = 2,
    F = 7,
    S = 1000,
}

struct My {
    type_now: Classic,
    i: i32,
    f: F,
    s: String,
}

I assume your thinking of a struct like this or similar. It has the problem that you have to have everything initialised and it is easy to read a field even when the type_now isn't on that field. Option as @alice mentions also risks having multiple fields set as Some.

1 Like

I find all this talk of "product types" and "sum types" confusing. It's not anything I ever heard of after decades or programming... until I came to Rust.

Bottom line is that a struct contains data representing something in your program, it can only represent one thing at a time (Or it's best used that way anyway). But an enum can represent many things in your program, only one at any given time though.

I think the magic and usefulness of enums starts to shine through when used with ´match´ statements.

For example:

Imagine I have a bicycle and a car. I could model them in a program with structs Bicycle and Car. At any given time if I check my garage I might find either a bicycle or a car (never mind why, that is how it goes in my house). So in my program the result of looking in my garage is either a Car struct or a Bicycle struct. I could model this with an enum called Transport that can contain either. Now if I find a car I drive it, if I find a bicycle I ride it. Very different activities. I can model this in my program with a match statement that finds a Car or Bicycle and calls drive() or ride()methods accordingly.

My programming model starts to look like this:

struct Car {}
impl Car {
    fn drive(&self) {}
}

struct Bicycle {}
impl Bicycle {
    fn ride(&self) {}
}

enum Transport {
    Car(Car),
    Bicycle(Bicycle),
}

fn check_garage() -> Transport {
    if true {
        let c = Car {};
        return Transport::Car(c);
    } else {
        let b = Bicycle {};
        return Transport::Bicycle(b);
    }
}
...
...
    match check_garage() {
        Transport::Car(c) => {
            c.drive();
        }
        Transport::Bicycle(b) => {
            b.ride();
        }
    }

2 Likes

Thanks, yes, I was thinking there are similarities with unions in C. And then I started to read about patter matching, and I forgot about C :slight_smile:

All of you, I am really grateful by the way. I had to choose between C++ and Rust, and as StackOverflow reported that Rust was the most beloved language nobody uses, I liked it. But what really made the choice easy was the community. And I can’t say otherwise, it is just so great to find that even the dumbest questions (yes, I know my questions are dumb from time to time, I am not ashamed to admit it) get answered, that people try to explain, and explain more, and even more.

I think all this stuff about speed and safety, and ease of use, are great about Rust. But it is people like you that make it a fantastic language.

3 Likes

Not a dumb question at all. I have used many languages over many years and Rust enums are very different to the enums they had. Like many Rust features looked at on their own one might wonder what the point of them is. But when combined with Rust's pattern matching (another thing those other languages did not have) it all starts to make a lot of sense. Hence my little example.

Have fun.

I don't think enums really can have named fields of different types. This is my understanding.

A struct is used to define a kind of object (I don't mean "object" as in OOP but literal objects like e.g Car, Shape, Food,...). You can define fields to store properties of that object. For food you for example want to store the amount of fat, the amount of calories, and the type of food.

It could like this:

struct Food {
    name: String,
    fat: f32,   // in mg
    kcal: u32,  // in kcal
    food_type: String,
}

let apple = Food {
    name: "Apple".to_string(),
    fat: 200.00,
    kcal: 76,
    food_type: "Fruit".to_string(),
};

let pizza = Food {
    name: "Pizza".to_string(),
    fat: 10000.00,
    kcal: 270,
    food_type: "Fastfood".to_string(),
};

For a struct you can implement functions.

impl Food {
    fn is_healthy(&self) -> bool {
        self.fat < 500.00 && self.kcal < 150
    }
}

// Is healthy?
assert_eq!(apple.is_healthy(), true);
assert_eq!(pizza.is_healthy(), false);

A enum is rather a list of possible options that enum can have. For cars you could have a enum called Brands containing Bmw, Porsche, Audi,... for shapes that could be Triangle, Rectangle, Square,...

In our example we could make a enum for the type of foods:

enum FoodType {
    Fruit,
    Vegetable,
    Fastfood,
}

 let apple = Food {
        name: "Apple".to_string(),
        fat: 200.00,
        kcal: 76,
        food_type: FoodType::Fruit,
};

This is a lot cleaner then using a String to store the type of food.

A enum can't really have named types I think but you can do things like:

pub enum Day {
    Mon = 1,
    Tue = 2,
    Wed = 3,
    Thu = 4,
    Fri = 5,
    Sat = 6,
    Sun = 7,
}

So you can convert a u64 to the enum type.

Enums in Rust can have struct variants:

enum Message {
    Move { x: i32, y: i32 },
    // other variants
}

You can also add methods to enums

This is better represented with #[repr(u8)]. You can read more about it here.

1 Like

Alright, thanks for the clarification

I'm not sure what you mean by that. You can write:

enum MyEnum {
    A(String),
    B(Vec<f32>),
    C(UdpSocket),
    D{ x: f32, y: f32 },
    E(f32, u8, String),
}

I would say the "A", "B", "C", "D", "E" are the names of the fields. Although that is probably not what they are technically called ("variant names" perhaps?). They certainly all have different types.

Exactly.

Nitpick: they contain different types, but are all of the type MyEnum.

4 Likes

Really?

When I have this:

enum MyEnum {
    A { x: f32, y: f32 },
    B(f32, u8, String),
}
...
   match my_enum() {
        MyEnum::A { x, y } => {
            println!("{x}: {y}");
        }
        MyEnum::B(f, u, s) => {
            println!("{f}: {u}: {s}");
        }
    ) 

It looks to me as if I have an enum named "MyEnum". Which can contain one of the variants "A" or "B". Which are of different types, a struct and a tuple respectively in this case. The type of "MyEnum" is not the same as the type of "MyEnum::A" or "MyEnum::B"

So I'm not sure what you are trying to say there.

MyEnum::A and MyEnum::B are not types at all, they aren't treated separately by a type checker.

2 Likes

The fields of the variants may be different types, but the variants themselves all have the same type: that of the enum.

    let a = MyEnum::A { x: 0.0, y: 0.0 };
    let b = MyEnum::B(0.0, 0, "".into());
    // Succeeds
    assert_eq!(
        type_id_of_val(&a),
        type_id_of_val(&b),
    )
2 Likes

Ok. Curious. I guess it must be so, else I could not write:

    let mut a = MyEnum::A { x: 1.0, y: 2.0 };
    a = MyEnum::B(3.0, 4, "Hello".into());

without a type mismatch. But I can.

You can also return both Ok and Err from something that returns Result<_, _>, say.[1]


  1. And so far Rust doesn't have subtyping other than those of lifetimes and higher-ranked types, and it would be challenging to add it backwards compatibly. ↩︎

Ah yes, of course!