Defining an Enum - The Rust Programming Language (2024)

Defining an Enum

Where structs give you a way of grouping together related fields and data, likea Rectangle with its width and height, enums give you a way of saying avalue is one of a possible set of values. For example, we may want to say thatRectangle is one of a set of possible shapes that also includes Circle andTriangle. To do this, Rust allows us to encode these possibilities as an enum.

Let’s look at a situation we might want to express in code and see why enumsare useful and more appropriate than structs in this case. Say we need to workwith IP addresses. Currently, two major standards are used for IP addresses:version four and version six. Because these are the only possibilities for anIP address that our program will come across, we can enumerate all possiblevariants, which is where enumeration gets its name.

Any IP address can be either a version four or a version six address, but notboth at the same time. That property of IP addresses makes the enum datastructure appropriate because an enum value can only be one of its variants.Both version four and version six addresses are still fundamentally IPaddresses, so they should be treated as the same type when the code is handlingsituations that apply to any kind of IP address.

We can express this concept in code by defining an IpAddrKind enumeration andlisting the possible kinds an IP address can be, V4 and V6. These are thevariants of the enum:

enum IpAddrKind { V4, V6,}fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6; route(IpAddrKind::V4); route(IpAddrKind::V6);}fn route(ip_kind: IpAddrKind) {}

IpAddrKind is now a custom data type that we can use elsewhere in our code.

Enum Values

We can create instances of each of the two variants of IpAddrKind like this:

enum IpAddrKind { V4, V6,}fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6; route(IpAddrKind::V4); route(IpAddrKind::V6);}fn route(ip_kind: IpAddrKind) {}

Note that the variants of the enum are namespaced under its identifier, and weuse a double colon to separate the two. This is useful because now both valuesIpAddrKind::V4 and IpAddrKind::V6 are of the same type: IpAddrKind. Wecan then, for instance, define a function that takes any IpAddrKind:

enum IpAddrKind { V4, V6,}fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6; route(IpAddrKind::V4); route(IpAddrKind::V6);}fn route(ip_kind: IpAddrKind) {}

And we can call this function with either variant:

enum IpAddrKind { V4, V6,}fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6; route(IpAddrKind::V4); route(IpAddrKind::V6);}fn route(ip_kind: IpAddrKind) {}

Using enums has even more advantages. Thinking more about our IP address type,at the moment we don’t have a way to store the actual IP address data; weonly know what kind it is. Given that you just learned about structs inChapter 5, you might be tempted to tackle this problem with structs as shown inListing 6-1.

fn main() { enum IpAddrKind { V4, V6, } struct IpAddr { kind: IpAddrKind, address: String, } let home = IpAddr { kind: IpAddrKind::V4, address: String::from("127.0.0.1"), }; let loopback = IpAddr { kind: IpAddrKind::V6, address: String::from("::1"), };}

Listing 6-1: Storing the data and IpAddrKind variant ofan IP address using a struct

Here, we’ve defined a struct IpAddr that has two fields: a kind field thatis of type IpAddrKind (the enum we defined previously) and an address fieldof type String. We have two instances of this struct. The first is home,and it has the value IpAddrKind::V4 as its kind with associated addressdata of 127.0.0.1. The second instance is loopback. It has the othervariant of IpAddrKind as its kind value, V6, and has address ::1associated with it. We’ve used a struct to bundle the kind and addressvalues together, so now the variant is associated with the value.

However, representing the same concept using just an enum is more concise:rather than an enum inside a struct, we can put data directly into each enumvariant. This new definition of the IpAddr enum says that both V4 and V6variants will have associated String values:

fn main() { enum IpAddr { V4(String), V6(String), } let home = IpAddr::V4(String::from("127.0.0.1")); let loopback = IpAddr::V6(String::from("::1"));}

We attach data to each variant of the enum directly, so there is no need for anextra struct. Here, it’s also easier to see another detail of how enums work:the name of each enum variant that we define also becomes a function thatconstructs an instance of the enum. That is, IpAddr::V4() is a function callthat takes a String argument and returns an instance of the IpAddr type. Weautomatically get this constructor function defined as a result of defining theenum.

There’s another advantage to using an enum rather than a struct: each variantcan have different types and amounts of associated data. Version four IPaddresses will always have four numeric components that will have valuesbetween 0 and 255. If we wanted to store V4 addresses as four u8 values butstill express V6 addresses as one String value, we wouldn’t be able to witha struct. Enums handle this case with ease:

fn main() { enum IpAddr { V4(u8, u8, u8, u8), V6(String), } let home = IpAddr::V4(127, 0, 0, 1); let loopback = IpAddr::V6(String::from("::1"));}

We’ve shown several different ways to define data structures to store versionfour and version six IP addresses. However, as it turns out, wanting to storeIP addresses and encode which kind they are is so common that the standardlibrary has a definition we can use! Let’s look at howthe standard library defines IpAddr: it has the exact enum and variants thatwe’ve defined and used, but it embeds the address data inside the variants inthe form of two different structs, which are defined differently for eachvariant:

#![allow(unused)]fn main() {struct Ipv4Addr { // --snip--}struct Ipv6Addr { // --snip--}enum IpAddr { V4(Ipv4Addr), V6(Ipv6Addr),}}

This code illustrates that you can put any kind of data inside an enum variant:strings, numeric types, or structs, for example. You can even include anotherenum! Also, standard library types are often not much more complicated thanwhat you might come up with.

Note that even though the standard library contains a definition for IpAddr,we can still create and use our own definition without conflict because wehaven’t brought the standard library’s definition into our scope. We’ll talkmore about bringing types into scope in Chapter 7.

Let’s look at another example of an enum in Listing 6-2: this one has a widevariety of types embedded in its variants.

enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32),}fn main() {}

Listing 6-2: A Message enum whose variants each storedifferent amounts and types of values

This enum has four variants with different types:

  • Quit has no data associated with it at all.
  • Move has named fields, like a struct does.
  • Write includes a single String.
  • ChangeColor includes three i32 values.

Defining an enum with variants such as the ones in Listing 6-2 is similar todefining different kinds of struct definitions, except the enum doesn’t use thestruct keyword and all the variants are grouped together under the Messagetype. The following structs could hold the same data that the preceding enumvariants hold:

struct QuitMessage; // unit structstruct MoveMessage { x: i32, y: i32,}struct WriteMessage(String); // tuple structstruct ChangeColorMessage(i32, i32, i32); // tuple structfn main() {}

But if we used the different structs, each of which has its own type, wecouldn’t as easily define a function to take any of these kinds of messages aswe could with the Message enum defined in Listing 6-2, which is a single type.

See Also
Null

There is one more similarity between enums and structs: just as we’re able todefine methods on structs using impl, we’re also able to define methods onenums. Here’s a method named call that we could define on our Message enum:

fn main() { enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } impl Message { fn call(&self) { // method body would be defined here } } let m = Message::Write(String::from("hello")); m.call();}

The body of the method would use self to get the value that we called themethod on. In this example, we’ve created a variable m that has the valueMessage::Write(String::from("hello")), and that is what self will be in thebody of the call method when m.call() runs.

Let’s look at another enum in the standard library that is very common anduseful: Option.

The Option Enum and Its Advantages Over Null Values

This section explores a case study of Option, which is another enum definedby the standard library. The Option type encodes the very common scenario inwhich a value could be something or it could be nothing.

For example, if you request the first item in a non-empty list, you would geta value. If you request the first item in an empty list, you would get nothing.Expressing this concept in terms of the type system means the compiler cancheck whether you’ve handled all the cases you should be handling; thisfunctionality can prevent bugs that are extremely common in other programminglanguages.

Programming language design is often thought of in terms of which features youinclude, but the features you exclude are important too. Rust doesn’t have thenull feature that many other languages have. Null is a value that means thereis no value there. In languages with null, variables can always be in one oftwo states: null or not-null.

In his 2009 presentation “Null References: The Billion Dollar Mistake,” TonyHoare, the inventor of null, has this to say:

I call it my billion-dollar mistake. At that time, I was designing the firstcomprehensive type system for references in an object-oriented language. Mygoal was to ensure that all use of references should be absolutely safe, withchecking performed automatically by the compiler. But I couldn’t resist thetemptation to put in a null reference, simply because it was so easy toimplement. This has led to innumerable errors, vulnerabilities, and systemcrashes, which have probably caused a billion dollars of pain and damage inthe last forty years.

The problem with null values is that if you try to use a null value as anot-null value, you’ll get an error of some kind. Because this null or not-nullproperty is pervasive, it’s extremely easy to make this kind of error.

However, the concept that null is trying to express is still a useful one: anull is a value that is currently invalid or absent for some reason.

The problem isn’t really with the concept but with the particularimplementation. As such, Rust does not have nulls, but it does have an enumthat can encode the concept of a value being present or absent. This enum isOption<T>, and it is defined by the standard libraryas follows:

#![allow(unused)]fn main() {enum Option<T> { None, Some(T),}}

The Option<T> enum is so useful that it’s even included in the prelude; youdon’t need to bring it into scope explicitly. Its variants are also included inthe prelude: you can use Some and None directly without the Option::prefix. The Option<T> enum is still just a regular enum, and Some(T) andNone are still variants of type Option<T>.

The <T> syntax is a feature of Rust we haven’t talked about yet. It’s ageneric type parameter, and we’ll cover generics in more detail in Chapter 10.For now, all you need to know is that <T> means that the Some variant ofthe Option enum can hold one piece of data of any type, and that eachconcrete type that gets used in place of T makes the overall Option<T> typea different type. Here are some examples of using Option values to holdnumber types and string types:

fn main() { let some_number = Some(5); let some_char = Some('e'); let absent_number: Option<i32> = None;}

The type of some_number is Option<i32>. The type of some_char isOption<char>, which is a different type. Rust can infer these types becausewe’ve specified a value inside the Some variant. For absent_number, Rustrequires us to annotate the overall Option type: the compiler can’t infer thetype that the corresponding Some variant will hold by looking only at aNone value. Here, we tell Rust that we mean for absent_number to be of typeOption<i32>.

When we have a Some value, we know that a value is present and the value isheld within the Some. When we have a None value, in some sense it means thesame thing as null: we don’t have a valid value. So why is having Option<T>any better than having null?

In short, because Option<T> and T (where T can be any type) are differenttypes, the compiler won’t let us use an Option<T> value as if it weredefinitely a valid value. For example, this code won’t compile, because it’strying to add an i8 to an Option<i8>:

fn main() { let x: i8 = 5; let y: Option<i8> = Some(5); let sum = x + y;}

If we run this code, we get an error message like this one:

$ cargo run Compiling enums v0.1.0 (file:///projects/enums)error[E0277]: cannot add `Option<i8>` to `i8` --> src/main.rs:5:17 |5 | let sum = x + y; | ^ no implementation for `i8 + Option<i8>` | = help: the trait `Add<Option<i8>>` is not implemented for `i8` = help: the following other types implement trait `Add<Rhs>`: <&'a i8 as Add<i8>> <&i8 as Add<&i8>> <i8 as Add<&i8>> <i8 as Add>For more information about this error, try `rustc --explain E0277`.error: could not compile `enums` (bin "enums") due to 1 previous error

Intense! In effect, this error message means that Rust doesn’t understand howto add an i8 and an Option<i8>, because they’re different types. When wehave a value of a type like i8 in Rust, the compiler will ensure that wealways have a valid value. We can proceed confidently without having to checkfor null before using that value. Only when we have an Option<i8> (orwhatever type of value we’re working with) do we have to worry about possiblynot having a value, and the compiler will make sure we handle that case beforeusing the value.

In other words, you have to convert an Option<T> to a T before you canperform T operations with it. Generally, this helps catch one of the mostcommon issues with null: assuming that something isn’t null when it actually is.

Eliminating the risk of incorrectly assuming a not-null value helps you to bemore confident in your code. In order to have a value that can possibly benull, you must explicitly opt in by making the type of that value Option<T>.Then, when you use that value, you are required to explicitly handle the casewhen the value is null. Everywhere that a value has a type that isn’t anOption<T>, you can safely assume that the value isn’t null. This was adeliberate design decision for Rust to limit null’s pervasiveness and increasethe safety of Rust code.

So how do you get the T value out of a Some variant when you have a valueof type Option<T> so that you can use that value? The Option<T> enum has alarge number of methods that are useful in a variety of situations; you cancheck them out in its documentation. Becoming familiarwith the methods on Option<T> will be extremely useful in your journey withRust.

In general, in order to use an Option<T> value, you want to have code thatwill handle each variant. You want some code that will run only when you have aSome(T) value, and this code is allowed to use the inner T. You want someother code to run only if you have a None value, and that code doesn’t have aT value available. The match expression is a control flow construct thatdoes just this when used with enums: it will run different code depending onwhich variant of the enum it has, and that code can use the data inside thematching value.

Defining an Enum - The Rust Programming Language (2024)
Top Articles
Python Virtual Environments in Windows
Cheap Wine vs. Expensive Wine: Which is Better?
Bleak Faith: Forsaken – im Test (PS5)
55Th And Kedzie Elite Staffing
Thor Majestic 23A Floor Plan
Metra Union Pacific West Schedule
Trevor Goodwin Obituary St Cloud
Instructional Resources
What is Mercantilism?
Immobiliare di Felice| Appartamento | Appartamento in vendita Porto San
Voorraad - Foodtrailers
Skip The Games Norfolk Virginia
What is IXL and How Does it Work?
Simple Steamed Purple Sweet Potatoes
Knaben Pirate Download
Cincinnati Bearcats roll to 66-13 win over Eastern Kentucky in season-opener
Https://Gw.mybeacon.its.state.nc.us/App
Audrey Boustani Age
Evil Dead Rise Showtimes Near Regal Columbiana Grande
Craigslist Blackshear Ga
Unit 33 Quiz Listening Comprehension
Tamilrockers Movies 2023 Download
Voy Boards Miss America
Dirt Removal in Burnet, TX ~ Instant Upfront Pricing
Roll Out Gutter Extensions Lowe's
The Menu Showtimes Near Regal Edwards Ontario Mountain Village
Van Buren County Arrests.org
Beryl forecast to become an 'extremely dangerous' Category 4 hurricane
Robin D Bullock Family Photos
Dr Ayad Alsaadi
European city that's best to visit from the UK by train has amazing beer
Stihl Dealer Albuquerque
Kimoriiii Fansly
Myaci Benefits Albertsons
Kacey King Ranch
Wisconsin Volleyball Team Leaked Uncovered
Miss America Voy Board
Texters Wish You Were Here
Radical Red Doc
Pawn Shop Open Now
19 Best Seafood Restaurants in San Antonio - The Texas Tasty
Hindilinks4U Bollywood Action Movies
craigslist | michigan
Pay Entergy Bill
Rhode Island High School Sports News & Headlines| Providence Journal
Armageddon Time Showtimes Near Cmx Daytona 12
Bob And Jeff's Monticello Fl
Kent And Pelczar Obituaries
Arigreyfr
M&T Bank
Graduation Requirements
Adams-Buggs Funeral Services Obituaries
Latest Posts
Article information

Author: Frankie Dare

Last Updated:

Views: 5430

Rating: 4.2 / 5 (73 voted)

Reviews: 88% of readers found this page helpful

Author information

Name: Frankie Dare

Birthday: 2000-01-27

Address: Suite 313 45115 Caridad Freeway, Port Barabaraville, MS 66713

Phone: +3769542039359

Job: Sales Manager

Hobby: Baton twirling, Stand-up comedy, Leather crafting, Rugby, tabletop games, Jigsaw puzzles, Air sports

Introduction: My name is Frankie Dare, I am a funny, beautiful, proud, fair, pleasant, cheerful, enthusiastic person who loves writing and wants to share my knowledge and understanding with you.