9.5 KiB
title |
---|
Rust |
Programming Rust
Chapter 3
-
Treats characters as distinct from the numeric types; a char is neither a u8 nor an i8
-
Requires array indices to be usize
-
In debug builds, rust checks for integer overflow in arithmetic. In release build, the addition would wrap to a negative number. When we want wrapping arithmetic, use methods
let x = big_val.wrapping_add(1);
-
Rust currently permits an extra trailing comma everywhere commas are used: function arguments, arrays, struct and enum definitions
Arrays, Vectors and Slices
-
Given a value v of any of
Arrays
,Vectors
orSlices
,i
inv[i]
must be ausize
value, can't use any other integer type as index -
Rust has no notion of an uninitialized array
-
The type
[T; N]
represents an array of N values, each of type T. An array's size is a constant determined at compile time, and is part of the type; you can't append new elements or shrink an array. -
The type
Vec<T>
, called a vector ofTs
, is a dynamically allocated, growable sequence of values of type T. A vector's elements live on the heap, so you can resize vectors at will. -
The types
&[T]
and&mut[T]
, called a shared slice ofTs
and mutable slice ofTs
, are references to a series of elements that are part of some other value, like an array or vector. One can think of a slice as a pointer to it's first element, together with the count of the number of elements one can access starting at that point. A mutable slice&mut [T]
lets one read and modify elements, but can't be shared; a shared slice&[T]
lets one share access among several readers, but doesn't let one modify elements. -
The useful methods one did likes to see on arrays - iterating over elements, searching, sorting, filling, filtering and so on - all appear as methods of slices, not arrays. But, Rust implicitly converts a reference to an array to a slice when searching for methods, so you can call any slice method on an array directly.
-
Slices
-
A slice, written
[T]
without specifying the length, is a region of an array or vector. Since a slice can be any length, slices cannot be stored directly in variables or passed as function arguments. -
A reference to a slice is a fat pointer: a two word value comprising a pointer to the slice's first element, and the number of elements in the slice.
-
Whereas an ordinary reference is a non-owning pointer to a single value, a reference to a slice is non-owning pointer to several values. This makes slices a good choice when you want to write a function that operates on any homogenous data series, whether stored in an array, vector, stack or heap.
-
For example, here's a function that prints a slice of numbers, one per line:
fn print (n: &[f64]) { for elt in n { println!("{}", elt); } }
-
Because the above function takes a slice reference as an argument, you can apply it to either a vector or an array.
-
String
- For creating new string at run time use
String
. - The type
&mut str
does exist, but it is not useful, since almost any operation on UTF-8 can change its overall byte length, and a slice cannot reallocate its referent.
Chapter 4 - Ownership
- As a rule of thumb, any type that needs to do something special when a value is dropped cannot be copy.
- By default,
struct
andenum
types are notCopy
.
Reference as Values
-
In C++, references are created implicitly by conversion, and dereferenced implicitly too.
let x = 10; int &r = x; assert (r == 10); r = 20;
-
In Rust, references are created explicity with the
&
operator, and deferenced explicity with the*
operator.let x = 10; let r = &x; assert!(*r == 10);
-
Since references are widely used in Rust, the
.
operator implicitly deferences its left operand. -
The
.
operator can also implicity borrow a reference to its left operand, if needed for a method call. -
Where as C++ converts implicitly between references and lvalues (that is, expressions referring to locations in memory), with these conversions appearing anywhere they are needed, in Rust you use the
&
and*
operators to create and follow references, with the exception of.
operator, which borrows and dereferences implicitly. -
In C++, assigning to a reference stores the value in its referent. There is no way to point a C++ reference to a location other than the one it was initialized with.
References to References
- Rust permits references to references.
struct Point { x: i32, y: i32 }
let point = Point { x: 1000, y: 729 };
let r: &Point = &point;
let rr: &&Point = &r;
let rrr: &&&Point = &rr;
- The
.
operator follows as many references as it takes to find its target. So an expressionrrr.y
, guided by the type ofrrr
, actually traverses three references to get to thePoint
before fetching itsy
field.
Sharing vs Mutation
- Shared access is read only access.
- Mutable access is exclusive access.
Chapter 6 - Expressions
Functions and Method calls
One quirk of Rust syntax is that in a function call or method call, the usual syntax for generic types, Vec<T>
, does not work.
return Vec<i32>::with_capacity(1000); // Error: Something about chained comparisons
let ramp = (0 .. n).collect<Vec<i32>>(); // Same error
The problems is that in expressions, <
is the less-than operator. The Rust compiler helpfully suggests writing ::<T>
in this case, and that solves the problem:
return Vec::<i32>::with_capacity(1000);
let ramp = (0 .. n).collect::<Vec<i32>>();
The symbol ::<...>
is affectionately known in the Rust community as the turbofish.
Alternatively, it is often possible to drop the type parameters and let Rust infer them.
return Vec::with_capacity(1000);
let ramp : Vec<i32> = (0 .. n).collect();
It's considered good style to omit the types whenever they can be inferred.
Fields and elements
Rust ranges are half open: they include the start value, if any, but not the end value.
Reference operators
The unary *
operator is used to access the value pointed to by a reference. Rust automatically follows references when one uses the .
operator to access a field or method, so the *
operator is necessary only when we want to read or write the entire value that the reference points to.
For example, sometimes an iterator produces references, but the program needs the underlying values:
let padovan: Vec<u64> = compute_padovan_sequence(n);
for elem in &padovan {
draw_triangle(turtle, *elem);
}
In this example, the type of elem
is &u64
, so *elem
is a u64
.
Chapter 7 - Error Handling
Result Type Aliases
Sometimes you will see Rust documentation that seems to omit the error type of a Result
:
fn remove_fule(path: &Path) -> Result<()>
This means a Result
type alias is being used.
A type alias is a kind of shorthand for type names. Modules often define a Result
type alias to avoid having to repeat an error type that's used consistently by almost every function in the module. For example, the standard library's std::io
module includes this line of code.
pub type Result<T> = result::Result<T, Error>;
This defines a public type std::io::Result<T>
. It's an alias for Result<T, E>
, but hardcoding std::io::Error
as the error type. In practical terms, this means that if you write use std::io
, then Rust will understand io::Result<String>
as shorthand for Result<String, io::Error>
.
When something like Result<>
appears in the online documentation, you can click on the identifier Result
to see which type alias is being used and learn the error type. In practice, it's usually obvious from the context.
Chapter 8 - Crates and Modules
Chapter 9 - Structs
The fact that any type can have methods is one reason Rust doesn't use the term object
much, preferring to call everything a value
.
Chapter 10 - Enums and Patterns
Chapter 11 - Traits and Generics
Chapter 13 - Utility Traits
-
Drop
- If a type implements
Drop
, it cannot implement theCopy
trait.
- If a type implements
-
Deref and DerefMut
- Rust doesn't try deref coercions to satisfy type variable bounds.
Chapter 14 - Closures
-
Rust can infer the argument type and return type from how the closure is used.
-
Closures do not have the same type as functions.
-
Every closure has its own type, because a closure may contain data: values either borrowed or stolen from enclosing scopes. This can be any number of variables, in any combination of types. So every closure has an ad hoc type created by the compiler, large enough to hold that data. No two closures have exactly the same type. But every closure implements a
Fn
trait. -
Any closure that requires
mut
access to a vvalue, but doesn't drop any values, is aFnMut
closure. -
Closures summary
-
Fn
is the family of closures and functions that one can call multiple times without restriction. This highest category also includes all functions. -
FnMut
is the family of closures that can be called multiple times if the closure itself is declaredmut
. -
FnOnce
is the family of closures that can be called once, if the caller owns the closure.
-