A Tour of Rust
Contents
TL;DR
enum Result<T, E> {
Ok(T),
Err(E),
}
A Result
value is one of two variants:
- A value written
Ok(v)
, indicating that the parse succeeded andv
is the value produced- If the result is
Ok(v)
,expect
simply returnsv
itself.
- If the result is
- A value written
Err(e)
, indicating that the parse failed ande
is an error value explaining why.- If the result is an
Err(e)
,expect
prints a message that includes a description ofe
and exits the program immediately.
- If the result is an
Rust does not have exceptions: all errors are handled using either Result
or panic.
enum Option<T> {
None,
Some(T),
}
Option
is an enumerated type, often called an enum, because its definition enumerates several variants that a value of this type could be: for any typeT
, a value of typeOption<T>
is eitherSome(v)
, wherev
is a value of typeT
, orNone
, indicating noT
value is available.Option
is a generic type.
Hello Rust
The best way to install Rust is to use rustup
.
# https://rustup.rs/
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo --version
# cargo 1.65.0 (4bc8f24d3 2022-10-20)
rustc --version
# rustc 1.65.0 (897e37553 2022-11-02)
rustdoc --version
# rustdoc 1.65.0 (897e37553 2022-11-02)
# 安装特定版本
rustup install 1.49.0
# 切版本
rustup default 1.49.0
cargo --version
# cargo 1.49.0 (d00d64df9 2020-12-05)
rustc --version
# rustc 1.49.0 (e1884a8e3 2020-12-29)
rustdoc --version
# rustdoc 1.49.0 (e1884a8e3 2020-12-29)
Rust commands:
cargo
is Rust’s compilation manager, package manager, and general-purpose tool.- You can use Cargo to start a new project, build and run your program, and manage any external libraries your code depends on.
rustc
is the Rust compiler.- Usually we let Cargo invoke the compiler for us, but sometimes it’s useful to run it directly.
rustdoc
is the Rust documentation tool.- If you write documentation in comments of the appropriate form in your program’s source code,
rustdoc
can build nicely formatted HTML from them. - Like
rustc
, we usually let Cargo runrustdoc
for us. ///
are documentation comments; therustdoc
utility knows how to parse them, together with the code they describe, and produce online documentation.
- If you write documentation in comments of the appropriate form in your program’s source code,
cargo
in action:
-
cargo new repo_name
creates a Rust package with some standard metadata.Cargo.toml
holds metadata for the package.- Use
--vsc none
to skip the step of setting git metadata. - Use
--lib
flag to create a library crate.
-
Invoke the
cargo run
command from any directory in the package to build and run our program.- Cargo places the executable in the
target
subdirectory at the top of the package.
- Cargo places the executable in the
-
cargo clean
cleans up the generated files.cargo new hello cd hello cargo run # Finished dev [unoptimized + debuginfo] target(s) in 0.03s # Running `target/debug/hello` # Hello, world! cargo clean # vscode format on save: install rust-analyzer and add the following to settings # "[rust]": { # "editor.defaultFormatter": "rust-lang.rust-analyzer" # }
Functions
The fn
keyword (pronounced “fun”) introduces a function; the ->
token precedes the return type.
fn gcd(mut n: u64, mut m: u64) -> u64 {
assert!(n != 0 && m != 0);
while m != 0 {
if m < n {
let t = m;
m = n;
n = t;
}
m = m % n;
}
n
}
- By default, once a variable is initialized, its value can’t be changed; placing the
mut
keyword (pronounced “mute,” short for mutable) before the parametersn
andm
allows our function body to assign to them.- In practice, most variables don’t get assigned to.
- The function’s body starts with a call to the
assert!
macro.- The
!
character marks this as a macro invocation, not a function call. - Rust’s
assert!
checks that its argument is true, and if it is not, terminates the program with a helpful message including the source location of the failing check; this kind of abrupt termination is called a panic. - Unlike C and C++, in which assertions can be skipped, Rust always checks assertions regardless of how the program was compiled.
- There is also a
debug_assert!
macro, whose assertions are skipped when the program is compiled for speed.
- The
- A
let
statement declares a local variable.- We don’t need to write out
t
’s type, as long as Rust can infer it from how the variable is used.- We can spell out
t
’s type like this:let t: u64 = m;
- We can spell out
- Rust only infers types within function bodies: you must write out the types of function parameters and return values.
- We don’t need to write out
- Rust has a
return
statement.-
If a function body ends with an expression that is not followed by a semicolon, that’s the function’s return value.
- It’s typical in Rust to use this form to establish the function’s value when control “falls off the end” of the function, and use
return
statements only for explicit early returns from the midst of a function.
- It’s typical in Rust to use this form to establish the function’s value when control “falls off the end” of the function, and use
-
Any block surrounded by curly braces can function as an expression.
{ println!("evaluating cos x"); x.cos() }
- This is an expression that prints a message and then yields
x.cos()
as its value.
- This is an expression that prints a message and then yields
-
Four-space indentation is standard Rust style.
Unit Tests
Rust has simple support for testing built into the language.
#[test]
fn test_gcd() {
assert_eq!(gcd(14, 15), 1);
assert_eq!(gcd(2 * 3 * 5 * 11 * 17, 3 * 7 * 11 * 13 * 19), 3 * 11);
}
- The
#[test]
atop the definition markstest_gcd
as a test function, to be skipped in normal compilations, but included and called automatically if we run our program with thecargo test
command. - We can have test functions scattered throughout our source tree, placed next to the code they exercise, and
cargo test
will automatically gather them up and run them all.
The #[test]
marker is an example of an attribute.
- Attributes are an open-ended system for marking functions and other declarations with extra information (like attributes in C++ and C#, or annotations in Java).
- They’re used to control compiler warnings and code style checks, include code conditionally (like
#ifdef
in C and C++), tell Rust how to interact with code written in other languages, and so on.
Handling Command-line Arguments
// bring in trait
use std::str::FromStr;
// bring in module
use std::env;
fn main() {
let mut numbers = Vec::new(); // Vec<u64> inferred: push u64; gcd(d, )
for arg in env::args().skip(1) {
numbers.push(u64::from_str(&arg).expect("error parsing argument"));
}
if numbers.len() == 0 {
eprintln!("Usage: gcd NUMBER ...");
std::process::exit(1);
}
let mut d = numbers[0];
for m in &numbers[1..] {
d = gcd(d, *m);
}
println!("The greatest common divisor of {:?} is {}", numbers, d);
}
- A trait is a collection of methods that types can implement. The first
use
declaration brings the standard library traitFromStr
into scope.- Any type that implements the
FromStr
trait has afrom_str
method that tries to parse a value of that type from a string. Theu64
type implementsFromStr
. - Although we never use the name
FromStr
elsewhere in the program, a trait must be in scope in order to use its methods (from_str
).
- Any type that implements the
- The second
use
declaration brings in thestd::env
module that provides several useful functions and types for interacting with the execution environment (including theargs
function). Vec
is Rust’s growable vector type, analogous to C++’sstd::vector
, a Python list, or a JavaScript array.- Even though vectors are designed to be grown and shrunk dynamically, we must still mark the variable
mut
for Rust to let us push numbers onto the end of it.
- Even though vectors are designed to be grown and shrunk dynamically, we must still mark the variable
- The
std::env
module’sargs
function returns an iterator, a value that produces each argument on demand, and indicates when we’re done.- The first value produced by the iterator returned by
args
is always the name of the program being run. - The iterator’s
skip
method produces a new iterator that omits that first value.
- The first value produced by the iterator returned by
u64::from_str
attempts to parsearg
as an unsigned 64-bit integer.- Rather than a method we’re invoking on some
u64
value we have at hand,u64::from_str
is a function associated with theu64
type, akin to a static method in C++ or Java.
- Rather than a method we’re invoking on some
- The
eprintln!
macro writes our error message to the standard error output stream. - The
&
operator in&numbers[1..]
borrows a reference to the vector’s elements from the second onward.- We are telling Rust that ownership of the vector remains with numbers; we are merely borrowing its elements for the loop.
- The
*
operator in*m
dereferencesm
, yielding the value it refers to. - The
println!
macro takes a template string, substitutes formatted versions of the remaining arguments for the{...}
forms as they appear in the template string, and writes the result to the standard output stream. - Rust assumes that if
main
returns at all, the program finished successfully.- Only by explicitly calling functions like
expect
orstd::process::exit
can we cause the program to terminate with an error status code. - C and C++ require
main
to return zero if the program finished successfully, or a nonzero exit status if something went wrong.
- Only by explicitly calling functions like
from_str
returns a Result
value that indicates whether the parse succeeded or failed.
enum Result<T, E> {
Ok(T),
Err(E),
}
A Result
value is one of two variants:
- A value written
Ok(v)
, indicating that the parse succeeded andv
is the value produced- If the result is
Ok(v)
,expect
simply returnsv
itself.
- If the result is
- A value written
Err(e)
, indicating that the parse failed ande
is an error value explaining why.- If the result is an
Err(e)
,expect
prints a message that includes a description ofe
and exits the program immediately.
- If the result is an
Rust does not have exceptions: all errors are handled using either Result
or panic.
View the standard library documentation in your browser with rustup doc --std
.
Serving Pages to the Web
A Rust package, whether a library or an executable, is called a crate. We need only name those crates directly in our Cargo.toml
; cargo
takes care of bringing in whatever other crates those need in turn.
When we simply request version "1"
of a crate in a Cargo.toml
file, Cargo will use the newest available version of the crate before 2.0
.
[package]
name = "actix-gcd"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web = "1.0.8"
serde = { version = "1.0", features = ["derive"] }
use actix_web::{web, App, HttpResponse, HttpServer};
fn main() {
// the whole part inside parentheses is a closure
let server = HttpServer::new(|| {
App::new()
.route("/", web::get().to(get_index))
.route("gcd", web::post().to(post_gcd))
});
println!("Serving on http://localhost:3000...");
server
.bind("127.0.0.1:3000")
.expect("error binding server to address")
.run()
.expect("error running server");
}
fn get_index() -> HttpResponse {
HttpResponse::Ok().content_type("text/html").body(
r#"
<title>GCD Calculator</title>
<form action="/gcd" method="post">
<input type="text" name="n"/>
<input type="text" name="m"/>
<button type="submit">Compute GCD</button>
</form>
"#,
)
}
#[derive(Deserialize)]
struct GcdParameters {
n: u64,
m: u64,
}
fn post_gcd(form: web::Form<GcdParameters>) -> HttpResponse {
if form.n == 0 || form.m == 0 {
return HttpResponse::BadRequest()
.content_type("text/html")
.body("Computing the GCD with zero is boring.");
}
let response = format!(
"The greatest common divisor of the numbers {} and {} \
is <b>{}</b>\n",
form.n,
form.m,
gcd(form.n, form.m)
);
HttpResponse::Ok().content_type("text/html").body(response)
}
- When we write use
actix_web::{...}
, each of the names listed inside the curly brackets becomes directly usable in our code; instead of having to spell out the full nameactix_web::HttpResponse
each time we use it, we can simply refer to it asHttpResponse
. || { App::new() ... }
is a Rust closure expression. A closure is a value that can be called as if it were a function.- This closure takes no arguments, but if it did, their names would appear between the
||
vertical bars. - The
{ ... }
is the body of the closure.
- This closure takes no arguments, but if it did, their names would appear between the
- When we start our server, Actix starts a pool of threads to handle incoming requests. Each thread calls our closure to get a fresh copy of the
App
value that tells it how to route and handle requests.- The closure calls
App::new
to create a new, emptyApp
and then calls itsroute
method to add routes for paths. - The
route
method returns the sameApp
it was invoked on, now enhanced with the new route. - Since there’s no semicolon at the end of the closure’s body, the
App
is the closure’s return value, ready for theHttpServer
thread to use.
- The closure calls
- Rust “raw string” syntax: the letter
r
, zero or more hash marks (that is, the#
character), a double quote, and then the contents of the string, terminated by another double quote followed by the same number of hash marks.- Any character may occur within a raw string without being escaped.
- We can always ensure the string ends where we intend by using more hash marks around the quotes than ever appear in the text.
- Placing a
#[derive(Deserialize)]
attribute above a type definition tells theserde
crate to examine the type when the program is compiled and automatically generate code to parse a value of this type from data in the format that HTML forms use forPOST
requests.- Actix knows how to extract a value of any type
web::Form<T>
from an HTTP request if, and only if,T
can be deserialized from HTML formPOST
data. - These relationships between types and functions are all worked out at compile time; if you write a handler function with an argument type that Actix doesn’t know how to handle, the Rust compiler lets you know of your mistake immediately.
- Actix knows how to extract a value of any type
- The
format!
macro is just like theprintln!
macro, except that instead of writing the text to the standard output, it returns it as a string.
Concurrency
use num::Complex;
fn complex_square_add_loop(c: Complex<f64>) {
let mut z = Complex { re: 0.0, im: 0.0 };
// In real life, Rust can see that z is never used for anything and so might not bother computing its value.
loop {
z = z * z + c;
}
}
/// Try to determine if `c` is in the Mandelbrot set, using at most `limit`
/// iterations to decide.
///
/// If `c` is not a member, return `Some(i)`, where `i` is the number of
/// iterations it took for `c` to leave the circle of radius 2 centered on the
/// origin. If `c` seems to be a member (more precisely, if we reached the
/// iteration limit without being able to prove that `c` is not a member),
/// return `None`.
fn escape_time(c: Complex<f64>, limit: usize) -> Option<usize> {
let mut z = Complex { re: 0.0, im: 0.0 };
for i in 0..limit {
if z.norm_sqr() > 4.0 {
return Some(i);
}
z = z * z + c;
}
None
}
- The Mandelbrot set is defined as the set of complex numbers
c
for whichz
does not fly out to infinity. Some experimentation shows that ifc
is greater than 0.25 or less than –2.0, thenz
eventually becomes infinitely large; otherwise, it stays somewhere in the neighborhood of zero. - Since a complex number
c
has both real and imaginary componentsc.re
andc.im
, we’ll treat these as thex
andy
coordinates of a point on the Cartesian plane, and color the point black ifc
is in the Mandelbrot set, or a lighter color otherwise. So for each pixel in our image, we must run the preceding loop on the corresponding point on the complex plane, see whether it escapes to infinity or orbits around the origin forever, and color it accordingly.- If we give up on running the loop forever and just try some limited number of iterations, it turns out that we still get a decent approximation of the set. How many iterations we need depends on how precisely we want to plot the boundary.
- It’s been shown that, if
z
ever once leaves the circle of radius 2 centered at the origin, it will definitely fly infinitely far away from the origin eventually.
enum Option<T> {
None,
Some(T),
}
Option
is an enumerated type, often called an enum, because its definition enumerates several variants that a value of this type could be: for any typeT
, a value of typeOption<T>
is eitherSome(v)
, wherev
is a value of typeT
, orNone
, indicating noT
value is available.Option
is a generic type.
struct Complex<T> {
/// Real portion of the complex number
re: T,
/// Imaginary portion of the complex number
im: T,
}
- Read the
<T>
after the type name (Complex<T>
) as “for any typeT
.”
use std::str::FromStr;
/// Parse the string `s` as a coordinate pair, like `"400x600"` or `"1.0,0.5"`.
///
/// Specifically, `s` should have the form <left><sep><right>, where <sep> is
/// the character given by the `separator` argument, and <left> and <right> are
/// both strings that can be parsed by `T::from_str`. `separator` must be an
/// ASCII character.
///
/// If `s` has the proper form, return `Some<(x, y)>`. If it doesn't parse
/// correctly, return `None`.
fn parse_pair<T: FromStr>(s: &str, separator: char) -> Option<(T, T)> {
match s.find(separator) {
None => None,
// index is the separator’s position in the string
Some(index) => {
match (T::from_str(&s[..index]), T::from_str(&s[index + 1..])) {
// this pattern matches only if both elements of the tuple are Ok variants of the Result type
(Ok(l), Ok(r)) => Some((l, r)),
_ => None
}
}
}
}
#[test]
fn test_parse_pair() {
assert_eq!(parse_pair::<i32>("", ','), None);
assert_eq!(parse_pair::<i32>("10,", ','), None);
assert_eq!(parse_pair::<i32>(",10", ','), None);
assert_eq!(parse_pair::<i32>("10,20", ','), Some((10, 20)));
assert_eq!(parse_pair::<i32>("10,20xy", ','), None);
assert_eq!(parse_pair::<f64>("0.5x", 'x'), None);
assert_eq!(parse_pair::<f64>("0.5x1.5", 'x'), Some((0.5, 1.5)));
}
/// Parse a pair of floating-point numbers separated by a comma as a complex
/// number.
fn parse_complex(s: &str) -> Option<Complex<f64>> {
match parse_pair(s, ',') {
Some((re, im)) => Some(Complex { re, im }),
None => None
}
}
#[test]
fn test_parse_complex() {
assert_eq!(parse_complex("1.25,-0.0625"),
Some(Complex { re: 1.25, im: -0.0625 }));
assert_eq!(parse_complex(",-0.0625"), None);
}
parse_pair
is a generic function. Read the<T: FromStr>
as “For any typeT
that implements theFromStr
trait…”- This effectively lets us define an entire family of functions at once:
parse_pair::<i32>
is a function that parses pairs ofi32
values,parse_pair::<f64>
parsespairs
of floating-point values, and so on.
- This effectively lets us define an entire family of functions at once:
T
a type parameter ofparse_pair
. When you use a generic function, Rust will often infer type parameters of a generic function.- This is very much like a function template in C++.
- The
parse_pair
function doesn’t use an explicit return statement, so its return value is the value of the last (and the only) expression in its body. - The wildcard pattern
_
matches anything and ignores its value.
fn pixel_to_point(bounds: (usize, usize),
pixel: (usize, usize),
upper_left: Complex<f64>,
lower_right: Complex<f64>)
-> Complex<f64>
pixel.0
refers to the first element of the tuplepixel
.pixel.0 as f64
convertspixel.0
to anf64
value.- Rust generally refuses to convert between numeric types implicitly.
The unit type (zero-tuple) ()
has only one value, also written ()
. The unit type is akin to void
in C and C++.
Handle File::create
’s result:
let output = match File::create(filename) {
Ok(f) => f,
Err(e) => {
return Err(e);
}
};
// shorthand
let output = File::create(filename)?;
- On success, let
output
be theFile
carried in theOk
value. On failure, pass along the error to the caller. - This kind of
match
statement is such a common pattern in Rust that the language provides the?
operator as shorthand for the whole thing.- Attempting to use
?
in themain
function won’t work because it doesn’t return a value. Use amatch
statement, or one of the shorthand methods likeunwrap
andexpect
. There’s also the option of simply changingmain
to return aResult
.
- Attempting to use
The macro call vec![v; n]
creates a vector n
elements long whose elements are initialized to v
.
The crossbeam
crate provides a number of valuable concurrency facilities.
crossbeam::scope(|spawner| {
// ...
}).unwrap();
spawner.spawn(move |_| {
render(band, band_bounds, band_upper_left, band_lower_right);
});
- The argument
|spawner| { ... }
is a Rust closure that expects a single argument,spawner
.- Unlike functions declared with
fn
, we don’t need to declare the types of a closure’s arguments; Rust will infer them, along with its return type. crossbeam::scope
calls the closure, passing as thespawner
argument a value the closure can use to create new threads.- 调用方传
spawner
参数给闭包,这一参数不用手动创建。
- 调用方传
- The
crossbeam::scope
function waits for all such threads to finish execution before returning itself.
- Unlike functions declared with
- The
move
keyword at the front indicates that this closure takes ownership of the variables it uses.- The argument list
|_|
means that the closure takes one argument, which it doesn’t use (another spawner for making nested threads).
- The argument list
The num_cpus
crate provides a function that returns the number of CPUs available on the current system.
Filesystems and Command-Line Tools
#[derive(Debug)]
struct Arguments {
target: String,
replacement: String,
filename: String,
output: String,
}
fn main() {
let args = parse_args();
println!("{:?}", args);
// println!("{}", args);
// = help: the trait `std::fmt::Display` is not implemented for `Arguments`
// = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
}
// cargo run "find" "replace" file output
- The
#[derive(Debug)]
attribute tells the compiler to generate some extra code that allows us to format theArguments
struct with{:?}
inprintln!
.
use text_colorizer::*;
fn print_usage() {
eprintln!("{} - change occurrences of one string into another",
"quickreplace".green());
eprintln!("Usage: quickreplace <target> <replacement> <INPUT> <OUTPUT>");
}
text-colorizer
creates colorful output in the terminal.
use regex::Regex;
fn replace(target: &str, replacement: &str, text: &str)
-> Result<String, regex::Error>
{
let regex = Regex::new(target)?;
Ok(regex.replace_all(text, replacement).to_string())
}
Regex::new
compiles the user-provided regex, and it can fail if given an invalid string.- If
replace_all
finds matches, it returns a newString
with those matches replaced with the text we gave it. Otherwise,replace_all
returns a pointer to the original text, avoiding unnecessary memory allocation and copying. In this case, however, we always want an independent copy, so we use theto_string
method to get aString
in either case and return that string wrapped inResult::Ok
, as in the other functions.
References