《Rust Programming Language》- EX - Cheatsheet
struct Wrapper<T> { value: T, } impl<T> Wrapper<T> { pub fn new(value: T) -> Self { Wrapper { value } } }
struct ColorClassicStruct { red: i32, green: i32, blue: i32, } let green = ColorClassicStruct { red: 0, green: 255, blue: 0, }; assert_eq!(green.red, 0); assert_eq!(green.green, 255); assert_eq!(green.blue, 0);
struct ColorTupleStruct(i32, i32, i32); let green = ColorTupleStruct(0, 255, 0); assert_eq!(green.0, 0); assert_eq!(green.1, 255); assert_eq!(green.2, 0);
Unit-like Struct: No fields, no data stored, behave similarly to ()
struct UnitLikeStruct; let unit_like_struct = UnitLikeStruct; let message = format!("{:?}s are fun!", unit_like_struct);
DO NOT DO THIS:
fn greet(name = 1)
, this is WRONG
Here's an example of using optional arguments in Rust:
fn main() { // Call greet function with no arguments greet(None); // Call greet function with an optional argument greet(Some("Alice".to_string())); } fn greet(name: Option<String>) { // Match the optional argument match name { // If there is a name, print a personalized greeting Some(name) => println!("Hello, {}!", name), // If there is no name, print a generic greeting None => println!("Hello, world!"), } }
fn get_char(data: String)
takes the String by value, allowing you to modify the string data, but it will make a full copy of the string data, which can be expensive for large strings.fn get_char(data: &String)
takes a reference to a String, allowing you to access the string data, but not modify it.Wrong syntax - you cannot take a mutable reference to an immutable reference.fn get_char(mut data: &String)
fn get_char(mut data: String)
takes a mutable String, allowing you to modify the string data, but it will also make a full copy of the string data.
Syntax | Ownership | Mutable |
fn get_char(data: &String) | Caller | No |
fn get_char(mut data: &String) | N/A | N/A |
fn get_char(data: String) | Transferred to Function | No |
fn get_char(mut data: String) | Transferred to Function | Yes |
enum Message { Quit, Move(Point), Echo(String), ChangeColor((u8, u8, u8)), } fn triggerSpecificEvent(message: Message) { // TODO: create a match expression to process the different message variants match message { Message::ChangeColor((a, b, c)) => change_color((a, b, c)), Message::Echo(s) => echo(s), Message::Move(p) => move_position(p), Message::Quit => quit(), } }
- String is a string type that is stored as a vector (
Vec<u8>
) of bytes, but it guarantees that it is a valid UTF-8 sequence. String is heap-allocated, growable, and not null-terminated. - &str is a string slice type that always points to a slice (
&[u8]
) of a valid UTF-8 sequence, and can be used to view the contents of a String, much like how&[T]
is toVec<T>
. &str does not own the data it references, so it is typically used as a function parameter type. - The two differ primarily in ownership and heap allocation. When you need to modify strings, it is recommended to use the
String
type because it is mutable and owns the data, allowing you to modify its contents at will. When you only need to view or reference a string, it is recommended to use the&str
type instead, because it is immutable and does not own the heap memory, reducing memory overhead and copying operations. - String processing functions in Rust usually take and return
&str
types instead ofString
types. This is because&str
is more flexible and easier to use. If you need to convert aString
to an&str
type, you can use the&
symbol or theas_str()
method. If you need to convert an&str
type to aString
type, you can use theto_string()
orto_owned()
methods.
fn string_slice(arg: &str) { println!("{}", arg); } fn string(arg: String) { println!("{}", arg); } fn main() { let string1 = "blue"; // &str let string2 = String::from("hi"); // String let string3 = "rust is fun!".to_owned(); // String let string4 = "nice weather".into(); // String let string5 = format!("Interpolation Station"); // String let string6 = &String::from("abc")[0..1]; // &str let string7 = " hello there ".trim(); // &str let string8 = "Happy Monday!".replace("Monday", "Tuesday"); // String let string9 = "my SHIFT KEY IS STUCK".to_lowercase(); // String string_slice(string1); string(string2); string(string3); string(string4); string(string5); string_slice(string6); string_slice(string7); string(string8); string(string9); }
String
is a heap-allocated data type owned by the current thread. It can be mutated and resized.&str
is a reference to a string literal or aString
instance, and it can't be mutated or resized. It's a view into theString
's memory without ownership of the allocation.
fn main() { // Create a new String instance with the value "green" let word = String::from("green"); // Call the is_a_color_word function with a reference to the "word" String instance as an argument. if is_a_color_word(&word) { // If is_a_color_word returns true, print "That is a color word I know!" println!("That is a color word I know!"); } else { // If is_a_color_word returns false, print "That is not a color word I know." println!("That is not a color word I know."); } } // This function takes a reference to a string slice as an argument. fn is_a_color_word(attempt: &str) -> bool { // Check if "attempt" is equal to "green", "blue", or "red" attempt == "green" || attempt == "blue" || attempt == "red" }
To convert a &str
to a String
, you can use the to_string()
or to_owned()
methods. For example:
let s1: &str = "hello"; let s2: String = s1.to_string(); let s3: String = s1.to_owned();
Both to_string()
and to_owned()
will create a new String
instance that contains a copy of the original &str
.
let num = 5; match num { 1 => println!("The number is one"), 2 => println!("The number is two"), 3 => println!("The number is three"), 4 => println!("The number is four"), 5 => println!("The number is five"), 6..=8 => println!("The number is between 6 and 8 (inclusive)"), 9..11 => println!("The number is between 9 and 11 (exclusive)"), _ => println!("The number is not in the range 1 to 10"), }
Some common use cases for Option<T>
include:
- Returning a value that may not exist: In this case, the function would return an
Option<T>
rather than aT
. For example, if you were searching for a specific item in a list, you might returnOption<T>
to indicate that the item was either found or not found. - Dealing with errors: You might use
Option<T>
to represent an error condition, such as when a file can't be opened. - Simplifying code:
Option<T>
can be useful for simplifying code that would otherwise require multipleif
statements to check for a value's existence
let maybe_number: Option<i32> = Some(42); match maybe_number { Some(n) => println!("The number is {}", n), None => println!("There is no number."), }
// main function fn main() { let result = divide(10, 2); // call divide function with arguments 10 and 2, and store the result in a variable match result { // match the result against two possible outcomes: Ok and Err Ok(value) => println!("Result is {}", value), // if the result is Ok, print the value of the result Err(error) => println!("Error occurred: {}", error), // if the result is Err, print the error message } } // divide function fn divide(a: i32, b: i32) -> Result<i32, String> { if b == 0 { // check for division by zero return Err(String::from("Cannot divide by zero")); // if b is 0, return an Err with the message "Cannot divide by zero" } Ok(a / b) // if b is not 0, return an Ok with the result of dividing a by b }
// enum ParsePosNonzeroError { Creation(CreationError), ParseInt(ParseIntError), } impl ParsePosNonzeroError { fn from_creation(err: CreationError) -> ParsePosNonzeroError { ParsePosNonzeroError::Creation(err) } fn from_parseint(err: ParseIntError) -> ParsePosNonzeroError { ParsePosNonzeroError::ParseInt(err) } } fn parse_pos_nonzero(s: &str) -> Result<PositiveNonzeroInteger, ParsePosNonzeroError> { // let x: i64 = s.parse().unwrap(); // if we use unwrap, we will get a panic if the parse fails // now we will use map_err to convert the error type let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parseint)?; PositiveNonzeroInteger::new(x).map_err(ParsePosNonzeroError::from_creation) }
TL;DR: impl
is used to define a method for a struct.
struct Circle { radius: f64, } impl Circle { fn area(&self) -> f64 { std::f64::consts::PI * (self.radius * self.radius) } }
In this code, we do the following:
- Define a trait called
AppendBar
without real implementation. - Implement the AppendBar trait for the String type and add the implementation for the append_bar function.
- Use the append_bar function on a String instance.
// Define a new trait called AppendBar trait AppendBar { // Define a function called append_bar that takes ownership of the object and returns the same type fn append_bar(self) -> Self; } // Implement the AppendBar trait for the String type impl AppendBar for String { // Implement the append_bar function for the String type fn append_bar(self) -> Self { let mut s = self; s.push_str("Bar"); s } } // Usage for the AppendBar trait fn main() { let s = String::from("Foo"); let s = s.append_bar(); println!("s: {}", s); }
Trait implementations can have default implementations for some or all of the methods. For example:
trait Foo { fn foo(&self); fn bar(&self) { println!("This is a default implementation."); } }
Here we have 1 trait Implemented for 2 structs.
We can use impl {trait_name}
as a parameter type, so we can pass any type that implements the trait.
pub trait Licensed { fn licensing_info(&self) -> String { "some information".to_string() } } struct SomeSoftware {} struct OtherSoftware {} impl Licensed for SomeSoftware {} impl Licensed for OtherSoftware {} fn compare_license_types( // for below two parameters, we can pass any type that implements the Licensed trait software: impl Licensed, software_two: impl Licensed ) -> bool { software.licensing_info() == software_two.licensing_info() }
For a more complex example, we can use impl {trait_name} + {trait_name}
to specify multiple traits.
pub trait SomeTrait { fn some_function(&self) -> bool { true } } pub trait OtherTrait { fn other_function(&self) -> bool { true } } struct SomeStruct {} struct OtherStruct {} impl SomeTrait for SomeStruct {} impl OtherTrait for SomeStruct {} impl SomeTrait for OtherStruct {} impl OtherTrait for OtherStruct {} // Here we can pass any type that implements both SomeTrait and OtherTrait fn some_func(item: impl SomeTrait + OtherTrait) -> bool { item.some_function() && item.other_function() }
Why do we need lifetime?
The main aim of lifetimes is to
prevent dangling references
, which cause a program to reference data other than the data it’s intended to reference. Consider the following example, which will not compile:
// The lifetime `'a` is defined here between the function name and the parameter list. // This means that the function will take two string slices with the same lifetime `'a`. // The return value will also have the same lifetime `'a`. fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { // The if statement checks which string slice is longer and returns a reference to it. // Since both `x` and `y` have the same lifetime `'a`, the returned reference will also have the same lifetime. if x.len() > y.len() { x } else { y } }
There are 2 ways to handle concurrency in Rust:
- Threads
- MPSC (Multiple Producer, Single Consumer)
use std::sync::{ Mutex, Arc }; use std::thread; use std::time::Duration; struct JobStatus { jobs_completed: u32, } fn main() { // Create a new JobStatus instance wrapped in a Mutex and Arc to allow for shared ownership // Mutex is required if we want to mutate the value let status = Arc::new(Mutex::new(JobStatus { jobs_completed: 0 })); let mut handles = vec![]; // Spawn 10 threads, each with a clone of the shared JobStatus instance for _ in 0..10 { let status_shared = Arc::clone(&status); // Spawn a new thread and move the clone of the shared JobStatus instance into the closure let handle = thread::spawn(move || { thread::sleep(Duration::from_millis(250)); // Lock the Mutex and update the jobs_completed field let mut s_shared = status_shared.lock().unwrap(); s_shared.jobs_completed += 1; }); handles.push(handle); } for handle in handles { // Trigger the thread to start executing handle.join().unwrap(); // Print the value of the jobs_completed field println!("jobs completed {}", status.lock().unwrap().jobs_completed); } }
#![forbid(unused_imports)] use std::sync::Arc; use std::thread; fn main() { let numbers: Vec<_> = (0..100u32).collect(); // Arc is important here because we are using threads. // Arc makes it possible to share ownership across threads and make it thread-safe. let shared_numbers = Arc::new(numbers); let mut joinhandles = Vec::new(); for offset in 0..8 { // We are using shared_numbers across threads, so we need to clone it. let child_numbers = shared_numbers .iter() .cloned() .filter(|&n| n % 8 == offset) .collect::<Vec<_>>(); println!("Child numbers: {:?}", child_numbers); joinhandles.push( thread::spawn(move || { let sum: u32 = child_numbers .iter() .filter(|&&n| n % 8 == offset) .sum(); println!("Sum of offset {} is {}", offset, sum); }) ); } for handle in joinhandles.into_iter() { handle.join().unwrap(); } }
The key point is that we need to use Box
to the recursive(self referencing) to work;
#[derive(PartialEq, Debug)] pub enum List { // This is a enum, apparently, it references List itself, we need to use Box to make it work // Cons is a enum, it has two fields, a number and next pointer Cons(i32, Box<List>), // This is a enum, represents a empty list` Nil, } fn main() { println!("This is an empty cons list: {:?}", create_empty_list()); println!("This is a non-empty cons list: {:?}", create_non_empty_list()); } pub fn create_empty_list() -> List { // Simply return an empty list, List::Nil is a enum let list = List::Nil; list } pub fn create_non_empty_list() -> List { // Simply return a non-empty list, List::Cons is a enum let list = List::Cons(1, Box::new(List::Nil)); list }
-
Add below code to
Cargo.toml
:[[bin]] name = "src" path = "main.rs" [[bin]] name = "bar" path = "bar.rs"
-
You can run
cargo run --bin bar
to run thebar
binary.
This happens when a scope of a variable is too short.
// this function expects the lifetime of arguments(`x` and `y`) to be the same as the return value(`&'a str` fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } // THIS CODE WILL NOT COMPILE fn main() { let string1 = String::from("long string is long"); let result; { // string2 does not have same lifetime as string1 let string2 = String::from("xyz"); result = longest(string1.as_str(), string2.as_str()); } println!("The longest string is '{}'", result); }
Solutions:
- Make the lifetime of
string2
the same asstring1
: - Delete println!() to make the scope of
result
the same asstring2
:
关于本文
文章标题 | 《Rust Programming Language》- EX - Cheatsheet |
发布日期 | 2023-02-07 |
文章分类 | Tech |
相关标签 | #Rust |
留言板
PLACE_HOLDER
PLACE_HOLDER
PLACE_HOLDER
PLACE_HOLDER