Structs, Enums and Implementation
There is no class
and no inheritance. Rust has only struct
, impl
and trait
.
Struct
A struct
is like an object's data attribute.
It's possible for structs to store references to data owned by something else, but to do so require the use of lifetimes.
#![allow(unused)] fn main() { struct Rectangle { width: u32, height: u32, } let rect = Rectangle { width: 3, height: 2 }; rect.width; // => 3 }
Tuple Structs
Tuple structs are useful when you want to give the whole tuple a name and make the tuple be a different type from other tuples, and naming each field as in a regular struct would be verbose or redundant.
#![allow(unused)] fn main() { struct Color(i32, i32, i32); let black = Color(0, 0, 0); }
Unit-Like Structs
You can also define structs that don't have any fields! These are called unit-like structs because they behave similarly to ()
, the unit type. Unit-like structs can be useful in situations in which you need to implement a trait on some type but don't have any data that you want to store in the type itself.
Method Syntax: Implementation
#![allow(unused)] fn main() { struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } } let rect = Rectangle { width: 30, height: 50 }; println!("The are is {} square pixels.", rect.area()); }
Methods are similar to functions. To define the function within the context of Rectangle
, we start an impl
block. Then we move the area
function within the impl
curly brackets.
We can use the method syntax to call the area method on our Rectangle
instance.
In the signature for area
we use &self
instead of rectangle: &Rectangle
because Rust knows the type of self
is Rectangle
due to this method's being inside the impl Rectangle
context. Methods can take multiple parameters that we add to the signature after the self
parameter, and those parameters work just like parameters in functions.
Each struct is allowed to have multiple
impl
blocks.
Associated functions
Another useful feature of impl
blocks is that we are allowed to define functions within impl
blocks that do not take self
as a parameter. These are called associated functions because they are associated with the struct. They are called after the ::
syntax.
They're still functions, not methods, because they don't have an instance of the struct to work with, like
String::from
. An associated function is implemented on a type, rather than on a particular instance of aRectangle
. Some language call this a static method.
Associated functions are often used for constructors that will return a new instance of the struct. A common usage in the standard library and in the community is to define a new
function. You can use the field init shorthand syntax to initialize a struct with variables.
#![allow(unused)] fn main() { impl Rectangle { fn new(width: u32, height: u32) -> Self { Rectangle { width, height } } } let rect = Rectangle::new(30, 20); }
Rest and Destructuring
The rest operator ..
allows to fill the holes. It is called the struct update syntax.
The rest must be the last and not be followed by a comma.
#![allow(unused)] fn main() { #[derive(Debug)] struct Vec2 { x: f32, y: f32 } // Rest let v1 = Vec2 { x: 1.0, y: 3.0 }; let v2 = Vec2 { y: 2.0, ..v1 }; // Destructuring a tuple let (a, b) = (3, 7); // Destructuring with Rest let Vec2 { x,..} = v2; println!("{:?}, {:?}, {:?}", v2, b, x) // => Vec2 { x: 1.0, y: 2.0 }, 7, 1.0 }
Throw away a value
During destructuring, if you don't want to deal with all values you can omit some with an underscore.
In use with the rest operator ..
it's very easy to just export what you need.
#![allow(unused)] fn main() { let _ = get_stuff(); // throws away the returned value // The value 3 and the rest will not be assigned to a variable let (_, b, ..) = (3, 7, 14, 45); let (width, _) = get_size(); }
Function parameters
Function parameters can also be pattern.
fn print_coordinates(&(x, y): &(i32, i32)) { println!("Current location: ({}, {})", x, y); } fn main() { let point = (3, 5); print_coordinates(&point); }
Enums
Rust's enums are most similar to algebraic data types in functional languages.
Note that the variants of the enum are namespaced (::
) under its identifier.
We can put data directly into each enum variant. You can put any kind of data inside an enum variant: strings, numeric types, or struct, for example. You can even include another enum.
#![allow(unused)] 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")); }
The Billion Dollars Mistake
The problem with null
values is that if you try to use a null
value as a not-null value, you will get an error of some kind.
As such, Rust does not have nulls, but it does have an enum that can encode the concept of a value being present or absent.
It is replaced by the Option<T>
enumeration. The variants of Option
are Some
and None
. The None
variant represents no value while Some
can hold one piece of data of any type.
#![allow(unused)] fn main() { enum Option<T> { Some(T), None, } }
If we use None
rather that Some
, we need to tell Rust what type of Option<T>
we have.
#![allow(unused)] fn main() { let val1: Option<i32> = None; let val2: Option<_> = Some(32); println!("{:?}, {:?}", val1, val2); }
Because Option<T>
and T
(where T
can be any type) are different types, the compiler won't let us use an Option<T>
value as if it were definitely a valid value.
#![allow(unused)] fn main() { let x: i8 = 5; let y: Option<i8> = Some(5); let sum = x + y; // ^ no implementation for `i8 + Option<i8>` }
Everywhere that a value has a type that is not an
Option<T>
, you can safely assume that the value is not null.
Error management
Rust doesn't have exceptions. Instead, it has the type Result<T, E>
for recoverable errors and the panic!
macro that stops execution when the program encounters an unrecoverable error.
Result
is, like Option
, also an enumeration. For Result<T, E>
, the variants are Ok<T>
and Err<E>
. The Ok
variant indicates the operation was successful, and inside Ok
is the successfully generated value.
The Err
variant means the operation failed, and Err
contains information about how or why the operation failed.
#![allow(unused)] fn main() { use std::fs::File; let f = File::open("hello.txt"); let f = match f { Ok(file) => file, Err(error) => { panic!("Problem opening the file: {:?}", error) } } }
Shortcuts for Panic on Error: unwrap and expect
Using match
can be a bit verbose. The Result<T, E>
type has many helper methods.
One of those method is called unwrap
. If the Result
value is the Ok
variant, unwrap
will return the value inside the Ok
. If the Result
is the Err
variant, unwrap
will call the panic!
macro.
#![allow(unused)] fn main() { let f = File::open("hello.txt").unwrap(); }
Another method, expect
, which is similar to unwrap
, lets us also choose the panic!
error message.
#![allow(unused)] fn main() { let f = File::open("hello.txt").expect("Failed to open hello.txt"); }
It would be appropriate to call
unwrap
when you have some other logic that ensures theResult
will have anOk
value, but the logic isn't something the compiler understands.
Propagating Errors
This pattern of propagating errors is so common in Rust that Rust provides the question mark operator ?
to make this easier. Error values that have the ?
operator called on them go through the from
function, defined in the From
trait in the standard library, which is used to convert errors from one type into another.
The ?
operator eliminates a lot of boilerplate and makes this function's implementation simpler. We could even shorten the code further by chaining method calls immediately after the ?
.
#![allow(unused)] fn main() { fn read_username_from_file() -> Result<String, io::error> { let mut s = String::new(); File::open("hello.txt")?.read_to_string(&mut s)?; Ok(s); } }
The
?
operator can only be used in functions that have a return type ofResult
.
use std::error::Error; use std::fs::File; fn main() -> Result<(), Box<dyn Error>> { let f = File::open("hello.txt")?; Ok(()); }
The Box<dyn Error>
type is called a trait object. For now, you can read Box<dyn Error>
to mean "any kind of error".