Enumeration types (enum)

In C#, an enum is a value type that maps symbolic names to integral values:

enum DayOfWeek
{
    Sunday = 0,
    Monday = 1,
    Tuesday = 2,
    Wednesday = 3,
    Thursday = 4,
    Friday = 5,
    Saturday = 6,
}

Rust has practically identical syntax for doing the same:

enum DayOfWeek
{
    Sunday = 0,
    Monday = 1,
    Tuesday = 2,
    Wednesday = 3,
    Thursday = 4,
    Friday = 5,
    Saturday = 6,
}

Unlike in .NET, an instance of an enum type in Rust does not have any pre-defined behaviour that's inherited. It cannot even participate in equality checks as simple as dow == DayOfWeek::Friday. To bring it somewhat on par in function with an enum in C#, use the #derive attribute to automatically have macros implement the commonly needed functionality:

#[derive(Debug,     // enables formatting in "{:?}"
         Clone,     // required by Copy
         Copy,      // enables copy-by-value semantics
         Hash,      // enables hash-ability for use in map types
         PartialEq  // enables value equality (==)
)]
enum DayOfWeek
{
    Sunday = 0,
    Monday = 1,
    Tuesday = 2,
    Wednesday = 3,
    Thursday = 4,
    Friday = 5,
    Saturday = 6,
}

fn main() {
    let dow = DayOfWeek::Wednesday;
    println!("Day of week = {dow:?}");

    if dow == DayOfWeek::Friday {
        println!("Yay! It's the weekend!");
    }

    // coerce to integer
    let dow = dow as i32;
    println!("Day of week = {dow:?}");

    let dow = dow as DayOfWeek;
    println!("Day of week = {dow:?}");
}

As the example above shows, an enum can be coerced to its assigned integral value, but the opposite is not possible as in C# (although that sometimes has the downside in C#/.NET that an enum instance can hold an unrepresented value). Instead, it's up to the developer to provide such a helper function:

impl DayOfWeek {
    fn try_from_i32(n: i32) -> Result<DayOfWeek, i32> {
        use DayOfWeek::*;
        match n {
            0 => Ok(Sunday),
            1 => Ok(Monday),
            2 => Ok(Tuesday),
            3 => Ok(Wednesday),
            4 => Ok(Thursday),
            5 => Ok(Friday),
            6 => Ok(Saturday),
            _ => Err(n)
        }
    }
}

The try_from_i32 function returns a DayOfWeek in a Result indicating success (Ok) if n is valid. Otherwise it returns n as-is in a Result indicating failure (Err):

let dow = DayOfWeek::try_from_i32(5);
println!("{dow:?}"); // prints: Ok(Friday)

let dow = DayOfWeek::try_from_i32(50);
println!("{dow:?}"); // prints: Err(50)

There exist crates in Rust that can help with implementing such mapping from integral types instead of having to code them manually.

An enum type in Rust can also serve as a way to design (discriminated) union types, which allow different variants to hold data specific to each variant. For example:

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));

This form of enum declaration does not exist in C#, but it can be emulated with (class) records:

var home = new IpAddr.V4(127, 0, 0, 1);
var loopback = new IpAddr.V6("::1");

abstract record IpAddr
{
    public sealed record V4(byte A, byte B, byte C, byte D): IpAddr;
    public sealed record V6(string Address): IpAddr;
}

The difference between the two is that the Rust definition produces a closed type over the variants. In other words, the compiler knows that there will be no other variants of IpAddr except IpAddr::V4 and IpAddr::V6, and it can use that knowledge to make stricter checks. For example, in a match expression that's akin to C#'s switch expression, the Rust compiler will fail code unless all variants are covered. In contrast, the emulation with C# actually creates a class hierarchy (albeit very succinctly expressed) and since IpAddr is an abstract base class, the set of all types it can represent is unknown to the compiler.