Rust language programming notes and references.
Table of Contents:
- SQLx - Database driver.
- Actix - Rust's powerful actor system and most fun web framework.
- Tide - Fast and friendly HTTP server framework for async Rust.
- Warp - A super-easy, composable, web server framework for warp speeds.
- Tower-Web - A fast, boilerplate free, web framework for Rust.
- Rocket - Rocket is a web framework for Rust.
- Rouille - Rust web micro-framework.
The following notes have been taken from the excellent resource called The Rust Programming Language.
Rust is an expression-based language:
- Statements are instructions that perform some action and do not return a value.
- Expressions evaluate to a resulting value and do not include ending semicolons.
- If you add a semicolon to the end of an expression, you turn it into a statement.
// Statement
let x = 5;
// Expression (no semicolon)
x + 1
// Statement with an expression
let y = x + 2;
Uses a command called rustup
:
curl https://sh.rustup.rs -sSf | sh
To upgrade:
rustup update
To compile a source code file:
rustc main.rs
To create a new project:
cargo new project_name
To configure a project:
Following is an example of a Cargo.toml configuration file:
[package]
name = "hello_cargo"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
edition = "2018"
[dependencies]
rand = "0.5.5"
To build a project:
cargo build
To run a project:
cargo run
To see if your code will compile:
cargo check
To build a release version of your application:
cargo build --release
Rust uses the following style points:
- Snake case for function and variable names (an_example_of_snake_case).
- Four spaces for tabs.
- Use
rustfmt
to standardize on code style.
Following is the guessing game from the The Rust Programming Language for reference:
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1, 101);
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
Bring types into scope:
use std::io;
use std::cmp::Ordering;
use rand::Rng;
Note: The prelude is the list of things that Rust automatically imports into every Rust program.
Rust requires a main
entry point:
fn main() {
}
Using an external type:
use rand::Rng;
fn main() {
let random_number = rand::thread_rng().gen_range(1, 101);
println!("{}", random_number)
}
By default variables are immutable.
Declare variables with either let
or const
:
let x = 4;
const MAX_SIZE: u32 = 100_000;
Constants or const
variables:
- Are always immutable.
- Must be annotated.
- Can't use
mut
with them (see next section). - Can be declared in any scope including global.
- Can only be set by a constant expression (not computed at runtime).
You can make variables mutable by adding mut
in front of the variable name:
let mut x = 5;
x = 6;
You can shadow variables by re-using the variable name with the let keyword. Shadowing enables changing the type assigned to the variable name:
let x = 5;
let x = format!("{}", 5);
The compiler can usually infer what type we want to use based on the value and how we use it.
A scalar type represents a single value. Rust supports four primary scalar types.
Rust performs two’s complement wrapping. Values greater than the maximum value the type can hold “wrap around” to the minimum of the values the type can hold:
let x: i32 = -1000;
Sizes:
Length | Signed | Unsigned |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
CPU Arch | isize | usize |
Notation:
Number Literals | Example |
---|---|
Decimal | 56_989 |
Hex | 0xff |
Octal | 0o77 |
Binary | 0b1111_0000 |
Byte (u8 only) | b'A' |
Rust has two primitive types for floating-point numbers:
f64
which is the default.f32
let x: f32 = 199.99;
Boolean true
or false
specified with the keyword bool
:
let x: bool = true;
The character type is four bytes in size and represents a Unicode Scalar Value.
Character literals are specified with single quotes:
let x = 'X';
Compound types can group multiple values into one type. Rust has two primitive compound types.
A tuple is a general way of grouping together a number of values with a variety of types into one compound type.
Tuples are created by writing a comma-separated list of values inside parentheses:
let tup: (i32, f64, u8) = (500, 6.4, 1);
// Pattern matching
let (x, y, z) = tup;
// Period access
let x = tup.0;
let y = tup.1;
let z = tup.2;
Every element of an array must have the same type and they are of a fixed length:
let a = [1,2,3,4,5];
// With type and length
let a: [i32; 5] = [1,2,3,4,5];
// Initialize array with the number three and a length of five
let a = [3, 5];
Array elements can be accessed using indexing:
let a = [1,2,3,4,5];
let first = a[0];
let second = a[1];
If you access an element in an array past its length, Rust will panic.
The following numeric operators are supported:
Operation | Symbol | Example |
---|---|---|
Addition | + |
5 + 10 |
Subtraction | - |
95.5 - 4.3 |
Multiplication | * |
4 * 30 |
Division | / |
56.7 / 32.2 |
Remainder | % |
43 % 5 |
Function definitions in Rust start with fn and have a set of parentheses after the function name.
Parameter or argument types are defined in the function signature.
Rust doesn’t care where you define your functions, only that they’re defined somewhere.
Function return types are declared after an arrow ->
.
The return value is the value of the final expression.
The return
keyword can also return a value and exit the function early.
fn main() {
let x = five()
let y = plus_one(x)
print_values(x, y);
}
fn print_values(x: i32, y: i32) {
println!("The value of x is: {}", x);
println!("The value of y is: {}", y);
}
fn five() -> i32 {
5
}
fn plus_one(x: i32) -> i32 {
x + 1
}
// Comment with two back slashes.
// There is no block comment, just use multiple line comments.
Let x = 5; // Comments can be on the same line as statements or expressions.
Condition in if
expressions must evaluate to a boolean.
Blocks of code associated with the conditions in if
expressions are sometimes called arms.
fn main() {
let number = 6;
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
}
Because if
is an expression it will return a value.
The arms of the if
expression must return the same type.
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {}", number);
// Error: if and else have incompatible types
let unknown = if condition { 5 } else { "string" }
}
A continuous loop:
fn main() {
loop {
println!("again!");
}
}
Returning values from loops:
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {}", result);
}
An example of a while
loop:
fn main() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number -= 1;
}
println!("LIFTOFF!!!");
}
An example of a for
loop:
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
println!("the value is: {}", element);
}
}
An example using a Range in a for loop:
fn main() {
for number in (1..4).rev() {
println!("{}!", number);
}
println!("LIFTOFF!!!");
}
Ownership enables Rust to make memory safety guarantees without needing a garbage collector.
Ownership rules:
- Each value in Rust has a variable that's called its owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
Scope example:
// String pushed onto the stack
{ // s is not valid here, it’s not yet declared
let s = "hello"; // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no longer valid
// String allocated on the heap
{
let s = String::from("hello"); // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no
// longer valid and "drop" is called
Simple values are stored on the stack and can easily be copied:
let x = 5;
let y = x;
Complex values are stored on the heap and, following rule number 2 above, can only have one owner:
let s1 = String::from("hello");
let s2 = s1; // This is a move, not a copy
// s1 is no longer valid and cannot be used
Complex types can be cloned:
let s1 = String::from("hello");
let s2 = s1.clone();
// s1 is still valid
As a general rule, any group of simple scalar values can be Copied.
- All the integer types, such as
u32
. - The Boolean type, bool, with values
true
andfalse
. - All the floating point types, such as
f64
. - The character type,
char
. - Tuples, if they only contain types that are also Copy. For example,
(i32, i32)
is Copy, but(i32, String)
is not.
Passing a variable to a function will move or copy, just as assignment does:
fn main() {
let s = String::from("hello"); // s comes into scope
takes_ownership(s); // s's value moves into the function...
// ... and so is no longer valid here
let x = 5; // x comes into scope
makes_copy(x); // x would move into the function,
// but i32 is Copy, so it’s okay to still
// use x afterward
} // Here, x goes out of scope, then s. But because s's value was moved, nothing
// special happens.
fn takes_ownership(some_string: String) { // some_string comes into scope
println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing
// memory is freed.
fn makes_copy(some_integer: i32) { // some_integer comes into scope
println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.
Returning values from a function can also transfer ownership:
fn main() {
let s1 = gives_ownership(); // gives_ownership moves its return
// value into s1
let s2 = String::from("hello"); // s2 comes into scope
let s3 = takes_and_gives_back(s2); // s2 is moved into
// takes_and_gives_back, which also
// moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 goes out of scope but was
// moved, so nothing happens. s1 goes out of scope and is dropped.
fn gives_ownership() -> String { // gives_ownership will move its
// return value into the function
// that calls it
let some_string = String::from("hello"); // some_string comes into scope
some_string // some_string is returned and
// moves out to the calling
// function
}
// takes_and_gives_back will take a String and return one
fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
// scope
a_string // a_string is returned and moves out to the calling function
}
References allow you to refer to some value without taking ownership of it.
References are immutable by default.
Rules of references:
- At any given time, you can have either one mutable reference or any number of immutable references.
- References must always be valid.
The ampersands in the code below define references:
fn main() {
let s1 = String::from("hello");
// The `&s1` syntax lets us create a reference that refers to the value of s1 but does not own it.
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize { // s is a reference to a String
s.len()
} // Here, s goes out of scope. But because it does not have ownership of what
// it refers to, nothing happens.
Using references as function parameters is called borrowing.
References can be changed to be mutable:
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
You can have only one mutable reference to a particular piece of data in a particular scope.
This restriction allows for mutation but in a very controlled fashion.
The benefit of having this restriction is that Rust can prevent data races at compile time. A data race is similar to a race condition and happens when these three behaviors occur:
- Two or more pointers access the same data at the same time.
- At least one of the pointers is being used to write to the data.
- There’s no mechanism being used to synchronize access to the data.
You can work around this restriction using curly brackets to create a new scope:
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 goes out of scope here, so we can make a new reference with no problems.
let r2 = &mut s;
You cannot have a mutable reference while we have an immutable one:
let mut s = String::from("hello");
let r1 = &s; // Immutable reference: no problem
let r2 = &s; // Immutable reference: no problem
let r3 = &mut s; // Mutable reference: BIG PROBLEM
println!("{}, {}, and {}", r1, r2, r3); // Mutable with immutable!!!
The following example works with a mix of immutable references and a mutable reference:
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2);
// r1 and r2 are no longer used after this point
let r3 = &mut s; // no problem
println!("{}", r3);
In Rust the compiler guarantees that references will never be dangling references:
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle returns a reference to a String
let s = String::from("hello"); // s is a new String
&s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory goes away.
// Danger! Rust wont let you do this.
Slices let you reference a contiguous sequence of elements in a collection rather than the whole collection.
A string slice is a reference to part of a String, and it looks like this:
let s = String::from("hello world");
let hello = &s[..5]; // Range supports dropping the first index if starting at 0
let world = &s[6..11];
let remainder = &s[12..]; // Range supports dropping the last index if it is the length
let full = &s[..] // This is a slice of the entire string
The type that signifies “string slice” is written as &str
:
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
Better still, write the signature above using the following signature because it allows us to use the same function on both &String values and &str values:
fn main() {
let my_string = String::from("hello world");
// first_word works on slices of `String`s
let word = first_word(&my_string[..]);
let my_string_literal = "hello world";
// first_word works on slices of string literals
let word = first_word(&my_string_literal[..]);
// Because string literals *are* string slices already,
// this works too, without the slice syntax!
let word = first_word(my_string_literal);
}
// Note the change from &String to &str
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
Array slices work the same way:
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
A struct, or structure, is a custom data type that lets you name and package together multiple related values that make up a meaningful group.
An example to define and use a struct:
// Define the User struct
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
// Create an instance of the User struct
// Note that the entire instance must be mutable
let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");
// As a constructor function
// Using the field init shorthand syntax
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
// Using the struct update syntax to copy user1 details
let user2 = User {
email: String::from("another@example.com"),
username: String::from("anotherusername567"),
..user1
};
Tuple structs have the added meaning that the struct name provides but don’t have names associated with their fields:
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
To print a struct use the #[derive(Debug)]
annotation:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {:?}", rect1); // One line
println!("rect1 is {:#?}", rect1); // Multiple lines
}
A full example using a struct:
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
area(&rect1)
);
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
Methods are different from functions in that they’re defined within the context of a struct, and their first parameter is always self, which represents the instance of the struct the method is being called on.
Defining a method and an associated function on a struct:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
// Uses `self` reference
impl Rectangle {
// Method using self
fn area(&self) -> u32 {
self.width * self.height
}
// Another method with an extra parameter
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
// Associated function (does not use self)
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
Without associated values:
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"),
};
With associated values and no need for a struct:
// Using a string
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
// Using integers and string
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
Enums can have methods defined on them:
impl Message {
fn call(&self) {
// method body would be defined here
}
}
let m = Message::Write(String::from("hello"));
m.call();
Rust does not support null
. This was a deliberate design decision for Rust to limit null’s pervasiveness and increase the safety of Rust code.
There is an Option<T>
enum from the standard library that can encode the concept of a value being present or absent. It is so popular you don't need to bring it into scope:
// The definition from the standard library
enum Option<T> {
Some(T),
None,
}
// Using the Option enum with no need to bring Some or None into scope
let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None;
The match
operator allows you to compare a value against a series of patterns and then execute code
based on which pattern matches:
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
// An alternative with a `state` variable
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
}
}
}
Using match
with Option
:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
The match
operator supports the _
pattern to match all other values:
fn main() {
let some_u8_value = 0u8;
match some_u8_value {
1 => println!("one"),
3 => println!("three"),
5 => println!("five"),
7 => println!("seven"),
_ => (),
}
}
Think of if let
as syntax sugar for a match
that runs code when the value matches one pattern and then ignores all other values.
// Using `match` to count non-quarter coins
let mut count = 0;
match coin {
Coin::Quarter(state) => println!("State quarter from {:?}!", state),
_ => count += 1,
}
// Same using `if let`
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {:?}!", state);
} else {
count += 1;
}
Package rules:
- Must contain at least one library or binary crate.
- Only supports one library crate, no more.
- Can contain as many binary crates as you would like.
Modules:
- Define Rust’s privacy boundary.
- Group related definitions together and name why they’re related.
- Define a module using the
mod
keyword. - Modules are private by default.
- Use the
pub
keyword to define public resources. - Structs are private by default.
- Enums are public by default
Cargo uses the following convention:
src/main.rs
for a binary crate.src/bin
directory for multiple binary crates.src/lib.rs
for a library crate.
Here is an example of a library crate:
// File: src/lib.rs
mod front_of_house {
// `pub` exposes `hosting` and `add_to_waitlist` to `eat_at_restaurant` below
pub mod hosting {
pub fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
mod back_of_house {
pub enum Appetizer {
Soup,
Salad,
}
fn fix_incorrect_order() {
cook_order();
// `super` references the parent module like `..` does on a file system.
super::serve_order();
}
fn cook_order() {}
// Exposing a struct
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}
}
}
// Resulting module tree
// crate
// ├── front_of_house
// │ ├── hosting
// │ │ ├── add_to_waitlist
// │ │ └── seat_at_table
// │ └── serving
// │ ├── take_order
// │ ├── serve_order
// │ ├── take_payment
// │ └── back_of_house
// │ ├─ fix_incorrect_order
// │ └── cook_order
// └── eat_at_restaurant
// Public function
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
// Order a breakfast in the summer with Rye toast
let mut meal = back_of_house::Breakfast::summer("Rye");
// Change our mind about what bread we'd like
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);
// The next line won't compile if we uncomment it; we're not allowed
// to see or modify the seasonal fruit that comes with the meal
// meal.seasonal_fruit = String::from("blueberries");
// Using the public enum
let order1 = back_of_house::Appetizer::Soup;
let order2 = back_of_house::Appetizer::Salad;
}
Declare a module with only a semicolon after mod
rather than using a block, tells Rust to load the contents of the module from another file with the same name as the module:
// src/main.rs
mod other_file;
The use
keyword saves us from using long absolute or relative paths.
Rules for the path use
keyword:
- Use the parent path for functions.
- Use the full path for enums and structs.
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
// Only bring the parent module into scope. Not the function. Saves confusion.
use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
You can bring two modules into the same scope with the same name using the as
keyword:
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
}
fn function2() -> IoResult<()> {
// --snip--
}
You can re-export by combining pub
and use
to bring an item into scope making that item available for others:
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
Other examples of the use
keyword:
// Single `use` statements
use rand::Rng;
use std::cmp::Ordering;
use std::io;
// Equivelant using nested paths
use rand::Rng;
use std::{cmp::Ordering, io};
// We can bring `io` and `Write` into scope
use std::io;
use std::io::Write;
// Using one statement with the `self` keyword
use std::io::{self, Write};
// We could also bring all public items into scope
// Glob can make it harder to tell what names are in scope
use std::collections::*;
Because Rust compiles generic code into code that specifies the type in each instance, we pay no runtime cost for using generics. When the code runs, it performs just as it would if we had duplicated each definition by hand. The process of monomorphization makes Rust’s generics extremely efficient at runtime.
Defining a function that uses generics:
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
}
Define structs
to use a generic type parameter:
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}
Define a Point struct
where x
and y
are both generics but could have different types:
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let both_integer = Point { x: 5, y: 10 };
let both_float = Point { x: 1.0, y: 4.0 };
let integer_and_float = Point { x: 5, y: 4.0 };
}
Define enums
to hold generic data types in their variants:
enum Option<T> {
Some(T),
None,
}
Define enums
to use multiple generic types:
enum Result<T, E> {
Ok(T),
Err(E),
}
Implement methods on structs
and enums
and use generic types:
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
// The mixup method supports multiple Point types
impl<T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point {
x: self.x,
y: other.y,
}
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
let p1 = Point { x: 5, y: 10.4 };
let p2 = Point { x: "Hello", y: 'c' };
let p3 = p1.mixup(p2);
}
We can implement a trait on a type only if either the trait or the type is local to our crate.
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
We can define a default implementation for the summarize
method:
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
// No need to implement summarize
impl Summary for NewsArticle {}
To use this version of Summary
, we only need to define summarize_author
when we implement the trait on a type:
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
To define traits as parameters:
// Using the `impl trait` syntax
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
println!("Breaking news! {}", item2.summarize());
}
// Or using `trait bound` syntax
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
// This is forcing item1 and item2 to be the same type or T
pub fn notify<T: Summary>(item1: &T, item2: &T) {
println!("Breaking news! {}", item2.summarize());
}
// Multiple trait bounds using the `+` syntax
pub fn notify(item: &(impl Summary + Display)) {}
pub fn notify<T: Summary + Display>(item: &T) {}
// Using the `where` clause to simplify the signature
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug
{}
Returning types that implement traits:
// You can only return impl Trait if you’re returning a single type
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
}
}
Example using the largest
function:
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
}
Using trait bounds to conditionally implement methods:
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
Lifetime annotations in function signatures:
// Input lifetimes
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
Lifetime annotations in struct definitions:
// Input lifetimes
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
}
Lifetime annotations in method definitions:
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
Static lifetime:
let s: &'static str = "I have a static lifetime.";
Generic type parameters, trait bounds, and lifetimes all in one function:
use std::fmt::Display;
fn longest_with_an_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}
Running tests:
- Use
cargo test
to run tests in Rust. - Use
cargo test full_fn_name
to filter to a single test. - Use
cargo test part_fn_name
to filter to multiple tests that match the part name. - Use
cargo test -- --show-output
to seeprintln
messages in passing tests. - Use
cargo test -- --ignored
to run#[ignore]
tests.
Tests are defined using attributes:
- The
#[cfg(test)]
annotation tells Rust to compile and run the test code only when you run cargo test. - The
#[test]
attribute indicates a test function. - The
#[should_panic]
attribute indicates a test function that should panic. - Use the optional
expected
parameter eg:#[should_panic(expected = "Expected panic message here.")]
- Use
#[ignore]
attribute to ignore expensive tests. - In libraries, private functions can be tested by bringing them into scope.
Integration tests:
- Defined in a
tests
directory next to thesrc
directory. - Do not need the
#[cfg(test)]
annotation. - Helper functions get defined in a file structure similar to
/tests/common/mod.rs
.
The standard library provides assertion macros:
assert
for boolean expressions.assert_eq
for value comparison.assert_ne
for not equal value comparison.- Arguments specified after the required arguments are passed to the
format!
macro.
Common operators in tests include ==
and !=
for comparison.
Values being compared must implement the PartialEq
and Debug
traits:
- Implement
PartialEq
to assert that values of those types are equal or not equal. - Implement
Debug
to print the values when the assertion fails. - Add the
#[derive(PartialEq, Debug)]
annotation to yourstruct
orenum
definition.
pub fn add_two(a: i32) -> i32 {
a + 2
}
pub fn greeting(name: &str) -> String {
String::from("Hello!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
#[test]
fn another() {
panic!("Make this test fail");
}
#[test]
fn it_adds_two() {
assert_eq!(4, add_two(2));
}
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(
result.contains("Carol"),
"Greeting did not contain name, value was `{}`",
result
);
}
#[test]
#[should_panic]
fn greater_than_100() {
// Statements that should panic
}
#[test]
#[should_panic(expected = "Guess value must be less than or equal to 100")]
fn greater_than_100() {
// Statements that should panic
}
}
Using the Result<T, E>
in tests. You can’t use the #[should_panic]
annotation on tests that use Result<T, E>
. Instead, you should return an Err
value directly when the test should fail:
#![allow(unused_variables)]
fn main() {
#[cfg(test)]
mod tests {
#[test]
fn it_works() -> Result<(), String> {
if 2 + 2 == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
}
}