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.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(),
}
}
Vec<u8>
) of bytes, but it guarantees that it is a valid UTF-8 sequence. String is heap-allocated, growable, and not null-terminated.&[u8]
) of a valid UTF-8 sequence, and can be used to view the contents of a String, much like how &[T]
is to Vec<T>
. &str does not own the data it references, so it is typically used as a function parameter type.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.&str
types instead of String
types. This is because &str
is more flexible and easier to use. If you need to convert a String
to an &str
type, you can use the &
symbol or the as_str()
method. If you need to convert an &str
type to a String
type, you can use the to_string()
or to_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 a String
instance, and it can't be mutated or resized. It's a view into the String
'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:
Option<T>
rather than a T
. For example, if you were searching for a specific item in a list, you might return Option<T>
to indicate that the item was either found or not found.Option<T>
to represent an error condition, such as when a file can't be opened.Option<T>
can be useful for simplifying code that would otherwise require multiple if
statements to check for a value's existencelet 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:
AppendBar
without real implementation.// 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:
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 the bar
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:
string2
the same as string1
:result
the same as string2
:文章标题 | 《Rust Programming Language》- EX - Cheatsheet |
发布日期 | 2023-02-07 |
文章分类 | Tech |
相关标签 | #Rust |