TL;DR
Fallible functions in Rust should return a Result
value, which is either Ok(s)
on success, where s
is the successful value, or Err(e)
on failure, where e
is an error code.
Hello Rust
The best way to install Rust is to use rustup
.
|
|
cargo
is Rust’s compilation manager, package manager, and general-purpose tool. 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.- We usually let Cargo run
rustdoc
for us. ///
are documentation comments; therustdoc
utility knows how to parse them, together with the code they describe, and produce online documentation.
- We usually let Cargo run
cargo
commands:
cargo new <repo_name>
creates a Rust package with some standard metadata.- It creates a Cargo.toml file that holds metadata for the package. If our program ever acquires dependencies on other libraries, we can record them in this file, and Cargo will take care of downloading, building, and updating those libraries for us.
- It creates a
.git
metadata subdirectory and a.gitignore
file.- Use
--vsc none
to skip the step of setting git metadata.
- Use
- The
src
subdirectory contains the actual Rust code.
- 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.
|
|
Functions
The fn
keyword (pronounced “fun”) introduces a function.
|
|
By default, once a variable is initialized, its value can’t be changed; placing the mut
keyword (pronounced “mute,” short for mutable) before the parameters n
and m
allows our function body to assign to them.
- In practice, most variables don’t get assigned to; the
mut
keyword on those that do can be a helpful hint when reading code.
The function’s body starts with a call to the assert!
macro. The !
character marks this as a macro invocation, not a function call. Like the assert macro in C and C++, 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.
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. Rust only infers types within function bodies: you must write out the types of function parameters and return values.
- We can spell out
t
’s type like this:let t: u64 = m;
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. Any block surrounded by curly braces can function as an expression.
|
|
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.
The isize
and usize
types hold pointer-sized signed and unsigned integers, 32 bits long on 32-bit platforms, and 64 bits long on 64-bit platforms.
Four-space indentation is standard Rust style.
Unit Tests
Rust has simple support for testing built into the language.
|
|
We define a function named test_gcd
. The #[test]
atop the definition marks test_gcd
as a test function, to be skipped in normal compilations, but included and called automatically if we run our program with the cargo 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
|
|
A trait is a collection of methods that types can implement. Any type that implements the FromStr
trait has a from_str
method that tries to parse a value of that type from a string. The u64
type implements FromStr
. u64::from_str
parses the command-line arguments. Although we never use the name FromStr
elsewhere in the program, a trait must be in scope in order to use its methods.
Vec
is Rust’s growable vector type, analogous to C++’s std::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.
The std::env
module provides several useful functions and types for interacting with the execution environment, including the args
function. The args
function returns an iterator, a value that produces each argument on demand, and indicates when we’re done. Iterators are ubiquitous in Rust; the standard library includes other iterators that produce the elements of a vector, the lines of a file, messages received on a communications channel, and almost anything else that makes sense to loop over. Rust’s iterators are very efficient: the compiler is usually able to translate them into the same code as a handwritten loop.
Iterators also include a broad selection of methods you can use directly. For example, 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.
u64::from_str
attempts to parse arg
as an unsigned 64-bit integer. It is a function associated with the u64
type, akin to a static method in C++ or Java. The from_str
function doesn’t return a u64
directly, but rather a Result
value that indicates whether the parse succeeded or failed. A Result
value is one of two variants:
- A value written
Ok(v)
, indicating that the parse succeeded andv
is the value produced - A value written
Err(e)
, indicating that the parse failed ande
is an error value explaining why
|
|
Functions that do anything that might fail can return Result
types whose Ok
variants carry successful results and whose Err
variants carry an error code indicating what went wrong. Unlike most modern languages, Rust does not have exceptions: all errors are handled using either Result
or panic.
We use Result
’s expect
method to check the success of our parse. If the result is Ok(v)
, expect
simply returns v
itself. If the result is an Err(e)
, expect
prints a message that includes a description of e
and exits the program immediately.
A vector could be of any size—possibly very large. Rust is cautious when handling such values: it wants to leave the programmer in control over memory consumption, making it clear how long each value lives, while still ensuring memory is freed promptly when no longer needed. When we iterate, we want to tell Rust that ownership of the vector should remain with numbers
; we are merely borrowing its elements for the loop. The &
operator in &numbers[1..]
borrows a reference to the vector’s elements from the second onward. The for
loop iterates over the referenced elements, letting m
borrow each element in succession. The *
operator in *m
dereferences m
, yielding the value it refers to. Since numbers
owns the vector, Rust automatically frees it when numbers
goes out of scope at the end of main
.
The eprintln!
macro writes our error message to the standard error output stream.
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.
C and C++ require main
to return zero if the program finished successfully, or a nonzero exit status if something went wrong. Rust assumes that if main
returns at all, the program finished successfully. Only by explicitly calling functions like expect
or std::process::exit
can we cause the program to terminate with an error status code.
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.
|
|
Rust crates that have reached version 1.0
, as these have, follow the “semantic versioning” rules: until the major version number 1
changes, newer versions should always be compatible extensions of their predecessors. So if we test our program against version 1.2
of some crate, it should still work with versions 1.3
, 1.4
, and so on; but version 2.0
could introduce incompatible changes. 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
.
We need only name those crates we’ll use directly; cargo takes care of bringing in whatever other crates those need in turn.
Crates can have optional features: parts of the interface or implementation that not all users need, but that nonetheless make sense to include in that crate. The serde
crate offers a wonderfully terse way to handle data from web forms, but according to serde
’s documentation, it is only available if we select the crate’s derive
feature, so we’ve requested it in our Cargo.toml file.
|
|
- 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 path"/"
. Theroute
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
HttpResponse::Ok()
represents an HTTP200 OK
status, indicating that the request succeeded. We call itscontent_type
andbody
methods to fill in the details of the response; each call returns theHttpResponse
it was applied to, with the modifications made.- 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.- This attribute is sufficient to let you parse a
GcdParameters
value from almost any sort of structured data: JSON, YAML, TOML, or any one of a number of other textual and binary formats.
- This attribute is sufficient to let you parse a
- For a function to serve as an Actix request handler, its arguments must all have types Actix knows how to extract from an HTTP request. 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. Since we’ve placed the#[derive(Deserialize)]
attribute onGcdParameters
type definition, Actix can deserialize it from form data, so request handlers can expect aweb::Form<GcdParameters>
value as a parameter.- 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.
- 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.
Rust programmers typically gather all their use
declarations together toward the top of the file, but this isn’t strictly necessary: Rust allows declarations to occur in any order, as long as they appear at the appropriate level of nesting.
Concurrency
The same rules that ensure Rust programs are free of memory errors also ensure threads can share memory only in ways that avoid data races. For example:
- If you use a mutex to coordinate threads making changes to a shared data structure, Rust ensures that you can’t access the data except when you’re holding the lock, and releases the lock automatically when you’re done. In C and C++, the relationship between a mutex and the data it protects is left to the comments.
- If you want to share read-only data among several threads, Rust ensures that you cannot modify the data accidentally. In C and C++, the type system can help with this, but it’s easy to get it wrong.
- If you transfer ownership of a data structure from one thread to another, Rust makes sure you have indeed relinquished all access to it. In C and C++, it’s up to you to check that nothing on the sending thread will ever touch the data again.
If your program compiles, it is free of data races. All Rust functions are thread-safe.
Consider the following loop:
|
|
- Some experimentation shows that if
c
insquare_add_loop
is greater than 0.25 or less than –2.0, thenz
eventually becomes infinitely large; otherwise, it stays somewhere in the neighborhood of zero. Complex
is a generic structure. Read the<T>
after the type name as “for any typeT
.”
The Mandelbrot set is defined as the set of complex numbers c
for which z
does not fly out to infinity. Values of c
greater than 0.25 or less than –2 cause z
to fly away. But expanding the game to complex numbers produces truly bizarre and beautiful patterns.
- 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.
Since a complex number c
has both real and imaginary components c.re
and c.im
, we’ll treat these as the x
and y
coordinates of a point on the Cartesian plane, and color the point black if c
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.
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.
The program takes several command-line arguments controlling the resolution of the image we’ll write and the portion of the Mandelbrot set the image shows.
The definition of 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 is very much like a function template in C++.
- 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, and you won’t need to write them out as we did in the test code.- 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
String
type’sfind
method searches the string for a character that matchesseparator
. Iffind
returnsNone
, meaning that the separator character doesn’t occur in the string, the entirematch
expression evaluates toNone
, indicating that the parse failed. Otherwise, we takeindex
to be the separator’s position in the string. - The wildcard pattern
_
matches anything and ignores its value. - It’s common to initialize a struct’s fields with variables of the same name, so rather than forcing you to write
Complex { re: re, im: im }
, Rust lets you simply writeComplex { re, im }
as a shorthand notation.
The program needs to work in two related coordinate spaces: each pixel in the output image corresponds to a point on the complex plane. The relationship between these two spaces depends on which portion of the Mandelbrot set we’re going to plot, and the resolution of the image requested, as determined by command-line arguments. The following function converts from image space to complex number space:
|
|
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.
To plot the Mandelbrot set, for every pixel in the image, we simply apply escape_time
to the corresponding point on the complex plane, and color the pixel depending on the result.
|
|
- If
escape_time
says that point belongs to the set,render
colors the corresponding pixel black (0
). Otherwise,render
assigns darker colors to the numbers that took longer to escape the circle.
Fallible functions in Rust should return a Result
value, which is either Ok(s)
on success, where s
is the successful value, or Err(e)
on failure, where e
is an error code.
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:
|
|
- 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
.1 2 3 4 5 6 7 8 9 10 11
fn main() { // ... match fs::write(&args.output, &data) { Ok(_) => {}, Err(e) => { eprintln!("{} failed to write to file '{}': {:?}", "Error:".red().bold(), args.filename, e); std::process::exit(1); } }; }
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.
|
|
- 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.
- Unlike functions declared with
crossbeam::scope
calls the closure, passing as thespawner
argument a value the closure can use to create new threads. Thecrossbeam::scope
function waits for all such threads to finish execution before returning itself.- 调用方传
spawner
参数给闭包,这一参数不用手动创建。
- 调用方传
- 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).- 需要嵌套创建线程时可以写成
|spawner_nested|
。
- 需要嵌套创建线程时可以写成
The num_cpus
crate provides a function that returns the number of CPUs available on the current system.
Filesystems and Command-Line Tools
Rust gives programmers a toolbox they can use to assemble slick command-line interfaces that replicate or extend the functionality of existing tools.
The #[derive(Debug)]
attribute tells the compiler to generate some extra code that allows us to format the Arguments
struct with {:?}
in println!
.
|
|