ted.neward@newardassociates.com | Blog: http://blogs.newardassociates.com | Github: tedneward | LinkedIn: tedneward
Get a big-picture overview
Learn how to get started
Explore some core parts of the language
C-derived
native-compilation
procedural
object-ish
systems-level
memory-safe
C-derived
curly brackets and semi-colons
native-compilation
single, independent executable artifact
procedural
named functions, declared parameters, single-entry
object-ish
some facilities for working with objects as first-class constructs
systems-level
we can see the pointers and addresses (if we want)
memory-safe
compiler verification/validation to prevent memory-based bugs
use "rustup"
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
maintain with rustup update
make sure .cargo/bin
in home directory is on the PATH
Homebrew: brew install rust
Chocolatey: choco install rust
Windows: future MSI support (someday)
rustc --help
returns compiler installed version
cargo --version
returns build tool installed version
create project
modify scaffolded entry file
compile and run
cargo new
PATH
creates new project (package) in PATH directory
cargo init
creates new package in an existing directory
either command creates the same scaffold
Cargo.toml file (manifest)
src/ directory
src/main.rs entry source file
main.rs
fn main() { println!("Hello, world!"); }
Cargo.toml
[package] name = "hello" version = "0.1.0" edition = "2021" # See more keys and their definitions at # https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies]
cargo build
produces built artifact in target
subdirectory
cargo run
compiles and runs target
cargo test
compiles and runs unit tests
(of which there are none in the default project)
a calculator...
cardinal math operations (+, -, *, /)
... library...
so it can be linked in from elsewhere
... with tests
cargo init --lib --name Calc
whoops! "warning: the name Calc
is not snake_case or kebab-case which is recommended for package names, consider calc
"
modify the "name" in Cargo.toml
(or re-gen the project)
src/lib.rs instead of main.rs
includes tests!
lib.rs
pub fn add(left: usize, right: usize) -> usize { left + right } #[cfg(test)] mod tests { use super::*; #[test] fn it_works() { let result = add(2, 2); assert_eq!(result, 4); } }
Rust is a procedural systems-level language
looking to "fix" many of the problems in other languages
and incorporate some ideas popular in language design
{
... }
denotes scope blocks
semicolon-terminated (except in certain cases)
identifiers prefer "snake case" or "kebab case"
strong C influence here
Basics
pub fn add(left: usize, right: usize) -> usize { left + right }
set off by curly braces: {
and }
last expression in a block is its return value
comments are double-slash end-of-line
documentation comments are triple-slash end-of-line
use Markdown for contents
generate HTML using rustdoc
Comments
// This is a standard comment /// This is a documentation comment. /// It supports Markdown syntax. /// /// # Examples /// /// ``` /// let arg = 5; /// let answer = rust_basics::add(5, arg); /// /// assert_eq!(10, answer); /// ``` /// /// NOTE: rust_doc will sanity-check the code inside /// the Examples part of these comments! /// NOTE: cargo test will test the code here too!
statically-typed
scalar
compound
integer (8-, 16-, 32-, 64-, 128-bit), signed/unsigned
i8
, i16
, i32
, i64
, i128
: signed
u8
, u16
, u32
, u64
, u128
: unsigned
isize
, usize
: word-sized
floating-point (32-, 64-bit)
f32
(single-precision), f64
(double-precision)
default is f64
Boolean (true
, false
)
character (Unicode scalar value)
Scalars
pub fn math_on_scalars() { // addition let _sum = 5 + 10; // unused, so prefix with "_" // subtraction let _difference = 95.5 - 4.3; // multiplication let _product = 4 * 30; // division let _quotient = 56.7 / 32.2; let _truncated = -5 / 3; // Results in -1 // remainder/modulo let _remainder = 43 % 5; }
let
keyword
type-inferenced, and/or type-suffixed
immutable by default; use mut
modifier for mutability
constants declared with const
instead of let
Variables
pub fn immutable_x() -> i32 { let x = 5; //x = 6; // ERROR! println!("The value of x is: {x}"); return x; } pub fn redeclaring_x() -> i32 { let x = 5; println!("The value of x is: {x}"); let x = x + 1; println!("The value of x is: {x}"); return x; } pub fn mutable_x() -> i32 { let mut x = 5; println!("The value of x is: {x}"); x = 6; println!("The value of x is: {x}"); return x; }
always immutable; incompatible with mut
declared with const
instead of let
must be type-annotated
must be set to a constant compile-time-known expression
arrays: sequence of values in contiguous storage
tuples: collection of values in a single binding
structs: colleciton of values in a single binding
if
boolean-condition block
optional else
block
chain via else if
if
/else
is an expression
evaluates to last expression in executed block
as per block rules
boolean-condition must be boolean; will not auto-convert
If
fn if_example() -> i32 { let test = 12; let mut result = -1; if test % 6 == 0 { result = 1; } else { result = 0; } // shorter version: let result = if test % 6 == 0 { 1 } else { 0 }; result // or even shorter: //if test % 6 == 0 { 1 } else { 0 } }
match
value {
one-or-more-match-targets }
where match-targets look like condition =>
expression ,
list of match targets must be exhaustive!
we can do value-binding in the condition space (captures whatever the value)
or use _
to do wildcard/ignore binding
often used in conjunction with
Option
type (Some
, None
) to deal with absent values
other enumerated types (custom or out-of-the-box)
Match
enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } }
Match with Option type
fn might_return_nothing() -> Option<i32> { // do some complex randomization here // then always return 42 Some(42) } fn get_something_from_nothing() -> i32{ match might_return_nothing() { Some(x) => { println!("We got {}", x); return x; }, None => { println!("Nope, nada"); return 0; } } }
infinite loop
break using break
expression; return result with break
Loop
fn loop_example() -> i32 { let mut counter = 0; let result = loop { println!("Looping on {counter}..."); counter += 1; if counter == 10 { break counter * 2; } }; return result; }
while
boolean-condition block
tests at top of loop, breaks when false
Loop
fn while_countdown() { let mut number = 3; while number != 0 { println!("{number}!"); number -= 1; }; println!("LIFTOFF!!!"); }
for
value in
source block
source can be collection or range value
binds each individual element to value and executes block
Loop
fn for_examples() { let a = [10, 20, 30, 40, 50]; for element in a { println!("the value is: {element}"); } // Use a Range object, reversed for number in (1..4).rev() { println!("{number}!"); } println!("LIFTOFF!!!"); }
(optional) pub
modifier
makes function visible outside of the module
fn
keyword
named using "snake-case" (underscore separation)
parens (optional paramters)
(optional) arrow and return type
scope/block
(optional) return value
explicit return
keyword and value
last expression in scope/block
Function example
fn function_call() { let x = plus_one(5); println!("The value of x is: {x}"); } fn plus_one(x: i32) -> i32 { x + 1 // NOTE: semicolon would make this a statement // and therefore not an implicit return value // and therefore an error! //return x + 1 // NOTE: acceptable //return x + 1; // NOTE: acceptable
"Statements are instructions that perform some action and do not return a value."
"Expressions evaluate to a resultant value."
(from https://doc.rust-lang.org/stable/book/ch03-03-how-functions-work.html#statements-and-expressions)
let
is a statement, for an example, so cannot be chained
Rust makes this distinction much more distinct in its syntax
makes use of ||
to denote an anonymous function
parameters are still declared and strongly-typed
otherwise follow all the same rules as named functions
Closure example
fn closure_call() { let print_int = |x: i32| println!("{}", x); print_int(5); let printing_add = |l: i32, r: i32| -> i32 { println!("Adding {} and {}.", l, r); l + r // This closure can be as long as we want, just like a function. }; printing_add(5, 5); }
Rust is procedural
... and we can do a lot with just that
Procedural example
fn rectangle_area(width: u32, height: u32) -> u32 { width * height } fn use_prims() { let rect_w = 12; let rect_h = 24; let area = rectangle_area(rect_w, rect_h); println!("The area of a {rect_w} x {rect_h} rectangle is {area}"); }
Rust tuples are essentially structs with unnamed fields
still strongly typed
structurally typed
minimal ceremony
... and we can do a lot with just that
Tuple example
fn rect_tuple_area(dimensions: (u32, u32)) -> u32 { dimensions.0 * dimensions.1 } fn use_tuples() { let rect = (12, 24); let area = rect_tuple_area(rect); println!("The area of a {} x {} rectangle is {}", rect.0, rect.1, area); }
Rust supports "structs": bundles with named fields
the struct
keyword defines the "surface area" (fields)
fields are strongly typed
nominatively typed
... and we can do a lot with just that
Struct example
struct RectStruct { width: u32, height: u32, } fn rect_area(rect: &RectStruct) -> u32 { rect.width * rect.height } fn use_struct() { let rect = RectStruct{ width: 12, height: 24, }; let area = rect_area(&rect); println!("The area of a {} x {} rectangle is {}", rect.width, rect.height, area); }
the struct
keyword defines the "surface area" of the type
a secondary impl
keyword defines "methods" on the type
function must have a parameter named self
before any others
self
is implicitly typed to be a reference to the impl
-named type
make sure to use reference notation to preserve ownership
build "constructors" by defining functions that return Self
(implicit type)
these aren't "objects" in the traditional sense
no access control (unless you build accessors/mutators manually)
no implementation inheritance
... and we can do a lot with just that
Structs-and-Impls example
struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } fn new(h: u32, w: u32) -> Self { Self { height: h, width: w } } fn square(size: u32) -> Self { Self { height: size, width: size } } } fn use_object() { //let rect = Rectangle{ width: 12, height: 24, }; let rect = Rectangle::new(12, 24); println!("The area of a {} x {} rectangle is {}", rect.width, rect.height, rect.area()); let rect = Rectangle::square(12); println!("The area of a {} x {} square is {}", rect.width, rect.height, rect.area()); }
"traits" provide some implementation to structs
Display
and Debug
are two popular out-of-the-box traits
custom traits are also possible
this is still not full-blown inheritance; more like "mixins"
Structs-and-Impls-and-Traits example
#[derive(Debug)] // this is an "attribute" using the Debug "trait" struct DRectangle { width: u32, height: u32, } impl DRectangle { fn area(&self) -> u32 { self.width * self.height } } fn use_trait() { let rect = DRectangle{ width: 12, height: 24, }; dbg!(&rect); // "[src/main.rs:95:5] &rect = DRectangle { // width: 12, // height: 24, // }" printed to stderr println!("The area of a {} x {} rectangle is {}", rect.width, rect.height, rect.area()); }
"a set of rules that govern how a Rust program manages memory"
memory is managed through a system of ownership with compiler-checked rules
this helps ensure good hygiene without incurring large overhead
Stack
LIFO order (push/pop)
size must be known and fixed
parameters and local variables
Heap
no allocation order
sizes need not be known ahead of time
every variable has a scope associated with it
a lifecycle
when does it come into existence
when does it go away
what happens when it goes away
this is part of compiler's validity checking
scopes are most often associated with function/method blocks
Rust doesn't do GC
lifecyle of allocated variables must be clear
variable scope is a natural timeframe
Simple scope/lifecycle example
{ let s = String::from("hello"); // s is valid from this point forward println!("{} world", s); } // this scope is now over // s is no longer valid
general rule: stack follows scope very well
if it can't follow stack rules, allocate from the heap
but heap-allocated requires explicit allocation and deallocation steps
Scope example
let x = 20; { let y = 15; x = y; } // What is x here? What is y here?
So what should happen here?
{ let s1 = String::from("hello"); let s2 = s1; // ... more code ... }
Or here?
let s1 = String::from("hello"); { let s2 = String::from("world"); s1 = s2; }
each value in Rust has an owner
there can only be one owner at a time
when the owner goes out of scope, the value will be dropped/deallocated
if the size is known at compile time, we can store on the stack
stack is tied to scope: new scope, new stack frame
values stored in a stack frame live in exact symmetry with the scope
stack values are always copied when assigned
any heap-allocated memory must be explicitly allocated/deallocated
allocation is usually easy: on first need
but who is responsible for doing the deallocation?
this leads us to the concept of "ownership": own it? deallocate it!
parameters will either "move" or "copy"
stack values: copy
heap pointers: move (and therefore transfer ownership)
this is wildly different from GC-based languages!
Watch our string... disappear!
fn taking() { let s = String::from("hello"); // s comes into scope takes_ownership(s); // s's value moves into the function... // ... and so is no longer valid here println!("{}", s); // compile error! } fn takes_ownership(some_string: String) { // some_string comes into scope println!("{}", some_string); } // Here, some_string goes out of scope and `drop` is called. The backing // memory is freed.
Watch what Rust tells us
error[E0382]: borrow of moved value: `s` --> src/main.rs:7:20 | 2 | let s = String::from("hello"); // s comes into scope | - move occurs because `s` has type `String`, which does not implement the `Copy` trait 3 | 4 | takes_ownership(s); // s's value moves into the function... | - value moved here ... 7 | println!("{}", s); // compile error! | ^ value borrowed here after move | note: consider changing this parameter type in function `takes_ownership` to borrow instead if owning the value isn't necessary --> src/main.rs:10:33 | 10 | fn takes_ownership(some_string: String) { // some_string comes into scope | --------------- ^^^^^^ this parameter takes ownership of the value | | | in this function = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info) help: consider cloning the value if the performance cost is acceptable | 4 | takes_ownership(s.clone()); // s's value moves into the function... | ++++++++ For more information about this error, try `rustc --explain E0382`. error: could not compile `ownership` (bin "ownership") due to 1 previous error
functions can "give" ownership as well
values returned are assumed to be "moved"
Follow the ownership chain
fn takeing_and_giving() { let s1 = gives_ownership(); // move let s2 = String::from("hello"); let s3 = takes_and_gives_back(s2); // move into takes_and_gives_back, which also // moves its return value into s3 } fn gives_ownership() -> String { let some_string = String::from("yours"); some_string // move out to caller } fn takes_and_gives_back(a_string: String) -> String { a_string // moves out to caller }
example: a function that takes a string to calculate its length
by default, passing string in takes ownership
so then we have to use a tuple to return the length and string back
bleargh
C was easier!
pointers are explicit
everything is copied
function parameters passed on stack
C++: don't ask (added references)
references are pointers
... with guarantees to point to something
... and well-defined ownership semantics
use &
to prefix the type (&String
)
(kinda like C++ references!)
but references do not allow for mutability!
Implementing that string-length function
fn references_basics() { let s = String::from("hello"); let len = calculate_length(&s); // notice the "&" in the call println!("The length of '{s1}' is {len}."); } fn calculate_length(s: &String) -> usize { // notice the "&" in the declaration //s.push_str(" I am in you!"); // compile error! s.len() }
combining references with mut
reference must refer to a mutable source
syntax combines mut
and &
parameter reference must be &mut
-prefixed (&mut String
)
call site must be &mut
-prefixed
... and no two mutable references to the same source
Mutating the passed string
fn mutable_references_basics() { let mut s = String::from("hello"); change(&mut s); println!("{}", s); } fn change(some_string: &mut String) { some_string.push_str(", world"); }
At any given time, you can have either:
one mutable reference or
any number of immutable references
References must always be valid
a system-level language
... with explicit ownership and mutability semantics
... as well as some interesting language features
... and a lot(!) of interest
The Rust Book: "The Rust Programming Language"
https://doc.rust-lang.org/book/title-page.html
also available as print from No Starch Press
Architect, Engineering Manager/Leader, "force multiplier"
http://www.newardassociates.com
http://blogs.newardassociates.com
Sr Distinguished Engineer, Capital One
Educative (http://educative.io) Author
Performance Management for Engineering Managers
Books
Developer Relations Activity Patterns (w/Woodruff, et al; APress, forthcoming)
Professional F# 2.0 (w/Erickson, et al; Wrox, 2010)
Effective Enterprise Java (Addison-Wesley, 2004)
SSCLI Essentials (w/Stutz, et al; OReilly, 2003)
Server-Based Java Programming (Manning, 2000)