Installation
Install Rust
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
Update Rust
rustup update
Uninstall Rust
rustup self uninstall
IDE
Vscode
-
rust-analyzer: A fast, featureful language server for Rust
-
CodeLLDB: Native debugger for Rust and other languages
-
Even Better TOML: Better TOML support
-
Error Lens: Improve highlighting of errors, warnings and other language diagnostics
Cargo
-
cargo new project-name: Create a new project -
cargo build: Build the project,--releasefor release build -
cargo run: Run the project,--releasefor release build -
cargo check: Check the project -
Cargo.toml: Project configuration file
-
Cargo.lock: Dependency lock file
Basic Syntax
Variable
-
variable binding
let x = 5; -
mutable variable
let mut x = 5; -
never used variable
let _x = 5; // can use _ to ignore the warning -
variable destructuring
let (x, y) = (1, 2); -
variable shadowing
let x = 5; let x = x + 1; -
constant
const MAX_POINTS: u32 = 100_000; // can't use mut and must be type annotated
Data Type
Scalar Type
-
integer
Length Signed Unsigned 8-bit i8 u8 16-bit i16 u16 32-bit i32 u32 64-bit i64 u64 128-bit i128 u128 arch isize usize -
integer overflow
let x: u8 = 255; let y = x + 1; // if debug build, panic; if release build, two's complement wrapping
-
-
float
Length Type 32-bit f32 64-bit f64 - avoid comparing floats
-
NaN: to represent the result of an operation that can’t return a valid value
let x = 0.0 / 0.0; // NaN, you can use is_nan() to check
Math Operation
-
addition
let sum = 5 + 10; -
subtraction
let difference = 95.5 - 4.3; -
multiplication
let product = 4 * 30; -
division
let quotient = 56.7 / 32.2; -
remainder
let remainder = 43 % 5;
Logical Operation
-
and
let x = true && false; -
or
let x = true || false; -
not
let x = !true; -
bit
let x = 1 | 2; // 3 let x = 1 & 2; // 0 let x = 1 ^ 2; // 3 let x = 1 << 2; // 4 let x = 4 >> 2; // 1 let x = !1; // -2 -
Range
let x = 1..=5; // 1, 2, 3, 4, 5 let x = 1..5; // 1, 2, 3, 4 -
Use
Asto Convert Data Typelet x: i32 = 5; let y = x as i64;
Character、Boolean、Unit Type
-
character
let x = 'z'; let y = '😻'; let z = '草' println!("{}", std::mem::size_of_val(&z)); // 4 // char use `'` and string use `"` -
boolean
let x = true; let y: bool = false; -
unit type
let x = (); // usually used as placeholder, not consume memory
Statement and Expression
-
statement
let x = 5; // `let x = 5` is a statement -
expression
let y = { let x = 3; x + 1 // `x + 1` is an expression, expression don't include ending semicolon };
Function
-
function definition
fn add(x: i32, y: i32) -> i32 { x + y }- use snake case for function and variable names
- any position of the function definition is ok
- use
->to specify the return type - every parameter must have a type annotation
- return value is the last expression in the function body
- use
returnto return early - use
;to return() !is the never type, represents a function that never returns
Ownership
Move and Clone
-
principle
- each value in Rust has a variable that’s called its owner
- there can only be one owner at a time
- when the owner goes out of scope, the value will be dropped
-
stack and heap
- stack: LIFO, fixed size, fast
- heap: dynamic size, slower
-
ownership move
let s1 = String::from("hello"); let s2 = s1; // s1 is invalid, rust will directly copy basic data type, but String is not basic data type, so the ownership of s1 is moved to s2 -
deep copy
let s1 = String::from("hello"); let s2 = s1.clone(); // deep copy, use a lot of resources -
shallow copy
any type implement
Copytrait can use shallow copy. This include all the combination of basic data type and types that do not require memory or resource allocation. -
ownership and function
fn take_ownership(s: String) { println!("{}", s); } fn main() { let s = String::from("hello"); take_ownership(s); // s is invalid }fn make_copy(x: i32) { println!("{}", x); } fn main() { let x = 5; make_copy(x); // x is valid } -
ownership and return value
fn return_ownership() -> String { let s = String::from("hello"); s } fn main() { let s1 = return_ownership(); // ownership is moved to s1 }fn return_ownership() -> String { let s = String::from("hello"); &s } fn main() { let s1 = return_ownership(); // s1 is invalid, s is dropped }
Reference and Borrowing
-
reference
let x = 5; let y = &x; // y is a reference to x assert_eq!(5, x); assert_eq!(5, *y); // dereference -
immutable reference
fn calculate_length(s: &String) -> usize { s.len() } fn main() { let s = String::from("hello"); let len = calculate_length(&s); } -
mutable reference
fn change(s: &mut String) { s.push_str(", world"); } fn main() { let mut s = String::from("hello"); change(&mut s); }- In the same scope, you can only have one mutable reference to a particular piece of data.
- mutable reference can’t coexist with immutable reference.
- the scope of the reference is the last time it is used.
-
Non-lexical Lifetimes: used to find the lifetime of the reference
fn main() { let r; { let x = 5; r = &x; } println!("{}", r); // error, x is dropped } -
Dangling Reference: a reference that refers to a location in memory that may have been given to someone else, so the data has been deallocated
fn dangle() -> &String { let s = String::from("hello"); &s }
Compound Data Type
String
-
create
let s = String::from("hello"); -
update
let mut s = String::from("hello"); s.push_str(", world"); s.push('!') -
insert
let mut s = String::from("hello"); s.insert(5, ','); s.insert_str(6, " world"); -
replace
let mut s = String::from("hello, world"); s.replace("hello", "hi"); -
replacen
let mut s = String::from("hello, world"); let s1 = s.replacen("hello", "hi", 1); -
replace_range
let mut s = String::from("hello, world"); s.replace_range(0..5, "hi"); -
pop
let mut s = String::from("hello"); let x = s.pop(); // x is Some('o') -
remove
let mut s = String::from("hello"); let x = s.remove(0); // x is 'h' -
truncate
let mut s = String::from("hello"); s.truncate(3); // s is "hel" -
clear
let mut s = String::from("hello"); s.clear(); // s is "" -
concatenate
let s1 = String::from("hello"); let s2 = String::from("world"); let s3 = s1 + &s2; // s1 is invalid let s4 = format!("{}-{}", s1, s2); // s1 is valid -
slice
let s = String::from("hello, world"); let hello = &s[0..5];- use
&s[..]to get the whole string
- use
-
the index should be valid UTF-8 character boundary
let s = String::from("你好"); let s1 = &s[0..1]; // error let s2 = &s[0..3]; // ok- Char is Unicode type in Rust that is 4 bytes, but Char in String is UTF-8, so the length of the string is the number of bytes, not the number of characters, a character may be 1-4 bytes.
-
convert between string and &str
let s = String::from("hello"); // "hello".to_string() let s1: &str = &s; -
Escape Characters
let s = "hello\nworld"; let s = r"hello\nworld"; // raw string let s = r#"hello "world""#; // include "
operate UTF-8 String
-
char
let s = "你好"; let c = s.chars().nth(0); // Some('你') -
bytes
let s = "你好"; let b = s.bytes().nth(0); // Some(228) -
traverse
let s = "你好"; for c in s.chars() { println!("{}", c); }
Tuple
-
create
let x: (i32, f64, u8) = (500, 6.4, 1); -
destructure
let (x, y, z) = x; -
access
let x = x.0; -
return multiple values
fn get_point() -> (i32, i32) { (3, 5) }
Struct
-
define and create
struct User { username: String, email: String, sign_in_count: u64, active: bool, } let user = User { username: String::from("someone"), email: String::from("someone@gmail.com") sign_in_count: 1, active: true, }; -
simple create
fn build_user(username: String, email: String) -> User { User { username, email, sign_in_count: 1, active: true, } }when the field name and the variable name are the same, you can use the shorthand
-
access
let username = user.username; -
update
let user2 = User { username: String::from("someone2"), ..user }; -
Tuple Struct
struct Color(i32, i32, i32); // no field name let black = Color(0, 0, 0); -
Unit-Like Struct
struct AlwaysActive; let always_active = AlwaysActive; // we don't care about the data, just the behavior impl AlwaysActive { fn is_active(&self) -> bool { true } } -
Print Struct
#[derive(Debug)] struct User { username: String, email: String, sign_in_count: u64, active: bool, } println!("{:?}", user); // print to standard stdout println!("{:#?}", user); // pretty print dbg!(user); // Macro, print and return the value to stderr
Enum
-
define
enum IpAddr { V4(u8, u8, u8, u8), V6(String), } -
create
let home = IpAddr::V4(127, 0, 0, 1); let loopback = IpAddr::V6(String::from("::1")); -
Assimilation Type
enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } -
Option
enum Option<T> { Some(T), None, } let x = Some(5); let s = Some("hello") let y: Option<i32> = None; // must specify the type when use None
Array & Vector
-
define
let a = [1, 2, 3, 4, 5]; let a: [i32; 5] = [1, 2, 3, 4, 5]; let a = [3; 5]; // [3, 3, 3, 3, 3] -
from_fn
let arr: [String; 8] = std::array::from_fn(|_i| String::from("hello")); -
access
let first = a[0]; -
slice
let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; // [2, 3]
Control Flow
if
-
if
let x = 5; if x < 5 { println!("less than 5"); } else if x == 5 { println!("equal to 5"); } else { println!("greater than 5"); }
loop
-
for
let a = [10, 20, 30, 40, 50]; for element in a.iter() { println!("{}", element); } for idx in 0..5 { println!("{}", a[idx]); }Method Equivalent Ownership for item in collection from item in IntoIterator::into_iter(collection) collection is moved for item in &collection from item in collection.iter() collection is borrowed for item in &mut collection from item in collection.iter_mut() collection is mutably borrowed -
enumerate
let a = [10, 20, 30, 40, 50]; for (idx, element) in a.iter().enumerate() { println!("{}: {}", idx, element); } -
continue
for number in 1..=100 { if number % 3 == 0 { continue; } println!("{}", number); } -
while
let mut number = 3; while number != 0 { println!("{}", number); number -= 1; } -
loop
let mut number = 3; loop { println!("{}", number); number -= 1; if number == 0 { break; } }
Pattern Matching
match
-
match
let number = 3; match number { 1 => println!("one"), 2 | 3 => println!("two or three"), 4..=10 => println!("four to ten"), _ => println!("other"), } -
pattern destructuring
let x = Some(5); match x { Some(i) => println!("{}", i), None => (), } -
matches!
v.iter().filter(|x| matches!(x, Some(i) if i > 2)).count()
if let
-
if let
let x = Some(5); if let Some(i) = x { println!("{}", i); }when you only care about one pattern and ignore the rest, you can use
if let
while let
-
while let
let mut v = vec![Some(1), Some(2), Some(3)]; while let Some(i) = v.pop() { println!("{}", i); }
Pattern Matching Examples
Method
-
define
struct Rectangle { width: u32, height: u32, } impl Rectangle { new(width: u32, height: u32) -> Rectangle { Rectangle { width, height } } fn area(&self) -> u32 { self.width * self.height } } println!("The area of the rectangle is {} square pixels.", rect.area()); -
self
&self: borrow self&mut self: mutable borrow selfself: take ownership of self
-
related function
impl Rectangle { fn square(size: u32) -> Rectangle { Rectangle { width: size, height: size } } } let sq = Rectangle::square(3); -
enum method
enum Option<T> { Some(T), None, } impl<T> Option<T> { fn unwrap(self) -> T { match self { Some(val) => val, None => panic!("called `Option::unwrap()` on a `None` value"), } } } let x = Some(5); let v = x.unwrap();
Generics
Generics
-
define
struct Point<T> { x: T, y: T, } impl<T> Point<T> { fn x(&self) -> &T { &self.x } } let p = Point { x: 5, y: 10 };the compiler will generate the code for each type that is used in the code(single monomorphization)
-
use multiple types
struct Point<T, U> { x: T, y: U, } let p = Point { x: 5, y: 10.4 }; -
use in enum
enum Result<T, E> { Ok(T), Err(E), } enum Option<T> { Some(T), None, } -
use in function
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T { let mut largest = list[0]; for &item in list.iter() { if item > largest { largest = item; } } largest } -
define method for specific type
trait Summary { fn summarize(&self) -> String; } impl Summary for i32 { fn summarize(&self) -> String { format!("i32: {}", self) } } let x = 5; println!("{}", x.summarize());
Const Generics
-
define
struct Array<T, const N: usize> { data: [T; N], } let a = Array { data: [1, 2, 3] };
Trait
-
define
trait Summary { fn summarize(&self) -> String; } struct NewsArticle { headline: String, location: String, content: String, } impl Summary for NewsArticle { fn summarize(&self) -> String { format!("{}, by {} ({})", self.headline, self.location, self.content) } } -
orphan rule: if a trait is defined for a type, the type or trait must be defined in the current crate at least one of them
-
default implement
trait Summary { fn summarize(&self) -> String { String::from("Read more...") } } impl Summary for NewsArticle {} article.summarize(); -
use trait as parameter
fn notify(item: impl Summary) { println!("Breaking news! {}", item.summarize()); } fn notify<T: Summary>(item: T) { println!("Breaking news! {}", item.summarize()); } -
multiple trait bounds
fn notify(item: impl Summary + Display) { println!("Breaking news! {}", item.summarize()); } -
where clause
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 { 0 } fn some_function<T, U>(t: T, u: U) -> i32 where T: Display + Clone, U: Clone + Debug, { 0 } -
use trait to implement method
struct Pair<T> { x: T, y: T, } impl<T> Pair<T> { fn new(x: T, y: T) -> Self { Self { x, y } } } impl<T: Display + PartialOrd> Pair<T> { fn cmp_display(&self) { if self.x >= self.y { println!("The largest member is x = {}", self.x); } else { println!("The largest member is y = {}", self.y); } } } -
return trait
fn returns_summarizable() -> impl Summary { NewsArticle { headline: String::from("Penguins win the Stanley Cup Championship!"), location: String::from("Pittsburgh, PA, USA"), content: String::from( "The Pittsburgh Penguins once again are the best \ hockey team in the NHL.", ), } } -
examples
use std::ops::Add; // use derive to implement Debug trait #[derive(Debug)] struct Point<T: Add<T, Output = T>> { // T must implement Add trait x: T, y: T, } impl<T: Add<T, Output = T>> Add for Point<T> { // implement Add trait for Point type Output = Point<T>; // define associated type fn add(self, p: Point<T>) -> Point<T> { // implement add method Point{ x: self.x + p.x, y: self.y + p.y, } } } fn add<T: Add<T, Output=T>>(a:T, b:T) -> T { // implement add function to add two values a + b } fn main() { let p1 = Point{x: 1.1f32, y: 1.1f32}; let p2 = Point{x: 2.1f32, y: 2.1f32}; println!("{:?}", add(p1, p2)); let p3 = Point{x: 1i32, y: 1i32}; let p4 = Point{x: 2i32, y: 2i32}; println!("{:?}", add(p3, p4)); }
trait object
-
define
pub trait Draw { fn draw(&self); } pub struct Button { pub width: u32, pub height: u32, pub label: String, } impl Draw for Button { fn draw(&self) { println!("Drawing a button: {}", self.label); } } pub struct SelectBox { pub width: u32, pub height: u32, pub options: Vec<String>, } impl Draw for SelectBox { fn draw(&self) { println!("Drawing a select box: {:?}", self.options); } } pub struct Screen { pub components: Vec<Box<dyn Draw>>, // Box is a smart pointer, can be replaced by Vec<&dyn Draw> } impl Screen { pub fn run(&self) { for component in self.components.iter() { component.draw(); } } } -
dynamic dispatch
- use
Box<dyn Trait>to store the trait object - use
&dyn Traitto store the trait object reference
- use
-
Self and self
impl Draw for Button { fn draw(&self) -> Self { // Self refers to the type that implements the trait or method self.clone() // self refers to the instance of Object } } -
limit of trait object
- can’t use generic type
- can’t use associated type
- can’t use Self
Advanced Trait
-
associated type
pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; } pub struct Counter { count: u32, } impl Iterator for Counter { type Item = u32; fn next(&mut self) -> Option<Self::Item> { if self.count < 5 { self.count += 1; Some(self.count) } else { None } } } -
default generic type
trait Add<RHS = Self> { type Output; fn add(self, rhs: RHS) -> Self::Output; } -
fully qualified syntax
trait Pilot { fn fly(&self); } trait Wizard { fn fly(&self); } struct Human; impl Pilot for Human { fn fly(&self) { println!("This is your captain speaking."); } } impl Wizard for Human { fn fly(&self) { println!("Up!"); } } impl Human { fn fly(&self) { println!("*waving arms furiously*"); } } let person = Human; Pilot::fly(&person); Wizard::fly(&person); // if without self, the compiler can't determine which fly method to call //so need to use fully qualified syntax like <Type as Trait>::method person.fly(); -
super trait
trait Pilot { fn fly(&self); } trait Wizard { fn fly(&self); } trait Greet: Pilot + Wizard { fn greet(&self) { self.fly(); } } -
newtype
struct Wrapper(Vec<String>); impl fmt::Display for Wrapper { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.0.join(", ")) } }
Dynamic Sized Type
Vector
-
create
let v: Vec<i32> = Vec::new(); let v = vec![1, 2, 3]; -
update
let mut v = Vec::new(); v.push(5); -
access
let v = vec![1, 2, 3, 4, 5]; let third: &i32 = &v[2]; let third: Option<&i32> = v.get(2); -
iterate
let v = vec![100, 32, 57]; for i in &v { println!("{}", i); } -
mutable iterate
let mut v = vec![100, 32, 57]; for i in &mut v { *i += 50; } -
enum
enum SpreadsheetCell { Int(i32), Float(f64), Text(String), } let row = vec![ SpreadsheetCell::Int(3), SpreadsheetCell::Text(String::from("blue")), SpreadsheetCell::Float(10.12), ]; -
trait
trait IpAddr { fn print(&self); } struct V4(u8, u8, u8, u8); impl IpAddr for V4 { fn print(&self) { println!("{}.{}.{}.{}", self.0, self.1, self.2, self.3); } } struct V6(String); impl IpAddr for V6 { fn print(&self) { println!("{}", self.0); } } let v: Vec<Box<dyn IpAddr>> = vec![ Box::new(V4(127, 0, 0, 1)), Box::new(V6(String::from("::1"))), ]; -
examples
assert!(!v.is_empty()); v.insert(2, 10); assert_eq!(v.remove(2), 10); assert_eq!(v.pop(), Some(5)); v.clear(); let mut v1 = [1, 2].to_vec(); v.append(&mut v1); v.truncate(2); v.extend([1, 2].iter().cloned()); v.reserve(10); // reserve capacity v.shrink_to_fit(); // shrink the capacity to the length v.retain(|x| *x > 2); // remove element that not satisfy the condition let mut m: Vec<i32> = v.drain(1..3).collect(); // delete and return the deleted elements let v2 = m.split_off(1); // split the vector into two vectors let slice = &v[1..=3]; -
sort
let mut v = vec![1, 5, 10, 2, 15]; v.sort(); v.sort_by(|a, b| b.cmp(a)); v.sort_unstable(); v.sort_unstable_by(|a, b| b.cmp(a));
HashMap
-
create
use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); // assign specific size let teams = HashMap::with_capacity(100); // use iterator and collect let teams = vec![(String::from("Blue"), 10), (String::from("Yellow"), 50)]; let scores: HashMap<_, _> = teams.into_iter().collect(); -
search
let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); let team_name = String::from("Blue"); let score = scores.get(&team_name); -
iterate
let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); for (key, value) in &scores { println!("{}: {}", key, value); } -
update
let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); let score = scores.entry(String::from("Blue")).or_insert(0); *score += 1;or_insert will return a mutable reference to the value, so you can update the value
Lifetime
-
define
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } -
lifetime marker don’t change the lifetime of the reference
let string1 = String::from("long string is long"); let result; { let string2 = String::from("xyz"); result = longest(string1.as_str(), string2.as_str()); } println!("The longest string is {}", result);the lifetime of the reference is the same as the lifetime of the shorter string, not the longer string
-
lifetime in struct
struct ImportantExcerpt<'a> { part: &'a str, } let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().expect("Could not find a '.'"); let i = ImportantExcerpt { part: first_sentence }; -
lifetime elision
- each parameter that is a reference gets its own lifetime parameter
- if there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters
- if there are multiple input lifetime parameters, but one of them is
&selfor&mut self, the lifetime ofselfis assigned to all output lifetime parameters - if there are multiple input lifetime parameters, and none of them are
&selfor&mut self, an error will be thrown
-
lifetime in method
impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 } } -
static lifetime
let s: &'static str = "I have a static lifetime.";
Return and Error Handling
-
panic
panic!("crash and burn"); -
unwrap
let f = File::open("hello.txt").unwrap(); // success or panic -
expect
let f = File::open("hello.txt").expect("Failed to open hello.txt"); // success or panic with message -
Result
use std::fs::File; fn read_username_from_file() -> Result<String, io::Error> { let f = File::open("hello.txt"); let mut f = match f { Ok(file) => file, Err(e) => return Err(e), }; let mut s = String::new(); match f.read_to_string(&mut s) { Ok(_) => Ok(s), Err(e) => Err(e), } } -
?
use std::fs::File; fn read_username_from_file() -> Result<String, io::Error> { let mut f = File::open("hello.txt")?; let mut s = String::new(); f.read_to_string(&mut s)?; Ok(s) } -
chain
use std::fs::File; fn read_username_from_file() -> Result<String, io::Error> { let mut s = String::new(); File::open("hello.txt")?.read_to_string(&mut s)?; Ok(s) }
Package and Module
-
define
-
package: a Cargo feature that lets you build, test, and share crates
-
workspace: a directory that contains multiple packages
-
crate: a binary or library
-
module: a collection of items, such as functions, structs, traits, implementations, and modules
-
-
module
mod sound { mod instrument { fn clarinet() { // function body } } } -
use
use sound::instrument::clarinet; -
as
use std::io::Result as IoResult; -
pub use
pub use sound::instrument;
comment
-
line comment
// line comment -
block comment
/* block comment */ -
doc line comment
/// doc comment -
doc block comment
/** doc comment */ -
doc test
/// ``` /// let x = 5; /// let y = 10; /// /// assert_eq!(add(x, y), 15); /// ``` fn add(x: i32, y: i32) -> i32 { x + y } -
package line comment
//! package line comment -
package block comment
/*! package block comment */ -
example
#![allow(unused)] fn main() { //! # Art //! //! 未来的艺术建模库,现在的调色库 pub use self::kinds::PrimaryColor; pub use self::kinds::SecondaryColor; pub use self::utils::mix; pub mod kinds { //! 定义颜色的类型 /// 主色 pub enum PrimaryColor { Red, Yellow, Blue, } /// 副色 #[derive(Debug,PartialEq)] pub enum SecondaryColor { Orange, Green, Purple, } } pub mod utils { //! 实用工具,目前只实现了调色板 use crate::kinds::*; /// 将两种主色调成副色 /// ```rust /// use art::utils::mix; /// use art::kinds::{PrimaryColor,SecondaryColor}; /// assert!(matches!(mix(PrimaryColor::Yellow, PrimaryColor::Blue), SecondaryColor::Green)); /// ``` pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor { SecondaryColor::Green } } }
Formatting
-
simple format
#![allow(unused)] fn main() { println!("Hello"); // => "Hello" println!("Hello, {}!", "world"); // => "Hello, world!" println!("The number is {}", 1); // => "The number is 1" println!("{:?}", (3, 4)); // => "(3, 4)" println!("{value}", value=4); // => "4" println!("{} {}", 1, 2); // => "1 2" println!("{:04}", 42); // => "0042" with leading zeros } -
format!
let s = format!("{}-{}", 1, 2); -
eprint!, eprintln!
eprint!("error: {}", "error message"); eprintln!("error: {}", "error message"); -
positional arguments
println!("{0}, this is {1}. {1}, this is {0}", "Alice", "Bob"); // => "Alice, this is Bob. Bob, this is Alice" -
named arguments
println!("{subject} {verb} {object}", object="the lazy dog", subject="the quick brown fox", verb="jumps over"); // => "the quick brown fox jumps over the lazy dog"named arguments must be used after positional arguments