Programming With Rust Early Release - Donis Marshalls
Programming With Rust Early Release - Donis Marshalls
com
Programming with Rust
Donis Marshall
With Early Release eBooks, you get books in their earliest form
—the author’s raw and unedited content as they write—so you
can take advantage of these technologies long before the official
release of these titles.
OceanofPDF.com
Contents
OceanofPDF.com
Table of Contents
OceanofPDF.com
Chapter 1. Introduction to Rust
Functional Programming
Pattern-Oriented
Features
Let’s explore the core features that contribute to Rust and its
popularity.
Safeness
Ownership
As the owner, Bob can always drive the car, except when he has
loaned it to someone else. If someone else (Ari) wants to drive
the car, there are two possibilities: Bob must either sell or lend
the vehicle to Ari. Either way, Bob loses possession of the
vehicle, at least temporarily.
Lifetimes
Fearless Concurrency
Zero-Cost Abstraction
Rust Terminology
Many technologies, including programming languages, have
their own terminology. Familiarizing yourself with that
terminology can be helpful when communicating with peers
and others in the larger Rust community.
Rust: Let’s start with the most important term: “Rust” itself.
Rust is not an acronym or a special technology term. The name
Rust comes from the term rust fungi, which is a robust pathogen
that attacks living plants.
Tools
$ rustc source.rs
Cargo is the pivotal tool in the Rust environment. You can use
Cargo for many of the tasks required to maintain your
environment and packages.
Summary
Safeness
Ownership
Lifetimes
Trustworthy concurrency
Zero-cost abstraction
OceanofPDF.com
Chapter 2. Getting Started
Preliminaries
You can also request that Rustup install specific versions of the
Rust environment. This is especially useful if your organization
has not upgraded to the latest release.
https://visualstudio.microsoft.com/visual-cpp-build-tools/
Installing Rust
https://rustup.rs
www.rust-lang.org/tools/install
For even more details, visit the Rust Getting Started page. This
page documents how to install Rust, with some options, and it
provides some helpful “getting started” commands for the
Cargo tool, the Rust build tool and package manager. The
bottom of the web page lists various editors and integrated
development environments (IDEs) available to Rust developers:
www.rust-lang.org/learn/get-started
Windows: \users\{user}\.cargo\bin
Linux: home/.cargo/bin
macOS: /users/{user}/.cargo/bin
$ cargo --version
cargo 0.00.0 (a748cf5a3 0000-00-00)
$ rustc --version
rustc 0.00.0 (a8314ef7d 0000-00-00)
Advanced Rustup
$ rustup
$ rustup install beta
$ rustup install nightly
Now that Rust is installed, it’s time for our “Hello, World”
application.
“Hello, World”
fn main() {
println!( "Hello, world!");
}
First of all, the source code for the executable crate is saved in a
file with an .rs extension, such as hello.rs. This is the standard
extension for Rust source files.
fn func_name(parameters)->returnval
rustc hello.rs
From the hello.rs source file, rustc creates the hello.exe and
hello.pdb files.
The rustc compiler is talkative, providing verbose warnings
and error messages. References may also be provided for more
detail. Here is the compiler error message displayed when the
println! macro is improperly used without the obligatory
exclamation point:
Cargo
You can use the cargo tool to compile Rust crates and create
binaries. This is in lieu of using the rustc compiler directly.
cargo is a versatile tool that can perform various tasks,
including creating and managing packages, compiling binaries,
maintaining a secure environment, and managing
dependencies. For compilation, cargo delegates to the rustc
compiler.
cargo init
[package]
name = "packagename"
version = "0.1.0"
edition = "2021"
[dependencies]
fn main() {
println!("Hello, world!");
}
You can compile the crate with the following command and
create a binary executable:
cargo build
cargo run
This must also be done from within the package. If the binary is
not already built, cargo build will be done first. For this
reason, some skip the separate build step entirely for
executable crates and rely entirely on the cargo run
command.
Library
cargo test
This command will run unit tests in the crate. For lib.rs, this
provides the opportunity to execute and test the source code in
the library. As a result, the cargo test command displays
whether the unit test passed or failed.
Listing 2.4 shows the lib.rs that cargo creates, with sample
code.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
Let’s explore the lib.rs crate. The file starts with the #
[cfg(test)] annotation. This annotation asks the cargo
build command to ignore unit tests and not to include them in
the resulting binary. Within the file, each unit test is labeled
with the #[test] annotation. The sample unit test performs a
simple addition. The result of the operation is compared to the
expected value within the assert_eq! macro. You update the
code in the unit tests to reference the specific public functions
of your library. There should be a unit test for each public
function in the library.
fn get_hello()->String {
"Hello, world!".to_string()
}
#[cfg(test)]
mod tests {
#[test]
fn test_get_hello() {
let result = get_hello();
assert_eq!(result, "Hello, world!");
}
}
[package]
name = "use_hello"
version = "0.1.0"
edition = "2021"
[dependencies]
hello={path = "../hello" }
With the preceding cargo.toml file, the executable crate can
access the public functions of the library. The syntax for
accessing functions in the library is libraryname::function .
The code in Listing 2.7 calls the get_hello function found in
the hello library.
fn main() {
println!(“{}”, hello::get_hello());
}
You can now use the cargo run command to run the
executable crate and display the “Hello, world!” string.
Comments
/// # Examples
///
/// ```
/// let greeting=hello::hello_world("hiIN");
/// assert_eq!(greeting, " !"
///
///
Published Crates
[package]
name = "use_rust_ascii"
version = "0.1.0"
edition = "2021"
[dependencies]
rustascii = "0.1.1"
Main Function
From the main function, you can return an integer value to the
operating system using the Termination trait. The default
implementation of the trait returns libc::EXIT_SUCCESS or
libc::EXIT_FAILURE as an exit code. These values are most
likely 1 and 0, but that is implementation specific.
Instead of returning directly from main , you can call the exit
function ( std::process::exit ) to prematurely exit an
application. However, the exit function immediately
terminates the application without performing cleanup for the
functions currently on the call stack, possibly preventing an
orderly shutdown. The parameter of the exit function sets the
process return value for the operating system.
Command-Line Arguments
fn main() {
// display arg 0
if let Some(arg)=std::env::args().nth(0) {
println!("{}", arg);
}
Summary
Next, we will explore the Rust type system. Until you have a
better understanding of the type safeness and immutability and
the various types available in Rust, your applications will be
limited in scope and functionality.
OceanofPDF.com
Chapter 3. Variables
Another benefit of the Rust type system is the refined types that
are uncommon to other languages. These help developers
efficiently manage memory utilization, especially in
applications where every bit matters.
Terminology
Variables
let varname:type=value;
let varname=value;
let varname:type;
let varname;
Primitives
Primitives are the basic types and the building blocks of Rust.
You can combine primitives to create more complex types, such
as structs and enums. Primitives are intrinsic and implemented
by the Rust compiler. Functions and attributes of the primitives
are implemented in Rust libraries. For example, i32::MAX is an
associated constant implemented for the i32 primitive.
signed integer
unsigned integer
float
bool
reference
array
tuple
slice
String
str
raw pointer
Integer Types
Except for isize and usize, integer types are fixed sized, where
the suffix of the type name defines the bit size. For example, i64
is a signed 64-bit integer.
isize
i8
i16
i32
i64
i128
usize
u8
u16
u32
u64
u128
The size of the isize and usize types depends on the operating
environment. It is the size of a pointer in the host architecture.
If curious, you can confirm the size of any type with the
size_of method function, as shown in Listing 3.2. This
method is found in the std::mem module.
Code Listing 3.2. Confirming the size of isize
fn print_type_of<T>(_: &T) {
println!("{}", std::any::type_name::<T>())
}
fn main() {
let value=1; // inferred
print_type_of(&value);
}
For increased readability, underscores can be inserted as
separators within numeric literals, for both integer and floating
point values. Typically, underscores demarcate 103 segments.
However, Rust is not the underscore police. You can place the
underscore anywhere within a number, as demonstrated in
Listing 3.4.
let typical1=123_456_678;
let typical2=123_456.67;
let interesting=12_3_456;
Overflow
Notations
Base 10 is the default base for integer values. You can change
the base with the proper notation:
0b for binary
0o for octal
0x for hexadecimal
println!("{}", 10); // 10
println!("{:04b}", 0b10); // 0010
println!("{}", 0o12); // 10
println!("{}", 0xA); // 10
fn main() {
let radius=4.234;
let diameter=2.0*radius;
let area=consts::PI*radius;
Neither float types, f32 or f64, are ideal for fixed-point numbers.
This is especially true for currency values where exact
precision is important. Lost dollars and pennies can add up!
The Decimal type, found in the rust_decimal crate, is the
type for fixed-point floating point numbers. You can find the
rust_decimal crate in the crates.io repository. Create a
Decimal number with the new constructor or the dec! macro,
as shown in Listing 3.10.
use rust_decimal::prelude::*;
use rust_decimal_macros::dec;
fn main() {
let mut number1 = Decimal::from_str("-1.23656
let mut number2 = dec!(-1.23656); // alte
let space:f32=f32::INFINITY;
let stars:f64=f64::INFINITY;
let circle_radius=10.0;
let pointw=0.0;
let number_of_points=circle_radius/pointw;
if number_of_points==f64::INFINITY {
// Do something
} else {
// Do something else
}
NaN
let n=0.0;
let result=f64::sqrt(n);
if f64::NAN!=result {
// handle proper result
} else {
// handle NAN
}
Numeric Ranges
Code Listing 3.13. Displaying the MIN and MAX const values
Casting
You can cast a value, variable or literal, from its current type to
another type. Rust does not provide wide support for implicit
casting. Many languages allow implicit casting when no
precision is lost. However, Rust requires explicit casting even in
this circumstance. Listing 3.14 provides an example of casting.
let val1=10i8;
let val2=20f64;
Boolean Types
let condition:bool=true;
Internally, bool values are bitwise values 0x01 for true and 0x00
for false. You can cast bool values to i8 types. True becomes 1,
while false becomes 0.
Char
let ref1=&15;
let ref2=&20;
let value1=ref1+10;
let value2=ref1*ref2;
println!("{} {}", value1, value2); // 25 300
let fruit_grove=32;
println!("{}", basket);
let num_of_eggs=10;
let num_of_pizza=10;
let eggs=&num_of_eggs;
let pizza=&num_of_pizza;
eggs==pizza; // true
ptr::eq(eggs, pizza); // false
Operators
Mathematical operators
Boolean operators
Logical operators
Bitwise operators
The mathematical operators, shown in Table 3.6, are primarily
binary operators that perform various numerical calculations,
such as addition and subtraction.
value=value+5; // + operator
value+=5; // compound + operator
Table 3.7 is a list of compound mathematical operators.
For the && and || operators, the & and | operators are
companion operators that do not short-circuit. Listing 3.22
provides an example.
Code Listing 3.22. Display message is an expression that does
not short-circuit.
Summary
OceanofPDF.com
Chapter 4. Strings
Str
The str type is a primitive type and part of the core language.
The str type has all the characteristics of a slice, including being
unsized and read-only. Because str is a slice, you normally
borrow a str, &str . A str consists of two fields: pointer to the
string data and length.
String literals, defined within quotes ( " … " ), are str values that
exist for the entirety of the program. As such, the lifetime of a
string literal is static. The following is the notation for a string
literal including the lifetime. You can find more about lifetimes
in Chapter 9, “Lifetimes.”
&'static str
String
You can create an empty string for String with the new
constructor. Typically, this is a mutable String where text can be
added later.
Length
For ASCII, the length in bytes and the number of characters are
identical. However, that may differ for other character sets. The
len function returns the number of bytes in the string.
let english="Hello".to_string();
let greek="γεια".to_string();
let chinese="你好".to_string();
english.chars().count());
// Greek γεια: Bytes 8 Length 4
println!("Greek {}: Bytes {} Length {}",
greek, greek.len(),
greek.chars().count());
Extending a String
You can extend the value of a String, but not a str type. String
has several functions for this purpose:
push
push_str
insert
insert_str
// ab | one two
println!("{} | {}", alphabet, numbers);
You may not want to append text to a string but rather insert
within it. For inserting text within a string, the insert
function inserts a char value, while insert_str inserts a
string. For the insert function, the first parameter is implicit
and refers to the current String. The second parameter is the
position where the character should be inserted. The final
parameter is the character to insert. The insert_str function
is identical to the insert function, except the final parameter
inserts a string. Here is the function definition of each:
Capacity
The String type has the same functions for managing the
capacity as any vector.
string_1.push('乐'); // b
println!("Capacity {} Length {}",
string_1.capacity(), string_1.len());
string_1.push_str("的"); // c
println!("Capacity {} Length {}",
string_1.capacity(), string_1.len()); /
2. Add the next character to the string. The length is now 6 and
exceeds the capacity. This forces a reallocation.
string_1.push('快');
println!("Capacity {} Length {}",
string_1.capacity(), string_1.len()); // Cap
string_1.push('乐');
println!("Capacity {} Length {}",
string_1.capacity(), string_1.len()); // Cap
string_1.push_str("的");
println!("Capacity {} Length {}",
string_1.capacity(), string_1.len()); // Cap
let string_1="hello".to_string();
let character=string_1[1];
let slice=&string_1[0..=7];
let str_1="快乐的";
println!("{}", str_1.is_char_boundary(0) );
println!("{}", str_1.is_char_boundary(1) );
String Characters
let czech_skiing="lyžování".to_string();
println!("{}", czech_skiing.chars().nth(2).un
Deref Coercion
You can substitute a borrowed String, &String , anywhere
&str is expected. At that time, the String inherits the methods
of the str type. This is possible because the String type
implements the deref trait for str. Appropriately, this
conversion is called deref coercion. The reverse is not available
—that is, converting from str to a String type.
fn FuncA(str_1: &str) {
println!("{}", str_1);
}
fn main() {
let string_1="Hello".to_string();
FuncA(&string_1);
}
Formatted String
For creating fancy strings, the format! macro is convenient.
The format! macro is similar to the print family of macros,
except it returns a formatted string. That includes identical
parameters. The format specifiers are explained in detail in
Chapter 5, “Console.” Both the print! and format! macros
rely on the std::fmt module.
let left=5;
let right=10;
let result=format!("{left} + {right} = {resul
println!("{}", result); // 5 + 10 = 15
Helpful Functions
fn clear(&mut self)
Listing 4.18, the contains function scans the String for the “fun”
pattern, and returns true. The println! macro displays the
result. A raw string, “r” prefix, is used within the macro because
of the nested string within the display string.
let string_6=
"Bob went shopping; then Bob went home."
let result_string=string_6.replace("Bob", "Al
let string_9="Cool!";
println!("{} : {}", string_9, string_9.to_upp
// Cool! : COOL!
Summary
The standard Rust strings are the str and String types. The str
type represents a string slice and is often used for string literals.
Typically, strs are borrowed: &str . The String type is mutable,
growable, and ownable. With deref coercion, you can freely
substitute &string for &str .
OceanofPDF.com
Chapter 5. Console
The print and println macros are variadic and can have a
variable number of arguments. The print macro must have at
least one argument, the format string. The println macro can
have no arguments and simply display a linefeed.
Positional Arguments
let a = 1;
let b = 2;
println!("Total {1} + {0} = {2}",
a, b, a+ b); // Total 2 + 1 = 3
Variable Arguments
Named Arguments
println!(
"{} + {} + {} + {} = {result}", first,
second, third, fourth,
result = first + second + third +
);
Both named and positional arguments can appear within
placeholders in the format string. However, positional
arguments cannot follow the named arguments in the
parameter list. In Listing 5.6, prefix , a named argument,
precedes any positional arguments in the println macro.
println!(
"{prefix} {first} + {second} + {third} +
= {result}",
prefix = "Average: ",
result = result as f32 / 4.0
);
Within the format string, you can set the padding, alignment, or
numeric precision of the placeholders. This is great for creating
professional looking displays and reports. You can fine-tune the
format specification after a : (colon) character in the
placeholder, {:format} .
Text Value
==== =====
one 10
two 2000
three 400
For floating point numbers, you can add precision within the
placeholder. You can control the number of positions displayed
after the decimal place. In the placeholder, stipulate floating
point precision after the padding. The syntax is
padding.precision . If padding is not present, the syntax is
simply .precision . For integral types, precision in the
placeholder is ignored.
Here is the result. Both floating point numbers have the same
precision.
Base
For the print macros, the default base for numbers is Base 10.
However, you can specify a wide variety of other bases,
including binary and hexadecimal. There is a letter designation
for each supported base type. For hexadecimal, there are two
designations: upper- and lowercase “x” for upper- and
lowercase hexadecimal characters.
Listing 5.10 is an example demonstrating the various base
types.
Developer Facing
let vec1=vec![1,2,3];
println!("{:?}", vec1);
#[derive(Debug)]
fn main() {
let earth = Planet {
name: "Earth",
radius: 3958.8,
};
println!("{:#?}", earth);
Here is the result:
Planet {
name: "Earth",
radius: 3958.8222,
}
Write Macro
3958.8, "miles")
.unwrap();
Display Trait
struct Exponent {
base: i32,
pow: u32,
}
Debug Trait
struct Rectangle {
top_left: (isize, isize),
bottom_right: (isize, isize),
}
use std::fmt;
impl fmt::Debug for Rectangle {
fn fmt(&self, formatter: &mut fmt::Format
->
write!(formatter, "({} {}) ({} {})",
self.top_left.0, self.top_left.1,
self.bottom_right.0, self.bottom_
}
}}
Format Macro
io::stdin().read_line(&mut name).unwrap_o
if name != "" {
print!("Hello, {}!", name.trim_end())
} else {
print!("No name entered.");
}
}
io::stdout().write_all(b"Hello, world!")
Summary
The console can be an effective tool for communicating with
users for a variety of reasons. The simplicity and
straightforwardness of the console are sometimes preferred to
a traditional graphical user interface.
You have learned how to read and write to the console using
various instructions. This includes macros that write to the
standard output. The print and println macros are the
most common. These macros display formatted strings to the
console. The formatted string is derived from the format string,
placeholders, and substitution values, which are input
parameters.
OceanofPDF.com
Chapter 6. Control Flow
if
loop
while
for
match
Of course, Rust applies its own viewpoint to these familiar
constructs. Most notably, these are expressions, not statements,
as in many other languages. Like other Rust features, this can
have a significant impact on the look and feel of Rust code. In
addition, certain programming artifacts are not included for
safeness, such as the ternary operator or “goto” statement.
The if Expression
if condition {
// true block
}
You can combine the if with an else block. This provides
both a true and false block for the if . The if block is
executed when the conditional expressions evaluates as
true. Otherwise, the else block is executed.
if condition {
// true block
} else {
// false block
}
let city="Honolulu";
let is_new_york="New York City"==city;
if is_new_york{
println!("Welcome to NYC");
} else {
println!("Not NYC");
}
We declare city as a string literal that is compared to "New
York City" . The result is used as the conditional expression
for the if. If true, the Welcome to New York greeting is
displayed.
if boolean condition {
// true block
} else if condition {
// nested true block
} else {
// false block
}
enum Font {
Name(String),
Default,
}
let font=Font::Name("Arial".to_string());
if let Font::Name(chosen) = font {
println!("{} font selected", chosen);
} else {
println!("Use default font");
}
This example creates a Font enum. The enum has two variants:
the Name, for a specific, and Default. Afterwards, we create a
font for Arial. The if let will match the Font::Name pattern.
If the pattern is found, the underlying font is assigned to
chosen, which is then displayed.
let flag=true;
println!("{}", value);
The flag is hardcoded as true. For that reason, 1 is assigned to
value , which is then displayed.
while condition {
// while block
}
The while expression in Listing 6.5 calculates a factorial
result.
The while let iterates the while block while the pattern
matches Some(value) . The value is decremented within each
iteration and multiplied against the previous product, result .
When the underlying value becomes 1, the pattern is set to
None, which ends the iteration. At that time, result will have the
final factorial value.
fn main() {
let r=Range {start: 0, end: 3};
for value in r {
println!("{}", value);
}
}
Here is the result of the application, where the first field of the
tuple is the index and the second field is the value:
(0, 1)
(1, 2)
(2, 3)
(3, 4)
(4, 5)
let values=vec![1,2,3];
for item in values {
println!("{}", item);
}
let values=vec![1,2,3];
for item in values.iter().enumerate() {
println!("{:?}", item);
}
The version in Listing 6.16 also does not work. Since the default
iterator yields T , the string values are moved with the for
in . The reason is that strings have move semantics. This causes
the final println! macro, after the for block, to generate a
compiler error from the borrow checker.
This can also be fixed with the correct iterator for the
circumstances, not the default iterator. Call the iter function
instead to obtain an iterator that returns &T, a reference. For
our latest example, this means the strings are borrowed, not
moved, within the for block. This allows the strings to be used
later after the for block, as shown in Listing 6.17.
println!("{}", values[1]);
loop {
match msgid {
APPLICATION_EXIT=>return,
&mut std::io::stderr(),
"Invalid message").unwrap(),
}
};
Until now, break was used to interrupt any loop, while , for ,
or loop . However, the break can also return the loop result,
but only for loop . This feature is not available for either
while or for . The default return type of the break
expression is the ! type, the never type. The never type means
no value.
Code Listing 6.19. A loop that returns the first even number
let values=[1,5,6,4,5];
let mut iterator=values.iter();
let mut value;
let even=loop {
value=iterator.next().unwrap();
if value%2 == 0 {
break value;
}
};
'label:loop
'label:while
'label:for
break 'label;
continue 'label;
// state machine
struct Triangular(u32);
self.0+=1;
Some((self.0*(self.0+1))/2)
}
fn main() {
let tri=Triangular(0);
println!("{}", count);
if expression
for expression
while expression
loop expression
OceanofPDF.com
Chapter 7. Collections
Arrays
array_name[type; length]
Array literals are also described with square brackets. There
are two notations. You can simply list the values, of the same
type, within the square brackets, as shown here:
[value; repeat]
println!("{:?}", array_1); // 1, 2, 3, 4 - d
println!("{:?}", array_2); // 1, 2, 3, 4
println!("{:?}", array_3); // 10, 10, 10, 10,
In the example, we declare and initialize three arrays.
Multidimensional Arrays
let array_1=[[1,2,3],[4,5,6]];
println!("{}", array_1.len()); // 2
[["Alice","Sarah","Adam"],["Jeff","Jason"
["Cal","Sal","Edith"],["Alice","Ted",
let users=users[1];
let users=
[["bob".to_string(), "alice".to_string(),
["sarah".to_string(), "fred".to_string()
let user=&users[1][0]; // sarah
println!("{}", user);
Slices
arrayname[starting_index..ending_index]
let array_1=[2,4,6,8];
println!("{:?}", &array_1[1..=3]); // [4, 6
Within the slice notation, the starting and ending indexes can
be omitted. When they are omitted, the default is the extents of
the array. Here are the possibilities:
let array_1=[0,1,2,3,4,5];
println!("{:?}", &array_1[..]); // [0, 1,
println!("{:?}", &array_1[2..]); // [2, 3,
println!("{:?}", &array_1[..3]); // [0, 1,
println!("{:?}", &array_1[..=4]); // [0, 1,
println!("{:?}", &array_1[4..]); // [4, 5]
Comparing Arrays
let array_1=[1,2,3,4]; // a
let array_2=[1,2,3,4];
let array_3=[1,2,5,4];
let array_4=[1,2,3];
Iteration
The previous example listed the values of the array. You can
also iterate both the value and associated index. The iter
function returns an iterator for the function. The enumerate
function for the iterator then returns the current index and
value, as a tuple.
let array_1=[1,2,3,4];
let e=array_1.iter();
for element in e.enumerate() {
println!("{:?}", element); // (0, 1) (1,
}
Coercion
It is sometimes necessary to convert an array to a slice or to
convert a slice to an array. Fortunately, converting from an
array to a slice happens implicitly in Rust. However, converting
from a slice to an array is not implicit. You can call the
try_into function on a slice to create an array. The array and
slice must be the same length. The try_into function returns
Result<T, E> . If successful, the function returns the array
within the Ok<T> variant. Otherwise, an error is returned, as
Err(E) .
let slice_1=&[1,2,3,4][1..3];
let array_1:[i32; 2]=slice_1.try_into().unwrap
Vectors
Vectors are dynamic arrays. In Rust, Vec is the vector type and
found in the standard prelude. Vectors are an excellent
counterpoint to arrays. Often, a collection may need to grow
and even shrink. This is allowed with vectors, but not arrays.
1. Capacity is increased.
You can also initialize binding for a vector with the vec!
macro, as shown in Listing 7.16. You can use either array
notation or a repeat expression.
Code Listing 7.16. A vec! macro example
let vec_1=vec![1,2,3,4];
println!("{:?}", vec_1); // 1,2,3,4
Code Listing 7.17. For a vector, the length and capacity are
functions.
let vec_1=vec![1,2,3,4];
let length=vec_1.len(); // 4
let capacity=vec_1.capacity(); // 4
println!("Length {} Capacity {}", length, cap
Multidimensional
This example declares a vector with two rows and four columns
and a second vector with three rows and two columns.
Access
let vec_1=vec![1,2,3];
let vec_2=vec![[1,2,3],[4,5,6]];
let var_1=&vec_1[2]; // 3
let var_2=&vec_2[1][1]; // 5
As a slice, you can return multiple values from a vector.
Remember, slices are partial arrays, not a vector. The to_vec
function is handy for converting a slice to a vector, as shown in
Listing 7.20.
let vec_1=vec![1,2,3]; // a
let vec_2=vec![[1,2,3],[4,5,6]]; // b
let slice_1=&vec_1[0..2]; // c
let slice_2=&vec_2[0][..]; // d
let vec_3=&vec_1[..=1].to_vec(); // e
let vec_1=vec![1,2,3];
let var_2=vec_1[3]; // panic occurs
let vec_1=vec![1,2,3];
if let Some(var_1)=vec_1.get(3) {
println!("{}", var_1);
} else {
println!("Not found");
}
Iteration
Resizing
You can add or remove values from a mutable vec. The push
and pop functions treat the vector as a stack. The push
function adds an element to the end of vec, with no return
value. The pop function removes the last element and returns
Option<T> . If successful, the pop function returns the
removed value as Some<T> . Otherwise, None is returned.
Code Listing 7.24. Resizing a vector with the push and pop
functions
vec_1.push(5);
vec_1.push(10);
vec_1.push(15);
vec_1.pop();
Capacity
// Length 3 Capacity 3
println!("Length {} Capacity {}",
vec_1.len(), vec_1.capacity());
vec_1.push(4);
// Length 4 Capacity 6
println!("Length {} Capacity {}",
vec_1.len(), vec_1.capacity());
// Length 3 Capacity 4
vec_1.push(4);
// Length 4 Capacity 4
println!("Length {} Capacity {}",
vec_1.len(), vec_1.capacity());
HashMap
Creating a HashMap
map_2.remove(&"English".to_string());
let famous_numbers=HashMap::from([
("Archimedes' Constant", 3.1415),
("Euler's Number", 2.7182),
("The Golden Ratio", 1.6180),
let result=map_1.get(&"Spanish");
match result {
Some(value)=> println!("Found {}", va
None=>println!("Not found")
}
Updating an Entry
match result {
Some(previous)=>println!("Previous: {}",
None=>println!("New entry")
}
Iteration
Code Listing 7.35. Iterating a hash map to display the keys and
values
let map_1=HashMap::from([('a',2), ('b',3)]);
Summary
OceanofPDF.com
Chapter 8. Ownership
Figure 8.2 Showing the result of a deep copy when pointers are
included
For deep copies, the additional logic for safe pointers makes the
deep copy more expensive and less transparent. Regardless,
deep copies are sometimes required for appropriate memory
management, especially when you’re implementing the
“resource acquisition is initialization” (RAII) pattern.
Car Analogy
Move Semantics
Rust supports both move and copy semantics, where the default
is move semantics. With move semantics, ownership of an
object value is transferred during an assignment. This is the
behavior regardless of the type of assignment: declaration,
function argument, or function return. After the assignment,
the assigning object no longer has access to the moved valued.
fn buy_car(new_owner: String) {
println!("{}", new_owner);
}
fn main() {
let owner = String::from("automobile");
buy_car(owner);
println!("{}", owner); // compile error
}
fn main() {
let mut owner = String::from("automobile");
owner = buy_car(owner); // reestablish owners
println!("{}", owner);
}
Borrow
fn borrow_car(borrower: &String) {
println!("{}", borrower);
}
fn main() {
let owner = String::from("automobile");
borrow_car(&owner);
println!("{}", owner); // works
}
Copy Semantics
Types with the Copy trait have copy semantics. When copied, a
bitwise copy is performed. Types that reference external
resources or memory, such as Strings, do not support copy
semantics.
In Rust, all scalar types have the copy trait. In Listing 8.6, both
width and height are integers, which is a scalar type. During
the assignment, the width value is copied to height . The
result it that width and height own separate values.
Clone trait
The String type, for example, implements the Clone trait. Why?
Strings include a pointer to the underlying string, which is on
the heap. For this reason, the String type does not support copy
semantics. If allowed, this would create problematic
dependencies. Instead, the String type implements a deep copy
using the Clone trait. Here are the general steps for cloning
Strings:
1. For the target String, allocate memory on the heap for the
String buffer.
2. Copy the buffer from the originating String into the new
buffer.
The result of this are Strings with the same value, stringa
and stringb , at different locations in memory (see Figure 8.4).
Copy Trait
You can assign the Copy trait to types and that support a
shallow copy.
fn main() {
let t1=Transaction{debit: true, amount: 32.12
let t2=t1; // moved
}
Code Listing 8.9. Applying the Copy trait using the derive
attribute
#[derive(Copy, Clone)]
struct Transaction {
debit: bool,
amount: f32,
}
struct Transaction {
description: String,
debit: bool,
amount: f32,
}
#[derive(Copy, Clone)]
struct Transaction<'a> {
description: &'a String,
debit: bool,
amount: f32,
}
. . .
let t2 = t1; // copied
You can implement the Copy trait manually, not using the #
[derive] attribute. As a marker trait, there is no
implementation for the Copy trait. The existence of the Copy
trait instructs Rust to perform a bitwise copy. However, the
Clone trait must be implemented as the supertrait. This means
implementing the clone function. The function should return
the current object by value, which is consistent with a bitwise
copy. See Listing 8.11 for the implementation of the Copy trait
for Transaction .
struct Transaction {
debit: bool,
amount: f32,
}
Clone Trait
The Clone trait implements a deep copy for types that do not
support a bitwise copy.
For the Clone trait, you must implement clone function, which
returns the cloned value. The implementation must also
remove any dependencies caused by pointers.
For struct, assuming all the fields are cloneable, you can simply
add the #[derive] attribute for the Clone trait. This attribute
calls the clone method on each field.
#[derive(Clone)]
struct Transaction {
description: String,
debit: bool,
amount: f32,
}
...
let t1 = Transaction {
description: String::from("ATM"),
debit: true,
amount: 32.12,
};
struct Transaction {
description: String,
debit: bool,
amount: f32,
}
Summary
You implement the Clone trait for types that require a deep
copy. The actual implementation of the trait depends on the
type.
OceanofPDF.com
Chapter 9. Lifetimes
fn main() {
let ref1;
{ // --------------------- inner block (
let num1=1;
ref1=&num1;
} // --------------------- inner block (
println!("{}", ref1); // dangling refere
}
Introduction to Lifetimes
5 | ref1=&num1;
| ^^^^^ borrowed value does not li
6 | }
| - `num1` dropped here while still borrowe
This version of the example does not compile. Why? The output
lifetime for the function return is lifetime 'b . This is the
lifetime for num1 . However, the target lifetime for result is
lifetime 'a . Regrettably, the target lifetime outlives the output
lifetime (i.e., result outlives num1 ). When this occurs, there
is a potential for a dangling reference. The borrow checker will
highlight the problem in an error message.
Lifetime Annotation
Lifetime Elision
fn do_something(ref1:&i32)->&i32{
ref1
}
Complex Lifetimes
In this example, the output lifetime does not live long enough.
This means num2 will be dropped from memory while
result is borrowing the value. This creates a dangling
reference. This is another example where the source code will
not compile.
Sharing a Lifetime
Table 9.5. Another example of the output lifetime not living long
enough
Static Lifetimes
Code Listing 9.13. The impl block for a struct with lifetimes
The struct in the Listing 9.15 has two methods. Both use free
lifetimes.
struct Data<'a>{
field1:&'a i32,
}
impl<'a> Data<'a> {
fn do_something1<'b>(self: &Self,
ref1:&'b i32)->&'b i32 {
ref1
}
}
}
Free lifetimes, even the same lifetime, are not shared across
methods. Lifetime 'b in the previous example was not shared
across both methods. However, lifetimes declared in a struct are
shareable across the methods of the struct.
Subtyping Lifetimes
’subtype:’basetype
Fn either_one<'a:'b>(ref1:&'a i32,
ref2:&'b i32, flag:bool)->&'b i32
if flag {
ref1
} else {
ref2
}
}
fn main() {
let num1=1;
{
let num2=2;
let result=either_one(&num1, &num2, t
}
}
Anonymous Lifetimes
struct Data<'a>{
field1:&'a i32,
}
impl<'a> Data<'a> {
fn do_something<'b>(&self, ref1:&'b i32)-
ref1
}
}
impl Data<'_> {
fn do_something<'b>(&self, ref1:&'b i32)-
ref1
}
}
Summary
The lifetimes feature is a difficult subject that cannot be
ignored. It is a component of the ownership system that ensures
memory safeness. Specifically, lifetimes remove the possibility
of dangling references, which are the leading cause of
vulnerable memory with non-Rust programming languages.
You have learned that when the output lifetime is less than the
target lifetime, a dangling reference can occur. The lifetime
table is a helpful tool in understanding the relationship
between lifetimes and uncover poor behavior before the
borrow checker.
Lifetime elision elides the lifetimes, which makes the syntax
more concise and readable. In some circumstances, when this is
not available, anonymous lifetimes can help.
OceanofPDF.com
Chapter 10. References
Assume there are two local variables on the stack. The variable
val_a is 10, and ref_a is a reference to val_a . The size of
val_a is 4 bytes, while ref_a is 8 bytes, assuming a 64-bit
architecture. Figure 10.1 provides a view of the stack in
memory.
Declaration
let ref_a:&i32=&10;
let ref_a=&10;
let val_a=10;
let ref_a: &i32=&val_a;
let ref_b=ref_a;
The lifetime of the owner must outlive the borrower (i.e., the
reference). When the owner no longer exists, the referenced
value is no longer available, as discussed in the previous
chapter. The borrower cannot continue to use the value.
ref_a=&val_a;
^^^^^^ borrowed value does not live long en
Dereferencing
let ref_a=&10;
let ref_b=&20;
let result=*ref_a + *ref_b;
let ref_a=&10;
let ref_b=&20;
let result=ref_a + ref_a;
let ref_a=&10;
println!("{}", ref_a); // displays 10
Code Listing 10.8. Display the reference and not the value.
let ref_a=&10;
println!("{:p}", ref_a); // displays {memory
Comparing References
You may want to compare reference types with a comparison
operator, such as the == or != operator. Reference types, &T and
&mut T , implement the PartialOrd trait for that reason. The
implementation performs a comparison based on value, not
identity.
let num_of_eggs=10;
let num_of_pizza=10;
let eggs=&num_of_eggs;
let pizzas=&num_of_pizza;
use std::ptr;
fn main() {
let num_of_eggs=10;
let num_of_pizza=10;
let eggs=&num_of_eggs;
let pizza=&num_of_pizza;
Reference Notation
Reference to Reference
let val_a=10;
let ref_a=&val_a;
let ref_ref_a=&ref_a;
In this code, we first declare a reference, ref_a . We then
create a reference to ref_a called ref_ref_a . Figure 10.2
diagrams the relationship of the references in the previous
example.
print!("&&&i32", ref_a);
print!(" &&i32", *ref_a);
print!(" &i32", **ref_a);
Mutability
Reference itself
Referenced value
Conversely, you can make the value mutable but not the
reference. That means the referenced value could change but
not the reference.
Let’s start with a basic example and then add more, and more,
complexity.
The code in Listing 10.16 will not compile because of the two
mutable references to the same referenced value, val_a .
*ref_a=20;
Summary
Rust has two types of pointers: safe pointers and raw pointers.
References represent safe pointers in Rust and adhere to the
rules of ownership and lifetimes for safeness.
OceanofPDF.com
Chapter 11. Functions
Function Definition
fn say_hello() {
println!("Hello, world!");
}
Parameters
fn get_cubed(number:i32) {
println!("{}", number*number*number);
}
fn main() {
let value=5;
get_cubed(value);
}
fn main() {
let (num1, num2)=(5,10);
swap_values(num1, num2);
}
When passed by reference, you can dereference ( * ) the function
parameter to access the referenced value. If it is a mutable
reference, you can also change the referenced value. As a
reference to the argument, the original argument changes also.
fn main() {
let (mut num1, mut num2)=(5,10);
swap_values(&mut num1, &mut num2);
println!("{} {}", num1, num2);
}
Function Return
fn get_cubed(value:i32)->i32 {
value*value*value
}
fn main() {
let num=5;
let result=get_cubed(num);
println!("{}", result);
}
fn is_odd(num:i32)->bool {
if (num%2) == 0 {
return false;
} else {
return true;
}
}
fn main() {
println!("{}", is_odd(5));
}
fn is_odd(num:i32)->bool {
if (num%2) == 0 {
true // return value
} else {
false // return value
}
}
fn get_min_max(data:Vec<i32>)->(i32, i32){
let mut min=data[0];
let mut max=data[0];
for item in data {
if item < min {
min=item;
}
if item > max {
max=item;
}
}
return (min, max);
}
fn main() {
let values=vec![5,6,-6,10,6];
let (min, max)=get_min_max(values);
println!("{} {}", min, max);
}
fn lift_value()->&i32 {
let value=5;
&value
}
Const Functions
const fn get_eulers_number()->f64{
2.7182
}
fn main() {
const EULER:f64=get_eulers_number();
}
Nested Functions
Code Listing 11.14. The start and driving functions are nested
functions.
enum Command{
Start,
Drive(i8)
}
fn operate_vehicle(command:Command) {
match command {
Command::Start=>start(),
Command::Drive(speed)=>driving(speed)
}
fn main() {
operate_vehicle(Command::Start);
operate_vehicle(Command::Drive(10));
}
Function Pointers
RefUnwindSafe
PartialEq Eq PartialEq
Debug
fn func_one(p1:i32){}
fn func_two(p1:String, p2:f64)->f64{0.00}
fn func_three(){}
fn func_four(p1:String, p2:f64)->f64{0.00}
fn func_five(p1:(i8, i8, i8)){}
fn main(){
let fptr1:fn(i32)=func_one;
let fptr2:fn(String, f64)->f64=func_two;
let fptr3:fn()=func_three;
let fptr4=func_four;
enum Pace {
Walking,
Running,
}
fn set_pace(pace:Pace)->fn(i32) {
fn walking(distance:i32) {
println!("Walking {} step(s)", distance)
}
fn running(distance:i32) {
println!("Running {} step(s)", distance)
}
match pace {
Pace::Walking=>walking,
Pace::Running=>running
}
}
fn main() {
let move_forward=set_pace(Pace::Running);
move_forward(40); // calling function through
}
The Pace enum has a walk and run variant. The set_pace
function includes two nested functions: walking and
running . Based on the Pace enum, the set_pace function
returns a function pointer to either the walking or running
nested function. In main , the move_forward variable is
initialized with the function pointer returned from the
set_pace function. Lastly, whatever function that is now
associated with move_forward is called.
Function Aliases
Summary
OceanofPDF.com
Chapter 12. Error Handling
Philosophical differences.
Rust does not have exceptions! This is part of the overall shift
away from reactive error handling. Panics are a replacement
for exceptions. There are similarities, such as both being stack
based, but there are also significant differences. With panics,
you can attempt to emulate exception handling, but that is not
idiomatic Rust. Instead, embrace the new paradigm that Rust
presents to you.
In this example, you can both predict and recover from this
problem—a divide-by-zero panic. What if the problem was not
recoverable? The best practice is to avoid a panic and report the
problem to the caller within a function result. This is the role of
either the Result or Option type.
Code Listing 12.2. The Result type used for error handling
fn winning_ratio(team: &String)->Result<f32,
if let Some(&(wins,losses))=TEAMS.get(tea
if wins== 0 {
Ok(wins as f32)
} else {
Ok(((wins+losses) as f32)/ wins a
}
} else {
return Err(&"Invalid team");
}
}
None
}
}
Panics
Panics are thread specific. When a panic occurs, the stack for
the current thread is unwound. This continues until the panic is
handled or the stack is exhausted. If the stack is exhausted
(unwound), the current thread is terminated. However, if this is
the primary thread, the application is terminated. The process
of unwinding the stack provides an opportunity for an orderly
cleanup of memory and resources before the thread exits.
The example shown in Listing 12.4 has source code that causes
a divide-by-zero panic in the division function. Remember,
Rust does not have exceptions—only panics!
fn logic() {
division(1,0);
}
fn main() {
logic();
}
2: core::panicking::panic
at /rustc/897e37553bba8b42751c676589
core\src\panicking.rs:48
3: divide_by_zero::division
at .\src\main.rs:2
4: divide_by_zero::main
at .\src\main.rs:6
5: core::ops::function::
FnOnce::call_once<void (*)(),tuple$<
at /rustc/897e37553bba8b42751c676589
library\core\src\ops\function.rs:248
note: Some details are omitted, run with `RUST_BA
` for a verbose backtrace.
error: process didn't exit successfully:
`target\debug\divide_by_zero.exe` (exit code
When there is a panic, the stack unwind is an opportunity for
an application to perform an orderly exit, most notably
releasing resources and memory. Some cleanup is automatic,
such as removing local variables. However, references to the
heap or external resources may require special handling.
During the unwind, the drop function is automatically called
on droppable objects, which implement the Drop trait. For
many, the drop function is equivalent to a destructor.
struct Tester {
fname:String,
}
fn division(numerator:isize, divisor:isize)->
let temp=Tester{fname:"division".to_strin
numerator/divisor
}
fn logic() {
let temp=Tester{fname:"logic".to_string()
division(1,0);
}
fn main() {
let temp=Tester{fname:"main".to_string()}
logic();
}
division unwound
logic unwound
main unwound
[profile.dev]
panic = 'abort'
Importantly, this is an external configuration that anyone can
set. This potentially could be done outside of your control. In
addition to backtraces, this another reason why panics are
unpredictable and should be avoided, if possible.
Panic! Macro
Here is the panic message from this short program. Notice your
description within the message:
fn main() {
let num1=f64::INFINITY;
Handling Panics
fn get_data(request:usize)->i8{
let vec:Vec<i8>=(0..100).collect();
vec[request]
}
fn ask_user(){
// get from user
let data:usize=105;
get_data(data);
}
fn main() {
ask_user();
}
use std::panic;
use std::any::Any;
fn get_data(request:usize)->Result<i8, Box<dy
let vec:Vec<i8>=(0..100).collect();
let result = panic::catch_unwind(|| {
vec[request]
});
result
}
fn ask_user(){
fn main() {
ask_user();
}
panic::set_hook(Box::new(|_info| {
// do nothing
}));
Unwrapping
fn main() {
let items=vec![1,2,3,4];
let value=items.get(5);
let value=value.unwrap();
}
fn main() {
let items=vec![1,2,3,4];
let value=items.get(5);
let value=value.expect("out of range");
}
From this example, the panic will result in the following error
message. The expect function sets the message from the panic
to “out of range.”
fn main() {
let items=vec![1,2,3,4];
let value=items.get(5);
let value=value.unwrap_or(&1);
println!("{}", value);
}
fn main() {
let items=vec![1,2,3,4,];
let value=items.get(5);
let value=value.unwrap_or_else(||&1);
println!("{}", value);
}
fn main(){
funca();
}
Code Listing 12.18. Standard error handling for the Option type
use std::collections::HashMap;
fn into_celsius(cities:&HashMap<&str, f64>,
city:&str)->Option<f64>{
let result=cities.get(&city);
match result {
Some(temp)=>Some((*temp-32.0)*0.5666)
None=>None
}
}
fn main() {
let cities = HashMap::from([
("Seattle", 72.0),
("San Francisco", 69.0),
("New York", 76.0),
]);
Map
For the Option type, here is the definition of the map function:
For the Result type, the map function behaves similarly, except
the transformation is from Result<T, E> to Result<U, E> .
Once again, we are transforming T to U except for Ok variant.
If an Err result is found, the map function will propagate the
error.
fn into_celsius(cities:&HashMap<&str, f64>,
city:&str)->Option<f64>{
cities.get(&city).map(|temp|(((*temp as f
}
fn f_to_c(f:i8)->Option<f64> {
let f=f as f64;
Some(((f-31.5)*0.56660))
}
fn into_celsius(cities:&HashMap<&str, i8>, ci
cities.get(&city).map(|temp|f_to_c(*temp)
}
This does not compile! What is the problem? Both map and the
f_to_c function return Option<i8> . This means the map
function is returning Option<Option<f64>> . However,
into_celsius is expecting Option<f64> . This is the reason
for the compiler error.
fn into_celsius(cities:&HashMap<&str, i8>, ci
cities.get(&city).and_then(|temp|f_to_c(*
}
use std::collections::HashMap;
fn f_to_c(f:i8)->Option<f64> {
let f=f as f64;
Some(((f-31.5)*0.56660))
}
fn into_celsius(cities:&HashMap<&str, i8>, ci
->Result<f64, String>{
cities.get(&city).and_then(|temp|f_to_c(*
ok_or("Conversion error".to_string())
}
fn main() {
let cities = HashMap::from([
("Seattle", 81),
("San Francisco", 62),
("New York", 84),
]);
Not all error objects are the same. Some possess rich
information that can provide important additional detail. At a
minimum, error objects implement the Error and Display traits.
use std::fs::File;
use std::io;
use std::io::prelude::*;
fn create_file()->Result<String, io::Error>{
let file=File::create(r#"z1:\doesnotexist
// read data
Ok("Data".to_string())
}
fn main() {
let result=create_file();
let error=result.unwrap_err();
match error.kind() {
NotFound=>println!("not found"),
_=>println!("something else")
}
}
Custom Errors
You can also create custom errors that include rich information.
Adding rich information helps an application respond correctly
to an error.
use std::error::Error;
use std::fmt;
divisor: i8
}
fn is_divisible(dividend:i8, divisor:i8)->Res
{
if divisor==0 {
let error=DivisibleError{dividend:div
return Err(error);
}
if (dividend%divisor)==0 {
Ok(true)
} else {
Ok(false)
}
}
fn main() {
let err=is_divisible(5,0).unwrap_err();
println!("{}", err);
}
fn is_divisible(numerator:i8, divisor:i8)->Re
DivisibleError> {
if divisor==0 {
let error=DivisibleError{numerator:nu
divisor:divisor};
return Err(error);
}
if (numerator%divisor)==0 {
Ok(true)
} else {
Ok(false)
}
}
fn main() {
let err=is_divisible(5,0).unwrap_err();
println!("{}", err);
}
Summary
OceanofPDF.com
Chapter 13. Structs
Structs and tuples are close relatives. Both are customer types
with heterogeneous fields. Structs, however, are named types,
while tuples are unnamed. In addition, structs also have named
fields, while fields within tuples are numbered. These
differences mean structs are generally are easier to use, more
reusable, and more flexible than tuples.
struct Structname {
field1:type,
field2:type,
fieldn:type,
}
struct RGB {
red:u8,
green:u8,
blue:u8,
}
#[derive(Debug)]
struct RGB {
red:u8,
green:u8,
blue:u8,
}
Source file
Source line
Variable name
Value
There are gradations of gray from light gray to dark gray. When
the three elements of RBG are the same, the resulting color is
some degree of gray. Black is the darkest gray, where red, green,
and blue are zero. The is_gray function returns true if the
colors provided represent gray; that is, the values of the three
RBG fields are equal. In Listing 13.3, the is_gray function is
called. Using dot notation, the fields of an RGB instance are
provided as function parameters.
Alternate Initialization
You can even combine both longhand and shorthand syntax for
separate fields, as shown here:
struct CMYK {
cyan:u8,
magenta:u8,
yellow:u8,
key:u8
}
let school_bus_yellow=CMYK{
key:0,
cyan:0,
magenta:15,
yellow:100 };
let other_color=CMYK{yellow:school_bus_yellow
cyan:school_bus_yellow.cyan,
magneta:school_bus_yellow.magenta
key:100 };
Move Semantics
light_gray.red+=125;
light_gray.green+=125;
light_gray.blue+=125;
println!("{:?}", dark_gray);
Here is the error message from the previous example:
You can apply the Copy trait to a struct only if every field
already supports the trait. If this criterion is met, you can add
the Copy attribute to the struct for copy semantics.
The RBG fields are u8, which has the Copy trait. Therefore, copy
semantics can be added to the struct, as shown in Listing 13.8.
#[derive(Debug)]
#[derive(Copy, Clone)]
struct RGB {
red:u8,
green:u8,
blue:u8,
}
With the Copy trait added, the preceding code listing would
compile successfully. The dark_gray variable will not lose
ownership. This allows the subsequent println! macro to
execute correctly.
Mutability
Methods
#[derive(Debug)]
struct RGB {
red:u8,
green:u8,
blue:u8,
}
impl RGB {
fn is_gray(self: &Self)->bool{
(self.red==self.blue)&&(self.blue==self.g
}
}
You call the method using the dot notation, the same as other
struct members. When calling a method, the Self parameter
is provided implicitly. It refers to some flavor of the current
instance (the left side of the dot syntax). In Listing 13.11, the
is_gray method is called on an RGB instance. Notice that the
self parameter is not provided.
Code Listing 13.12. The is_gray function uses short syntax for
self:&Self .
#[derive(Debug)]
struct RGB {
red:u8,
green:u8,
blue:u8,
}
impl RGB {
fn is_gray(&self)->bool{
(self.red==self.blue)&&(self.blue==self.g
}
}
enum Color {
Red,
Blue,
Green,
}
impl RGB {
fn is_pure_color(&self, color:Color)->bool {
match color {
Color::Red=>(self.red==255)&&
((self.blue+self.green)==0),
Color::Blue=>(self.blue==255)&&
((self.red+self.green)==0),
Color::Green=>(self.green==255)&&
((self.blue+self.red)==0)
}
}
}
let result=pure_blue.is_pure_color(Color::Blu
&Self − &T
Self − T
impl RGB {
fn invert(&mut self) {
self.red=255-self.red;
self.green=255-self.green;
self.blue=255-self.blue;
}
}
fn main() {
let mut color=RGB{red:150, green:50, blue:75}
color.invert();
println!("{:?}", color);
}
Associated Functions
The new function for the Factory pattern is a common use case
for associated functions. A factory function abstracts the
creation of an instance. Listing 13.15 is an example of a factory
function, also known as a constructor function.
struct Large {
// data
}
impl Large {
fn new()->Box<Larssssge> {
Box::new(Large{})
}
fn task1(&self) {
// do something
}
fn task2(&self) {
// do something
}
}
fn main() {
let instance=Large::new();
instance.task1();
}
Impl Blocks
In Listing 13.16, there are multiple impl blocks for the RGB
struct. The getters and setter methods are grouped in separate
impl blocks. Separating them in this manner makes the code
more readable and provides additional context beyond just the
struct.
#[derive(Debug)]
struct RGB {
red:u8,
green:u8,
blue:u8,
}
// Getters
impl RGB {
const fn get_red(&self)->u8{self.red}
const fn get_green(&self)->u8{self.green}
const fn get_blue(&self)->u8{self.blue}
}
// Setters
impl RGB {
fn set_red(&mut self, value:u8){self.red=valu
fn set_green(&mut self, value:u8){self.green=
fn set_blue(&mut self, value:u8){self.blue=va
}
Operator Overloading
use std::ops;
#[derive(Debug)]
struct RGB {
red:u8,
green:u8,
blue:u8,
}
fn neg(self) ->Self {
RGB{red: 255-self.red, blue:255-self.blue
green:255-self.green}
}
}
fn main() {
let color1=RGB{red:200, green: 75, blue:125,
let color2=-color1; // use the overloa
println!("{:?}", color2); // RGB { red: 55,
// green
}
In the impl block, Output sets the return type of the operation
to RGB. Next, the neg function is implemented to invert the
RGB color. This behavior is consistent with the meaning of the
unary negation operator ( - ). In main , the overloaded unary
negation operator ( - ) negates the value of color1 . The result
is placed in color2 and displayed.
use std::ops;
#[derive(Debug)]
#[derive(Copy, Clone)]
struct RGB {
red:u8,
green:u8,
blue:u8,
}
The type parameter of the Add trait defaults to Self (i.e., RGB).
In addition, Output sets the return type to RBG. The
implementation of the add method performs addition of self
and rhs (i.e., adding their fields together). The result is
returned as a new RGB instance.
fn main() {
let color1=RGB{red:200, green: 75, blue:125,
let color2=RGB{red:50, green: 75, blue:25, };
let color3=color1+color2;
println!("{:?}", color3);
}
use std::ops;
#[derive(Debug)]
#[derive(Copy, Clone)]
struct RGB {
red:u8,
green:u8,
blue:u8,
}
fn main() {
let color1=RGB{red:200, green: 75, blue:125,
let color2=color1+(10, 25, 15);
println!("{:?}", color2);
}
The type parameter for the Add trait sets the right-hand side
operator to (u8, u8, u8) , which is a tuple. Within the impl
block, Output establishes the return type as RGB. The
implementation of the add method adds the fields of the tuple
to the current RGB instance, self . The result is returned as a
new RBG.
Note
[profile.dev]
overflow-checks = false
Tuple Struct
fn main(){
Unit-Like Struct
A struct with no fields is considered “unit-like.” Unit-like structs
occupy no memory space and are considered a zero-sized type
(ZST). This is one of the exotic types in Rust. An empty tuple,
() , is called the unit type. Because of similarities with the
empty tuple, both are ZST, structs with no fields are considered
unit-like.
struct Something;
Summary
You can use structs to create custom types. Structs combine
both state and behavior in a single entity. They are convenient
for modeling entities in the real world problem domain.
You define a struct with the struct keyword, name, and then
fields, within curly braces. For instances, access the fields of a
struct using dot notation.
OceanofPDF.com
Chapter 14. Generics
Generic Functions
Generic functions are templates for creating concrete function,
using type parameters. You declare type parameters within
angle brackets, after the function name. By convention, the first
type parameter is T . Subsequent generic type parameters, if
any, are named U , V , and so on. However, you are not
restricted to this naming convention. Generic type parameters
have the same naming convention as variables, which is
UpperCamelCasing. You can utilize type parameters within the
function definition and body.
fn functionname<T>(param:T)->T {
let variable :T ;
}
fn swap_string(tuple:(String, String))->(Stri
(tuple.1, tuple.0)
}
fn main() {
let tuple1=(10, 20);
let tuple2=("ten".to_string(), "twenty".t
let tuple3=(10.0, 20.0);
let result=swap(tuple1);
let result=swap_string(tuple2);
let result=swap_float(tuple3);
}
fn swap<T>(tuple:(T, T))->(T,T){
(tuple.1, tuple.0)
}
Code Listing 14.5. The same swap function called with various
types
fn main() {
let tuple1=(10, 20);
let tuple2=("ten".to_string(), "twenty".t
let tuple3=(10.0, 20.0);
You can have multiple type parameters. The next version of the
swap method has two type parameters: T and U . In this
version, the fields of the tuple can be different types, which is
demonstrated in Listing 14.6.
Code Listing 14.6. The swap function with type parameter T
and U
fn main() {
let tuple1=(10, "ten");
let result=swap(tuple1);
println!("{:?}", result);
}
|
8 | let result: _=do_something();
| +++
function_name::<type,…>(arg1, …)
fn main() {
let result=do_something::<i8>();
println!("{}", result);
}
fn main() {
let mut vec1=Vec::with_capacity(2);
vec1.push(1);
vec1.push(2);
let result=vec_push_within(&mut vec1, 3);
match result {
Ok(_)=>println!("Original {:?}", vec1
Err(value)=>println!("New {:?}", valu
}
}
Bounds
For generics, type parameters can refer to any type. That’s the
value of generics! However, that can also be a limitation. The
type parameter represents a type that is non-specific. This
means a compile time, the compiler knows very little about the
type parameter. For that reason, the compiler rightfully limits
what can be done with it.
***
*2*
***
fn border_value<T>(value:T)->String {
let formatted=format!("* {} *", value);
let len=formatted.len();
let line="*".repeat(len).to_string();
fn border_value<T:Display>(value:T)->String {
let formatted=format!("* {} *", value);
let len=formatted.len();
let line="*".repeat(len).to_string();
<T:Trait1+Trait2+…>
Code Listing 14.13. For the largest function, trait bounds Ord
and Display are applied to type parameter T.
use std::fmt::Display;
use std::cmp::Ordering;
fn main() {
largest(10, 20);
largest("ten".to_string(), "twenty".to_st
fn do_something<T:Debug>(arg1:T){
println!("{:?}", arg1);
}
fn do_something<T>(arg1:T)
where T:Debug
{
println!("{:?}", arg1);
}
The where clause can simplify the syntax for generics. In the
next example, the do_something function is generic over type T.
The func parameter is the FnOnce type, which is a trait. As a
parameter, the trait requires either static or dynamic dispatch.
In Listing 14.16, dynamic dispatch is used.
Code Listing 14.17. This syntax is simpler with the where clause.
fn do_something<T, U>(func: U)
where
U: FnOnce(&T),
{
}
The where clause also has a unique capability. With the where
clause, you can assign trait bounds directly to arbitrary types.
This feature assigns trait bounds to concrete types, not type
parameters.
#[derive(Copy, Clone)]
struct XStruct {}
fn do_something(arg:XStruct)
where
XStruct:Copy
{
}
fn main() {
do_something(XStruct{})
}
Structs
Functions should not have all the fun. Structs can also be
generic! You declare the type parameters, within angle
brackets, after the struct name. You can then use the type
parameter within the struct, including methods.
struct IntWrapper {
internal:i8
}
struct Wrapper<T> {
internal:T
}
fn main() {
let obj=Wrapper{internal:1};
/* Monomorphization
struct Wrapper {
internal:i8
}
*/
}
Note
impl<T:Copy> Wrapper<T> {
fn get(&self)->T{
self.internal
}
fn main() {
let obj1=Wrapper{internal:1};
println!("{}", obj1.get());
let obj2=Wrapper{internal:1.1};
println!("{}", obj2.get());
}
use std::fmt::Display;
struct Wrapper<T> {
internal:T
}
impl<T:Copy+Display> Wrapper<T> {
...
}
}
fn main() {
let obj=Wrapper{internal:1.1};
obj.display("< ", " >");
}
use std::fmt::Display;
impl<T:Copy+Display> Wrapper<T> {
...
fn perform<F>(mut self, operation:F)->Sel
where
Self:Copy+Clone,
F: Fn(T) -> T
{
self.internal=operation(self.internal
self
}
}
Associated Functions
impl<T> XStruct<T> {
fn do_something(a:T) {
let a:T;
}
}
fn main() {
XStruct::do_something(5);
}
structname::<type, …>::function_call
impl<T> XStruct<T> {
fn do_something() {
let a:T;
}
}
fn main() {
XStruct::<i8>::do_something();
}
Enums
enum Option<T> {
None,
Some(T),
}
The Result and Option types do not encompass all use cases for
return values. You can create other uses cases similar to either
the Result or Option types. For example, the Repeat enum,
shown next, is for a different use case. It is generic over types
T and U . This enum controls whether a function should be
called repeatedly. There are multiple variants:
fn find_div_5(number:i8)->Repeat<i8> {
if number % 5 == 0 {
println!("Found {}", number);
Repeat::Done
} else {
Repeat::Continue(number+1)
}
}
fn main() {
let mut value=1;
loop {
if let Repeat::Continue(recommend) = find
value=recommend;
} else { // Done found
break;
}
}
}
enum Employee<T:Clone>{
EmplId(T),
Name(String)
}
fn get_employee()->Employee<String> {
Employee::Name("Carol".to_string())
}
Generic Traits
Code Listing 14.34. ATrait and XStruct are generic over type
T.
trait ATrait<T> {
fn do_something(&self, arg:T);
}
struct XStruct<T> {
field1:T,
}
The syntax for the impl block header appears fairly complex,
with all the angle brackets, but it is not. Remember that type
parameters are part of the type name. With that understanding,
the syntax is actually straightforward, as shown in Figure 14.2.
trait ATrait<T:Display> {
fn do_something1(&self, arg:T);
}
struct XStruct<T> {
field1:T,
}
use chrono::Local;
use std::fmt::Display;
trait TimeStamp<T:Display+Copy> {
fn set_value(& mut self, value: T);
}
fn main() {
let mut value1=10;
value1.set_value(20);
23:24:04 10 -> 20
23:24:04 10.1 -> 20.1
Explicit Specialization
Until now, type parameters have been resolved at compile time,
either implicitly or explicitly. For example, inferring the type
from type arguments. You can also assign types directly to type
parameters, which is explicit specialization. This has already
been shown with the Result and Option enum. With explicit
specialization, type parameters are constrained by a type, not a
trait bound. This capability allows for unique solutions for
specific use cases, such as state machines.
struct XStruct<T>{
field1: T,
}
|
6 | fn do_something(arg:T){
Let’s simplify our example and remove the generic impl for
XStruct . That leaves the explicit specialization for i8. In
main , this means we can only call the do_something function
with a i8 type argument. Calling the function with any other
types, such as a float, is invalid (see Listing 14.42).
fn main() {
let obj1=XStruct{field1:1}; // integer
let obj2=XStruct{field1:1.1}; // float
obj1.do_something(2);
obj2.do_something(2.0); // invalid
}
struct On;
struct Neutral;
struct Off;
struct Motor<T> {
status:T,
rpm:i8
}
impl Motor<Off> {
fn on(mut self)->Motor<On> {
self.rpm=20;
println!("Motor running | RPM: {}", s
*Box::new(Motor{status:On, rpm:self.r
}
}
Within the impl block, the on function is the only action for
this state. It transforms the motor from off to on and returns a
motor in the new state, which is created on the heap. You’ll
learn more about Box in Chapter 20, “Memory.” The motor
starts running at 20 rpm. Importantly, the current motor
( self ) is no longer available after the function call because the
state of that motor is no longer valid after the transformation.
With mut self as the method parameter, ownership is
removed from the caller.
impl Motor<On> {
fn off(mut self)->Motor<Off>{
self.rpm=0;
println!("Motor off | RPM: {}", self
*Box::new(Motor{status:Off, rpm:self
}
fn neutral(self)->Motor<Neutral>{
println!("Motor neutral | RPM: {}", s
*Box::new(Motor{status:Neutral, rpm:s
}
}
impl Motor<Neutral> {
fn in_gear(&mut self)->Motor<On>{
println!("Motor in gear | RPM: {}", s
*Box::new(Motor{status:On, rpm:self.r
}
}
The initial state for a motor should be off. This is done using a
simple factory pattern. For this, we have a standard impl
block for Motor , which is a generic over type T . Within the
block, the new constructor function, returns a new motor in
the off state (see Listing 14.46), using type specialization.
impl<T> Motor<T>{
fn new()->Motor<Off> {
println!("Motor off | RPM: 0");
*Box::new(Motor{status:Off, rpm:0})
}
}
fn main() {
let mut motor=Motor::<Off>::new(); // of
let mut motor=motor.on(); // on
let mut motor=motor.neutral(); // ne
let mut motor=motor.in_gear(); // on
let mut motor=motor.off(); // of
}
fn main() {
let obj1=ZStruct{field1:1, field2:true};
let obj2=ZStruct{field1:1.0, field2:12};
let obj3=ZStruct{field1:1.0, field2:true}
let obj4=ZStruct{field1:'a', field2:12_i8
Summary
You can also assign trait bounds to type parameters with the
where clause, which is more expressive, often more readable
than conventional trait bounds.
OceanofPDF.com
Chapter 15. Patterns
Core Syntax
let a=1;
let b=(1,2);
Wildcards
Hierarchical Patterns
Patterns can be hierarchical to match and destructure similar
types.
let (a,b)=data;
println!("{:?}, {:?}", a, b); // (1,2) (3,4)
let ((a,b),(c,d))=data;
println!("{}, {}, {}, {}", a, b, c, d); // 1
Here is an example.
let ref1=&5;
let (&value)=ref1;
println!("{}", *value);
Ownership
When destructuring, ownership can be moved. Destructuring
with binding is an assignment. For values that support copy
semantics, the value is simply copied. For others however, such
as structs, ownership is moved by default. For this reason, there
may be unintended side effects from destructuring values, as
demonstrated here.
fn main() {
let tuple=("Bob".to_string(), 42);
let (a, b)=tuple; // destructures
println!("{}", tuple.0); // fail
println!("{}", tuple.1); // works
}
fn main() {
l t t t l ("B b" t t i () 42)
let mut tuple=("Bob".to_string(), 42);
let (ref mut a, b)=tuple;
a.push_str(" Wilson");
println!("{}", a); // Bob Wilson
println!("{}", tuple.0); // Bob Wilson
}
fn main() {
let tuple=("Bob".to_string(), 42);
let (mut a, b)=tuple;
a.push_str(" Wilson");
Irrefutable
fn get_value()->(i8, i8){
// Implementation not shown
}
fn main() {
let tuple=get_value();
if let (1..=15, 1..=25)=tuple {
println!("It matches!");
}
}
binding@range
Multiple Patterns
fn get_value()->i8{
// Not shown
}
fn main(){
let value=get_value();
if let 5|10|20= value {
println!("Match found"); // Match fo
}
}
Control Flow
if let pattern=expression {
// match
} else {
// no match (optional)
}
if let Some(result)=option {
println!("Found: {}", result);
} else {
println!("Nothing found")
}
Structs
struct Rectangle {
p1: (u32, u32),
p2: (u32, u32),
}
fn main() {
let r = Rectangle { p1: (10, 20), p2: (30
let Rectangle { p1, p2 } = r; // destr
struct Triangle {
p1: (u32, u32),
p2: (u32, u32),
p3: (u32, u32),
}
fn main() {
let tri = Triangle { p1: (10, 20), p2: (30, 4
Functions
fn main() {
let values=(5, 6);
do_something(&values);
}
#[derive(Debug)]
struct Rectangle { p1: (u32, u32), p2: (u32,
fn main() {
let r1 = Rectangle {p1: (10, 10), p2: (20
let r2 = Rectangle {p1: (30, 30), p2: (40
let r3=combine(&r1, &r2);
println!("Combined {:?}", r3);
}
Match Expressions
match expression {
pattern1=>expression,
pattern2=>expression,
patternn=>expression,
_=>expression,
}
let value=get_value();
match value {
1=>println!("One"),
2=>println!("Two"),
_=>println!("Unknown, but not 1 or 2")
}
The patterns here consist of literals, which will match identical
values. The patterns in this example do not exhaust the scope of
the match type, which is an integer. Therefore, the default arm,
with the underscore pattern ( _ ), was necessary to encompass
the remaining integer values.
match value {
1=>println!("One"),
2=>println!("Two"),
other=>println!("Default: {}", other)
}
let value = 7;
match value {
1..=5 => println!("1..=5"),
6..=10=> println!("6..=10"), // match
_ => println!("other value"),
}
fn main() {
let value = 7;
match value {
a@1..=5 => println!("Value is {}", a),
b@6..=10=> println!("Value is {}", b),
_ => println!("other value"),
}
}
A match arm can also include multiple patterns combined with
the pipe ( | ) operator. The patterns are evaluated in order, left
to right. If any of the patterns match, control is transferred to
that arm’s expression.
fn main() {
let value = 25;
match value {
25 | 30 | 35 => println!("25 |30 | 35"),
_ => println!("other"),
}
println!("{}", value);
}
Note
Variants that do not carry data cannot be
destructured.
let result=do_something();
match result {
Ok(result)=>println!("{}", result),
Err(msg)=>println!("{}", msg)
}
enum Person {
Name(String),
GovId(i32),
}
fn main() {
let bob: Person=Person::Name("Bob" to str
let bob: Person=Person::Name( Bob .to_str
match bob {
Person::Name(ref name)=>println!("Nam
borrow
Person::GovId(id)=>println!("Governme
copy
}
}
#[derive(Debug)]
struct Rectangle { p1: (u32, u32), p2: (u32,
fn main() {
let r = Rectangle {p1: (10, 10), p2: (20,
match r {
Rectangle { p1: (10 10) p2 } => pri
Rectangle { p1: (10, 10), p2 } => pri
"Found: (10, 10), {:?}", p2),
Rectangle { p1: (10, 20), p2 } => pri
"Found: (10, 20), {:?}", p2),
Rectangle { p1, p2: (20, 20) } => pri
"Found {:?}, (20, 20)", p1),
_ => println!("No match”),
}
}
match r {
Rectangle { p1: p1@(10, 10), p2 } => prin
"Found: {:?}, {:?}", p1, p2),
Rectangle { p1: p1@(10, 20), p2 } => prin
"Found: {:?}, {:?}", p1, p2),
Rectangle { p1, p2: p2@(20, 20) } => prin
"Found: {:?}, {:?}", p1, p2),
r => println!("No match {:?}", r),
}
Match Guards
Within a match expression, a match guard provides an
additional criterion to pattern matching. It is a Boolean filter
applied after the pattern is evaluated. A match guard is added
to a pattern with the if keyword and then a Boolean condition.
If the pattern matches and the match guard is true, the match
arm is executed. Table 15.1 displays the possible outcomes of a
match guard.
#[derive(Debug)]
struct Rectangle { p1: (u32, u32), p2: (u32,
fn main() {
let r = Rectangle {p1: (10, 10), p2: (20,
match r {
Rectangle { p1:(x1, _), p2:(x2,_ )}if
=> println!("Left invalid {} {}",
Rectangle { p1:(_, y1), p2:(_, y2 )}i
=> println!("Bottom invalid {} {}
The first pattern destructures the left side and right side of a
rectangle, as variables x1 and x2 . The top-side and bottom-
side are ignored. The match guard confirms if the right-side is
less than the left-side. If true, the rectangle is not proper and a
message is displayed. The second pattern performs the same
analysis for y1 and y2 , which define the top-side and bottom-
side of the rectangle. The default pattern is reached when the
rectangle is properly formed.
This example displays only gray colors. RGB and CMYK are the
color schemes. The match guards will confirm gray colors, and
filter everything else. There are degrees of gray from light gray
to black. For RGB, gray is when the primary colors, red, green,
and blue, have the same intensity. CMYK color is gray when
cyan, magenta, and yellow all have zero values.
The example starts with the Colors enum with the RGB and
CMYK variants. Both are associated with structs that hold their
values. For example, the RgbColor struct has fields for the red,
green, and blue values.
use Colors::RGB;
use Colors::CMYK;
enum Colors {
RGB(RgbColor),
CMYK(CmykColor),
}
For the Colors enum, we implement the match guard. The
is_gray method returns true if either a RGB or CMYK color is
gray.
impl Colors{
fn is_gray(&self) -> bool {
match self {
RGB(color)=>(color.red==color.gre
(color.green==color.blue),
CMYK(color)=>(color.cyan+color.ma
}
}
fn display_gray(&self) {
match *self {
RGB(value)
if self.is_gray()=>println!("
value),
CMYK(value)
if self.is_gray()=> println!(
value),
RGB(value)=>println!("RGB {:?} is
CMYK(value)=> println!("CMYK {:?}
}
}
}
fn main() {
let rgb=RGB(RgbColor{red:100, green:155,
rgb.display_gray();
fn is_monday()->bool {
// determines whether Monday, or not
}
fn main() {
let monday=is_monday();
match 1 {
1 | 11 | 21 if monday=>println!("Valu
_=>println!("Not Monday!")
}
}
In Conclusion
For specific use cases, include literals in the patterns. This can
be combined with other details in the pattern.
OceanofPDF.com
Chapter 16. Closures
“Hello, World”
You can also call the closure directly. In Listing 16.2, the closure
is immediately called with the call operator. It is not assigned to
a variable first.
fn main() {
(|| println!("Hello, world!"))();
}
Closure Syntax
Some of the details of the previous closure were implied. That
allowed the syntax to be shortened. Here is the complete syntax
of a closure:
fn main() {
let cubed=|number:usize|->usize{
number*number*number
};
let value=5;
let result=cubed(value);
println!("{}", result);
}
When called, the closure here returns a cubed value. Both the
parameter and return value of the closure are usize types. The
parameter value is cubed and the result returned. The closure
is assigned to the cubed variable. It is then called with the call
operator, while accepting a local variable as the function
parameter. The result is saved in a variable and displayed.
let cubed=|number|number*number*number;
Closed Over
fn main() {
let value=5;
let cubed=||value*value*value;
let result=cubed();
println!("{}", result);
}
The example shown in Listing 16.5 will not work! The values1
variable is a mutable tuple. When used within the closure, It
performs a mutable borrow of values1. It will retain that
mutable borrow until immediately after the closure is called.
However, the values2 variable requests a second mutable
borrow within that scope. Rust does not allow multiple mutable
borrows on the same variable. Therefore, this will cause a
compiler error.
Code Listing 16.5. In closures, closed-over variables are
borrowed.
fn main() {
let mut values1=(5,10);
println!("{:?}", result);
}
fn main() {
let values1=(5,10);
println!("{:?}", result);
}
Joy! The next version of the application works! There are no
compiler errors. We initiate the second mutable borrow after
the final swap_values call. For that reason, there is no overlap
(see Listing 16.7).
Code Listing 16.7. Mutable borrows, but not at the same time
fn main() {
let mut values1=(5,10);
fn do_closure(run:impl Fn()){
run();
}
fn main() {
let display=||println!("message");
do_closure(display);
}
enum Calculation {
Cubed,
Quad
}
fn main() {
let cubed=|value:i32|value*value*valu
let quad=|value:i32|value*value*value
let calculation_type=Calculation::Cub
let result=match calculation_type {
Calculation::Cubed => get_result(
Calculation::Quad => get_result(q
};
println!("{}", result);
}
This example can either cubed or quad a value. Both operations
are implemented as closures and are Fn(i32)->i32 types.
The get_result function has a parameter, run, that accepts
closures of the same type. The run parameter is a Fn(i32)-
>i32 type, which is consistent with the cubed and quad
closures. Within the function, the run closure is executed and
the result returned.
fn main() {
let cubed=get_closure();
let result=cubed(5);
println!("{}", result);
}
Implementation of Closures
let a=1;
let b=2;
let adder=|prefix:String|println!("{} {}", pr
adder("Add Operation:".to_string());
Listing 16.12 depicts the internal struct created for the adder
closure. The adder closure is converted to a struct with two
fields, for the a and b captured variables. The closure
function is implemented as a method, with &Self as the first
parameter. The second parameter of the method is the initial
parameter of the closure, which is the prefix. Remember, this is
only an approximation of the implementation. Furthermore,
the internal representation of a closure is subject to change at
any time.
struct adder {
a: i32,
b: i32
}
fn do_closure(closure:impl Fn()){
closure();
}
fn main() {
let hello=||println!("Hello");
do_closure(&hello);
}
fn main() {
let hello_string="Hello".to_string();
let hello=||println!("{}", hello_string);
do_closure(&hello);
}
fn do_closure(closure:impl Fn()){
closure();
}
fn main() {
fn hello(){
println!(“Hello”);
}
do_closure(hello);
}
fn main() {
let value="hello".to_string();
let hello=||value; // value moved
do_closure(hello);
}
fn do_closure_fn(closure:impl FnOnce()){
closure();
closure(); // not compile
}
fn main() {
let value="data".to_string();
let dropper=||drop(value);
do_closure_fn(dropper);
}
fn do_closure(c1osure:impl FnOnce()){
c1osure();
c1osure(); // not compile
}
fn main() {
let hello=||println!("hello");
do_closure(hello);
}
fn get_closure()->impl Fn()->i32 {
let number=1;
||number+2
}
3 | ||number+2
| ^^^^^^^^^^
help: to force the closure to take ownership of `
other referenced variables), use the `move` keywo
|
3 | move ||number+2
|
| ++++
Except for the move annotation on the closure, the source code
in Listing 16.22 is identical to the previous example. However,
this version compiles without errors. Excellent!
fn get_closure()->impl Fn()->i32 {
let number=1;
move||number+2 // move closure
}
fn main() {
let add_one=get_closure();
let result=add_one();
println!("{}", result);
}
Traits are unsized. For that reason, you cannot create instances
of a trait. Static and dynamic dispatch are solutions for
managing concrete instances of a trait. The subject of traits,
including static and dynamic dispatch, is reviewed in detail in
Chapter 17, “Traits.”
fn main() {
let value=AStruct{hello:||println!("Hello
(value.hello)();
}
Matrix Example
Let’s look at an additional example that uses closures. The goal
of this example is applying mathematical operations to each
row of a matrix. Operations such as addition, subtraction,
multiplication, and division are supported.
The first column of the matrix indicates the operation for that
row, such as A for addition. The LHS and RHS columns are the
left-hand side and right-hand side operands, respectively, for
the binary operation. The result of the operation is placed in the
Result column, the final column.
use std::collections::HashMap;
fn main() {
let mut matrix=vec![('a', 4, 5, 0),
('m', 2, 6, 0),
('d', 9, 3, 0),
('s', 5, 6, 0),
];
println!("{:?}", matrix);
}
The program starts with two aliases. There is an alias for a tuple
describing each row, including the operation, LHS, RHS, and
result. OperationType is an alias defining the type of closures
for the various binary operations. Dynamic dispatch is used to
reference the Fn trait (see Listing 16.26).
Code Listing 16.26. Alias for the row type and mathematical
operations
Next, the HashMap is declared (see Listing 16.28), and maps the
mathematical operations, as closures, to characters. The
HashMap key indicates the operation as a character, such ‘a’ for
addition. The values are the mathematical operations, as the
OperationType type, which is a previously defined alias. Each
operation is added to the HashMap with the insert method.
operation.insert('a', &|row|row.1+row.2);
operation.insert('m', &|row|row.1*row.2);
operation.insert('d', &|row|row.1/row.2);
operation.insert('s', &|row|row.1-row.2);
println!("{:?}", matrix);
We can now celebrate a job well done.
Summary
OceanofPDF.com
Chapter 17. Traits
Trait Definition
Here is the Car trait. Types that are a kind of car would
implement this trait. It is an abstraction and includes the
common functionality any car requires.
trait Car {
fn ignition(&mut self, drive:bool);
fn turn(&mut self, angle:u8)->u8;
fn brake(&mut self, amount:i8)->i8;
fn accelerate(&mut self, new_speed:i8)->i
fn stop(&mut self);
}
struct Battery {
charge:i8
}
struct ElectricCar {
battery:Battery,
started:bool,
speed:i8,
direction:u8,
}
impl ElectricCar {
fn get_charge_level(&self)->i8 {
self.battery.charge
}
}
In addition, the ElectricCar type implements the
impl block. You cannot implement the direct meth
same block. They must be in separate dedicated b
self.speed
}
fn stop(&mut self){
self.speed=0;
}
}
}
fn main() {
let mut mycar=ElectricCar {
battery:Battery{charge:0},
started:false,
speed:0,
direction:0,
};
mycar.ignition(true);
mycar.accelerate(25);
mycar.brake(5);
mycar.stop();
mycar.ignition(false);
mycar.get_charge_level();
}
Default Functions
As shown, traits contain function definitions that types are
required to implement. However, there can also be default
functions that are implemented within the trait. Types that
implement a trait then have a choice − accept or override each
default function. Default functions within a trait are an
example of code reuse. The code is shared with every type that
implements the trait. In addition, you can implicitly add code to
existing types using default functions. This is another feature
that contributes to your source code being extensible.
trait Shape {
fn draw(&self){println!("drawing...");}
fn erase(&self);
}
struct Rectangle{}
struct Ellipse{}
Next is the Light trait. The trait has the light_on and light_off
methods that confirm whether a light is on. The light_off
method is implemented as a default method. It simply negates
the result of the light_on method.
trait Light {
fn light_on(&self)->bool;
fn light_off(&self)->bool{
!self.light_on()
}
}
Marker Trait
Send
Sized
Sync
Unpin
Associated Functions
trait Singleton {
fn new()->Self
where Self:Sized;
fn get_instance()->Self
where Self:Sized;
}
The Self for a trait is not sized; the reason being is that traits
do not have a constant size. Traits can be implemented by
various types, each potentially a different size. This is important
because Rust does not allow instances of unsized types.
However, the functions of the Singleton trait must return an
instance. Fortunately, the functions are implemented for a
specific concrete type in an impl block. In that context, Self
is sized. For that reason, we can update Self accordingly, with
the Sized marker trait. Without this change, we would be
prevented from creating instances of the singleton.
struct Chessboard{
}
impl Chessboard {
const INSTANCE:Chessboard=Chessboard{};
{};
fn start(&self) {
println!("chess game started");
}
}
fn new()->Self {
Chessboard::INSTANCE
}
fn get_instance()->Self {
Chessboard::INSTANCE
}
}
fn main() {
let board=Chessboard::new();
board.start();
}
Associated Types
Self::associated_type
trait Inventory {
type StockItem;
fn find(&self, stock_id:&String)->Self::S
fn add(&mut self, item:Self::StockItem)->
}
struct Chair{}
#[derive(Copy, Clone)]
struct Chairs{}
fn find(&self, stock_id:&String)->Self::S
Chair{}
}
fn main() {
let mut catalog=Chairs{};
let stock_id=catalog.add(Chair{});
let item=catalog.find(&stock_id);
println!("Stock id: {:?}", stock_id);
}
In this example, the ATrait has two associated types. They are
used to set the Result value for the do_something function.
Each implementation of the trait can decide what
do_something should return.
trait ATrait {
type ValueType;
type ErrorType;
fn do_something(&self)->Result<Self::Valu
Self::ErrorType>;
}
MyStruct implements the trait and sets the return value for
do_something to be Result<i8, String> .
struct MyStruct {
fn do_something(&self)->Result<Self::Valu
Self::ErrorType> {
Ok(42)
}
}
Extension Methods
trait Dump {
fn write_dump(&self) {
println!("{}", self); // mock
}
}
fn main(){
let num:i8=1;
num.write_dump();
}
trait Dump {
fn write_dump(&self) where Self: Display
println!("{}", self);
}
}
For this version of the Dump trait, the where clause requires
that Self implement both the Display and Debug traits.
This assures support for both the {} and {:?} placeholders in
the println macro.
trait Dump {
fn write_dump(&self) where Self: Display
println!("{:?}", self);
}
}
There are limitations on when extension methods can be
applied to avoid chaos. What would happen if an external
library changed the behavior of all the primitives,
unbeknownst to your application? That would not be fun! To
prevent this, there are two restrictions.
Either the trait or the type for the extension method must be
implemented within the application.
For example, this would not compile. Neither the trait nor the
type, the Iterator trait and i8 type, are defined within our
application.
obj.do_something(); /
XStruct::do_something(&obj); /
<Xstruct as Atrait>::do_something(&obj); /
Let’s review a practical example where the proper syntax is
required. Here, the Atrait and Btrait traits both implement
the get_name method. MyStruct implements both traits and
accepts the default implementation of get_name . Finally,
get_name is not implemented directly for MyStruct .
Therefore, the only versions of get_name come from the traits.
Trait Atrait {
fn get_name(&self){
println!(“Atrait”);
}
}
trait Btrait {
fn get_name(&self){
println!(“Btrait”);
}
}
struct MyStruct{}
fn main() {
let obj=MyStruct{};
obj.get_name(); // implicit syntax
}
fn main() {
let obj=MyStruct{};
ATrait::get_name(&obj); // explicit synt
}
struct MyStruct{}
impl MyStruct{
fn display_name() {
println!("MyStruct");
}
}
fn main() {
let obj=MyStruct{};
MyStruct::display_name(); // explicit
ATrait::display_name(); // explicit
}
fn main() {
let obj=MyStruct{};
MyStruct::display_name();
<MyStruct as ATrait>::display_name(); /
}
Supertraits
trait dependent:supertrait {
// dependent functions
}
Copy: Clone
Eq: PartialE
FnMut<Args>:FnOnce<Args>
Ord:Eq+PartialOrd
struct Text {}
trait Font {
fn set_font(&mut self, font_name:String)
}
fn main() {
let mut text=Text{};
text.set_font("arial".to_string());
text.set_style(true, false);
}
struct Text {}
trait Font {
fn set_font(&mut self, font_name:String)
}
trait Unicode {
}
struct Amphibious{
}
trait Vehicle {
fn drive(&self){}
}
fn main() {
let boat=Amphibious{};
boat.drive();
}
Static Dispatch
Using traits instead of specific types can make your source code
more extensible and concise, especially when used for function
arguments and return values. Types that implement the same
trait are interchangeable, within the context of that trait. You
are not limited to a specific type. Treat traits as placeholders for
any concrete type that implements the trait.
fn do_something(obj: ATrait) {
}
The example will not compile. The reason is because traits are
unsized, as mentioned earlier. Rust does not support instances
of unsized types − even traits. This problem can be resolved
with either static or dynamic dispatch. We will start with static
dispatch.
Note
trait Human {
fn get_name(&self)->String;
}
struct Adult(String);
impl Human for Adult {
...
}
struct Child(String);
impl Child {
...
}
trait Alien {
fn get_name(&self)->String;
}
fn main() {
let bob=Adult("Bob".to_string());
let janice=Child("Janice".to_string());
let fred=Martian("Fred".to_string());
invite_to_party(bob);
invite_to_party(janice);
invite_to_party(fred); // not allowed
}
Dynamic Dispatch
&dyn trait
Box<dyn trait>
fn create_person(adult:bool, name:String)->Bo
if adult {
Box::new(Adult(name))
} else {
Box::new(Child(name))
}
}
fn main() {
let shapes: Vec<&dyn Shape>=vec![&Rectang
&Ellipse{}, &Rectangle{}];
for shape in shapes {
shape.draw();
}
}
Enums can also implement traits. You are free to implement the
trait in any manner. There is, however, a standard approach for
implementing a trait method for an enum. You match on the
enum and provide a unique implementation for each variant.
trait Schemes {
fn get_rgb(&self)->(u8, u8, u8);
fn get_cmyk(&self)->(u8, u8, u8, u8);
}
You can implement the trait for the enum in an impl block for
the trait. The enum encompasses the core colors: red, green,
and blue. The implementation of the get_rgb function returns
the RGB color scheme for each of the core colors. Alternatively,
the get_cmyk function returns the CMYK color scheme for the
same colors.
enum CoreColor {
Red,
Green,
Blue
}
fn main() {
let green=CoreColor::Green;
let rgb=green.get_rgb();
let cmyk=green.get_cmyk();
println!("{:?} {:?}", rgb, cmyk);
}
In Conclusion
With all the traits floating around, ambiguous function calls are
not unheard of. The combination of the implicit, explicit, and
fully qualified syntax should resolve any ambiguous function
calls that may exist.
OceanofPDF.com
Chapter 18. Threads
Long ago in a far away land, there were mainly single processor
computers and devices. Despite this, a process could have
multiple paths of execution. Each path of execution in a process
is known as a thread. In this environment, the threads share the
processor. This is called concurrency. With this architecture,
operating systems schedule threads onto the processor in
mostly a round-robin fashion.
With main as the entry point for the primary thread, main and
the hello function are along the same path of execution. In
main, there is a synchronous function call to hello. Since the
functions share a single thread, main is suspended while hello
is executing. After hello is done, main is resumed and continues
until completion. When main exits, the primary thread and
process are also ended.
fn hello() {
println!("Hello, world!");
}
fn main() {
println!("In main")
hello();
println!("Back in Main")
}
In main
Hello, world!
Back in Main
fn main() {
let a=1;
println!("{}", a);
display();
}
use std::thread;
use std::thread;
fn main() {
let handle=thread::spawn(|| println!("Hel
let result = handle.join(); // wait for
println!("In Main");
}
Here, the new thread is a closure that returns 1. We call the join
method with the thread handle and wait for the new thread to
complete. The return value is bound to the result variable. The
unwrap method unpacks the inner return value from the
Result, which is then displayed.
use std::thread;
fn main() {
let handle=thread::spawn(|| 1);
let result = handle.join().unwrap(); //
value
println!("Return value {}", result);
}
use std::thread;
fn main() {
let handle=thread::spawn(|| panic!("kaboo
let result = handle.join(); // wait for
match result {
Ok(value)=>println!("Thread return va
Err(msg)=>println!("{:?}", msg)
}
}
}
use std::thread;
fn main() {
let a=1;
let b=2;
let handle=thread::spawn(move || {
let c=a+b;
println!("Sum {} + {}: {}", a, b, c);
});
let result = handle.join();
}
This example has scoped threads. The scope thread borrows the
count value as a mutable reference. This can be done directly
with a scoped thread.
use std::thread;
fn main() {
let mut count = 0; // 'env
thread::scope(|s| { // 'scope
s.spawn(|| {
count+=1;
});
println!("{}", count);
}
Thread
The Thread type is a handle to a thread. In addition, it is also an
obfuscated type and is managed via functions. As such, thread
objects are not created directly but with either the
thread::spawn or builder::spawn functions. More about the
Builder type shortly.
use std::thread;
fn main() {
let current_thread=thread::current();
println!("{:?}", current_thread.id());
println!("{:?}", current_thread.name());
}
The thread::id function returns a ThreadId, which is an opaque
type. It is the unique identifier for a thread for the entirety of
the process. And the ThreadId is not reused when the thread
expires. The name function returns the thread name as a string,
within a Result type. For named functions, the default name is
the function name. For closures, the thread is unnamed initially.
Processor Time
duration::as_millis : milliseconds
duration::as_nanos: nanoseconds
duration::as_secs: second
For the next example, we use the sleep function. In the for loop,
two threads are created that consume elements from a tuple
array. Each item in the tuple has an informal thread name and
a sleep duration. The threads run different instances of the
same closure in parallel. Within the while loop, the threads
display an increasing count and then sleep for the indicated
duration. Back in main, there is a 3 second sleep, which
mitigates a race condition with the other threads. They now
have time to finish.
use std::thread;
use std::time::Duration;
fn main() {
thread::sleep(Duration::from_secs(3));
}
use std::thread;
use std::time::Duration;
use std::thread::Thread;
fn store_open()-> Thread {
thread::spawn(|| {
thread::park();
loop {
println!("open and handling custo
// hopefully, lots of sales
}
}).thread().clone()
fn main() {
let open=store_open();
// Do setup
disable_alarm();
open_registers();
p g ()
open.unpark();
thread::sleep(Duration::from_secs(2));
}
Builder
The initial stack size for a thread can differ within various
operating system environments. Managing the stack size can
improve performance and reduce the memory footprint of a
process. In addition, this can improve scalability. For Rust, you
can set the stack size for any thread, except the primary thread,
which is operating system dependent. The RUST_MIN_STACK
environment variable, if present, sets the minimum stack size.
Programmatically, the stack size for a particular thread is
configurable with the Builder::stack_size function.
use std::thread;
use std::thread::Builder;
fn main() {
let builder = Builder::new()
.name("Thread1".to_string())
stack size(4096);
.stack_size(4096);
let result = builder.spawn(|| {
let thread = thread::current();
println!("{}", thread.name().unwrap()
});
let handle=result.unwrap();
let result=handle.join();
}
Asynchronous channel
use std::sync::mpsc;
use std::thread;
fn main() {
let (sender, receiver) = mpsc::channel();
thread::spawn(move || {
sender.send(1);
});
let data=receiver.recv().unwrap();
println!("{}", data);
}
use std::sync::mpsc::channel;
use std::thread;
fn main() {
let (sender1, receiver) = channel();
let sender2=sender1.clone();
thread::spawn(move || {
for i in 0..=5 {
sender1.send(i);
}
});
thread::spawn(move || {
for i in 10..=15 {
sender2.send(i);
}
});
handle.join();
}
If either half of a channel becomes disconnected, the channel
will return an error. This can occur when the Sender or
Receiver is dropped. However, you are allowed to receive
data already in the channel.
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (sender, receiver) = mpsc::channel();
thread::spawn(move || {
sender.send(1);
}); // sender dropped
thread::sleep(Duration::from_secs(1));
let data = receiver.recv().unwrap(); //
}
The new thread is short, and afterwards the Sender is
dropped. In main , the first recv method receives the existing
data in the channel. Because of the sleep , the second recv
clearly occurs after the Sender is disconnected. That will cause
an error result. Here is the error message:
Synchronous Channel
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (sender, receiver) = mpsc::sync_chann
let handle=thread::spawn(move || {
sender.send(1);
println!("Sent 1");
sender.send(2);
println!("Sent 2");
sender.send(3); // block
println!("Sent 3");
});
let data=receiver.recv().unwrap();
println!("Received {}", data);
handle.join();
}
Rendezvous Channel
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};
fn main() {
let (sender, receiver) = mpsc::sync_chann
let handle = thread::spawn(move || {
println!("SyncSender - before send");
let start = Instant::now();
sender.send(1);
let elapsed = start.elapsed();
println!("After send - waited {} seco
});
thread::sleep(Duration::from_secs(10));
receiver.recv();
handle.join();
}
Try Methods
use std::sync::mpsc;
use std::thread;
fn main() {
let (sender, receiver) = mpsc::sync_chann
sender.send(1);
sender.send(2);
let result=sender.try_send(3).unwrap_err(
The Receiver can block also! This occurs when the channel is
empty. It remains blocked until a sender inserts data into the
channel. At that time, the blocked thread awakens to receive
the data. The t ry_recv method is a non-blocking alternative.
For an empty channel, the try_recv method receives a
notification as a TryRecvError error. This allows the receiver
thread to continue executing when it would otherwise be
blocked.
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use std::thread::Builder;
fn idle_work() {
println!("Doing something else...")
}
fn main(){
let (sender, receiver) = mpsc::sync_chann
let builder = Builder::new()
.name("Message Pump".to_string())
.stack_size(4096);
let result = builder.spawn(move || {
let messages=["message 1".to_string()
"message 2".to_string(),
"message 3".to_string(),
"".to_string()];
for message in messages {
g g {
sender.send(message);
thread::sleep(Duration::from_
}
});
}
});
let handle=result.unwrap();
handle.join();
}
The recv_timeout method is another alternative to receiving
data from a channel. This method blocks if a channel is empty.
However, the method will awaken if the timeout is exceeded.
When this occurs, recv_timeout returns
RecvTimeoutError as the Err result. Here is the function
definition.
use std::sync::mpsc;
use std::time::Duration;
use std::thread;
fn main() {
let (sender, receiver) = mpsc::sync_chann
thread::spawn(move || {
thread::sleep(Duration::from_millis(2
sender.send(1);
});
wait
match data {
Ok(value)=>println!("Data received: {
Err(_)=>println!("Timed out: no data
}
}
use std::sync::mpsc;
use std::thread;
fn main() {
let (sender, receiver) = mpsc::sync_chann
sender.send(1);
sender.send(2);
sender.send(3);
In this example, the new thread inserts values into the channel.
In main, the for loop iterates the Receiver . This is possible
because Receiver implements the Iterator interface. The
for loop stops iterating when the Sender is dropped, and no
remaining items are in the channel.
use std::sync::mpsc;
use std::thread;
fn main() {
let (sender, receiver) = mpsc::channel();
thread::spawn(||{
sender.send(1);
sender.send(2);
}); // sender dropped
Store Example
fn main() {
let (sender, closing) = channel::<bool>()
store_open(closing);
thread::sleep(Duration::from_secs(2));
store_closing(sender);
}
fn store_open(closing:Receiver<bool>) {
thread::spawn(move || {
// Opening setup
let alarms=thread::spawn(|| println!(
let registers=thread::spawn(|| printl
alarms.join();
registers.join();
});
}
The store_closing function closes the store. The function
accepts a Sender as the function argument. This is the sender
half of the closing channel. The first step is sending a value to
the closing channel. This notifies the open thread, implemented
in the store_open function, to exit (i.e., stop handling
customers). The sleep function gives the open thread an
opportunity to complete final tasks and then exit. Next, the
alarms are reenabled and the registers are closed as separate
threads. We wait for them to complete. Now the store is closed!
fn store_closing(sender:Sender<bool>){
sender.send(true); // notification of th
thread::sleep(Duration::from_millis(300))
let alarms=thread::spawn(|| {
println!("turning on alarms");
});
let registers=thread::spawn(|| {
println!("closing registers");
});
alarms.join();
registers.join();
println!("Store closed!");
}
Here is the entire application.
use std::thread;
use std::time::Duration;
use std::sync::mpsc::{channel, Receiver, Send
fn store_open(closing:Receiver<bool>) {
thread::spawn(move || {
// Opening setup
let alarms=thread::spawn(|| println!(
let registers=thread::spawn(|| printl
alarms.join();
registers.join();
}
});
}
fn store_closing(sender:Sender<bool>){
sender.send(true);
thread::sleep(Duration::from_millis(300))
let alarms=thread::spawn(|| {
println!("turning on alarms");
});
let registers=thread::spawn(|| {
println!("closing registers");
});
alarms.join();
registers.join();
println!("Store closed!");
}
fn main() {
let (sender, closing) = channel::<bool>()
store_open(closing);
thread::sleep(Duration::from_secs(2));
store_closing(sender);
}
In Conclusion
OceanofPDF.com
Chapter 19. Threads 2
Mutex
use std::sync::{Mutex};
fn main() {
{
let mutex=Mutex::new(0);
let mut guard=mutex.lock().unwrap();
*guard+=1;
println!("{}", *guard);
} // mutex unlocked
}
Here is an example of using a Mutex with multiple threads. To
easily share the mutex, scoped threads are used, which were
introduced in the previous chapter. We create two threads. The
Mutex protects access to an integer value across them. We want
to safely increment and display the guarded value.
use std::thread;
use std::sync::Mutex;
fn main() {
let m=Mutex::new(0);
thread::scope(|scope|{
for count in 1..=2 {
scope.spawn(||{
let mut guard=mutex1.lock().unwrap
*guard+=1;
println!("{:?} Data {}",
thread::current().id(), *guard
});
}
});
}
You can inadvertently leak a Mutex. A Mutex remains locked
for the lifetime of the MutexGuard. If the MutexGuard is never
dropped, or simply delayed, the Mutex remains unavailable to
other threads, which can cause deadlocks. This may occur for a
variety of reasons, including poor management of the
MutexGuard. Here the mutex is locked potentially for an
extended period of time.
Non-scoped Mutex
You can share a Mutex with non-scoped threads also. The Arc
type is the best solution for sharing Mutexes.
Clone the arc to share with other threads. The reference count
increases with every clone. In addition, Arc implements the
Deref trait to provide access to the inner value.
use std::sync::Arc;
use std::thread;
fn main() {
let arc_orig=Arc::new(1);
let arc_clone=arc_orig.clone();
let handle=thread::spawn(move || {
println!("{}", arc_clone); // Deref
});
handle.join();
}
let arc=Arc::new(1);
{ // new block
let arc=arc.clone();
let handle=thread::spawn(move || {
println!("{}", arc); // Deref
});
handle.join();
} // end block
println!("{}", arc);
use std::thread;
use std::sync::{Mutex, Arc};
fn main() {
let arc_mutex = Arc::new(Mutex::new(0));
let mut handles=vec![];
for i in 0..=2 {
let arc_mutex=Arc::clone(&arc_mutex);
for i in handles {
i.join();
}
}
Mutex Poisoning
In this example, the first spawned thread will panic and drop
the MutexGuard. Consequently, the second thread receives an
error when locking the Mutex. The into_inner function is then
used to display the underlying data.
use std::thread;
use std::sync::{Mutex, Arc};
use std::time::Duration;
fn main() {
let arc_mutex1=Arc::clone(&arc_mutex);
let handle1=thread::spawn(move || {
let mut guard=arc_mutex1.lock().unwrap();
*guard+=1;
panic!("panic on mutex");
});
let arc_mutex2=Arc::clone(&arc_mutex);
let handle2=thread::spawn(move || {
thread::sleep(Duration::from_millis(20000)
let mut guard=arc_mutex2.lock();
match guard {
Ok(value)=>println!("Guarded {}", valu
Err(error)=>println!("Error: Guarded {
}
});
handle1.join();
handle2.join();
}
RwLock can become poisoned, but only for writers. When the
RwLockWriteGuard is dropped during a panic, the RwLock
becomes poisoned. If poisoned, both the read and write
functions will return an error.
use std::thread;
use std::sync::{Arc, RwLock};
use std::time::Duration;
fn main() {
let mut handles=Vec::new();
let rwlock = RwLock::new(0);
let arc=Arc::new(rwlock);
Writer lock
Reader Lock 1 Data 1
Reader Lock 3 Data 1
Reader UnLock
Reader UnLock
Writer lock
Writer lock
Reader Lock 2 Data 3
Reader UnLock
Condition Variables
fn notify_one(&self)
fn notify_all(&self)
fn main() {
let setup_event=Arc::new((Mutex::new(false
let setup_event2=setup_event.clone();
thread::spawn(move || {
let mutex=&setup_event2;
let cond=&setup_event2;
let mutex=&setup_event.0;
let cond=&setup_event.1;
The main thread locks the Mutex and waits for the setup to
complete. When notified of the event, main will continue to
execute.
Atomic
Rust includes a full complement of atomic types for the
primitives. These types are closely related to C++ 20 atomics.
The exact list of atomic types depends on the operating system.
AtomicBool
AtomicPtr
AtomicIsize and AtomicUsize
The load and store functions are the basic functions of atomic
types. Store updates the value, while load gets the value.
use std::sync::atomic::AtomicU8;
use std::sync::atomic::Ordering::Relaxed;
use std::thread;
fn do_something(value:u8) {
// performing operation
}
fn main() {
static STATUS:AtomicU8=AtomicU8::new(0);
let handle=thread::spawn(||{
for n in 0..100 {
do_something(n);
STATUS.store(n, Relaxed);
}
});
thread::spawn(||{
loop {
thread::sleep(Duration::from_milli
let value=STATUS.load(Relaxed);
println!("Pct done {}", value);
}
});
handle.join();
}
fn main() {
static TOTAL:AtomicU32=AtomicU32::new(0);
let handle1=thread::spawn(||{
for n in 1..=100 {
let a=TOTAL.fetch_add(n, Relaxed);
}
});
let handle2=thread::spawn(||{
for n in 101..=200 {
TOTAL.fetch_add(n, Relaxed);
}
});
handle1.join();
handle2.join();
use std::sync::atomic::AtomicU32;
use std::sync::atomic::Ordering::Relaxed;
use std::thread;
fn main() {
static A:AtomicU32=AtomicU32::new(0);
let handle1=thread::spawn(||{
A.compare_exchange(0, 1, Relaxed, Rela
});
let handle2=thread::spawn(||{
A.compare_exchange(0, 2, Relaxed, Rela
});
handle1.join();
handle2.join();
In Conclusion
OceanofPDF.com
Chapter 20. Memory
The three main areas of memory are static, stacks, and the
heap. You can place data in any of these locations. At times, Rust
provides some direction, such as placing the data of a vector on
the heap. However, it is primarily your decision where data is
located.
Stack
For Rust, the default stack size is 2k. You can explicitly set the
minimum stack size. Spawn the thread with the Builder type
and call the stack_size function. Alternatively, you can
change the default stack size with the RUST_MIN_STACK
environment variable. However, neither approach places a
ceiling on the stack size. Stacks are growable and will expand as
needed, within the available memory.
let a:i32=1;
let b:i32=2;
let c:i32=do_something();
The next example will not compile. In the nested block, variable
c is pushed onto the stack. It now resides at the top of the
stack. But it is then removed from the stack at the end of the
nested block; demonstrating that even within a function, data
can be added and removed from the stack. Therefore, c is not
available on the stack to be displayed with the println macro.
let a:i32=1;
let b:i32=2;
{
let c:i32=3;
}
fn do_something(a:Copy){
Some data types, such as vectors and strings are smart pointers.
When declared with the let statement, the value for these
types is allocated on the heap, discussed in the next section of
this chapter. A pointer to the value is placed on the stack. In this
example, the value [1, 2, 3, 4] is placed on the heap. The vp
variable, which is a pointer, is placed on the stack.
let vp=vec![1,2,3,4];
Static Values
Code Listing 20.4. Define a static variable for the golden ratio.
static GOLDEN_RATIO:f64=1.618;
let male_bees=100.0;
let female_bees:f64=male_bees*GOLDEN_RATIO;
println!("{}", female_bees as i32);
Code Listing 20.5. Show memory addresses for stack and static
regions.
static A:i8=1;
static B:i8=2;
let c:i8=3;
let d:i8=4;
Based on their addresses, the result shows the global and stack
variables grouped in different regions of memory.
[ Global A: 0x7ff6339ee3e8 B: 0x7ff6339ee3e9
[ Stack c: 0x5e9c6ff5ae d: 0x5e9c6ff5af]
Heap
fn main() {
let pa=Box::new(1);
let pb=Box::new(2);
let c=1;
let d=2;
let rawa=Box::into_raw(pa);
let rawb=Box::into_raw(pb);
unsafe {
Box::from_raw(rawa);
Box::from_raw(rawb);
}
}
You can also move values from the stack to the heap. In this
example, since integers support copy semantics, a copy of the
integer is placed on the heap. Therefore, dereferencing the
pointer and incrementing the integer, changes the value on the
heap, but not the original value.
let a=1.234;
let mut pa=Box::new(a);
*pa+=1.0;
println!("{} {}", a, *pa); // a:1.234 pa:2.2
Interior Mutability
struct Transaction {
storeid: i8,
txid: i32,
mut total:f64,
}
struct Transaction {
storeid: i8,
txid: i32,
total:f64,
}
fn main() {
let mut tx=Transaction{storeid: 100, txid
total:0.0};
tx.storeid=101 // oops
}
fn get(&self) -> T
fn set(&self, val: T)
use std::cell::Cell;
fn main() {
let a=Cell::new(1);
let b=a.get();
a.set(2);
println!("a={} b={}", a.get(), b); // a=2
}
struct Transaction {
storeid: i8,
txid: i32,
total:Cell<f64>,
}
fn main() {
let item_prices=[11.21, 25.45, 8.24, 9.87
let tx=Transaction{storeid: 100, txid: 21
total:Cell::new(0.0)};
println!("{ref3}");
let a=1;
let cell=Cell::new(a);
let ref1=&cell;
let ref2=&cell;
ref1.set(2);
ref2.set(3);
println!("{}", ref1.get()); // 3
RefCell
let refcell=RefCell::new(1);
*refcell.borrow_mut()+=10;
println!("refcell {}", refcell.borrow());
let refcell=RefCell::new(1);
let mut a=refcell.borrow_mut(); // mutable b
let mut b=refcell.borrow_mut(); // mutable b
*a+=10; // panic
let refcell=RefCell::new(1);
let mut a=refcell.borrow_mut(); // mutable b
let mut b=refcell.borrow(); // immutable
*a+=10;
let refcell=RefCell::new(1);
let mut a=refcell.borrow_mut();
let result=refcell.try_borrow();
match result {
Ok(b)=>println!("Interior value: {}", b),
Err(_)=>println!("Do something else")
}
struct Transaction {
storeid: i8,
txid: i32,
total:RefCell<f64>,
}
fn main() {
let item_prices=[11.21, 25.45, 8.24, 9.87]
let tx=Transaction{storeid: 100, txid: 213
total:RefCell::new(0.0)};
OnceCell
Updated
Not updated
Not updated
Final value: 1
In Conclusion
Static memory is for global memory and exists for the lifetime
of the application. The static keyword places a binding in
static memory.
OceanofPDF.com
Chapter 21. Macros
— Mae Jemison
Rust does not support robust reflection, but there are minor
capabilities here and there, such as any::type_name. Macros
provide limited reflection capabilities especially when
combined with flow control. This increases the importance of
macros within the language.
Tokens
Macros are evaluated after the AST creation stage. For this
reason, macros must contain valid syntax or the program will
not compile. In addition, the ability to invoke a macro is
provided as a syntax extension. At this stage, a macro can read
the tokens in the AST to understand an application or code
fragment and plan a response. The response could be replacing
existing tokens or inserting additional tokens into the token
stream.
Keywords
Identifiers
Literals
Lifetimes
Punctuation
Delimiters
Figure 21.1 shows a mapping of tokens, and their types, to
specific elements in the example code.
Declarative Macros
macro_rules!identifier {
(macro_matcher1)=>{ macro_transcriber } ;
(macro_matcher2)=>{ macro_transcriber } ;
(macro_matchern)=>{ macro_transcriber }
}
The identifier names the macro. You can then call the
macro with the macro operator ( ! ).
fn main() {
hello!() // replaced with println!...
}
In nonmacro code, you have types for values, such as i8, f64, or
even String. Macros also have a set of types. However, macros
manipulate code instead of values. These types relate to code,
instead of values, for that reason. The code types for a macro
are called fragment specifiers. Here is the list of fragment
specifiers.
block : a block
expr : expression
pat : a pattern
path : TypePath
stmt : a statement
tt : TokenTree
ty : a type
macro_rules!hello{
($name:expr)=>{
println!("Hello, {}!", $name);
};
}
fn main() {
hello!("Douglas");
}
#[derive(Debug)]
struct Test;
macro_rules!talk{
($lit:literal)=>{
println!("Literal: {:?}!", $lit);
};
}
fn main() {
let input=42;
talk!("Douglas");
talk!("Adams");
talk!(input); // Does not work
}
Repetition
Repetition is a construct that can repeat a transformation on
multiple code fragments. It can be done within either a macro
matcher or transcriber. This capability is important because it
provides support for variadic macros. This is doubly important
because Rust does not support variadic functions.
use evens_macro;
fn main() {
let answer=evens_macro::vec_evens![2,4,5,
println!("{:?}", answer); // 2, 4, 12
}
#[macro_export]
macro_rules! vec_evens {
( $( $item:expr ),* ) => { // repetition
{
let mut result = Vec::new();
$( // repetition
if ($item % 2) == 0 {
result.push($item);
}
)*
result
}
};
}
You can even refer to declarative macro from within a macro.
This example is a more elaborate version of the macro that
displays the hello, world greeting. The hello_world macro,
shown in the following, relies on the hello macro and world
macro to display the greeting. All three macros are declarative
and reside within the same crate.
Code Listing 21.6. A macro that invokes macros from the same
crate.
#[macro_export]
macro_rules! hello_world {
() => {
println!("{} {}", hello!(), name!());
}
#[macro_export]
macro_rules! hello {
() => {"Hello"}
}
#[macro_export]
macro_rules! name {
() => {"Bob"}
}
#[macro_export]
macro_rules! hello_world {
() => {
println!("{} {}", $crate::hello!(),
$crate::name!());
}
}
Here is the application that calls the hello_world macro.
use say::hello_world;
fn main() {
hello_world!();
}
fn main() {
let result=product!(1, 2);
let result2=product!(3, 4, 5);
Derive macros
Attribute macros
Function-like macros
[lib]
proc-macro=true
The syn and quote crates are optional helper crates for
procedural macros.
The quote crate contains the quote macro! that can convert
source code, as a string, into a sequence of tokens. You can call
the into function to convert the tokens to a TokenStream .
Alternatively, the TokenStream::from function converts tokens
to a TokenStream . The quote! macro is an alternative to
TokenStream::FromStr .
Derive Macros
structs
unions
enums
#[proc_macro_derive(Hello)]
pub fn hello(input: TokenStream) -> TokenStre
r##"fn hello_world(){ println!("Hello, wo
}"##.parse().unwrap()
}
use hello::Hello;
#[derive(Hello)]
struct Bob;
fn main() {
hello_world();
}
#[proc_macro_derive(Hello)]
pub fn hello(input: TokenStream) -> TokenStre
let token_string = input.to_string();
let derive_input =
syn::parse_derive_input(&token_string).unwrap();
let name=&derive_input.ident;
let code=format!(r##"impl Hello for {name
fn hello_world() {{
println!("Hello {name}");
}}
}}"##);
code.parse().unwrap()
}
trait Hello {
fn hello_world();
}
#[derive(Hello)]
struct Bob;
fn main() {
Bob::hello_world(); // Hello Bob
}
This is the final version of the Hello macro. The result is the
same as the previous version, which integrated the type
identifier in the greeting. In this version, we convert the input
TokenStream to a DeriveInput using the
parse_macro_input! macro. In addition, this
implementation replaces the string with the quote! macro.
The quote ! macro is easier to use when compared to a string,
especially for extended or complex macros. You may want to
access a variable from the macro function within quoted source
code. This is possible by preceding the variable with a pound
( # ).
Code Listing 21.14. Final version of the Hello macro.
use proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_derive(Hello)]
pub fn hello(input: TokenStream) -> TokenStre
let syn::DeriveInput { ident, .. } =
syn::parse_macro_input!{input};
use proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_derive(Type)]
pub fn get_type(input: TokenStream) -> TokenS
let syn::DeriveInput { ident, .. } =
syn::parse_macro_input!{input};
let tokens = quote! {
impl Type for #ident {
fn get(&self)->String {
std::any::type_name::<#ident>
}
}
};
TokenStream::from(tokens)
}
In this application, we create the Type trait.
Code Listing 21.16. Using the Type macro to display the type of a
related value.
use get_type_macro::Type;
trait Type {
fn get(&self)->String;
}
#[derive(Type)]
struct MyStruct;
fn main() {
let result=MyStruct;
println!("{}", result.get()); //MyStruct
}
Attribute Macros
#[proc_macro_attribute]
pub fn macro_name( parameter1: TokenStream,
parameter1: TokenStream) -> TokenStream
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro_attribute]
pub fn info( parameters: TokenStream, target
-> TokenStream {
let args=parameters.to_string();
let current=target.to_string();
let syn::DeriveInput { ident, .. } =
syn::parse_macro_input!{target};
quote!{
struct #ident{}
impl #ident {
fn describe(){
println!("Token 1: {}", #args);
println!("Token 2: {}", #current);
}
}
}.into()
}
use example_macro::info;
#[info(a,b)]
struct sample{}
fn main(){
sample::describe();
}
The result is:
Token 1: a, b
Token 2: struct sample {}
Function-like macro
#[proc_macro]
pub fn macro_name( parameter1: TokenStream,
parameter1: TokenStream) -> TokenStream
use proc_macro::TokenStream;
#[proc_macro]
pub fn create_hello(_item: TokenStream) -> To
r##"fn hello_world(){ println!("Hello,
world!");}"##.parse().unwrap()
}
use hello_macro::create_hello;
create_hello!();
fn main() {
hello_world();
}
In Conclusion
OceanofPDF.com
Chapter 22. Interoperability
Rust strings are Unicode and UTF-8 encoding. For C, the string
type can vary.
Even the Rust char is different from the C char. The Rust char is
a Unicode scalar value while the char for C supports Unicode
code points. A Unicode scalar value spans the core Unicode
character sequence. Conversely, Unicode code points can be
placed anywhere within the seven classes of Unicode values.
For this reason, you can use characters in C that are not
available in Rust.
The std::ffi module also offers types (Table 22.1) for marshaling
primitive values.
Basic Example
// Hello.c
#include <stdio.h>
void hello () {
printf("Hello, world!");
}
We compile the C source and create a static library using the
clang compiler and the LLVM tool. The library will be used to
bind the hello function implementation to our Rust program
clang hello.c -c
llvm-lib hello.o
Important
// greeter.rs
extern "C" {
fn hello();
}
fn main() {
unsafe {
hello();
}
}
It’s time to build the application. With the rustc compiler, you
can build an executable crate from a Rust source file while
linking to an external library. For the current example, this is
the rustc command that creates the executable crate.
Hello, world!
Code Listing 22.3. Build.rs file that builds Rust binary linked to C
library.
fn main() {
cc::Build::new().file("src/hello.c").comp
}
Like most Rust applications, main is the entry point for the
build application, and where the script starts executing. The
first step is to create a Build type using the Build::new
constructor. The Build::file function identifies the input
file, a C source file, to be compiled. The file::compile
function performs the actual compilation and links to the
library (.lib) file. These functions are found in the cc crate. You
must add cc, which is found in crates.io, to the build-
dependencies section in the cargo.toml file
[build-dependencies]
cc = "1.0"1
With the build process scripted within build.rs, you can call
cargo build to build the executable crate. Alternatively, you can
call cargo run to build and run.
Libc Crate
extern "C" {
fn atof(p:*const i8)->c_double;
fn atoi(p:*const i8)->c_longlong;
}
fn main() {
println!("{}", f_result);
println!("{}", i_result);
}
Structs
Until now, we have focused on interoperability with basic types,
such as integers, floats, and strings. However, you often need to
marshal compound types also, such as structs. System APIs, for
example, frequently require structs as a parameter or return
value.
Here is an example.
#[repr(C)]
pub struct astruct {
}
Structs are marshaled at their most basic level. This means
decomposing structs metaphorically into their constituent parts
first. Only then can you determine the correct marshaling. This
is a typical C struct where the constituent parts are the integers
fields, where each can be marshaled.
struct astruct {
int field1;
int field2;
int field3;
};
struct astruct {
field1:c_int,
field2:c_int,
field3:c_int,
}
// person.c
#include <stdio.h>
struct Person {
char *first;
char *last;
int age;
};
#[repr(C)]
pub struct Person{
pub first:*const i8, // C: char*
pub last:*const i8, // C: char*
pub age: c_int, // C: int
}
Here is the remainder of the Rust application.
extern "C" {
fn get_person()->*mut Person;
fn set_person(new_person:Person);
}
fn main() {
let mut person;
let new_person;
unsafe {
person=get_person();
println!("{:?}", (*person).age);
println!("{:?}", CStr::from_ptr((*person)
println!("{:?}", CStr::from_ptr((*person)
}
let first=CString::new("Sally".to_string()).u
let pfirst=first.as_ptr();
let last=CString::new("Johnson".to_string())
let plast=last.as_ptr();
new_person=Person{
first:pfirst,
last:plast,
age:12
};
unsafe {
set_person(new_person);
person=get_person();
println!("{:?}", (*person).age);
println!("{:?}", CStr::from_ptr((*person)
println!("{:?}", CStr::from_ptr((*person)
}
}
In the first unsafe block, we get and display the default value
for gPerson . Call get_person to return the default value, as
*Person . Dereference the pointer to access the Person fields.
Convert the first and last fields to string literals with the
CStr::from_ptr function. You can then display all three
fields within a println macro.
Next, we update the Person in the library with the
set_person function. Create the individual values for each
Person field; then update a new Person with the values.
Bindgen
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct tm {
pub tm_sec: ::std::os::raw::c_int,
pub tm_min: ::std::os::raw::c_int,
pub tm_hour: ::std::os::raw::c_int,
pub tm_mday: ::std::os::raw::c_int,
pub tm_mon: ::std::os::raw::c_int,
pub tm_year: ::std::os::raw::c_int,
pub tm_wday: ::std::os::raw::c_int,
pub tm_yday: ::std::os::raw::c_int,
pub tm_isdst: ::std::os::raw::c_int,
}
Code Listing 22.12. Rust code that uses time.h from C Standard
Library.
mod time;
use time::*;
use std::ffi::CStr;
fn main() {
let mut rawtime:i64=0;
let mut pTime:* mut __time64_t=&mut rawtime
unsafe {
let tm=_time64(pTime);
let ptm=&tm as *const __time64_t;
let tm2=_localtime64(ptm);
let result=asctime(tm2);
let c_str=CStr::from_ptr(result);
println!("{:#?}", c_str.to_str().unwrap()
}
}
#[no_mangle]
pub extern fn display_rust() {
println!("Greetings from Rust");
}
[lib]
name = "greeting"
crate-type = ["staticlib", "cdylib"]
// sample.h
void display_rust();
With the header file, you can now call the exported function as
a normal function.
Code Listing 22.14. C program calling Rust function.
// sample.c
#include "hello.h"
Cbindgen
#[no_mangle]
pub extern fn max3(first: i64, second: i64, t
let value= if first > second {
first
} else {
second
};
Third
}
In the cargo.lib file, cbindgen is added as a build dependency. It
will be used in build.rs during the build process. We also
request that both a static and dynamic library be generated.
Here is the final cargo.toml file.
[dependencies]
[build-dependencies]
cbindgen = "0.24.0"
[lib]
name = "example"
crate-type = ["staticlib", "cdylib"]
fn main() {
cbindgen::Builder::new()
.with_crate(".")
.generate()
.expect("Unable to generate bindings")
.write_to_file("max3.h");
}
Code Listing 22.18. The max3.h header file created with the
cbindgen tool.
// max3.h
#include <cstdarg>
#include <cstdint>
#include <cstdlib>
#include <ostream>
#include <new>
extern "C" {
This is a C++ application that calls the max3 function. For this
purpose, the header file that cbindgen generated is included.
// myapp.cpp
#include <stdio.h>
#include "max3.h"
int main() {
long answer=max3(10, 5, 7);
printf("Max value is: %ld", answe
return 0;
}
This command will compile the C++ program and link to the
Rust library.
In Conclusion
OceanofPDF.com
Chapter 23. Modules
There are two types of modules: module items and module files.
A module item is contained within a sources file, while a
module file is an external file.
Modules
Modules are declared with the mod keyword. You can declare a
module item within a source file. Here is the syntax.
mod name {
/* module items… */
}
Within the curly braces, you add items included in the module,
such as structs and other items. For visibility outside the
module, prefix the item with the pub keyword. The default is
private visibility for items. Private items are not accessible
outside of the module. This is the syntax for referencing an item
within a module.
module::item_name
fn hello() {
println!("Hello, world!");
}
mod australian {
pub fn hello(){
println!("G'day, world!");
}
}
fn main() {
hello();
australian::hello();
}
In the previous example, notice the pub prefix for the hello
function in the australian module. This makes the function
visible outside of the module. Consequently, we can call the
function in main .
mod example {
struct example {
fn main(){
|
1 | mod example {
| ----------- previous definition of the module
...
5 | struct example {
| ^^^^^^^^^^^^^^ `example` redefined here
|
= note: `example` must be defined only once in
type namespace of this module
math::algebra::addition(5, 10);
mod solar_system {
pub mod earth {
fn main() {
println!("{}",
solar_system::earth::get_name());
println!("Distance from Sun {}",
solar_system::earth::constants::DISTA
}
mod car {
impl Car {
pub fn ignition(&self) { // public
self.engine();
}
fn engine(&self) { // private
self.alternator();
println!("engine started");
}
fn alternator(&self) { // private
println!("alternator started");
}
fn throttle(&self){ // private
println!("throttle open...");
}
}
}
fn main() {
let mycar=car::Car{};
mycar.ignition();
mycar.gas_pedal();
mycar.throttle(); // does not work
}
Module Files
Modules files are dedicated to a specific module. Until now, we
have discussed module items, which are contained within a
source file. With module items, the curly braces define the
scope or perimeter of the module, within that file. With a
module file, the module spans an external file. You define a
module file in the parent module by simply using the mod
keyword and name. The external file is modname.rs .
fn main() {
hello::hello();
}
// hello.rs
pub fn hello(){
println!("Hello, world!");
}
// main.rs
mod greeting;
fn main() {
greeting::english::hello();
greeting::french::bonjour();
greeting::hindi:: ();
greeting::korean::안녕하세요();
}
The submodules for each language are declared in greeting.rs,
as shown in Listing 23.7. There will be an external file created
for each submodule, such as english.rs, french.rs, and so on.
Because the parent module is greeting, the external files are
placed in the greeting subdirectory.
// greeting.rs
pub fn hello(){
println!("Hello, world!");
}
In the greeting subdirectory, you will find the external files for
each submodule. The individual files are shown in Listing 23.8.
pub fn hello(){
println!("Hello, world");
}
// french.rs
pub fn bonjour(){
println!("Bonjour le monde!");
}
// hindi.rs
pub fn (){
println!(" !");
}
// korean.rs
pub fn 안녕하세요(){
println!("안녕, 세계!");
}
Path Attribute
With the path attribute, you can explicitly set the location of a
module, overriding the default module path. You apply the path
attribute directly to a module. The attribute names the external
file, including the directory path and where to find the module.
In Listing 23.9, the path attribute places the abc module in the
cooler.rs external file. In main, the funca is called from the
abc module.
#[path =".\\cool\\cooler.rs"]
mod abc;
fn main() {
abc::funca();
}
// cooler.rs ".\cool\cooler.rs"
pub fn funca(){
println!("Doing something!")
}
fn funca(input:bool) {
if input {
mod1::do_something();
} else {
mod2::do_something();
}
mod mod1 {
pub fn do_something(){
println!("in mod1");
}
}
mod mod2 {
pub fn do_something(){
println!("in mod1");
}
}
fn main(){
funca(true);
}
You can use the crate , super , and self keywords, with a
module path.
Code Listing 23.11. Example that includes the crate, super, and
self keywords.
mod mymod {
pub mod moda {
pub fn funca(){
crate::mymod::modb::funcb();
super::modb::funcb();
self::funcc();
}
pub fn funcc(){println!("moda::funcc"
}
Old Model
// main.rs
mod hello;
fn main(){
hello::all::hello_all();
}
Listing 23.14 shows the external file for each language module.
Each module contains a function to display the greeting in that
language.
// all.rs - crate::hello::all
pub fn hello_all(){
super::english::hello();
super::french::bonjour();
super::korean::안녕하세요();
super::hindi:: ();
}
// english.rs - crate::hello::englsh
pub fn hello(){
println!("Hello, world!");
}
// french.rs - crate::hello::french
pub fn bonjour(){
println!("Bonjour le monde!");
}
// hindi.rs - crate::hello::hindi
pub fn (){
println!(" ");
}
// korean.rs - crate::hello::korean
pub fn 안녕하세요(){
println!("안녕, 세계!");
}
Summary
Hierarchical organization
Grouping items
OceanofPDF.com
Author Bio
OceanofPDF.com