Table of Contents |
---|
Install Rust
Code Block |
---|
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh |
Hello World Example
Code Block | ||
---|---|---|
| ||
vi main.rs |
...
Code Block | ||
---|---|---|
| ||
./main |
Code Block |
---|
Hello, world! |
Using the Package Manager
Create a project using Cargo
Code Block | ||
---|---|---|
| ||
cargo new hello_cargo |
...
It has also initialized a new Git repository along with a .gitignore file. Git files won’t be generated if you run cargo new
within an existing Git repository; you can override this behavior by using cargo new --vcs=git
.
Cargo.toml
Code Block |
---|
[package] name = "hello_cargo" version = "0.1.0" edition = "2021" [dependencies] |
Building
Code Block | ||
---|---|---|
| ||
cargo build |
Build and Run
Code Block | ||
---|---|---|
| ||
cargo run |
Build for Release
Code Block | ||
---|---|---|
| ||
cargo build --release |
This command will create an executable in target/release instead of target/debug. The optimizations make your Rust code run faster, but turning them on lengthens the time it takes for your program to compile. This is why there are two different profiles: one for development, when you want to rebuild quickly and often, and another for building the final program you’ll give to a user that won’t be rebuilt repeatedly and that will run as fast as possible. If you’re benchmarking your code’s running time, be sure to run
cargo build --release
and benchmark with the executable in target/release.
Naming Conventions
Item | Convention |
---|---|
Crates | snake_case (but prefer single word) |
Modules | snake_case |
Types | CamelCase |
Traits | CamelCase |
Enum variants | CamelCase |
Functions | snake_case |
Methods | snake_case |
General constructors | new or with_more_details |
Conversion constructors | from_some_other_type |
Local variables | snake_case |
Static variables | SCREAMING_SNAKE_CASE |
Constant variables | SCREAMING_SNAKE_CASE |
Type parameters | concise CamelCase , usually single uppercase letter: T |
Lifetimes | short, lowercase: 'a |
Variables
Immutable/Mutable
Variables by default are immutable, meaning they can't be changed once set.
Code Block |
---|
fn main() {
let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
} |
The above code would generate an error since we are trying to overwrite the value of x.
We can make the variable mutable by adding mut the initial assignment:
Code Block |
---|
fn main() {
let mut x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
} |
The above code compiles.
Constants
You declare constants using the const
keyword instead of the let
keyword, and the type of the value must be annotated.
Constants can be declared in any scope, including the global scope, which makes them useful for values that many parts of code need to know about.
Code Block |
---|
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
const PI: f32 = 3.14; |
Rust’s naming convention for constants is to use all uppercase with underscores between words.
Shadowing
You can declare a new variable with the same name as a previous variable. Rustaceans say that the first variable is shadowed by the second, which means that the second variable is what the compiler will see when you use the name of the variable.
Code Block |
---|
fn main() {
let x = 5;
let x = x + 1;
{
let x = x * 2;
println!("The value of x in the inner scope is: {x}");
}
println!("The value of x is: {x}");
} |
Code Block | ||
---|---|---|
| ||
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/variables`
The value of x in the inner scope is: 12
The value of x is: 6 |
Data Types
Integer Types
Length | Signed | Unsigned |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
Number Literals
Number literals can also use _
as a visual separator to make the number easier to read, such as 1_000
, which will have the same value as if you had specified 1000
.
Number literals | Example |
---|---|
Decimal | 98_222 |
Hex | 0xff |
Octal | 0o77 |
Binary | 0b1111_0000 |
Byte (u8 only) | b'A' |
Floating-Point Types
Rust’s floating-point types are f32 and f64, which are 32 bits and 64 bits in size, respectively. The default type is f64 because on modern CPUs, it’s roughly the same speed as f32 but is capable of more precision. All floating-point types are signed.
Code Block |
---|
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
} |
Boolean Type
Booleans are one byte in size. The Boolean type in Rust is specified using bool.
Code Block |
---|
fn main() {
let t = true;
let f: bool = false; // with explicit type annotation
} |
Character Type
Rust’s char type is the language’s most primitive alphabetic type.
Note that we specify char literals with single quotes, as opposed to string literals, which use double quotes.
Code Block |
---|
fn main() {
let c = 'z';
let z: char = 'ℤ'; // with explicit type annotation
let heart_eyed_cat = '😻';
} |
Compound Types
Compound types can group multiple values into one type. Rust has two primitive compound types: tuples and arrays.
Tuple Type
A tuple is a general way of grouping together a number of values with a variety of types into one compound type. Tuples have a fixed length: once declared, they cannot grow or shrink in size.
Code Block |
---|
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {y}");
} |
This program first creates a tuple and binds it to the variable tup. It then uses a pattern with let to take tup and turn it into three separate variables, x, y, and z. This is called destructuring because it breaks the single tuple into three parts. Finally, the program prints the value of y, which is 6.4.
We can also access a tuple element directly by using a period (.) followed by the index of the value we want to access. For example:
Code Block |
---|
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
} |
Array Type
Unlike a tuple, every element of an array must have the same type. Unlike arrays in some other languages, arrays in Rust have a fixed length.
Code Block |
---|
fn main() {
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
let a: [i32; 5] = [1, 2, 3, 4, 5];
//access values
let first = a[0];
let second = a[1];
} |
Functions
Rust code uses snake case as the conventional style for function and variable names, in which all letters are lowercase and underscores separate words
Code Block |
---|
fn main() {
another_function(5);
}
fn another_function(x: i32) {
println!("The value of x is: {x}");
} |
Returning Values
Code Block |
---|
fn main() {
let x = plus_one(5);
println!("The value of x is: {x}");
}
fn plus_one(x: i32) -> i32 {
x + 1
} |
The value returned from function plus_one is the value of x+1 or 6. Notice that there is no semi-colon at the end of the line. This is an expression.
Comments
Rust uses two forward slashes ( // ) to indicate a comment.
For comments that extend beyond a single line, you’ll need to include //
on each line.
Comments can also be placed at the end of lines containing code
Code Block |
---|
// So we’re doing something complicated here, long enough that we need
// multiple lines of comments to do it! Whew! Hopefully, this comment will
// explain what’s going on.
fn main() {
let lucky_number = 7; // I’m feeling lucky today
} |
Control Flow
If/Else
Code Block |
---|
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");
}
} |
Using if in a let Statement
Code Block |
---|
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");
} |
Loops
Code Block |
---|
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {result}");
} |
While
Code Block |
---|
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");
} |
For
Looping Through a Collection with for
Code Block |
---|
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
} |
Here’s what the countdown would look like using a for loop and another method we’ve not yet talked about, rev, to reverse the range:
Code Block |
---|
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
} |
Iterating over a Range
Code Block |
---|
let mut sum = 0;
for i in 1..=5 {
sum += i;
} |
There are five kinds of ranges in Rust:
1..5
: A (half-open) range. It includes all numbers from 1 to 4. It doesn't include the last value, 5.1..=5
: An inclusive range. It includes all numbers from 1 to 5. It includes the last value, 5.1..
: An open-ended range. It includes all numbers from 1 to infinity (well, until the maximum value of the integer type)...5
: A range that starts at the minimum value for the integer type and ends at 4. It doesn't include the last value, 5...=5
: A range that starts at the minimum value for the integer type and ends at 5. It includes the last value, 5.
Ownership
When you assign a variable to another or pass a variable to a function, the receiver takes ownership and also becomes responsible for dropping he memory when out of scope
See https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html
Example1
Code Block |
---|
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. |
Example2
Code Block |
---|
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 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("yours"); // some_string comes into scope
some_string // some_string is returned and
// moves out to the calling
// function
}
// This function takes a String and returns 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
We can send a variable by reference by pre-pending with an ampersand. This way, the ownership is not transfered.
Note: The opposite of referencing by using & is dereferencing, which is accomplished with the dereference operator, *.
Code Block |
---|
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{s1}' is {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, it is not dropped. |
So, what happens if we try to modify something we’re borrowing? Spoiler alert: it doesn’t work!
Mutable Reference
We can allow the reference to be changed by adding the mutable (mut) keyword.
Code Block |
---|
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
} |
Mutable references have one big restriction: if you have a mutable reference to a value, you can have no other references to that value.
The following code would generate an error:
Code Block |
---|
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2); |
Slices
String Slices
A string slice is a reference to part of a String, and it looks like this:
Code Block |
---|
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11]; |
With Rust’s .. range syntax, if you want to start at index 0, you can drop the value before the two periods.
By the same token, if your slice includes the last byte of the String, you can drop the trailing number.
You can also drop both values to take a slice of the entire string.
Code Block |
---|
let s = String::from("hello world");
let hello = &s[..5];
let world = &s[6..];
let sentence = &s[..];
let sentence = &s[0..len]; |
Structs
In a struct you’ll name each piece of data so it’s clear what the values mean. Adding these names means that structs are more flexible than tuples: you don’t have to rely on the order of the data to specify or access the values of an instance.
Code Block |
---|
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let mut user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");
} |
Creating Instances from Other Instances with Struct Update Syntax
Code Block |
---|
fn main() {
// --snip--
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
} |
The syntax .. specifies that the remaining fields not explicitly set should have the same value as the fields in the given instance.
Code Block |
---|
fn main() {
// --snip--
let user2 = User {
email: String::from("another@example.com"),
..user1
};
} |
Defining Methods
Code Block |
---|
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
} |
Another Example
Code Block |
---|
pub struct Ticket {
title: String,
description: String,
status: String,
}
impl Ticket {
pub fn new(title: String, description: String, status: String) -> Ticket {
if title.is_empty() {
panic!("Title cannot be empty");
}
Ticket {
title,
description,
status,
}
}
pub fn title(&self) -> &String {
&self.title
}
pub fn description(&self) -> &String {
&self.description
}
pub fn status(&self) -> &String {
&self.status
}
} |
Static Methods
Code Block |
---|
struct Configuration {
version: u32,
active: bool
}
impl Configuration {
// `default` is a static method on `Configuration`
fn new() -> Configuration {
Configuration { version: 0, active: false }
}
}
// Call using following syntax:
let default_config = Configuration::new(); |
Modules
https://doc.rust-lang.org/book/ch07-02-defining-modules-to-control-scope-and-privacy.html
Modules Cheat Sheet
Before we get to the details of modules and paths, here we provide a quick reference on how modules, paths, the use
keyword, and the pub
keyword work in the compiler, and how most developers organize their code. We’ll be going through examples of each of these rules throughout this chapter, but this is a great place to refer to as a reminder of how modules work.
- Start from the crate root: When compiling a crate, the compiler first looks in the crate root file (usually src/lib.rs for a library crate or src/main.rs for a binary crate) for code to compile.
- Declaring modules: In the crate root file, you can declare new modules; say you declare a “garden” module with
mod garden;
. The compiler will look for the module’s code in these places:- Inline, within curly brackets that replace the semicolon following
mod garden
- In the file src/garden.rs
- In the file src/garden/mod.rs
- Inline, within curly brackets that replace the semicolon following
- Declaring submodules: In any file other than the crate root, you can declare submodules. For example, you might declare
mod vegetables;
in src/garden.rs. The compiler will look for the submodule’s code within the directory named for the parent module in these places:- Inline, directly following
mod vegetables
, within curly brackets instead of the semicolon - In the file src/garden/vegetables.rs
- In the file src/garden/vegetables/mod.rs
- Inline, directly following
- Paths to code in modules: Once a module is part of your crate, you can refer to code in that module from anywhere else in that same crate, as long as the privacy rules allow, using the path to the code. For example, an
Asparagus
type in the garden vegetables module would be found atcrate::garden::vegetables::Asparagus
. - Private vs. public: Code within a module is private from its parent modules by default. To make a module public, declare it with
pub mod
instead ofmod
. To make items within a public module public as well, usepub
before their declarations. - The
use
keyword: Within a scope, theuse
keyword creates shortcuts to items to reduce repetition of long paths. In any scope that can refer tocrate::garden::vegetables::Asparagus
, you can create a shortcut withuse crate::garden::vegetables::Asparagus;
and from then on you only need to writeAsparagus
to make use of that type in the scope.
Here, we create a binary crate named backyard
that illustrates these rules. The crate’s directory, also named backyard
, contains these files and directories:
Code Block |
---|
backyard
├── Cargo.lock
├── Cargo.toml
└── src
├── garden
│ └── vegetables.rs
├── garden.rs
└── main.rs |
Code Block |
---|
use crate::garden::vegetables::Asparagus;
pub mod garden;
fn main() {
let plant = Asparagus {};
println!("I'm growing {plant:?}!");
} |
Visibility
When you start breaking down your code into multiple modules, you need to start thinking about visibility. Visibility determines which regions of your code (or other people's code) can access a given entity, be it a struct, a function, a field, etc.
By default, everything in Rust is private.
A private entity can only be accessed:
- within the same module where it's defined, or
- by one of its submodules
You can modify the default visibility of an entity using a visibility modifier.
Some common visibility modifiers are:
pub
: makes the entity public, i.e. accessible from outside the module where it's defined, potentially from other crates.pub(crate)
: makes the entity public within the same crate, but not outside of it.pub(super)
: makes the entity public within the parent module.pub(in path::to::module)
: makes the entity public within the specified module.
You can use these modifiers on modules, structs, functions, fields, etc. For example:
Code Block |
---|
pub struct Configuration {
pub(crate) version: u32,
active: bool,
} |
Enums
Code Block |
---|
enum IpAddrKind {
V4,
V6,
}
let four = IpAddrKind::V4;
let six = IpAddrKind::V6; |
Code Block |
---|
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"),
}; |
Match
A match
statement that lets you compare a Rust value against a series of patterns.
There's one key detail here: match
is exhaustive. You must handle all enum variants.
If you forget to handle a variant, Rust will stop you at compile-time with an error.
Code Block |
---|
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
} |
If you don't care about one or more variants, you can use the _
pattern as a catch-all:
Code Block |
---|
enum Status {
ToDo,
InProgress,
Done
}
impl Status {
fn is_done(&self) -> bool {
match self {
Status::Done => true,
_ => false
}
}
}
|
Variants on Enums
Code Block |
---|
enum Status {
ToDo,
InProgress {
assigned_to: String,
},
Done,
} |
InProgress
is now a struct-like variant.
The syntax mirrors, in fact, the one we used to define a struct—it's just "inlined" inside the enum, as a variant.
To access the variant, we need to use a match statement.
Code Block |
---|
match status {
Status::InProgress { assigned_to } => {
println!("Assigned to: {}", assigned_to);
},
Status::ToDo | Status::Done => {
println!("Done");
}
} |
Concise Branching
if/let
The if let construct allows you to match on a single variant of an enum, without having to handle all the other variants.
Code Block |
---|
impl Ticket {
pub fn assigned_to(&self) -> &str {
if let Status::InProgress { assigned_to } = &self.status {
assigned_to
} else {
panic!(
"Only `In-Progress` tickets can be assigned to someone"
);
}
}
} |
let/else
If the else branch is meant to return early (a panic counts as returning early!), you can use the let/else construct:
Code Block |
---|
impl Ticket {
pub fn assigned_to(&self) -> &str {
let Status::InProgress { assigned_to } = &self.status else {
panic!(
"Only `In-Progress` tickets can be assigned to someone"
);
};
assigned_to
}
} |
Traits
Traits are Rust's way of defining interfaces. A trait defines a set of methods that a type must implement to satisfy the trait's contract.
Defining a trait
The syntax for a trait definition goes like this:
Code Block |
---|
trait <TraitName> {
fn <method_name>(<parameters>) -> <return_type>;
} |
We might, for example, define a trait named MaybeZero
that requires its implementors to define an is_zero
method:
Code Block |
---|
trait MaybeZero {
fn is_zero(self) -> bool;
} |
Implementing a trait
To implement a trait for a type we use the impl
keyword, just like we do for regular1 methods, but the syntax is a bit different:
Code Block |
---|
impl <TraitName> for <TypeName> {
fn <method_name>(<parameters>) -> <return_type> {
// Method body
}
} |
For example, to implement the MaybeZero
trait for a custom number type, WrappingU32
:
Code Block |
---|
pub struct WrappingU32 {
inner: u32,
}
impl MaybeZero for WrappingU32 {
fn is_zero(self) -> bool {
self.inner == 0
}
} |
Invoking a trait method
To invoke a trait method, we use the .
operator, just like we do with regular methods:
Code Block |
---|
let x = WrappingU32 { inner: 5 };
assert!(!x.is_zero()); |
To invoke a trait method, two things must be true:
- The type must implement the trait.
- The trait must be in scope.
To satisfy the latter, you may have to add a use
statement for the trait:
Code Block |
---|
use crate::MaybeZero; |
Example of Multiple Implementation of a Trait
Code Block |
---|
trait Power<Exp> {
fn power(&self, exp: Exp) -> Self;
}
impl Power<u32> for u32 {
fn power(&self, exp: u32) -> u32 {
self.pow(exp)
}
}
impl Power<&u32> for u32 {
fn power(&self, exp: &u32) -> u32 {
self.pow(*exp)
}
}
impl Power<u16> for u32 {
fn power(&self, exp: u16) -> u32 {
self.pow(exp as u32)
}
} |
Operator Overloading
In Rust, operators are traits.
For each operator, there is a corresponding trait that defines the behavior of that operator.
Arithmetic operators live in the std::ops module, while comparison ones live in the std::cmp module.
Code Block |
---|
struct WrappingU8 {
inner: u8,
}
impl PartialEq for WrappingU8 {
fn eq(&self, other: &WrappingU8) -> bool {
self.inner == other.inner
}
// No `ne` implementation here
} |
When you write x == y the compiler will look for an implementation of the PartialEq trait for the types of x and y and replace x == y with x.eq(y).
It's syntactic sugar!
Derive macros
A derive macro is a particular flavour of Rust macro. It is specified as an attribute on top of a struct.
Code Block |
---|
#[derive(PartialEq)]
struct Ticket {
title: String,
description: String,
status: String
} |
Derive macros are used to automate the implementation of common (and "obvious") traits for custom types.
In the example above, the PartialEq trait is automatically implemented for Ticket.
If you expand the macro, you'll see that the generated code is functionally equivalent to the one you wrote manually, although a bit more cumbersome to read:
Code Block |
---|
#[automatically_derived]
impl ::core::cmp::PartialEq for Ticket {
#[inline]
fn eq(&self, other: &Ticket) -> bool {
self.title == other.title
&& self.description == other.description
&& self.status == other.status
}
} |
Defer Trait
The Deref trait is the mechanism behind the language feature known as deref coercion.
By implementing Deref<Target = U>
for a type T
you're telling the compiler that &T
and &U
are somewhat interchangeable.
In particular, you get the following behavior:
- References to
T
are implicitly converted into references toU
(i.e.&T
becomes&U
) - You can call on
&T
all the methods defined onU
that take&self
as input.
String implements Deref with Target = str:
Code Block |
---|
impl Deref for String {
type Target = str;
fn deref(&self) -> &str {
// [...]
}
} |
From and Into Trait
In std's documentation you can see which std types implement the From trait. You'll find that String implements From<&str> for String.
Thus, we can write:
Code Block |
---|
let title = String::from("A title");
// or
let title:&str = "title";
let myString:String = String::from(title);
// or
let title = "A title".into(); |
From and Into are dual traits.
In particular, Into
is implemented for any type that implements From
using a blanket implementation:
Code Block |
---|
impl<T, U> Into<U> for T
where
U: From<T>,
{
fn into(self) -> U {
U::from(self)
}
} |
Implementing From Trait Example:
Code Block |
---|
use std::convert::From;
#[derive(Debug)]
struct Number {
value: i32,
}
impl From<i32> for Number {
fn from(item: i32) -> Self {
Number { value: item }
}
}
fn main() {
let num = Number::from(30);
println!("My number is {:?}", num);
} |
Clone Trait
Its method, clone, takes a reference to self and returns a new owned instance of the same type.
Code Block |
---|
fn consumer(s: String) { /* */ }
fn example() {
let mut s = String::from("hello");
let t = s.clone();
consumer(t);
s.push_str(", world!"); // no error
} |
To make a type Clone-able, we have to implement the Clone trait for it.
You almost always implement Clone by deriving it:
Code Block |
---|
#[derive(Clone)]
struct MyType {
// fields
} |
Copy Trait
If a type implements Copy, there's no need to call .clone() to create a new instance of the type: Rust does it implicitly for you.
Copy
is not equivalent to "automatic cloning", although it implies it.
Types must meet a few requirements in order to be allowed to implement Copy
.
First of all, it must implement Clone
, since Copy
is a subtrait of Clone
. This makes sense: if Rust can create a new instance of a type implicitly, it should also be able to create a new instance explicitlyby calling .clone()
.
That's not all, though. A few more conditions must be met:
- The type doesn't manage any additional resources (e.g. heap memory, file handles, etc.) beyond the
std::mem::size_of
bytes that it occupies in memory. - The type is not a mutable reference (
&mut T
).
If both conditions are met, then Rust can safely create a new instance of the type by performing a bitwise copy of the original instance—this is often referred to as a memcpy
operation, after the C standard library function that performs the bitwise copy.
Implementing Copy
Code Block |
---|
#[derive(Copy, Clone)]
struct MyStruct {
field: u32,
} |
Drop Trait
The Drop trait is a mechanism for you to define additional cleanup logic for your types, beyond what the compiler does for you automatically.
Whatever you put in the drop method will be executed when the value goes out of scope.
Code Block |
---|
pub trait Drop {
fn drop(&mut self);
} |
If your type has an explicit Drop
implementation, the compiler will assume that your type has additional resources attached to it and won't allow you to implement Copy
.
Macros
You've already encountered a few macros in past exercises:
assert_eq!
andassert!
, in the test casesprintln!
, to print to the console
Rust macros are code generators.
They generate new Rust code based on the input you provide, and that generated code is then compiled alongside the rest of your program. Some macros are built into Rust's standard library, but you can also write your own.
Generics
Generics allow us to write code that works with a type parameter instead of a concrete type:
Code Block |
---|
fn print_if_even<T>(n: T)
where
T: IsEven + Debug
{
if n.is_even() {
println!("{n:?} is even");
}
} |
print_if_even
is a generic function.
It isn't tied to a specific input type. Instead, it works with any type T
that:
- Implements the
IsEven
trait. - Implements the
Debug
trait.
This contract is expressed with a trait bound: T: IsEven + Debug
.
The +
symbol is used to require that T
implements multiple traits. T: IsEven + Debug
is equivalent to "where T
implements IsEven
and Debug
".
If the trait bounds are simple, you can inline them directly next to the type parameter:
Code Block |
---|
fn print_if_even<T: IsEven + Debug>(n: T) {
// ^^^^^^^^^^^^^^^^^
// This is an inline trait bound
// [...]
}
|
Error Handling
Option
Option is a Rust type that represents nullable values.
It is an enum, defined in Rust's standard library:
Code Block |
---|
enum Option<T> {
Some(T),
None,
} |
Option encodes the idea that a value might be present (Some(T)) or absent (None).
It also forces you to explicitly handle both cases. You'll get a compiler error if you are working with a nullable value and you forget to handle the None case.
This is a significant improvement over "implicit" nullability in other languages, where you can forget to check for null and thus trigger a runtime error.
Code Block |
---|
pub fn assigned_to(&self) -> Option<&String> {
if let Status::InProgress { assigned_to} = &self.status {
Option::Some(assigned_to)
} else {
Option::None
}
} |
Result
The Result type is an enum defined in the standard library:
Code Block |
---|
enum Result<T, E> {
Ok(T),
Err(E),
} |
It has two variants:
- Ok(T): represents a successful operation. It holds T, the output of the operation.
- Err(E): represents a failed operation. It holds E, the error that occurred.
Both Ok and Err are generic, allowing you to specify your own types for the success and error cases.
Code Block |
---|
impl Ticket {
pub fn new(title: String, description: String, status: Status) -> Result<Ticket,String> {
if title.is_empty() {
return Result::Err("Title cannot be empty".into());
}
if title.len() > 50 {
return Result::Err("Title cannot be longer than 50 bytes".into());
}
if description.is_empty() {
return Result::Err("Description cannot be empty".into());
}
if description.len() > 500 {
return Result::Err("Description cannot be longer than 500 bytes".into());
}
Result::Ok(
Ticket {
title,
description,
status,
}
)
}
} |
Processing the Result
When you call a function that returns a Result
, you have two key options:
Panic if the operation failed. This is done using either the unwrap
or expect
methods
Code Block |
---|
// Panics if `parse_int` returns an `Err`.
let number = parse_int("42").unwrap();
// `expect` lets you specify a custom panic message.
let number = parse_int("42").expect("Failed to parse integer"); |
Destructure the Result
using a match
expression to deal with the error case explicitly
Code Block |
---|
match parse_int("42") {
Ok(number) => println!("Parsed number: {}", number),
Err(err) => eprintln!("Error: {}", err),
} |
Code Block |
---|
let result = Ticket::new(title,d,status);
match result {
Ok(ticket) => ticket,
Err(err) => { panic!("{}",err); }
} |
References
Reference | URL |
---|---|
The Rust Programming Language | https://doc.rust-lang.org/book/title-page.html |
Rust by Example | https://doc.rust-lang.org/rust-by-example/index.html |
100 Exercises To Learn Rust | https://rust-exercises.com/100-exercises/04_traits/01_trait |
Advanced Traits in Rust | |
Rust Cheat Sheet | |
The Rust Reference | |
Docs.RS | |
The Rust community’s crate registry |