Rust Tiếng Việt

Deploy Book

The book is published at https://rust-tieng-viet.github.io.

    _~^~^~_
\) /  o o  \ (/
  '_   _   _'
  / '-----' \

Rust là một ngôn ngữ nhanh, an toàn và được bình chọn là ngôn ngữ được ưa thích nhất trong nhiều năm liền theo Stack Overflow Survey. Rust có một hệ thống tài liệu và sách đồ sộ, chi tiết. Nhưng đôi khi nó sẽ khó tiếp cận với một số người bởi đa số tài liệu là Tiếng Anh. Với những ghi chép trong sách này, hy vọng có thể giúp cho mọi người (cũng như các thành viên trong team mình tại Fossil) có thể tiếp cận với ngôn ngữ này một cách nhanh chóng và dễ dàng hơn. Cũng như truyền cảm hứng và mở rộng cộng đồng sử dụng Rust tại Việt Nam.

Mục tiêu của sách này không phải là dịch từ các sách tiếng Anh. Mà sẽ là những ghi chép, những lưu ý cho một người mới bắt đầu học Rust cho đến lúc thành thạo, từ lúc hello world cho đến các dự án thực tế. Bao gồm những khó khăn mà bạn sẽ phải gặp, những thuật ngữ, concepts mới, những thói quen lập trình mới mà bạn sẽ phải làm quen.

Mình vẫn luôn prefer đọc sách tiếng Anh nếu bạn có thể. Vui lòng xem mục references ở cuối các trang nếu bạn muốn đào sâu hơn.

Who is using Rust?

Công ty hoặc cộng đồng sử dụng Rust tại Việt Nam.

OrganizationContact (GitHub User Name)EnvironmentDescription of Use
Fossil Vietnam@duyetProductionData Platform

Đóng góp bổ sung tại đây.

Cài đặt Rust

Dưới đây là hướng dẫn chi tiết về cách cài đặt Rust trên hệ điều hành Windows, macOS và Linux. Bạn cũng có thể tự tham khảo trên trang chủ của Rust: https://www.rust-lang.org/tools/install

Cài đặt Rust trên macOS, Linux

Mở Terminal và chạy lệnh sau:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Sau khi hoàn thành quá trình cài đặt, bạn có thể kiểm tra phiên bản Rust bằng cách mở Terminal và chạy lệnh sau:

rustc --version

Cài đặt Rust trên Windows

Để tải xuống trình cài đặt Rust, bạn cần truy cập vào trang web chính thức của Rust tại địa chỉ https://forge.rust-lang.org/infra/other-installation-methods.html

Chạy tập tin rustup-init.exe để bắt đầu quá trình cài đặt Rust trên máy tính của mình.

References

Project đầu tiên

Sau khi cài đặt Rust thành công, bạn có thể bắt đầu sử dụng Cargo (một công cụ quản lý gói mạnh mẽ được tích hợp sẵn trong Rust) để khởi tạo project hello world đầu tiên.

Bước 1: Tạo một package mới

Để bắt đầu với Cargo, bạn cần tạo một package mới bằng lệnh cargo new <name>. Ví dụ, để tạo một package mới với tên là hello_world, bạn chạy lệnh sau đây:

$ cargo new hello_world
# Created binary (application) `hello_world` package

Cargo sẽ tạo ra một package mới với một file Cargo.toml và một thư mục src. File Cargo.toml chứa tất cả các thông tin liên quan đến package của bạn.

$ tree .
.
├── Cargo.toml
└── src
    └── main.rs

2 directories, 2 files

Hãy xem nội dung của src/main.rs

fn main() {
    println!("Hello, world!");
}

Bước 2: Biên dịch và chạy chương trình

Tiếp theo, để biên dịch chương trình của bạn, hãy chạy lệnh cargo build. Sau khi quá trình biên dịch kết thúc, Cargo sẽ tạo ra một thư mục mới có tên target, chứa tệp thực thi của chương trình.

$ cargo build

# Compiling hello_world v0.1.0 (/Users/duyet/project/hello_world)
#    Finished dev [unoptimized + debuginfo] target(s) in 0.67s

Để chạy chương trình, bạn có thể sử dụng lệnh ./target/debug/<name>. Ví dụ, để chạy chương trình hello_world, bạn có thể chạy lệnh sau đây:

$ ./target/debug/hello_world
# Hello, world!

Kết quả sẽ hiển thị trên màn hình là "Hello, world!". Ngoài ra, bạn cũng có thể biên dịch và chạy chương trình chỉ trong một bước bằng cách sử dụng lệnh cargo run.

$ cargo run
# Hello, world!

References

Variables

Variables trong Rust có kiểu dữ liệu tĩnh. Ta khai báo kiểu dữ liệu trong lúc khai báo biến. Trong đa số các trường hợp compiler có thể đoán được kiểu dữ liệu nên đôi khi ta có thể bỏ qua.

fn main() {
let an_integer = 1u32;
let a_boolean = true;
let unit = ();

// copy `an_integer` into `copied_integer`
let copied_integer = an_integer;
}

Mọi biến đều phải được sử dụng, nếu không, compiler sẽ warning. Để skip warning, thêm dấu underscore ở đầu tên biến.

fn check_error() {}
fn main() {
// The compiler warns about unused variable bindings; these warnings can
// be silenced by prefixing the variable name with an underscore
let _unused_variable = 3u32;

// Skip the result of function
let _ = check_error();
}

mut

Mọi biến trong Rust mặc định là immutable, có nghĩa là không thể thay đổi, không thể gán bằng một giá trị khác.

fn main() {
let a = 1;
a = 2;
}

// error[E0384]: cannot assign twice to immutable variable `a`
//  --> src/main.rs:4:1
//   |
// 3 | let a = 1;
//   |     -
//   |     |
//   |     first assignment to `a`
//   |     help: consider making this binding mutable: `mut a`
// 4 | a = 2;
//   | ^^^^^ cannot assign twice to immutable variable

Để có thể thay đổi giá trị của biến, ta thêm từ khóa mut sau let.

fn main() {
let mut a = 1;
a = 2;

println!("a = {}", a);
}

Ta cũng có thể khai báo lại biến đó để assign lại giá trị mới:

fn main() {
let a = 1;
let a = a + 1;
}

Scope

Giá trị của variables có thể được xác định tùy theo scope. Scope là một tập hợp các dòng code nằm trong {}.

let a = 1;

{
    let a = 2;
    println!("inner: a = {}", a); // 2
}

println!("outer: a = {}", a); // 1

Return trong scope

Ta cũng có thể return giá trị trong một scope cho một variable.

let a = {
    let y = 10;
    let z = 100;

    y + z
};

println!("a = {}", a);

mut

Mọi biến trong Rust mặc định là immutable, có nghĩa là không thể thay đổi, không thể gán bằng một giá trị khác.

fn main() {
let a = 1;
a = 2;
}

// error[E0384]: cannot assign twice to immutable variable `a`
//  --> src/main.rs:4:1
//   |
// 3 | let a = 1;
//   |     -
//   |     |
//   |     first assignment to `a`
//   |     help: consider making this binding mutable: `mut a`
// 4 | a = 2;
//   | ^^^^^ cannot assign twice to immutable variable

Để có thể thay đổi giá trị của biến, ta thêm từ khóa mut sau let.

fn main() {
let mut a = 1;
a = 2;

println!("a = {}", a);
}

Ta cũng có thể khai báo lại biến đó để assign lại giá trị mới:

fn main() {
let a = 1;
let a = a + 1;
}

uninitialized variable

Variable mà chưa được gán giá trị được gọi là uninitialized variable.

fn main() {
  let my_variable; // ⚠️
}

Rust sẽ không compile và bạn sẽ không thể sử dụng cho đến khi my_variable được gán giá trị nào đó. Ta có thể lợi dụng điều này:

  • Khai báo uninitialized variable.
  • Gán giá trị cho nó trong 1 scope khác
  • Vẫn giữ được giá trị của của variable đó khi ra khỏi scope.
fn main() {
  let my_number;
  {
    my_number = 100;
  }

  println!("{}", my_number);
}

Hoặc phức tạp hơn

fn loop_then_return(mut counter: i32) -> i32 {
  loop {
    counter += 1;
    if counter % 50 == 0 {
      break;
    }
  }

  counter
}

fn main() {
  let my_number;

  {
    // Pretend we need to have this code block
    let number = {
      // Pretend there is code here to make a number
      // Lots of code, and finally:
      57
    };

    my_number = loop_then_return(number);
  }

  println!("{}", my_number); // 100
}

Closure

Hay còn được gọi là anonymous functions hay lambda functions. Khác với function bình thường, kiểu dữ liệu của tham số đầu vào và kiểu dữ liệu trả ra là không bắt buộc.

Function bình thường:

fn get_square_value(i: i32) -> i32 {
  i * i
}

fn main() {
  let x = 2;
  println!("{}", get_square_value(x));
}

Closure:

fn main() {
  let x = 2;

  let square = |i: i32| -> i32 {
      i * i
  };

  println!("{}", square(x));
}

Closure không cần data type

fn main() {
  let x = 2;

  let square = |i| {
    i * i
  };

  println!("{}", square(x));
}

Tham số của của closure được đặt giữa 2 dấu: ||.

Closure không có tham số

Với closure không có tham số, ta viết như sau:

#![allow(unused)]
fn main() {
let func = || { 1 + 1 };
}

Closure chỉ có một mệnh đề

Dấu ngoặc {} cũng không bắt buộc nếu nội dung của closure chỉ có một mệnh đề.

#![allow(unused)]
fn main() {
let func = || 1 + 1;
}

Vừa định nghĩa, vừa thực thi

fn main() {
  let x = 2;

  let square = |i| -> i32 { // ⭐️ nhưng bắt buộc khai báo return type
    i * i
  }(x);

  println!("{}", square);
}

Cargo

cargo là package management tool official của Rust.

cargo có rất nhiều tính năng hữu ích để improve code quality và nâng cao tốc độ của lập trình viên. cargo có hẳn một quyển sách riêng: The Cargo Book

Những tính năng phổ biến mà bạn sẽ phải dùng hằng ngày:

  • cargo add <crate>: cài đặt crate mới từ https://crates.io, crate sẽ được thêm vào Cargo.toml.
  • cargo t hoặc cargo test: run mọi tests (unit tests, doc tests, integration tests).
  • cargo fmt: format code.
  • cargo clippy: lint để bắt các lỗi phổ biến trong lập trình, code đẹp hơn, chạy nhanh hơn, etc. https://github.com/rust-lang/rust-clippy

Packages và Crates

Package là một hoặc nhiều crates. Một package gồm một file Cargo.toml mô tả cách để build các crates đó.

Crate có thể là một binary crate hoặc library crate.

  • binary crate có thể được compile thành binary và có thể thực thi được, ví dụ như một command-line hoặc server. Một binary crate bắt buộc phải có một hàm main()
  • library crate không cần hàm main(). Library crate dùng để share các tính năng cho các project khác.

Package layout

Được mô tả trong Cargo book, một crate trong Rust sẽ có layout như sau:

.
├── Cargo.lock
├── Cargo.toml
├── src/
│   ├── lib.rs
│   ├── main.rs
│   ├── helper.rs
│   ├── utils/
│   │   ├── mod.rs
│   │   └── math.rs
│   └── bin/
│       ├── named-executable.rs
│       └── another-executable.rs
├── benches/
│   ├── large-input.rs
│   └── multi-file-bench.rs
├── examples/
│   ├── simple.rs
│   └── complex.rs
└── tests/
    ├── some-integration-tests.rs
    └── multi-file-test/
        ├── main.rs
        └── test_module.rs
  • Cargo.tomlCargo.lock dược đặt ở thư mục gốc của package. Thường để sử dụng các library nào đó, người ta sẽ hướng dẫn bạn thêm một dòng ví dụ log = "0.6" bên dưới section [dependencies] hoặc [dev-dependencies]. Không nên đụng đến file Cargo.lock do nó được generate tự động.
  • Source code được đặt trong thư mục src.
  • File chính của library crate là src/lib.rs.
  • File chính của binary crate là src/main.rs.
  • Benchmark code được đặt trong thư mục benches.
  • Code ví dụ (examples) được đặt trong thư mục examples.
  • Integration tests được đặt trong thư mục tests.
  • helper.rsutils/ được gọi là các module. Nếu module là một thư mục gồm nhiều file khác, file mod.rs được coi như là file index của module đó. Xem thêm về modules tại đây.

Crate

Crate có thể là một binary crate hoặc library crate.

  • binary crate có thể được compile thành binary và có thể thực thi được, ví dụ như một command-line hoặc server. Một binary crate bắt buộc phải có một hàm main()
  • library crate không cần hàm main(). Library crate dùng để share các tính năng cho các project khác.

Crate được publish trên https://crates.io.

Init crate

Để tạo một crate mới ta sử dụng cargo:

  • cargo new crate_name: binary crate.
  • cargo new crate_name --lib: library crate.

Layout của binary cratelibrary crate

// Binary crate

├── Cargo.toml
└── src
    └── main.rs
// Library crate

├── Cargo.toml
└── src
    └── lib.rs

Một crate có thể vừa có lib.rsmain.rs.

Binary crate khi cargo build hoặc cargo run sẽ build ra một file binary bỏ trong ./target/debug/<crate_name>.

Khi build cho môi trường production, ta thêm --release lúc này cargo sẽ build thành binary bỏ trong ./target/release/<crate_name>.

debug hay release được gọi là các build target. Build trong release sẽ được apply nhiều optimization hơn, kích thước nhỏ hơn, chạy nhanh hơn nhưng compile lâu hơn.

use crate

Để sử dụng (import) một crate từ https://crates.io, ví dụ https://crates.io/crates/log.

1. Thêm crate vào Cargo.toml

Có 2 cách

Cách 1: Edit trực tiếp file Cargo.toml

[dependencies]
log = "0.4"

Cách 2: Sử dụng cargo add, cargo sẽ tự động update file Cargo.toml cho bạn

cargo add log
[dependencies]
log = "0.4.17"

Để thêm crate vào dev dependencies (dùng cho tests), ta thêm --dev vào lệnh:

cargo add --dev log
[dev-dependencies]
log = "0.4.17"

2. Sử dụng crate trong code

fn main() {
    log::info!("hello");
    log::error!("oops");
}

Sử dụng keyword use. Chức năng chính của use là bind lại full path của element vào một tên mới, để chúng ta không cần phải lặp lại một tên dài mỗi lần sử dụng.

use log::info;
use log::error;

fn main() {
    info!("hello");
    error!("oops");
}

Nhóm các import lại với nhau:

use log::{info, error};

fn main() {
    info!("hello");
    error!("oops");
}

Import mọi thứ được public trong crate/module. Cách này thường hay tránh bởi sẽ khó biết được function, struct, ... nào đó đang thuộc crate nào, ngoại trừ các prelude::*.

use log::*;

fn main() {
    info!("hello");
    error!("oops");
}

use trong scope

use cũng thường được sử dụng import element vào trong scope hiện tại.

#![allow(unused)]
fn main() {
fn hello() -> String {
  "Hello, world!".to_string()
}

#[cfg(test)]
mod tests {
  use super::hello; // Import the `hello()` function into the scope
    
  #[test]
  fn test_hello() {
    assert_eq!("Hello, world!", hello()); // If not using the above `use` statement, we can run same via `super::hello()`
  }
}
}

Bạn sẽ sẽ hay gặp:

#![allow(unused)]
fn main() {
// ...

#[cfg(test)]
mod tests {
  use super::*;
  use log::info;
    
  // ...
}
}

self::, super::

Mặc định thì use sẽ import đường dẫn tuyệt đối, bắt đầu từ crate root. selfsuper thường dùng để import mod theo vị trí tương đối.

#![allow(unused)]
fn main() {
// src/level_1/level_2/mod.rs

use self::hello_1;
use super::super::level3::hello_2;
}

Re-export

Một trường hợp đặt biệt là sử dụng pub use là re-exporting, khi bạn thiết kế một module bạn có thể export một số thứ từ module khác (*) từ module của bạn. Do đó người sử dụng có thể sử dụng các module khác đó ngay từ module của bạn.

Module khác (*) đó có thể là một internal module, internal crate hoặc external crate.

#![allow(unused)]
fn main() {
// src/utils.rs
pub use log::*;

}
// src/main.rs
use crate::utils::info;

fn main() {
    info!("...");
}

Pattern này được sử dụng khá nhiều ở các thư viện lớn. Nó giúp ẩn đi các internal module phức tạp của library đối với user. Bởi vì user sẽ không cần quan tâm đến cấu trúc directory phức tạp khi sử dụng một library nào đó.

self::, super::

Mặc định thì use sẽ import đường dẫn tuyệt đối, bắt đầu từ crate root. selfsuper thường dùng để import mod theo vị trí tương đối.

#![allow(unused)]
fn main() {
// src/level_1/level_2/mod.rs

use self::hello_1;
use super::super::level3::hello_2;
}

Re-export

Một trường hợp đặt biệt là sử dụng pub use là re-exporting, khi bạn thiết kế một module bạn có thể export một số thứ từ module khác (*) từ module của bạn. Do đó người sử dụng có thể sử dụng các module khác đó ngay từ module của bạn.

Module khác (*) đó có thể là một internal module, internal crate hoặc external crate.

#![allow(unused)]
fn main() {
// src/utils.rs
pub use log::*;

}
// src/main.rs
use crate::utils::info;

fn main() {
    info!("...");
}

Pattern này được sử dụng khá nhiều ở các thư viện lớn. Nó giúp ẩn đi các internal module phức tạp của library đối với user. Bởi vì user sẽ không cần quan tâm đến cấu trúc directory phức tạp khi sử dụng một library nào đó.

Preludes

Preludes là những thứ được định nghĩa trong std, và được import sẵn, vì chúng thường sẽ phải được dùng trong mọi chương trình Rust. Bạn có thể sử dụng mà không cần phải import, ví dụ như: Option, Result, Ok, Err, ...

Mặc dù std của Rust có rất nhiều module và tính năng, nhưng không phải mọi thứ đều được preludes.

Đây là danh sách những thứ được preludes: https://doc.rust-lang.org/std/prelude/#prelude-contents


The prelude used in Rust 2021, std::prelude::rust_2021, includes all of the above, and in addition re-exports:

Ownership

Ownership là một trong những tính năng đặc trưng của Rust, đây là cách giúp Rust đảm bảo memory safety mà không cần đến garbage collector.

Ownership là gì?

Ownership là một concept mới. Tất cả programs đều cần phải quản lý memory mà nó sử dụng trong lúc thực thi. Một vài ngôn ngữ sử dụng garbage collection để tìm và giải phóng bộ nhớ lúc runtime, một số ngôn ngữ khác thì lập trình viên phải tự chi định (allocate) và giải phóng (free) bộ nhớ. Rust đi theo một hướng khác, memory được quản lý bởi một ownership system gồm tập rules được compiler sử dụng để kiểm tra (check) lúc compile. Bằng cách này thì Rust ép chúng ta viết code theo một cách an toàn memory-safe, Rust sẽ bắt lỗi ở lúc complie. Càng hiểu được concept của ownership, thì dần dần chúng ta có thể viết được code an toàn và hiệu quả hơn.

Để tìm hiểu kỹ hơn về Ownership, bạn có thể đọc Rust Book tại đây cực kỳ chi tiết: https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html

Ownership Rules

Nói đơn giản về ownership rules thì có một số điều cơ bản sau:

  • Mỗi giá trị trong Rust đều có một biến gọi là owner của nó.
  • Chỉ có một owner tại một thời điểm.
  • Khi owner ra khỏi scope, giá trị sẽ bị hủy.

Borrow checker

Bằng cách theo dõi data sử dụng thông qua bộ rules, borrow checker có thể xác định khi nào data cần được khởi tạo (initialized) và khi nào cần được giải phóng (freed, or dropped).
Thực tế sẽ có một trong ba trường hợp sau khi bạn sử dụng variable: tự move data và bỏ ownership; copy data sang một variable khác; hoặc sử dụng reference (con trỏ) đến data và vẫn giữ ownership, cho mượn (borrow) nó một thời gian.

Chỉ cần nhớ hai quy tắc quan trọng:

  1. Khi truyền một variable (thay vì reference tới variable) cho một function khác, ta sẽ mất quyền ownership. Function đó sẽ là owner của variable này và bạn không thể sử dụng lại được nữa ở context cũ.
  2. Khi truyền một reference tới variable, bạn có thể immutable borrow không giới hạn; hoặc mutable borrow một lần.

Ví dụ: đoạn chương trình sau sẽ không compile được

fn hold_my_vec<T>(_: Vec<T>) {}

fn main() {
  let x = vec![1, 2, 3];
  hold_my_vec(x);

  let z = x.get(0);
  println!("Got: {:?}", z);
}

Compiler sẽ báo lỗi như sau: rustc main.rs

error[E0382]: borrow of moved value: `x`
    --> main.rs:7:13
  |
4 |  let x = vec![1, 2, 3];
  |      - move occurs because `x` has type `Vec<i32>`, which does not implement the `Copy` trait
5 |  hold_my_vec(x);
  |              - value moved here
6 |
7 |  let z = x.get(0);
  |          ^^^^^^^^ value borrowed here after move
  |
  = note: borrow occurs due to deref coercion to `[i32]`

Lỗi nói rằng Vec<i32> không implement Copy trait, vì thế data sẽ được di chuyển (move) hoặc mượn (borrow) vào function hold_my_vec(). Do đó dòng 7 không thể thực hiện được do x được được move vào trong function kia.

Mặc dùng không thể implement Copy trait, Vec vẫn có Clone trait. Chỉ để cho code chạy được thì đây là một cách nhanh để compiler ngưng báo lỗi. Lưu ý thì việc clone thường sẽ tốn khá nhiều chi phí, nhất là đối với những object lớn.

fn hold_my_vec<T>(_: Vec<T>) {}

fn main() {
  let x = vec![1, 2, 3];
  hold_my_vec(x.clone()); // <-- x.clone()

  let z = x.get(0);
  println!("Got: {:?}", z);
}

Trong trường hợp này thì function hold_my_vec không làm gì ngoài việc take ownership. Có một cách tốt hơn là references. Thay vì để function take ownership, ta có thể cho nó mượn giá trị. Chúng ta sẽ truyền vào một reference — a borrowed value.

fn hold_my_vec<T>(_: &Vec<T>) {}

fn main() {
  let x = vec![1, 2, 3];
  hold_my_vec(&x); // <--- &x

  let z = x.get(0);
  println!("Got: {:?}", z);
}

Với cách này thì chúng ta sẽ để function mượn trong khi vẫn có thể tiếp tục sử sử dụng trong chương trình.

Bạn có thể đọc thêm về Ownership, References and BorrowingThe Slice Type tại the Rust Book.

Struct

Struct được sử dụng trong Rust rất nhiều, hầu như là mọi lúc. Với struct ta có thể định nghĩa một kiểu dữ liệu riêng.

Tên của struct thường là UpperCamelCase. Nếu bạn định nghĩa tên struct là lowercase, compiler sẽ nhắc nhở ngay.

warning: type `duyet_struct` should have an upper camel case name
 --> src/main.rs:1:8
  |
1 | struct duyet_struct;
  |        ^^^^^^^^^^^^ help: convert the identifier to upper camel case: `DuyetStruct`
  |
  = note: `#[warn(non_camel_case_types)]` on by default

Có 3 loại struct:

Unit struct

Unit struct là một struct mà không có gì cả:

struct FileDirectory;
fn main() {}

Tuple struct

Tuple struct hay còn gọi là Unnamed struct. Bạn chỉ cần định nghĩa kiểu dữ liệu, không cần định tên field name.

struct Colour(u8, u8, u8);

fn main() {
  let my_colour = Colour(50, 0, 50); // Make a colour out of RGB (red, green, blue)

  println!("The first part of the colour is: {}", my_colour.0);
  println!("The second part of the colour is: {}", my_colour.1);
}

// The first part of the colour is: 50
// The second part of the colour is: 0

Named struct

Phổ biến nhất, bạn sẽ phải định nghĩa field name trong block {}

struct Colour(u8, u8, u8); // Declare the same Colour tuple struct

struct SizeAndColour {
  size: u32,
  colour: Colour, // And we put it in our new named struct
		  // The last comma is optional, but recommended
}

fn main() {
  let colour = Colour(50, 0, 50);

  let size_and_colour = SizeAndColour {
    size: 150,
    colour: colour
  };
}

colour: colour có thể được viết gọn lại thành:

let size_and_colour = SizeAndColour {
  size: 150,
  colour
};

Xem về Trait

Trait

Rust có nhiều loại data types như primitives (i8, i32, str, ...), struct, enum và các loại kết hợp (aggregate) như tuples và array. Mọi types không có mối liên hệ nào với nhau. Các data types có các phương thức (methods) để tính toán hay convert từ loại này sang loại khác, nhưng chỉ để cho tiện lợi hơn, method chỉ là các function. Bạn sẽ làm gì nếu một tham số là nhiều loại kiểu dữ liệu? Một số ngôn ngữ như Typescript hay Python sẽ có cách sử dụng Union type như thế này:

function notify(data: string | number) {
  if (typeof data == 'number') {
    // ...
  } else if (typeof data == 'number') {
    // ...
  }
}

Còn trong Rust thì sao?

Trait implementations for Display

Trait là gì?

Có thể bạn đã thấy qua trait rồi: Debug, Copy, Clone, ... là các trait.

Trait là một cơ chế abstract để thêm các tính năng (functionality) hay hành vi (behavior) khác nhau vào các kiểu dữ liệu (types) và tạo nên các mối quan hệ giữa chúng.

Trait thường đóng 2 vai trò:

  1. Giống như là interfaces trong Java hay C# (fun fact: lần đầu tiên nó được gọi là interface). Ta có thể kế thừa (inheritance) interface, nhưng không kế thừa được implementation của interface*.* Cái này giúp Rust có thể hỗ trợ OOP. Nhưng có một chút khác biệt, nó không hẳn là interface.
  2. Vai trò này phổ biến hơn, trait đóng vai trò là generic constraints. Dễ hiểu hơn, ví dụ, bạn định nghĩa một function, tham số là một kiểu dữ liệu bất kỳ nào đó, không quan tâm, miễn sau kiểu dữ liệu đó phải có phương thức method_this(), method_that() nào đó cho tui. Kiểu dữ liệu nào đó gọi là genetic type. Function có chứa tham số generic type đó được gọi là generic function. Và việc ràng buộc phải có method_this(), method_that() , ... gọi là generic constraints. Mình sẽ giải thích rõ cùng với các ví dụ sau dưới đây.

Để gắn một trait vào một type, bạn cần implement nó. Bởi vì Debug hay Copy quá phổ biến, nên Rust có attribute để tự động implement:

#![allow(unused)]
fn main() {
#[derive(Debug)]
struct MyStruct {
  number: usize,
}
}

Nhưng một số trait phức tạp hơn bạn cần định nghĩa cụ thể bằng cách impl nó. Ví dụ bạn có trait Add (std::ops::Add) để add 2 type lại với nhau. Nhưng Rust sẽ không biết cách bạn add 2 type đó lại như thế nào, bạn cần phải tự định nghĩa:

use std::ops::Add;

#[derive(Debug, PartialEq)]
struct MyStruct {
  number: usize,
}

impl Add for MyStruct {    // <-- here
  type Output = Self;
  fn add(self, other: Self) -> Self {
    Self { number: self.number + other.number }
  }
}

fn main() {
  let a1 = MyStruct { number: 1 };
  let a2 = MyStruct { number: 2 };
  let a3 = MyStruct { number: 3 };

  assert_eq!(a1 + a2, a3);
}

Note: Mình sẽ gọi Define Trait là việc định nghĩa, khai báo một trait mới trong Rust (trait Add). Implement Trait là việc khai báo nội dung của function được liệu kê trong Trait cho một kiểu dữ liệu cụ thể nào đó (impl Add for MyStruct).

Chi tiết

Kết

Compiler sử dụng trait bound để kiểm tra các kiểu dữ liệu được sử dụng trong code có đúng behavior không. Trong Python hay các ngôn ngữ dynamic typed khác, ta sẽ gặp lỗi lúc runtime nếu chúng ta gọi các method mà kiểu dữ liệu đó không có hoặc không được định nghĩa.

Bạn có chắc chắn là a dưới đây có method summarize() hay không? Nhớ rằng typing hint của Python3 chỉ có tác dụng là nhắc nhở cho lập trình viên thôi.

# Python
func print_it(a: Union[NewsArticle, Tweet]):
  print(a.summarize())

print_it(1)
print_it("what")

Do đó Rust bắt được mọi lỗi lúc compile time và force chúng ta phải fix hết trước khi chương trình chạy. Do đó chúng ta không cần phải viết thêm code để kiểm tra behavior (hay sự tồn tại của method) trước khi sử dụng lúc runtime nữa, tăng cường được performance mà không phải từ bỏ tính flexibility của generics.

Xem về Struct.

Khai báo / định nghĩa một Trait

Nhắc lại là Trait định nghĩa các hành vi (behavior). Các types khác nhau có thể chia sẻ cùng cá hành vi. Định nghĩa một trait giúp nhóm các hành vi để làm một việc gì đó.

Theo ví dụ của Rust Book, ví dụ ta các struct chứa nhiều loại text:

  • NewsArticle struct chứa news story, và
  • Tweet struct có thể chứa tối đa 280 characters cùng với metadata.

Bây giờ chúng ta cần viết 1 crate name có tên là aggregator có thể hiển thị summaries của data có thể store trên NewsArticle hoặc Tweet instance. Chúng ta cần định nghĩa method summarize trên mỗi instance. Để định nghĩa một trait, ta dùng trait theo sau là trait name; dùng keyword pub nếu định nghĩa một public trait.

pub trait Summary {
  fn summarize(&self) -> String;
}

Trong ngoặc, ta định nghĩa các method signatures để định nghĩa hành vi: fn summarize(&self) -> String. Ta có thể định nghĩa nội dung của function. Hoặc không, ta dùng ; kết thúc method signature, để bắt buộc type nào implement trait Summary đều phải định nghĩa riêng cho nó, bởi vì mỗi type (NewsArticle hay Tweet) đều có cách riêng để summarize. Mỗi trait có thể có nhiều method.

Implement Trait cho một Type

Bây giờ ta định implement các method của trait Summary cho từng type. Ví dụ dưới đây ta có struct NewsArticlestruct Tweet, và ta định nghĩa summarize cho 2 struct này.

    pub trait Summary {
        fn summarize(&self) -> String;
    }

    pub struct NewsArticle {
        pub headline: String,
        pub location: String,
        pub author: String,
        pub content: String,
    }

    impl Summary for NewsArticle {
        fn summarize(&self) -> String {
            format!("{}, by {} ({})", self.headline, self.author, self.location)
        }
    }

    pub struct Tweet {
        pub username: String,
        pub content: String,
        pub reply: bool,
        pub retweet: bool,
    }

    impl Summary for Tweet {
        fn summarize(&self) -> String {
            format!("{}: {}", self.username, self.content)
        }
    }

Implement trait cho type giống như impl bình thường, chỉ có khác là ta thêm trait name và keyword for sau impl. Bây giờ Summary đã được implement cho NewsArticleTweet, người sử dụng crate đã có thể sử dụng các phương thức của trait như các method function bình thường. Chỉ một điều khác biệt là bạn cần mang trait đó vào cùng scope hiện tại cùng với type để có thể sử dụng. Ví dụ:

use aggregator::{Summary, Tweet}; // <-- same scope

fn main() {
  let tweet = Tweet {
    username: String::from("horse_ebooks"),
    content: String::from("of course, as you probably already know, people"),
    reply: false,
    retweet: false,
  };

  println!("1 new tweet: {}", tweet.summarize());
  // 1 new tweet: horse_ebooks: of course, as you probably already know, people
}

Rust Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=dc563051aecebae4344776c06fb1b49d

Chúng ta có thể implement trait cho mọi type khác bất kỳ, ví dụ implement Summary cho Vec<T> trong scope của crate hiện tại.

pub trait Summary {
  fn summarize(&self) -> String;
}

impl<T> Summary for Vec<T> {    // <-- local scope
  fn summarize(&self) -> String {
    format!("There are {} items in vec", self.len())
  }
}

fn main() {
  let vec = vec![1i32, 2i32];
  println!("{}", vec.summarize());
  // There are 2 items in vec
}

Rust Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=dcaa812fab222ec0c713a38b066bda20

Bạn sẽ không thể implement external traits trên external types. Ví dụ ta không thể implement Display cho Vec<T> bởi vì DisplayVec<T> được định nghĩa trong standard library, trong trong crate hiện tại. Rule này giúp tránh chống chéo và chắc chắn rằng không ai có thể break code của người khác và ngược lại.

Default Implementations

Đôi khi bạn cần có default behavior mà không cần phải implement content cho từng type mỗi khi cần sử dụng:

pub trait Summary {
  fn summarize(&self) -> String {
    String::from("(Read more...)")
  }
}

pub struct NewsArticle {
  pub headline: String,
  pub location: String,
  pub author: String,
  pub content: String,
}

impl Summary for NewsArticle {}; // <-- sử dụng {}

fn main() {
  let article = NewsArticle { ... };
  println!("New article: {}", article.summarize());
  // New article: (Read more...)
}

Traits as Parameters

Trở lại ví dụ Typescript ở đầu tiên, với Trait bạn đã có thể define một function chấp nhận tham số là nhiều kiểu dữ liệu khác nhau. Nói theo một cách khác, bạn không cần biết kiểu dữ liệu, bạn cần biết kiểu dữ liệu đó mang các behavior nào thì đúng hơn.

fn notify(data: &impl Summary) {
  println!("News: {}", data.summarize());
}

fn main() {
  let news = NewsArticle {};
  notify(news);
}

Ở đây, thay vì cần biết data là type nào (NewsArticle hay Tweet?), ta chỉ cần cho Rust compiler biết là notify sẽ chấp nhận mọi type có implement trait Summary, mà trait Summary có behavior .summarize(), do đó ta có thể sử dụng method .summary() bên trong function.

Trait Bound

Một syntax sugar khác mà ta có thể sử dụng thay cho &impl Summary ở trên, gọi là trait bound, bạn sẽ bắt gặp nhiều trong Rust document:

pub fn notify<T: Summary>(item: &T) {
  println!("News: {}", item.summarize());
}

Đầu tiên chúng ta định nghĩa trait bound bằng cách định nghĩa một generic type parameter trước, sau đó là : trong ngoặc <>. Ta có thể đọc là: item có kiểu generic là TT phải được impl Summary.

  • notify<T>( khai báo generic type T
  • notify<T: Summary>( generic type được implement trait Summary

Cú pháp này có thể dài hơn và không dễ đọc như &impl Summary, nhưng hãy xem ví dụ dưới đây:

pub fn notify(item1: &impl Summary, item2: &impl Summary) {}  // (1)
pub fn notify<T: Summary>(item1: &T, item2: &T) {}            // (2)

Dùng trait bound giúp ta tái sử dụng lại T, mà còn giúp force item1item2 có cùng kiểu dữ liệu, đây là cách duy nhất (cả 2 đều là NewsArticle hoặc cả 2 đều là Tweet) mà (1) không thể.

Specifying Multiple Trait Bounds with the + Syntax

Ta có cú pháp + nếu muốn generic T có được impl nhiều trait khác nhau. Ví dụ ta muốn item phải có cả Summary lẫn Display

pub fn notify(item: &(impl Summary + Display)) {}
pub fn notify<T: Summary + Display>(item: &T) {}

where Clauses

Đôi khi bạn sẽ có nhiều genenic type, mỗi generic type lại có nhiều trait bound, khiến code khó đọc. Rust có một cú pháp where cho phép định nghĩa trait bound phía sau function signature. Ví dụ:

fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {

Với where clause:

fn some_function<T, U>(t: &T, u: &U) -> i32
    where T: Display + Clone,
	  U: Clone + Debug,
{

Returning Types that Implement Traits

Chúng ta cũng có thể sử dụng impl Trait cho giá trị được trả về của function.

fn returns_summarizable() -> impl Summary {
    Tweet {
        username: String::from("horse_ebooks"),
        content: String::from("ahihi"),
        reply: false,
        retweet: false,
    }
}

Được đọc là: function returns_summarizable() trả về bất kỳ kiểu dữ liệu nào có impl Summary. Tuy nhiên bạn chỉ có thể return về hoặc Tweet hoặc NewsArticle do cách implement của compiler. Code sau sẽ có lỗi:

fn returns_summarizable(switch: bool) -> impl Summary {
    if switch { NewsArticle {} }
		else { Tweet {} }
}

Rust Book có một chương riêng để xử lý vấn đề này: Chapter 17: Using Trait Objects That Allow for Values of Different Types

Using Trait Bounds to Conditionally Implement Methods

Ta có thể implement 1 method có điều kiện cho bất kỳ type nào có implement một trait khác cụ thể. Ví dụ để dễ hiểu hơn dưới đây:

use std::fmt::Display;

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);
    }
  }
}

impl<T> Pair<T> implement function new trả về kiểu dữ liệu Pair<T> với T là generic (bất kỳ kiểu dữ liệu nào.

impl<T: Display + PartialOrd> Pair<T> implement function cmp_display cho mọi generic T với T đã được implement Display + PartialOrd trước đó rồi (do đó mới có thể sử dụng các behavior của Display (println!("{}")) và PartialOrd (>, <, ...) được.

Blanket implementations

Ta cũng có thể implement 1 trait có điều kiện cho bất kỳ kiểu dữ liệu nào có implement một trait khác rồi. Implementation của một trait cho 1 kiểu dữ liệu khác thỏa mãn trait bound được gọi là blanket implementations và được sử dụng rộng rãi trong Rust standard library. Hơi xoắn não nhưng hãy xem ví dụ dưới đây.

Ví dụ: ToString trait trong Rust standard library, nó được implement cho mọi kiểu dữ liệu nào có được implement Display trait.

impl<T: Display> ToString for T {
  // --snip--
}

Có nghĩa là, với mọi type có impl Display, ta có hiển nhiên thể sử dụng được các thuộc tính của trait ToString.

let s = 3.to_string(); // do 3 thoaỏa manãn Display

Do 3 thỏa mãn điều kiện là đã được impl Display for i32. (https://doc.rust-lang.org/std/fmt/trait.Display.html#impl-Display-11)

Trait Inheritance

pub trait B: A {}

Cái này không hẳn gọi là Trait Inheritance, cái này đúng hơn gọi là "cái nào implement cái B thì cũng nên implement cái A". AB vẫn là 2 trait độc lập nên vẫn phải implemenet cả 2.

impl B for Z {}
impl A for Z {}

Inheritance thì không được khuyến khích sử dụng.

Supertraits

Rust không có khái niệm "kế thừa" như trong OOP. Nhưng bạn có thể định nghĩa một trait là một tập hợp của các trait khác.

#![allow(unused)]
fn main() {
trait Person {
  fn name(&self) -> String;
}

// Person là một supertrait của Student.
// Implement Student yêu cầu bạn phải cũng phải impl Person.
trait Student: Person {
    fn university(&self) -> String;
}

trait Programmer {
    fn fav_language(&self) -> String;
}

// CompSciStudent (computer science student) là một subtrait
// của cả Programmer và Student.
//
// Implement CompSciStudent yêu cầu bạn phải impl tất cả supertraits.
trait CompSciStudent: Programmer + Student {
    fn git_username(&self) -> String;
}

fn comp_sci_student_greeting(student: &dyn CompSciStudent) -> String {
    format!(
        "My name is {} and I attend {}. My favorite language is {}. My Git username is {}",
        student.name(),
        student.university(),
        student.fav_language(),
        student.git_username()
    )
}

}

Auto Traits

Auto traits là các trait đánh dấu (marker trait) được tự động triển khai cho mọi kiểu dữ liệu, trừ khi kiểu dữ liệu hoặc một kiểu dữ liệu mà nó chứa được khai báo tường minh là không impl bằng cách sử dụng negative_impls.

Ta cần bật feature auto_traits để khai báo auto trait.

#![allow(unused)]
#![feature(auto_traits)]

fn main() {
auto trait Valid {}
}

Sau đó, ta có thể triển khai trait Valid cho các kiểu dữ liệu khác nhau:

#![feature(auto_traits)]
#![feature(negative_impls)]
auto trait Valid {}

struct True;
struct False;

// Negative impl
// Có nghĩa là Valid không được auto impl cho struct False
impl !Valid for False {}

// Nếu T được impl trait Valid, thì MaybeValid<T> cũng được impl trait Valid
struct MaybeValid<T>(T);

fn must_be_valid<T: Valid>(_t: T) { }

fn main() {
    // Hoạt động
    must_be_valid(MaybeValid(True));

    // Báo lỗi - do `False` không được impl trait Valid
    // must_be_valid(MaybeValid(False));
}

Auto trait Valid, sẽ tự động impl cho mọi struct, enum, ...

References

  • https://doc.rust-lang.org/beta/unstable-book/language-features/auto-traits.html

Copy, Clone

Có một số kiểu dữ liệu trong Rust rất đơn giản (simple types), bao gồm integers, floats, booleans (truefalse), và char. Các simple types này nằm trên stack bởi vì complier biết chính xác size của nó. Chúng được gọi là copy types. Bởi vì nó simple và nhỏ gọn nên dễ dàng để copy, do đó compiler luôn copy nếu bạn bỏ nó vào function.

Làm sao để biết đọc một kiểu dữ liệu có được implement Copy hay không. Bạn có thể xem trong Rust document. Ví dụ char: https://doc.rust-lang.org/std/primitive.char.html

Nếu bạn thấy:

  • Copy: có thể được copy nếu bạn bỏ nó vào function.
  • Display: bạn có thể sử dụng {} để print.
  • Debug: bạn có thể sử dụng {:?} để print.
fn prints_number(number: i32) {
  println!("{}", number);
}

fn main() {
  let my_number = 8;
  prints_number(my_number); // Prints 8. prints_number gets a copy of my_number
  prints_number(my_number); // Prints 8 again.
                            // No problem, because my_number is copy type!
}

Do i32 được Copy nên chúng ta có thể sử dụng my_number nhiều lần mà không cần borrow & như struct.

Clone trait

Nếu bạn đọc document của String: https://doc.rust-lang.org/std/string/struct.String.html

String không được implement Copy, thay vào đó là Clone. Clone cũng giúp copy giá trị nhưng sẽ cần rất nhiều memory, và ta phải tự gọi method .clone() chứ Rust sẽ không tự Clone.

fn prints_country(country_name: String) {
  println!("{}", country_name);
}

fn main() {
  let country = String::from("Duyet");
  prints_country(country);
  prints_country(country); // ⚠️
}

Sẽ báo lỗi, theo như compiler giải thích rằng countryString và không được implement Copy nên country bị move vào trong function. Do đó ta không thể sử dụng country được nữa.

error[E0382]: use of moved value: `country`
 --> src/main.rs:8:20
  |
6 | let country = String::from("Duyet");
  |     ------- move occurs because `country` has type `String`, which does not implement the `Copy` trait
7 | prints_country(country);
  |                ------- value moved here
8 | prints_country(country); // ⚠️
  |                ^^^^^^^ value used here after move

For more information about this error, try `rustc --explain E0382`.

Có hai cách:

(1) Sử dụng .clone()

fn prints_country(country_name: String) {
  println!("{}", country_name);
}

fn main() {
  let country = String::from("Duyet");
  prints_country(country.clone()); // <-- clone
  prints_country(country);
}

String rất lớn, do đó .copy() sẽ tốn rất nhiều bộ nhớ. Sử dụng & để reference sẽ nhanh hơn, nếu có thể.

(2) Sử dụng & reference

fn prints_country(country_name: &String) {
  println!("{}", country_name);
}

fn main() {
  let country = String::from("Duyet");
  prints_country(&country);
  prints_country(&country);
}

Bonus: String&str

Nếu bạn có một String& reference, Rust sẽ convert nó thành &str khi bạn cần.

fn prints_country(country_name: &str) {
  println!("{}", country_name);
}

fn main() {
  let country = String::from("Duyet");
  prints_country(&country);
  prints_country(&country);
}

&str là một kiểu hơi phức tạp.

  • Nó có thể vừa là String literals let s = "I am &str";. Trường hợp này s có kiểu &'static bởi vì nó được ghi trực tiếp vào binary.
  • &str cũng có thể là borrowed của str hoặc String.

Bonus: String&str

Nếu bạn có một String& reference, Rust sẽ convert nó thành &str khi bạn cần.

fn prints_country(country_name: &str) {
  println!("{}", country_name);
}

fn main() {
  let country = String::from("Duyet");
  prints_country(&country);
  prints_country(&country);
}

&str là một kiểu hơi phức tạp.

  • Nó có thể vừa là String literals let s = "I am &str";. Trường hợp này s có kiểu &'static bởi vì nó được ghi trực tiếp vào binary.
  • &str cũng có thể là borrowed của str hoặc String.

FromStr

FromStr là một trait để khởi tạo instance từ string trong Rust, nó tương đương abstract class nếu bạn có background OOP.

pub trait FromStr {
  type Err;
  fn from_str(s: &str) -> Result<Self, Self::Err>;
}

Thường phương thức from_str của FromStr thường được ngầm định sử dụng thông qua phương thức parse của str. Ví dụ:

// Thay vì
let one = u32::from_str("1");

// thì sử dụng phương thức parse
let one: u32 = "1".parse().unwrap();
assert_eq!(1, one);

// parse() sử dụng turbofish ::<>
let two = "2".parse::<u32>();
assert_eq!(Ok(2), two);

let nope = "j".parse::<u32>();
assert!(nope.is_err());

parse là một phương thức general nên thường được sử dụng với kiểu dữ liệu như trên hoặc sử dụng turbofish ::<> để thuật toán inference có thể hiểu để parse thành đúng kiểu bạn cần.

Parse str to Struct

Bạn có 1 struct và muốn parse 1 str thành struct đó, bạn sẽ cần impl trait FromStr

use std::str::FromStr;
use std::num::ParseIntError;

#[derive(Debug, PartialEq)]
struct Point {
  x: i32,
  y: i32
}

impl FromStr for Point {
  type Err = ParseIntError;

  fn from_str(s: &str) -> Result<Self, Self::Err> {
    let coords: Vec<&str> = s.trim_matches(|p| p == '(' || p == ')' )
                               .split(',')
                               .collect();

    let x_fromstr = coords[0].parse::<i32>()?;
    let y_fromstr = coords[1].parse::<i32>()?;

    Ok(Point { x: x_fromstr, y: y_fromstr })
  }
}

// Có nhiều cách
let p: Point = "(1,2)".parse();
let p = "(1,2)".parse::<Point>();
let p = Point::from_str("(1,2)");

assert_eq!(p.unwrap(), Point{ x: 1, y: 2} )

Parse str to Enum

Một điều mình nhận thấy để code dễ đọc, dễ maintain hơn là ta nên sử dụng Enum thay cho string để so sánh giá trị. Ví dụ:

fn print(color: &str, text: &str) { ... }
print("Foobar", "blue");

Thay vì đó mà hãy sử dụng enum:

enum Color { Red, Green, CornflowerBlue }

fn print(color: Color, text: &str) { ... }
print(Green, "duyet");

Cũng nên hạn chế sử dụng quá nhiều Boolean, thực tế Boolean cũng chỉ là 1 enum

enum bool { true, false }

Thay vào đó hãy tự định nghĩa enum cho các ngữ cảnh khác nhau để code dễ đọc hơn:

enum EnvVars { Clear, Inherit }
enum DisplayStyle { Color, Monochrome }

Chúng ta implement std::str::FromStr trait như sau:

use std::str::FromStr;

#[derive(Debug, PartialEq)]
enum Color {
  Red,
  Green,
  Blue
}

impl FromStr for Color {
  type Err = ();

  fn from_str(input: &str) -> Result<Color, Self::Err> {
    match input {
      "red"   => Ok(Color::Red),
      "green" => Ok(Color::Green),
      "blue"  => Ok(Color::Blue),
      _       => Err(()),
    }
  }
}

let c: Color = "red".parse().unwrap();
assert_eq!(c, Color::Red);

References

Enum

Giống như các ngôn ngữ khác, Enum là một kiểu giá trị đơn, chứa các biến thể (variants).

#![allow(unused)]
fn main() {
enum Day {
  Sunday,
  Monday,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday
}

let today = Day::Sunday;
}

Enum variant có thể là

  • unit variant
  • tuple variant
  • struct variant
#![allow(unused)]
fn main() {
enum FlashMessage {
  Success, // unit variant
  Error(String), // tuple variant
  Warning { category: i32, message: String }, // struct variant
}
}

match Enum

match cực kỳ mạnh và được dùng trong Rust phổ biến.

Ví dụ sau là cách để kiểm tra một giá trị enum là variant nào.

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,
  }
}

fn main() {}

match còn có thể trích xuất các giá trị từ tuple variant hoặc struct variant.

#![allow(unused)]
fn main() {
enum FlashMessage {
  Success, // unit variant
  Error(String), // tuple variant
  Warning { category: i32, message: String }, // struct variant
}

fn format_message(message: FlashMessage) -> String {
  match message {
    FlashMessage::Success => "success".to_string(),
    FlashMessage::Error(err) => format!("My error: {}", err),
    FlashMessage::Warning{ category, message } => format!("Warn: {} (category: {})", message, category),
  }
}

let m = format_message(FlashMessage::Error("something went wrong".to_string()));
println!("{m}");
}

References

use Enum::

Ta có thể mang variants ra ngoài scope của enum bằng use.

enum Coin {
  Penny,
  Nickel,
  Dime,
  Quarter,
}

// hoặc
// use self::Coin::{Penny, Nickel, Dime, Quarter};
use Coin::*;

fn value_in_cents(coin: Coin) -> u8 {
  match coin {
    Penny => 1,
    Nickel => 5,
    Dime => 10,
    Quarter => 25,
  }
}

fn main() {
  assert_eq!(value_in_cents(Penny), 1);
  assert_eq!(value_in_cents(Coin::Penny), 1);
}

impl Enum

Ta cũng có thể impl cho enum giống như struct.

#![allow(unused)]
fn main() {
enum Day {
  Sunday,
  Monday,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday
}

// impl enum
impl Day {
  fn today(self) -> Self {
    self
  }
}

// Trait
trait DayOff {
  fn day_off(self);
}

// impl trait for enum
impl DayOff for Day {
  fn day_off(self) {
    match self.today() {
      Self::Sunday | Self::Saturday => println!("day off"),
      _ => println!("noooo"),
    }
  }
}

let today = Day::Sunday;
today.day_off();
}

match Enum

match cực kỳ mạnh và được dùng trong Rust phổ biến.

Ví dụ sau là cách để kiểm tra một giá trị enum là variant nào.

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,
  }
}

fn main() {}

match còn có thể trích xuất các giá trị từ tuple variant hoặc struct variant.

#![allow(unused)]
fn main() {
enum FlashMessage {
  Success, // unit variant
  Error(String), // tuple variant
  Warning { category: i32, message: String }, // struct variant
}

fn format_message(message: FlashMessage) -> String {
  match message {
    FlashMessage::Success => "success".to_string(),
    FlashMessage::Error(err) => format!("My error: {}", err),
    FlashMessage::Warning{ category, message } => format!("Warn: {} (category: {})", message, category),
  }
}

let m = format_message(FlashMessage::Error("something went wrong".to_string()));
println!("{m}");
}

References

use Enum::

Ta có thể mang variants ra ngoài scope của enum bằng use.

enum Coin {
  Penny,
  Nickel,
  Dime,
  Quarter,
}

// hoặc
// use self::Coin::{Penny, Nickel, Dime, Quarter};
use Coin::*;

fn value_in_cents(coin: Coin) -> u8 {
  match coin {
    Penny => 1,
    Nickel => 5,
    Dime => 10,
    Quarter => 25,
  }
}

fn main() {
  assert_eq!(value_in_cents(Penny), 1);
  assert_eq!(value_in_cents(Coin::Penny), 1);
}

impl Enum

Ta cũng có thể impl cho enum giống như struct.

#![allow(unused)]
fn main() {
enum Day {
  Sunday,
  Monday,
  Tuesday,
  Wednesday,
  Thursday,
  Friday,
  Saturday
}

// impl enum
impl Day {
  fn today(self) -> Self {
    self
  }
}

// Trait
trait DayOff {
  fn day_off(self);
}

// impl trait for enum
impl DayOff for Day {
  fn day_off(self) {
    match self.today() {
      Self::Sunday | Self::Saturday => println!("day off"),
      _ => println!("noooo"),
    }
  }
}

let today = Day::Sunday;
today.day_off();
}

Option

Nhiều ngôn ngữ sử dụng kiểu dữ liệu null hoặc nil hoặc undefined để đại diện cho các giá trị rỗng hoặc không tồn tại, và sử dụng Exception để xử lý lỗi. Rust bỏ qua hai khái niệm này, để tránh gặp phải các lỗi phổ biến như null pointer exceptions, hay lộ thông tin nhạy cảm thông qua exceptions, ... Thay vào đó, Rust giới thiệu hai generic enums OptionResult để giải quyết các vấn đề trên.


Trong hầu hết các ngôn ngữ họ C (C, C#, Java, ...), để xác định một cái gì đó failed hay không tìm được giá trị thỏa mãn, chúng ta thường trả về một giá trị "đặc biệt" nào đó. Ví dụ indexOf() của Javascript scan một phần tử trong mảng, trả về vị trí của phần tử đó trong mảng. Và trả về -1 nếu không tìm thấy.

Dẫn đến, ta sẽ thường thấy một số đoạn code như sau đây:

// Typescript

let sentence = "The fox jumps over the dog";
let index = sentence.indexOf("fox");

if (index > -1) {
  let result = sentence.substr(index);
  console.log(result);
}

Như bạn thấy -1 là một trường hợp đặc biệt cần xử lý. Có khi nào bạn đã từng mắc lỗi ngớ ngẫn vì tưởng giá trị đặc biệt đó là 0 chưa?

// Typescript

if (index > 0) {
  // 3000 days of debugging
}

"" hay null hay None cũng là một trong những trường hợp đặc biệt đó. Bạn đã từng nghe đến Null References: The Billion Dollar Mistake?

Lý do cơ bản là không có gì chắc chắn và có thể ngăn bạn lại việc ... quên xử lý mọi trường hợp giá trị đặc biệt, hoặc do chương trình trả về các giá trị đặc biệt không như mong đợi. Có nghĩa là ta có thể vô tình làm crash chương trình với một lỗi nhỏ ở bất kỳ đâu, ở bất kỳ thời điểm nào.

Rust làm điều này tốt hơn, chỉ với Option.

Một giá trị optional có thể mang một giá trị nào đó Some(something) hoặc không mang giá trị nào cả (None).

#![allow(unused)]
fn main() {
// An output can have either Some value or no value/ None.
enum Option<T> { // T is a generic and it can contain any type of value.
  Some(T),
  None,
}
}

Theo thiết kế, mặc định bạn sẽ không bao giờ lấy được giá trị bạn cần nếu không xử lý các trường hợp có thể xảy ra với Option, là None chẳng hạn. Điều này được bắt buộc bởi compiler lúc compile code, có nghĩa là nếu bạn quên check, code sẽ không bao giờ được compile.

#![allow(unused)]
fn main() {
let sentence = "The fox jumps over the dog";
let index = sentence.find("fox");

if let Some(fox) = index {
  let words_after_fox = &sentence[fox..];
  println!("{}", words_after_fox);
}
}

Cách sử dụng Option

Option là standard library, do đã được preludes nên chúng ta không cần khai báo trước khi sử dụng. Ngoài enum Option thì các variant của nó cũng đã được preludes sẵn như SomeNone.

Ví dụ, ta có một function tính giá trị chia hai số, đôi khi sẽ không tìm ra được kết quả, ta sử dụng Some như sau:

fn get_id_from_name(name: &str) -> Option<i32> {
    if !name.starts_with('d') {
        return None;
    }

    Some(123)
}

fn main() {
    let name = "duyet";

    match get_id_from_name(name) {
        Some(id) => println!("User = {}", id),
        _ => println!("Not found"),
    }
}

Ta thường sử dụng match để bắt giá trị trả về (Some hoặc None).

Bạn sẽ bắt gặp rất nhiều method khác nhau để xử lý giá trị của Option

Option method overview: https://doc.rust-lang.org/std/option/#method-overview

.unwrap()

Trả về giá trị nằm trong Some(T). Nếu giá trị là None thì panic chương trình.

#![allow(unused)]
fn main() {
let x = Some("air");
assert_eq!(x.unwrap(), "air");

let x: Option<&str> = None;
assert_eq!(x.unwrap(), "air"); // panic!
}

.expect()

Giống .unwrap(), nhưng khi panic thì Rust sẽ kèm theo message

#![allow(unused)]
fn main() {
let x: Option<&str> = None;
x.expect("fruits are healthy"); // panics: `fruits are healthy`
}

.unwrap_or()

Trả về giá trị nằm trong Some, nếu không trả về giá trị nằm trong or

#![allow(unused)]
fn main() {
assert_eq!(Some("car").unwrap_or("bike"), "car");
}

.unwrap_or_default()

Trả về giá trị nằm trong Some, nếu không trả về giá default.

#![allow(unused)]
fn main() {
let good_year_from_input = "1909";
let bad_year_from_input = "190blarg";
let good_year = good_year_from_input.parse().ok().unwrap_or_default();
let bad_year = bad_year_from_input.parse().ok().unwrap_or_default();

assert_eq!(1909, good_year);
assert_eq!(0, bad_year);
}

.ok_or()

Convert Option<T> sang Result<T, E>, mapping Some(v) thành Ok(v)None sang Err(err).

#![allow(unused)]
fn main() {
let x = Some("foo");
assert_eq!(x.ok_or(0), Ok("foo"));
}

match

Chúng ta có thể sử dụng pattern matching để code dễ đọc hơn

#![allow(unused)]
fn main() {
fn get_name(who: Option<String>) -> String {
  match who {
    Some(name) => format!("Hello {}", name),
    None       => "Who are you?".to_string(), 
  }
}

get_name(Some("duyet"));
}

if let Some(x) = x

Có thể bạn sẽ gặp pattern này nhiều khi đọc code Rust. Nếu giá trị của xSome thì sẽ destruct giá trị đó bỏ vào biến x nằm trong scope của if.

#![allow(unused)]
fn main() {
fn get_data() -> Option<String> {
    Some("ok".to_string())
}

if let Some(data) = get_data() {
    println!("data = {}", data);
} else {
    println!("no data");
}
}

.unwrap()

Trả về giá trị nằm trong Some(T). Nếu giá trị là None thì panic chương trình.

#![allow(unused)]
fn main() {
let x = Some("air");
assert_eq!(x.unwrap(), "air");

let x: Option<&str> = None;
assert_eq!(x.unwrap(), "air"); // panic!
}

.expect()

Giống .unwrap(), nhưng khi panic thì Rust sẽ kèm theo message

#![allow(unused)]
fn main() {
let x: Option<&str> = None;
x.expect("fruits are healthy"); // panics: `fruits are healthy`
}

.unwrap_or_default()

Trả về giá trị nằm trong Some, nếu không trả về giá default.

#![allow(unused)]
fn main() {
let good_year_from_input = "1909";
let bad_year_from_input = "190blarg";
let good_year = good_year_from_input.parse().ok().unwrap_or_default();
let bad_year = bad_year_from_input.parse().ok().unwrap_or_default();

assert_eq!(1909, good_year);
assert_eq!(0, bad_year);
}

if let Some(x) = x

Có thể bạn sẽ gặp pattern này nhiều khi đọc code Rust. Nếu giá trị của xSome thì sẽ destruct giá trị đó bỏ vào biến x nằm trong scope của if.

#![allow(unused)]
fn main() {
fn get_data() -> Option<String> {
    Some("ok".to_string())
}

if let Some(data) = get_data() {
    println!("data = {}", data);
} else {
    println!("no data");
}
}

Result

Tương tự như Option. Một kết quả trả về (Result) của một function thường sẽ có hai trường hợp:

  • thành công (Ok) và trả về kết quả
  • hoặc lỗi (Err) và trả về thông tin lỗi.

Result là một phiên bản cao cấp hơn của Option. Nó mô tả lỗi gì đang xảy ra thay vì khả năng tồn tại giá trị hay không.

#![allow(unused)]
fn main() {
enum Result<T, E> {
  Ok(T),
  Err(E),
}
}

Ví dụ

fn get_id_from_name(name: &str) -> Result<i32, &str> {
    if !name.starts_with('d') {
        return Err("not found");
    }

    Ok(123)
}

fn main() -> Result<(), &'static str> {
    let name = "duyet";

    match get_id_from_name(name) {
        Ok(id) => println!("User = {}", id),
        Err(e) => println!("Error: {}", e),
    };

    Ok(())
}

Như bạn thấy thì main() cũng có thể return về Result<(), &'static str>

.unwrap()

Ví dụ trên nhưng sử dụng .unwrap() , chủ động panic (crash) dừng chương trình nếu gặp lỗi.

fn main() -> Result<(), &'static str> {
  let who = "duyet";
  let age = get_age(who).unwrap();
  println!("{} is {}", who, age);

  Ok(())
}

.expect()

Giống như unwrap(): chủ động panic (crash) dừng chương trình nếu gặp lỗi và kèm theo message. Sẽ rất có ích, nhất là khi có quá nhiều unwrap, bạn sẽ không biết nó panic ở đâu.

fn main() -> Result<(), &'static str> {
  let who = "ngan";
  let age = get_age(who).expect("could not get age");
  println!("{} is {}", who, age);

  Ok(())
}

Xem thêm mọi method khác của Result tại đây.

Convert Result sang Option

Đôi khi bạn sẽ cần convert từ

  • Ok(v) sang Some(v), hoặc
  • Err(e) sang Some(e)

.ok()

// .ok(v) = Some(v)
let x: Result<u32, &str> = Ok(2);
assert_eq!(x.ok(), Some(2));

let x: Result<u32, &str> = Err("Nothing here");
assert_eq!(x.ok(), None);

.err()

// .err()
let x: Result<u32, &str> = Ok(2);
assert_eq!(x.err(), None);

let x: Result<u32, &str> = Err("Nothing here");
assert_eq!(x.err(), Some("Nothing here"));

Toán tử ?

Khi viết code mà có quá nhiều functions trả về Result, việc handle Err sẽ khá nhàm chán. Toán tử chấm hỏi ? cho phép dừng function tại vị trí đó và return cho function cha nếu Result ở vị trí đó là Err.

Nó sẽ thay thế đoạn code sau:

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::prelude::*;
use std::io;

struct Info {
  name: String,
  age: i32,
  rating: i32,
}

fn write_info(info: &Info) -> io::Result<()> {
  // Early return on error
  let mut file = match File::create("my_best_friends.txt") {
    Err(e) => return Err(e),
    Ok(f) => f,
  };
  if let Err(e) = file.write_all(format!("name: {}\n", info.name).as_bytes()) {
    return Err(e)
  }
  if let Err(e) = file.write_all(format!("age: {}\n", info.age).as_bytes()) {
    return Err(e)
  }
  if let Err(e) = file.write_all(format!("rating: {}\n", info.rating).as_bytes()) {
    return Err(e)
  }
  Ok(())
}
}

thành

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::prelude::*;
use std::io;

struct Info {
  name: String,
  age: i32,
  rating: i32,
}

fn write_info(info: &Info) -> io::Result<()> {
  let mut file = File::create("my_best_friends.txt")?;
  // Early return on error
  file.write_all(format!("name: {}\n", info.name).as_bytes())?;
  file.write_all(format!("age: {}\n", info.age).as_bytes())?;
  file.write_all(format!("rating: {}\n", info.rating).as_bytes())?;
  Ok(())
}
}

Gọn đẹp hơn rất nhiều.

Toán tử ? sẽ unwrap giá trị Ok, hoặc return giá trị Err ở vị trí gần toán tử đó.

? chỉ có thể được dùng trong function có kiểu dữ liệu trả về là Result.

Convert Result sang Option

Đôi khi bạn sẽ cần convert từ

  • Ok(v) sang Some(v), hoặc
  • Err(e) sang Some(e)

.ok()

// .ok(v) = Some(v)
let x: Result<u32, &str> = Ok(2);
assert_eq!(x.ok(), Some(2));

let x: Result<u32, &str> = Err("Nothing here");
assert_eq!(x.ok(), None);

.err()

// .err()
let x: Result<u32, &str> = Ok(2);
assert_eq!(x.err(), None);

let x: Result<u32, &str> = Err("Nothing here");
assert_eq!(x.err(), Some("Nothing here"));

Toán tử ?

Khi viết code mà có quá nhiều functions trả về Result, việc handle Err sẽ khá nhàm chán. Toán tử chấm hỏi ? cho phép dừng function tại vị trí đó và return cho function cha nếu Result ở vị trí đó là Err.

Nó sẽ thay thế đoạn code sau:

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::prelude::*;
use std::io;

struct Info {
  name: String,
  age: i32,
  rating: i32,
}

fn write_info(info: &Info) -> io::Result<()> {
  // Early return on error
  let mut file = match File::create("my_best_friends.txt") {
    Err(e) => return Err(e),
    Ok(f) => f,
  };
  if let Err(e) = file.write_all(format!("name: {}\n", info.name).as_bytes()) {
    return Err(e)
  }
  if let Err(e) = file.write_all(format!("age: {}\n", info.age).as_bytes()) {
    return Err(e)
  }
  if let Err(e) = file.write_all(format!("rating: {}\n", info.rating).as_bytes()) {
    return Err(e)
  }
  Ok(())
}
}

thành

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::prelude::*;
use std::io;

struct Info {
  name: String,
  age: i32,
  rating: i32,
}

fn write_info(info: &Info) -> io::Result<()> {
  let mut file = File::create("my_best_friends.txt")?;
  // Early return on error
  file.write_all(format!("name: {}\n", info.name).as_bytes())?;
  file.write_all(format!("age: {}\n", info.age).as_bytes())?;
  file.write_all(format!("rating: {}\n", info.rating).as_bytes())?;
  Ok(())
}
}

Gọn đẹp hơn rất nhiều.

Toán tử ? sẽ unwrap giá trị Ok, hoặc return giá trị Err ở vị trí gần toán tử đó.

? chỉ có thể được dùng trong function có kiểu dữ liệu trả về là Result.

Generics

Generics là một khái niệm để tổng quát hóa các kiểu dữ liệu hoặc tính năng cho các trường hợp rộng hơn. Tổng quát hóa cực kỳ hữu ích trong việc giảm số lượng code duplication. Một trong những ví dụ phổ biến nhất của tổng quát hóa là tổng quát một function có thể input nhiều loại kiểu dữ liệu khác nhau (type parameters).

Generic type parameters thường được biểu diễn dưới dạng <T>.

Ví dụ, định nghĩa một generic function foo nhận một tham số T của mọi kiểu dữ liệu.

#![allow(unused)]
fn main() {
fn foo<T>(arg: T) { ... }
}

Xem các trang sau để biết chi tiết về generic type được ứng dụng trong các trường hợp khác nhau như thế nào.

Generic Functions

Định nghĩa một generic function bằng cách khai báo generic type <T> sau tên của function.

#![allow(unused)]
fn main() {
fn foo<T>(x: T) {} // x có kiểu T, T là generic type

fn bar<T>(x: T, y: T) {} // x và y đều có kiểu T

fn baz<T, U>(x: T, y: U) {} // sử dụng nhiều generic type
}

Gọi một generic function đôi khi yêu cầu chỉ định kiểu dữ liệu tường minh cho tham số đó. Đôi khi là do function được gọi trả về kiểu dữ liệu là generic, hoặc compiler không có đủ thông tin. Thực thi một function và chỉ định kiểu dữ liệu tường minh có cú pháp như sau:

#![allow(unused)]
fn main() {
function_name::<A, B>()
}

Ví dụ:

fn print_me<T: ToString>(content: T) {
    println!("{}", content.to_string());
}

fn main() {
    print_me::<i32>(100);
    print_me::<u64>(1_000_000);
}

Cú pháp <T: ToString> có nghĩa là: function print_me chấp nhận mọi tham số có kiểu T, miễn sau T được implement trait ToString.

Một ví dụ khác phức tạp hơn từ Rust By Example

struct A;          // Type tường minh `A`.
struct S(A);       // Type tường minh `S`.
struct SGen<T>(T); // Type Generic `SGen`.

// Các function sau sẽ take ownership của variable
// sau đó thoát ra khỏi scope {}, sau đó giải phóng variable.

// Định nghĩa function `reg_fn` nhận tham số `_s` có kiểu `S`.
// Không có `<T>` vì vậy đây không phải là một generic function.
fn reg_fn(_s: S) {}

// Định nghĩa function `gen_spec_t` nhận tham số `_s` có kiểu `SGen<T>`.
// Ở đây tường minh kiểu `A` cho `S`, và bởi vì `A` không được khai báo 
// như là một generic type parameter cho `gen_spec_t`, 
// nên đây cũng không phải là một generic function.
fn gen_spec_t(_s: SGen<A>) {}

// Định nghĩa function `gen_spec_i32` nhận tham số `_s` có kiểu `SGen<i32>`.
// Giống như ở trên, ta khai báo tường minh `i32` cho `T`.
// Bởi vì `i32` không phải là một a generic type, nên function này cũng không
// phải là một genenic function.
fn gen_spec_i32(_s: SGen<i32>) {}

// Định nghĩa một `generic` function, nhận tham số `_s` có kiểu `SGen<T>`.
// Bởi vì `SGen<T>` được đứng trước bởi `<T>`, nên function này generic bởi `T`.
fn generic<T>(_s: SGen<T>) {}

fn main() {
    // Gọi non-generic functions
    reg_fn(S(A));          // Concrete type.
    gen_spec_t(SGen(A));   // Implicitly specified type parameter `A`.
    gen_spec_i32(SGen(6)); // Implicitly specified type parameter `i32`.

    // Chỉ định cụ thể parameter `char` cho `generic()`.
    generic::<char>(SGen('a'));

    // Chỉ định cụ thể `char` to `generic()`.
    generic(SGen('c'));
}

Generic Struct

Giống như function, ta cũng có thể sử dụng generic type cho Struct

struct Point<T> {
  x: T,
  y: T,
}

fn main() {
  let point_a = Point { x: 0, y: 0 }; // T is a int type
  let point_b = Point { x: 0.0, y: 0.0 }; // T is a float type
}

Generic Enum

OptionResult là 2 ví dụ của generic struct.

#![allow(unused)]
fn main() {
enum Option<T> {
    Some(T),
    None,
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

Sử dụng Option<T>:

fn get_id_from_name(name: &str) -> Option<i32> {
    if !name.starts_with('d') {
        return None;
    }

    Some(123)
}

fn main() {
    let name = "duyet";

    match get_id_from_name(name) {
        Some(id) => println!("User = {}", id),
        _ => println!("Not found"),
    }
}

Xem thêm: Enum > Option

Sử dụng Result<T, E>:

fn get_id_from_name(name: &str) -> Result<i32, &str> {
    if !name.starts_with('d') {
        return Err("not found");
    }

    Ok(123)
}

fn main() -> Result<(), &'static str> {
    let name = "duyet";

    match get_id_from_name(name) {
        Ok(id) => println!("User = {}", id),
        Err(e) => println!("Error: {}", e),
    };

    Ok(())
}

Xem tưhêm: Enum > Result<T, E>

Generic Implementation

Ta cũng có thể sử dụng generic type cho implementation.

Khai báo generic type sau từ khóa impl: impl<T, U> ....

struct S; // Kiểu tường minh `S`
struct GenericVal<T>(T); // Generic type `GenericVal`

// impl cho GenericVal, chúng ta có thể chỉ định cụ thể kiểu dữ liệu cho type parameters:
impl GenericVal<f32> {} // `f32`
impl GenericVal<S> {} // `S` được định nghĩa ở trên

// cần khai báo `<T>` để duy trì tính tổng quát
impl<T> GenericVal<T> {}

fn main() {}

Một ví dụ khác

struct GenVal<T> {
    gen_val: T,
}

// impl of GenVal for a generic type `T`
impl<T> GenVal<T> {
    fn value(&self) -> &T {
        &self.gen_val
    }
}

fn main() {
    let x = GenVal { gen_val: 3i32 };
    let y = GenVal::<u32> { gen_val: 6 };

    println!("{}, {}", x.value(), y.value());
}

Generic Trait

Trait cũng có thể được tổng quát hóa.

// Non-copyable types.
struct Empty;
struct Null;

// A trait generic over `T`.
trait DoubleDrop<T> {
    // Định nghĩa một method trên type hiện tại, method nhận
    // một giá trị khác cũng có kiểu `T` và không làm gì với nó.
    fn double_drop(self, _: T);
}

// Implement `DoubleDrop<T>` cho mọi generic parameter `T` và
// caller `U`.
impl<T, U> DoubleDrop<T> for U {
    // Method này take ownership của cả 2 arguments,
    // sau đó giải phóng bộ nhớ cho cả 2, do ra khỏi scope {}
    // mà không làm gì cả.
    fn double_drop(self, _: T) {}
}

fn main() {
    let empty = Empty;
    let null  = Null;

    // Deallocate `empty` and `null`.
    empty.double_drop(null);

    // TODO: uncomment
    // empty;
    // null;
}

Bounds

Cái này rất khó giải thích bằng tiếng Việt bằng một từ đơn giản. Khi sử dụng tổng quát hóa, type parameter thường phải sử dụng các trait như các ràng buộc, giới hạn (bounds) để quy định chức năng (functionality) của kiểu đang được implement.

Trong ví dụ sau sử dụng trait Display để in, vì thế ta cần T bị ràng buộc (bound) bởi Display. Có nghĩa là, ta cần tham số có kiểu TT bắt buộc phải đã được implement Display.

use std::fmt::Display;
fn printer<T: Display>(t: T) {
    println!("{}", t);
}
fn main() {}

Bounding giới hạn lại generic type.

#![allow(unused)]
fn main() {
use std::fmt::Display;
// T phải được impl `Display`
struct S<T: Display>(T);

// error[E0277]: `Vec<{integer}>` doesn't implement `std::fmt::Display`
let s = S(vec![1]);
}

T: Display + Debug

Để bounding nhiều trait, ta sử dụng +. Ví dụ sau có nghĩa T phải được implement trait DisplayDebug.

#![allow(unused)]
fn main() {
use std::fmt::Display;
use core::fmt::Debug;
fn printer<T: Display + Debug>(t: T) {
    println!("{:?}", t);
}
}

Mệnh đề where

Một bound có thể được chỉ dẫn sau mệnh đề where ngay trước { của function.

where giúp cho việc định nghĩa các generic types và bounds một cách rõ ràng hơn.

#![allow(unused)]
fn main() {
use std::fmt::Display;
use core::fmt::Debug;
fn printer<T>(t: T) 
where
    T: Display + Debug
{
    println!("{:?}", t);
}
}
impl <A: TraitB + TraitC, D: TraitE + TraitF> MyTrait<A, D> for YourType {}

// Expressing bounds with a `where` clause
impl <A, D> MyTrait<A, D> for YourType where
    A: TraitB + TraitC,
    D: TraitE + TraitF {}

Vectors

Vector có để được xem là re-sizable array nhưng mọi phần tử trong vec phải có cùng kiểu dữ liệu.

Vector là một generic type: Vec<T>, T có thể là bất kỳ kiểu dữ liệu nào. Ví dụ một vector chứa i32 được viết là Vec<i32>.

Tạo vector

#![allow(unused)]
fn main() {
let a1: Vec<i32> = Vec::new();
let a2: Vec<i32> = vec![];

// Khai báo kiểu cho phần tử đầu tiên
let b2 = vec![1i32, 2, 3];

// Vec chứa mười số 0
let b3 = vec![0; 10];
}

In vector

#![allow(unused)]
fn main() {
let c = vec![5, 4, 3, 2, 1];
println!("vec = {:?}", c);
}

Push và Pop

#![allow(unused)]
fn main() {
let mut d: Vec<i32> = vec![];
d.push(1);
d.push(2);
d.pop();
}

Kiểm tra kích thước của vector

#![allow(unused)]
fn main() {
let d = vec![0; 10];

println!("len = {}", d.len());
}

Box

Tất cả giá trị trên Rust mặc định đều được allocated trên stack. Giá trị có thể được boxed, allocated trên heap bằng cách sử dụng Box<T>. Box<T> là một smart pointer của Rust cho phép allocated trên heap giá trị có kiểu T, còn pointer trỏ đến giá trị đó sẽ nằm trên stack. Xem thêm về stack và heap tại đây.

Khi một Box nằm ngoài scope, destructor sẽ được gọi để giải phóng bộ nhớ. Sử dụng Box không ảnh hưởng nhiều đến performance do Box không bổ sung thêm thông tin metadata nào khác.

fn main() {
  let b = Box::new(5);
  println!("b = {}", b);
}

Ở ví dụ trên, chúng ta định nghĩa b có giá trị của Box đang trỏ đến giá trị 55 đang được allocated trên heap. Chương trình sẽ in ra b = 5 , cách truy cập giống hệt cách allocated trên stack. Giống như owned value, khi box out of scope, cuối hàm main sẽ được giải phóng.

Lưu một giá trị đơn giản trên Box không mang lại lợi ích gì cả. Chúng ta sẽ thường dùng Box trong các trường hợp sau:

  1. Khi bạn có một type mà không biết trước size ở compile time, và bạn cần sử dụng type đó trong một số ngữ cảnh cần biết trước chính xác data size (ví dụ như recursive type).
  2. Bạn cần xử lý các kiểu dữ liệu nhưng chỉ muốn quan tâm đến type đó được implement trait nào.
  3. Khi bạn có một lượng lớn data cần transfer ownership nhưng muốn chắc là data sẽ không bị copy, sẽ ảnh hưởng đến hiệu năng và làm tăng bộ nhớ.

Chúng ta sẽ làm rõ ngay sau đây.

1. Recursive types với Box

Tại compile time, Rust cần biết cần phải biết cần bao nhiêu bộ nhớ. Một trong những kiểu dữ liệu mà Rust không biết trước được size là recursive type. Giá trị có thể là một phần của giá trị khác có cùng một kiểu. Bởi vì nesting of values theo lý thuyết có thể kéo dài đến vô hạn. Trong trường hợp này ta có thể dùng Box.

Cons list là một kiểu dữ liệu phổ biến trong các ngôn ngữ functional programming, là một ví dụ của recursive type. Cons là viết tắt của "construct function". Mỗi item trong cons list có 2 thành phần: giá trị của item hiện tại và next item. Item cuối cùng có giá trị Nil và không có next item.

#![allow(unused)]
fn main() {
enum List {
  Cons(i32, List),
  Nil,
}
}

Bây giờ hãy sử dụng List type để lưu list 1, 2, 3 như sau

enum List {
  Cons(i32, List),
  Nil,
}

use List::{Cons, Nil};

fn main() {
  let list = Cons(1, Cons(2, Cons(3, Nil)));
}

Nếu chúng ta compile đoạn code trên, compiler sẽ báo như sau:

$ cargo run
   Compiling cons-list v0.1.0 (file:///duyet/cons-list)
error[E0072]: recursive type `List` has infinite size
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^ recursive type has infinite size
2 |     Cons(i32, List),
  |               ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable
  |
2 |     Cons(i32, Box<List>),
  |               ++++    +

error[E0391]: cycle detected when computing drop-check constraints for `List`
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^
  |
  = note: ...which immediately requires computing drop-check constraints for `List` again
  = note: cycle used when computing dropck types for `Canonical { max_universe: U0, variables: [], value: ParamEnvAnd { param_env: ParamEnv { caller_bounds: [], reveal: UserFacing }, value: List } }`

Some errors have detailed explanations: E0072, E0391.
For more information about an error, try `rustc --explain E0072`.
error: could not compile `cons-list` due to 2 previous errors

Compiler nói rằng kiểu dữ liệu này has infinite size. Bởi vì List có variant là List::Cons chứa trực tiếp một List khác trong chính nó. Do đó Rust sẽ không biết được sẽ cần bao nhiêu bộ nhớ để lưu giá trị của List.

Dừng lại một chút để xem Rust tính toán bộ nhớ của một kiểu dữ liệu bình thường như thế nào:

#![allow(unused)]
fn main() {
enum Message {
  Quit,
  Move { x: i32, y: i32 },
  Write(String),
  ChangeColor(i32, i32, i32),
}
}

Để xác định bao nhiêu bộ nhớ cần để allocate cho Message, Rust sẽ kiểm tra từng variant (biến thể của enum) để xem variant nào cần bộ nhớ nhiều nhất. Rust thấy rằng Message::Quit không cần, Message::Move phải cần ít nhất bộ nhớ để lưu hai giá trị i32. Tương tự với các variant còn lại. Bởi vì một thời điểm cho có một variant được sử dụng, do đó bộ nhớ tối đa mà Message cần sẽ là một nhớ cần để lưu trữ variant lớn nhất.

Quay lại với Cons List, bộ nhớ mà Rust tính toán được có thể đến vô tận.

Theo như gợi ý của compiler, chúng ta có thể sử dụng Box<T> để có một Recursive Type với một kích thước bộ nhớ xác định:

help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable
  |
2 |     Cons(i32, Box<List>),
  |               ++++    +

Bởi vì Box<T> là một pointer, Rust luôn biết chính xác bao nhiêu bộ nhớ mà một Box<T> pointer cần.

Chương trình của chúng ta lúc này sẽ là:

enum List {
  Cons(i32, Box<List>),
  Nil,
}

use List::{Cons, Nil};

fn main() {
  let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}

2. Sử dụng trait objects cho phép sử dụng giá trị từ nhiều kiểu dữ liệu khác nhau

Một giới hạn của Vec là chỉ có thể lưu trữ các thành phần có kiểu dữ liệu giống nhau mà thôi. Ta có thể lách luật trong một số trường hợp bằng cách sử dụng enum có nhiều variant giữ nhiều kiểu dữ liệu khác nhau

#![allow(unused)]
fn main() {
enum Cell {
  Int(i32),
  Float(f64),
  Text(String),
}

let row = vec![
  Cell::Int(3),
  Cell::Text(String::from("blue")),
  Cell::Float(10.12),
];
}

Tuy nhiên, trong một số trường hợp mong muốn thư viện của chúng ta có thể dễ dàng được mở rộng một số trường hợp khác. Chúng ta đã biết được định nghĩa Trait cho các Common Behavior. Trong Rust, trait định nghĩa các hành vi, và các hành vi này có thể được impl cho struct hoặc enum, để giúp một struct hoặc enum mang đặc tính các hành vi đó.

#![allow(unused)]
fn main() {
pub trait Draw {
  fn draw(&self);
}

pub struct Screen {
  pub components: Vec<Box<dyn Draw>>,
}

impl Screen {
  pub fn run(&self) {
    for component in self.components.iter() {
      component.draw();
    }
  }
}
}

Hãy xem ví dụ trên, ta có components có kiểu dữ liệu là Vec<T> với <T> là một Box<dyn Draw>. Chúng ta đã định nghĩa một vector chứa kiểu dữ liệu là một trait object.

Một trait object được định nghĩa bằng cách định nghĩa pointer, ví dụ như &dyn T hoặc Box<dyn T> smart pointer.

Một trait object sẽ trỏ đến:

  • một instance của một kiểu dữ liệu có implement trait của chúng ta
  • và một bảng ghi look up đến các trait methods lúc runtime.

Sử dụng trait object, Rust type system sẽ chắc chắn là tại thời điểm compile, tất cả các giá trị sử dụng tại ngữ cảnh đó đều phải được implement trai của trait object đó. Nói tóm lại, chúng ta sẽ không cần quan tâm đó là kiểu dữ liệu gì, chỉ cần biết kiểu dữ liệu đó phải được implement trait chúng ta cần là được.

Lý do cần sử dụng pointer reference & hoặc smart pointer Box<T> bởi vì compiler không biết chính xác về kiểu dữ liệu, Rust sẽ dùng pointer của trait object để biết được method nào để cần được gọi. Xem thêm về Trait Objects Perform Dynamic Dispatch.

References

Cow

Cow là một enum cực kỳ tiện dụng, được định nghĩa là "clone on write". Tức ra nó sẽ trả về &str nếu bạn không cần một String, hoặc trả về một String nếu bạn cần String. Tương tự với array &[]Vec, v.v.

Đây là định nghĩa của Cow:

#![allow(unused)]
fn main() {
pub enum Cow<'a, B>
where
    B: 'a + ToOwned + ?Sized,
 {
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}
}

Hãy phân tích B:

'a có nghĩa là Cow làm việc được với references.

Trait ToOwned có nghĩa type này có thể convert thành owned type. Ví dụ, str thường là một reference (&str) hoặc bạn có thể convert nó thành owned String.

?Sized, có nghĩa là có thể có Sized hoặc là không. Hầu hết mọi type trong Rust đều là Sized, nhưng type như là str thì không. Vì thế chúng ta cần & cho str, bởi vì compiler không biết kích thước của str. Nếu bạn cần một trait có thể sử dụng giá trị nào tương tư như str, bạn thêm ?Sized.

Tiếp theo là enum variant: một giá trị Cow có thể là Borrowed hoặc Owned.

Ví dụ bạn có một function trả về giá trị Cow<'static, str>.

#![allow(unused)]
fn main() {
fn cow_function() -> Cow<'static, str> {
    // ...
}
}

Nếu bạn yêu cầu function đó trả về "My message".into(), nó sẽ xem "My message" là một str. Đây là một Borrowed type, do đó variant sẽ là Cow::Borrowed(&'static str).

#![allow(unused)]
fn main() {
fn cow_function() -> Cow<'static, str> {
    "My message".into()
}
}

Còn nếu bạn trả về

#![allow(unused)]
fn main() {
fn cow_function() -> Cow<'static, str> {
    format!("{}", "My message").into()
}
}

lúc này kết quả trả về sẽ là String, bởi vì format!() trả về String. Variant sẽ là Cow::Owned.

Lợi ích của Cow

Copy on write là một kỹ thuật giúp tối ưu hoá, nhất là trong các trường hợp reading nhiều hơn writing. Ý tưởng chính là không copy object ngay lập tức, mà chỉ reference (borrow) đến object gốc, khi cần một lượng lớn tác vụ reading. Và chỉ khi cần đến tác vụ writing (ít xảy ra hơn), object mới được copy và thay đổi. Tác vụ để thay đổi giá trị object sẽ copy, di chuyển object trong bộ nhớ là một tác vụ nặng, tốn kém. Do đó, ưu điểm của kỹ thuật này là các tác vụ reading object rất nhanh do chỉ sử dụng reference (borrow) đến giá trị gốc.

References

/// code comment sao cho đúng

Comment sao cho đúng để đồng đội bớt chửi.

#![allow(unused)]
fn main() {
// hello, world
}

Regular comments

Trong Rust comment bắt đầu bằng 2 slashes // được gọi là Regular comments, chú thích cho một đoạn code hoặc biểu thức theo sau nó. Compiler sẽ không quan tâm đến các Regular comments này.

fn main() {
  // I’m feeling lucky today
  let lucky_number = 7;
}

Nếu comment có nhiều hơn một dòng, hãy ngắt nó thành nhiều dòng -.-

#![allow(unused)]
fn main() {
// So we’re doing something complicated here, long enough that we need
// multiple lines of comments to do it! Whew! Hopefully, this comment will
// explain what’s going on.
}

Comment cũng có thể được đặt cuối dòng code, nếu nó ngắn gọn và đơn giản:

fn main() {
  let lucky_number = 7; // I’m feeling lucky today
}

Doc comments

Doc comments sẽ được Compiler parse thành HTML documentation khi render document bằng cargo doc.

#![allow(unused)]
fn main() {
/// Generate library docs for the following item.
//! Generate library docs for the enclosing item.
}

Doc comments sẽ cực kỳ hữu ích cho project lớn và cần một hệ thống document chính xác và up to date.

//! sẽ generate doc cho crate/mod trong file hiện tại.

#![crate_name = "playground"]

/// A human being is represented here
pub struct Person {
    /// A person must have a name, no matter how much Juliet may hate it
    name: String,
}

impl Person {
    /// Returns a person with the name given them
    ///
    /// # Arguments
    ///
    /// * `name` - A string slice that holds the name of the person
    ///
    /// # Examples
    ///
    /// ```
    /// // You can have rust code between fences inside the comments
    /// // If you pass --test to `rustdoc`, it will even test it for you!
    /// use doc::Person;
    /// let person = Person::new("name");
    /// ```
    pub fn new(name: &str) -> Person {
        Person {
            name: name.to_string(),
        }
    }

    /// Gives a friendly hello!
    ///
    /// Says "Hello, [name]" to the `Person` it is called on.
    pub fn hello(&self) {
        println!("Hello, {}!", self.name);
    }
}

fn main() {
    let john = Person::new("John");

    john.hello();
}

Chúng ta có thể thậm chí comment lại example code hoặc cách sử dụng một function nào đó, code này cũng sẽ được compile và test, đảm bảo được code và document luôn luôn chính xác với nhau, một giải pháp khá thông minh.

#![allow(unused)]
fn main() {
/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
    x + 1
}
}

Doc comments

Doc comments sẽ được Compiler parse thành HTML documentation khi render document bằng cargo doc.

#![allow(unused)]
fn main() {
/// Generate library docs for the following item.
//! Generate library docs for the enclosing item.
}

Doc comments sẽ cực kỳ hữu ích cho project lớn và cần một hệ thống document chính xác và up to date.

//! sẽ generate doc cho crate/mod trong file hiện tại.

#![crate_name = "playground"]

/// A human being is represented here
pub struct Person {
    /// A person must have a name, no matter how much Juliet may hate it
    name: String,
}

impl Person {
    /// Returns a person with the name given them
    ///
    /// # Arguments
    ///
    /// * `name` - A string slice that holds the name of the person
    ///
    /// # Examples
    ///
    /// ```
    /// // You can have rust code between fences inside the comments
    /// // If you pass --test to `rustdoc`, it will even test it for you!
    /// use doc::Person;
    /// let person = Person::new("name");
    /// ```
    pub fn new(name: &str) -> Person {
        Person {
            name: name.to_string(),
        }
    }

    /// Gives a friendly hello!
    ///
    /// Says "Hello, [name]" to the `Person` it is called on.
    pub fn hello(&self) {
        println!("Hello, {}!", self.name);
    }
}

fn main() {
    let john = Person::new("John");

    john.hello();
}

Chúng ta có thể thậm chí comment lại example code hoặc cách sử dụng một function nào đó, code này cũng sẽ được compile và test, đảm bảo được code và document luôn luôn chính xác với nhau, một giải pháp khá thông minh.

#![allow(unused)]
fn main() {
/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
    x + 1
}
}

Turbofish ::<>

Rust Turbofish

Trong trường hợp bạn cần chỉ định kiểu dữ liệu cho một generic function, method, struct, hoặc enum, Rust có một cú pháp đặc biệt để làm điều này gọi là turbofish. Quy tắc là khi nào bạn thấy

$ident<T>

trong bất kỳ định nghĩa nào, thì bạn có thể sử dụng nó dưới dạng

$ident::<T>

để chỉ định kiểu dữ liệu cho generic parameter. Sau đây là một số ví dụ để làm rõ hơn.

Generic Function

Ví dụ function std::mem::size_of() có definition như sau:

pub fn size_of<T>() -> usize

Khi gọi size_of với turbofish:

std::mem::size_of::<u32>()
// 4

sẽ cho ta biết size của u32 theo số bytes.

Generic Method

Phương thức parse() của str bạn cũng sẽ hay gặp cách sử dụng với cú pháp turbofish:

fn parse<F>(&self) -> Result<F, F::Err> where F: FromStr

Chúng ta có thể sử dụng turbofish để mô tả kiểu dữ liệu sẽ được parsed từ str

"1234".parse::<u32>()

Một ví dụ phổ biến nữa là collect() của Iterator

fn collect<B>(self) -> B where B: FromIterator<Self::Item> 

Bởi vì compiler đã biết kiểu dữ liệu của Self::Item mà ta đang collect rồi, chúng ta thường không cần ghi ra. Thay vào đó là sử dụng _ để compiler tự động infer ra. Ví dụ:

let a = vec![1u8, 2, 3, 4];

a.iter().collect::<Vec<_>>();

Sẵn tiện nói về Iterator chúng ta cũng có thể sử dụng turbofish syntax với sum()product().

fn sum<S>(self) -> S where S: Sum<Self::Item>
fn product<P>(self) -> P where P: Product<Self::Item>

Cú pháp như sau:

[1, 2, 3, 4].iter().sum::<u32>()
[1, 2, 3, 4].iter().product::<u32>()

Generic Struct

Trong trường hợp compiler không có đủ thông tin để infer khi tạo generic struct, chúng ta cũng có thể sử dụng turbofish syntax. Ví dụ struct Vec có định nghĩa như sau

pub struct Vec<T> { /* fields omitted */ }

Ví dụ để khởi tạo Vec mới với Vec::new() ta có thể viết

Vec::<u8>::new()

Nhớ là ta bỏ turbofish sau Vec:: không phải sau method new bởi vì struct sử dụng generic type chứ không phải method new. Hơi bựa nhưng nó vẫn thỏa quy tắc của turbofish. Một ví dụ khác

std::collections::HashSet::<u8>::with_capacity(10) 

Ta đang tạo một Hashset với 10 phần tử, bởi vì Hashset struct có định nghĩa như sau

pub struct HashSet<T, S = RandomState> { /* fields omitted */ } 

Chúng ta có thể sử dụng cú pháp này với mọi Rust collections.

Generic Enum

Tuy nhiên Enum lại không theo quy tắc trên, bởi vì enum trong Rust không được scoped tại enum name, do đó ta đặt turbofish sau enum variant. Ví dụ hãy xem enum Result được dùng rất nhiều trong Rust

#[must_use]
pub enum Result<T, E> {
  Ok(T),
  Err(E),
}

Chúng ta sử dụng như thế này:

Result::Ok::<u8, ()>(10)
Result::Err::<u8, ()>(())

Và bởi vì Result thường được prelude (import sẵn) trong Rust, thực tế mọi người sẽ viết như thế này:

Ok::<u8, ()>(10)
Err::<u8, ()>(()) 

References

macros!

Mới bắt đầu với Rust chúng ta thường sử dụng rất nhiều macro như println!.

Thực chất có 3 loại macro trong Rust.

  • Custom #[derive] macros that specify code added with the derive attribute used on structs and enums
  • Attribute-like macros that define custom attributes usable on any item
  • Function-like macros that look like function calls but operate on the tokens specified as their argument

Khác nhau giữa Macros và Functions

// Macros

macro_rules! print_message {
    (msg: $msg:expr) => {
        println!("Message: {}", $msg);
    };
}

fn main() {
    print_message!(msg: "Hello, world!");
}
/// Functions
fn print_message(msg: &str) {
    println!("Message: {}", msg);
}

fn main() {
    print_message("Hello, world!");
}

Điểm khác biệt trong thời điểm biên dịch

  • Functions được thực thi trong quá trình thực thi của chương trình, còn Macros được đánh giá và mở rộng trong quá trình biên dịch.
  • Functions chỉ có thể được gọi khi chương trình đang chạy, trong khi Macros có thể được gọi bất kỳ lúc nào trong quá trình biên dịch.

AST (Abstract Syntax Tree)

  • Macros có thể truy cập vào AST của code được viết, cho phép thay đổi code theo cách động.
  • Functions không có quyền truy cập vào AST của code được viết.

Input / Output

Rust MacrosRust Functions
InputToken streamTham số và đối số của hàm
OutputĐoạn mã Rust được mở rộngGiá trị hoặc hiệu ứng sẽ được trả về
macro_rules! math {
    ($x:expr + $y:expr) => { $x * $y };
}

fn main() {
    let result = math!(4 + 5);
    println!("4 * 5 = {}", result);
}

Khi biên dịch, macro math! sẽ được mở rộng và tạo ra đoạn mã 4 * 5 được tính toán thành 20. Tham số của macro lúc này là $x:expr + $y:exprtoken stream, cho phép khả năng mở rộng cú pháp không giới hạn.

Sử dụng và ứng dụng

  • Functions được sử dụng để đóng gói một khối lệnh nhất định, giúp tái sử dụng và quản lý code dễ dàng hơn.
  • Macros được sử dụng để thay đổi code tại thời điểm biên dịch, giúp viết code ngắn gọn và hiệu quả hơn.

Macros mặc định

Standard Macros được định nghĩa bởi compiler và std.

print!, println!, eprint!, eprintln!
format!, format_args!
write!, writeln!

concat!, concat_idents!, stringify // concat_idents: nightly-only experimental API

include!, include_bytes!, include_str!

assert!, assert_eq!, assert_ne!
debug_assert!, debug_assert_eq!, debug_assert_ne!

try!, panic!, compile_error!, unreachable!, unimplemented!

file!, line!, column!, module_path!
env!, option_env!
cfg!

select!, thread_local! // select: nightly-only experimental API

vec!

Khác nhau giữa Macros và Functions

// Macros

macro_rules! print_message {
    (msg: $msg:expr) => {
        println!("Message: {}", $msg);
    };
}

fn main() {
    print_message!(msg: "Hello, world!");
}
/// Functions
fn print_message(msg: &str) {
    println!("Message: {}", msg);
}

fn main() {
    print_message("Hello, world!");
}

Điểm khác biệt trong thời điểm biên dịch

  • Functions được thực thi trong quá trình thực thi của chương trình, còn Macros được đánh giá và mở rộng trong quá trình biên dịch.
  • Functions chỉ có thể được gọi khi chương trình đang chạy, trong khi Macros có thể được gọi bất kỳ lúc nào trong quá trình biên dịch.

AST (Abstract Syntax Tree)

  • Macros có thể truy cập vào AST của code được viết, cho phép thay đổi code theo cách động.
  • Functions không có quyền truy cập vào AST của code được viết.

Input / Output

Rust MacrosRust Functions
InputToken streamTham số và đối số của hàm
OutputĐoạn mã Rust được mở rộngGiá trị hoặc hiệu ứng sẽ được trả về
macro_rules! math {
    ($x:expr + $y:expr) => { $x * $y };
}

fn main() {
    let result = math!(4 + 5);
    println!("4 * 5 = {}", result);
}

Khi biên dịch, macro math! sẽ được mở rộng và tạo ra đoạn mã 4 * 5 được tính toán thành 20. Tham số của macro lúc này là $x:expr + $y:exprtoken stream, cho phép khả năng mở rộng cú pháp không giới hạn.

Sử dụng và ứng dụng

  • Functions được sử dụng để đóng gói một khối lệnh nhất định, giúp tái sử dụng và quản lý code dễ dàng hơn.
  • Macros được sử dụng để thay đổi code tại thời điểm biên dịch, giúp viết code ngắn gọn và hiệu quả hơn.

Macros mặc định

Standard Macros được định nghĩa bởi compiler và std.

print!, println!, eprint!, eprintln!
format!, format_args!
write!, writeln!

concat!, concat_idents!, stringify // concat_idents: nightly-only experimental API

include!, include_bytes!, include_str!

assert!, assert_eq!, assert_ne!
debug_assert!, debug_assert_eq!, debug_assert_ne!

try!, panic!, compile_error!, unreachable!, unimplemented!

file!, line!, column!, module_path!
env!, option_env!
cfg!

select!, thread_local! // select: nightly-only experimental API

vec!

println!

Đây là một trong những macro được dùng nhiều nhất trong Rust. Giúp in nội dung ra standard output, với một dấu newline xuống dòng.

println! có cùng cú pháp với format!.

Ví dụ:

println!(); // prints just a newline
println!("hello there!");
println!("format {} arguments", "some");

In một Struct

#[derive(Debug)]
struct MyStruct {
  item: String,
  n: i32,
}

let my_struct = MyStruct {
  item: "duyet".to_string(),
  n: 99,
};

println!("my struct = {:?}", my_struct); // my struct = MyStruct { item: "duyet", n: 99 }

format!

Đây là một trong những macro được dùng nhiều nhất trong Rust.

format!() giúp khởi tạo một String. Tham số đầu tiên của format! là chuỗi định dạng. Sức mạnh của format string này ở trong các {}.

Xem các ví dụ sau:

fn main() {
format!("test");
format!("hello {}", "world!");
format!("x = {}, y = {y}", 10, y = 30);

let z = 100;
format!("z = {z}");
}

.to_string() để convert một giá trị thành String

Để convert một giá trị thành String, thay vì sử dụng format!() thì người ta hay sử dụng to_string. Method này sẽ sử dụng Display formatting trait.

fn main() {
// Thay vì
format!("single string");

// Sử dụng
"single string".to_string();
}

References

  • https://doc.rust-lang.org/std/macro.format.html

todo!

Đôi khi bạn sẽ cần viết code một cách tổng quát, định nghĩa một loạt các function trước để hình dung ra flow của project, liệt kê sẵn một loạt các function mà bạn dự định sẽ implementent và sử dụng sau:

struct Book {} // Okay, first I need a book struct.
               // Nothing in there yet - will add later

enum BookType { // A book can be hardcover or softcover, so add an enum
    HardCover,
    SoftCover,
}

fn get_book(book: &Book) -> Option<String> {} // ⚠️ get_book should take a &Book and return an Option<String>

fn delete_book(book: Book) -> Result<(), String> {} // delete_book should take a Book and return a Result...
                                                    // TODO: impl block and make these functions methods...
fn check_book_type(book_type: &BookType) { // Let's make sure the match statement works
    match book_type {
        BookType::HardCover => println!("It's hardcover"),
        BookType::SoftCover => println!("It's softcover"),
    }
}

fn main() {
    let book_type = BookType::HardCover;
    check_book_type(&book_type); // Okay, let's check this function!
}

Chúng ta chưa dùng đến, nhưng compiler vẫn sẽ không happy với get_bookdelete_book.

error[E0308]: mismatched types
  --> src\main.rs:32:29
   |
32 | fn get_book(book: &Book) -> Option<String> {}
   |    --------                 ^^^^^^^^^^^^^^ expected enum `std::option::Option`, found `()`
   |    |
   |    implicitly returns `()` as its body has no tail or `return` expression
   |
   = note:   expected enum `std::option::Option<std::string::String>`
           found unit type `()`

error[E0308]: mismatched types
  --> src\main.rs:34:31
   |
34 | fn delete_book(book: Book) -> Result<(), String> {}
   |    -----------                ^^^^^^^^^^^^^^^^^^ expected enum `std::result::Result`, found `()`
   |    |
   |    implicitly returns `()` as its body has no tail or `return` expression
   |
   = note:   expected enum `std::result::Result<(), std::string::String>`
           found unit type `()`

Khi này chúng ta sử dụng todo!(), Rust sẽ compile và không complain gì về những function dang dở này nữa.

struct Book {}

fn get_book(book: &Book) -> Option<String> {
    todo!() // todo means "I will do it later, please be quiet"
}

fn delete_book(book: Book) -> Result<(), String> {
    todo!()
}

fn main() {}

References

macro_rules!

macro_rules! là một macro cho phép định nghĩa một macro khác. Với macro này, bạn có thể tạo ra các macros như println! hoặc vec!.

// This is a simple macro named `say_hello`.
macro_rules! say_hello {
    // `()` indicates that the macro takes no argument.
    () => {
        // The macro will expand into the contents of this block.
        println!("Hello!");
    };
}

fn main() {
    // This call will expand into `println!("Hello");`
    say_hello!()
}

Trong đó say_hello là tên của macro bạn đang định nghĩa. Sau đó, bạn có thể xác định các rule cho macro trong các block code phía sau. Các quy tắc này được xác định bằng cách sử dụng các pattern và các rule. Pattern được sử dụng để so khớp với các biểu thức mà macro được áp dụng. Rule được sử dụng để chỉ định mã được tạo ra bởi macro khi so khớp với pattern.

// This is a simple macro named `say_hello`.
macro_rules! say_hello {
    // `()` indicates that the macro takes no argument.
    () => {
        // The macro will expand into the contents of this block.
        println!("Hello!");
    };

    ($name: expr) => {
        println!("Hello {}!", $name);
    };
}

fn main() {
    // This call will expand into `println!("Hello {}", "Duyet");`
    say_hello!("Duyet")
}

Trong ví dụ trên, khi gọi say_hello!("Duyet") được so khớp với pattern ($name: expr) do đó sau khi biên dịch, đoạn mã được thực thi sẽ là println!("Hello {}!", "Duyet");

Repeat *, +

Dưới đây là một ví dụ khác về cách sử dụng macro_rules! để định nghĩa một macro trong Rust:

macro_rules! vec_of_strings {
    ( $( $x:expr ),* ) => {
        vec![ $( $x.to_string() ),* ]
    };
}

fn main() {
    let fruits = vec_of_strings!["apple", "banana", "cherry"];
    println!("{:?}", fruits);
}

Trong đó, pattern $( $x:expr ),* so khớp với một danh sách các biểu thức được chuyển vào macro, được phân tách bằng dấu phẩy ,. Rule vec![ $( $x.to_string() ),* ] được sử dụng để tạo ra một Vec các chuỗi được chuyển đổi từ các biểu thức được truyền vào.

Khi biên dịch, macro vec_of_strings! được mở rộng và tạo ra một Vec chứa các chuỗi "apple", "banana", và "cherry". Kết quả khi chạy chương trình sẽ là ["apple", "banana", "cherry"] .

Đệ quy macros

Dưới đây là một ví dụ về cách sử dụng đệ quy trong macro_rules! để tạo ra các macro phức tạp hơn:

macro_rules! countdown {
    ($x:expr) => {
        println!("{}", $x);
    };
    ($x:expr, $($rest:expr),+) => {
        println!("{}", $x);
        countdown!($($rest),*);
    };
}

fn main() {
    countdown!(5, 4, 3, 2, 1);
}

Trong quy tắc macro countdown!, chúng ta sử dụng cấu trúc $x:expr để lấy giá trị biểu thức được truyền vào và in ra nó bằng lệnh println!. Sau đó, chúng ta sử dụng cấu trúc $($rest:expr),+ để lấy danh sách các biểu thức còn lại, và gọi lại macro countdown! với danh sách này. Quá trình đệ quy này sẽ tiếp tục cho đến khi danh sách các biểu thức truyền vào rỗng, khi đó macro sẽ không còn gọi lại chính nó nữa.

References

  • https://doc.rust-lang.org/rust-by-example/macros.html

match

match được dùng khá phổ biến trong Rust (Pattern Syntax).

Matching giá trị

Matching Named Variables

Matching Multiple

#[attributes]

Attribute là metadata được apply cho một số module, crate hoặc item. Metadata này được dùng cho việc:

  • conditional compilation of code: compile code theo điều kiện, ví dụ một số code sẽ chỉ được compile cho tests, cho OS cụ thể, cho một số feature nào đó, etc.

    // This function only gets compiled if the target OS is linux
    #[cfg(target_os = "linux")]
    fn are_you_on_linux() {
        println!("You are running linux!");
    }
    
    // And this function only gets compiled if the target OS is *not* linux
    #[cfg(not(target_os = "linux"))]
    fn are_you_on_linux() {
        println!("You are *not* running linux!");
    }
  • set crate name, version and type (binary or library)

    // This crate is a library
    #![crate_type = "lib"]
    // The library is named "rary"
    #![crate_name = "rary"]
    
    pub fn public_function() {
        println!("called rary's `public_function()`");
    }
  • disable lints (warnings)

  • bật một số tính năng của compiler (macros, glob imports, etc.)

  • link đến foreign library

  • đánh dấu các function là unit tests

    #[test]
    fn test_hello() {
        assert!("hello");
    }
  • đánh dấu function là một phần của benchmark

Khi một attributes được apply cho cả crate, cú pháp là #![crate_attribute]. Khi apply cho một module hoặc item, cú pháp là #[item_attribute] (không có dấu !).

Attributes cũng có thể có tham số:

  • #[attribute = "value"]
  • #[attribute(key = "value")]
  • #[attribute(value)]

Attributes có thể có nhiều giá trị, có thể break thành nhiều dòng:

#[attribute(value, value2)]

#[attribute(value, value2, value3,
            value4, value5)]

Xử lý lỗi

Có rất nhiều cách để deal với error trong Rust. Bạn vui lòng xem trong các trang tiếp theo, mặc dù có nhiều cách để xử lý tùy theo các trường hợp khác nhau, có một quy luật chung:

  • panic hầu hết hữu ích trong khi tests (panic khi test fail) hoặc đối mặt với các lỗi không thể xử lý được.
  • Option khi một giá trị nào đó là không bắt buộc hoặc thiếu các giá trị này vẫn không gây lỗi cho chương trình. Chỉ sử dụng unwrap() khi prototyping hoặc các trường hợp chắc chắn luôn luôn có giá trị. Tuy nhiên expect() thì ổn hơn bởi nó giúp chúng ta bỏ thêm error message khi có biến.
  • Khi có khả năng một function nào đó có thể lỗi, và người gọi function bắt buộc phải xử lý lỗi đó, hãy sử dụng Result. Vui lòng chỉ sử dụng unwrap()expect() trong khi test hoặc prototype.

panic

Cơ chế đơn giản nhất để xử lý lỗi là panic!. Panic sẽ in error message và thường sẽ thoát chương trình.

fn drink(beverage: &str) {
    if beverage == "lemonade" { panic!("AAAaaaaa!!!!"); }

    println!("Some refreshing {} is all I need.", beverage);
}

fn main() {
    drink("water");
    drink("lemonade");
}

Option

Nhiều ngôn ngữ sử dụng kiểu dữ liệu null hoặc nil hoặc undefined để đại diện cho các giá trị rỗng hoặc không tồn tại, và sử dụng Exception để xử lý lỗi. Rust bỏ qua hai khái niệm này, để tránh gặp phải các lỗi phổ biến như null pointer exceptions, hay lộ thông tin nhạy cảm thông qua exceptions, ... Thay vào đó, Rust giới thiệu hai generic enums OptionResult để giải quyết các vấn đề trên.


Trong hầu hết các ngôn ngữ họ C (C, C#, Java, ...), để xác định một cái gì đó failed hay không tìm được giá trị thỏa mãn, chúng ta thường trả về một giá trị "đặc biệt" nào đó. Ví dụ indexOf() của Javascript scan một phần tử trong mảng, trả về vị trí của phần tử đó trong mảng. Và trả về -1 nếu không tìm thấy.

Dẫn đến, ta sẽ thường thấy một số đoạn code như sau đây:

// Typescript

let sentence = "The fox jumps over the dog";
let index = sentence.indexOf("fox");

if (index > -1) {
  let result = sentence.substr(index);
  console.log(result);
}

Như bạn thấy -1 là một trường hợp đặc biệt cần xử lý. Có khi nào bạn đã từng mắc lỗi ngớ ngẫn vì tưởng giá trị đặc biệt đó là 0 chưa?

// Typescript

if (index > 0) {
  // 3000 days of debugging
}

"" hay null hay None cũng là một trong những trường hợp đặc biệt đó. Bạn đã từng nghe đến Null References: The Billion Dollar Mistake?

Lý do cơ bản là không có gì chắc chắn và có thể ngăn bạn lại việc ... quên xử lý mọi trường hợp giá trị đặc biệt, hoặc do chương trình trả về các giá trị đặc biệt không như mong đợi. Có nghĩa là ta có thể vô tình làm crash chương trình với một lỗi nhỏ ở bất kỳ đâu, ở bất kỳ thời điểm nào.

Rust làm điều này tốt hơn, chỉ với Option.

Một giá trị optional có thể mang một giá trị nào đó Some(something) hoặc không mang giá trị nào cả (None).

#![allow(unused)]
fn main() {
// An output can have either Some value or no value/ None.
enum Option<T> { // T is a generic and it can contain any type of value.
  Some(T),
  None,
}
}

Theo thiết kế, mặc định bạn sẽ không bao giờ lấy được giá trị bạn cần nếu không xử lý các trường hợp có thể xảy ra với Option, là None chẳng hạn. Điều này được bắt buộc bởi compiler lúc compile code, có nghĩa là nếu bạn quên check, code sẽ không bao giờ được compile.

#![allow(unused)]
fn main() {
let sentence = "The fox jumps over the dog";
let index = sentence.find("fox");

if let Some(fox) = index {
  let words_after_fox = &sentence[fox..];
  println!("{}", words_after_fox);
}
}

Cách sử dụng Option

Option là standard library, do đã được preludes nên chúng ta không cần khai báo trước khi sử dụng. Ngoài enum Option thì các variant của nó cũng đã được preludes sẵn như SomeNone.

Ví dụ, ta có một function tính giá trị chia hai số, đôi khi sẽ không tìm ra được kết quả, ta sử dụng Some như sau:

fn get_id_from_name(name: &str) -> Option<i32> {
    if !name.starts_with('d') {
        return None;
    }

    Some(123)
}

fn main() {
    let name = "duyet";

    match get_id_from_name(name) {
        Some(id) => println!("User = {}", id),
        _ => println!("Not found"),
    }
}

Ta thường sử dụng match để bắt giá trị trả về (Some hoặc None).

Bạn sẽ bắt gặp rất nhiều method khác nhau để xử lý giá trị của Option

Option method overview: https://doc.rust-lang.org/std/option/#method-overview

.unwrap()

Trả về giá trị nằm trong Some(T). Nếu giá trị là None thì panic chương trình.

#![allow(unused)]
fn main() {
let x = Some("air");
assert_eq!(x.unwrap(), "air");

let x: Option<&str> = None;
assert_eq!(x.unwrap(), "air"); // panic!
}

.expect()

Giống .unwrap(), nhưng khi panic thì Rust sẽ kèm theo message

#![allow(unused)]
fn main() {
let x: Option<&str> = None;
x.expect("fruits are healthy"); // panics: `fruits are healthy`
}

.unwrap_or()

Trả về giá trị nằm trong Some, nếu không trả về giá trị nằm trong or

#![allow(unused)]
fn main() {
assert_eq!(Some("car").unwrap_or("bike"), "car");
}

.unwrap_or_default()

Trả về giá trị nằm trong Some, nếu không trả về giá default.

#![allow(unused)]
fn main() {
let good_year_from_input = "1909";
let bad_year_from_input = "190blarg";
let good_year = good_year_from_input.parse().ok().unwrap_or_default();
let bad_year = bad_year_from_input.parse().ok().unwrap_or_default();

assert_eq!(1909, good_year);
assert_eq!(0, bad_year);
}

.ok_or()

Convert Option<T> sang Result<T, E>, mapping Some(v) thành Ok(v)None sang Err(err).

#![allow(unused)]
fn main() {
let x = Some("foo");
assert_eq!(x.ok_or(0), Ok("foo"));
}

match

Chúng ta có thể sử dụng pattern matching để code dễ đọc hơn

#![allow(unused)]
fn main() {
fn get_name(who: Option<String>) -> String {
  match who {
    Some(name) => format!("Hello {}", name),
    None       => "Who are you?".to_string(), 
  }
}

get_name(Some("duyet"));
}

if let Some(x) = x

Có thể bạn sẽ gặp pattern này nhiều khi đọc code Rust. Nếu giá trị của xSome thì sẽ destruct giá trị đó bỏ vào biến x nằm trong scope của if.

#![allow(unused)]
fn main() {
fn get_data() -> Option<String> {
    Some("ok".to_string())
}

if let Some(data) = get_data() {
    println!("data = {}", data);
} else {
    println!("no data");
}
}

Result

Tương tự như Option. Một kết quả trả về (Result) của một function thường sẽ có hai trường hợp:

  • thành công (Ok) và trả về kết quả
  • hoặc lỗi (Err) và trả về thông tin lỗi.

Result là một phiên bản cao cấp hơn của Option. Nó mô tả lỗi gì đang xảy ra thay vì khả năng tồn tại giá trị hay không.

#![allow(unused)]
fn main() {
enum Result<T, E> {
  Ok(T),
  Err(E),
}
}

Ví dụ

fn get_id_from_name(name: &str) -> Result<i32, &str> {
    if !name.starts_with('d') {
        return Err("not found");
    }

    Ok(123)
}

fn main() -> Result<(), &'static str> {
    let name = "duyet";

    match get_id_from_name(name) {
        Ok(id) => println!("User = {}", id),
        Err(e) => println!("Error: {}", e),
    };

    Ok(())
}

Như bạn thấy thì main() cũng có thể return về Result<(), &'static str>

.unwrap()

Ví dụ trên nhưng sử dụng .unwrap() , chủ động panic (crash) dừng chương trình nếu gặp lỗi.

fn main() -> Result<(), &'static str> {
  let who = "duyet";
  let age = get_age(who).unwrap();
  println!("{} is {}", who, age);

  Ok(())
}

.expect()

Giống như unwrap(): chủ động panic (crash) dừng chương trình nếu gặp lỗi và kèm theo message. Sẽ rất có ích, nhất là khi có quá nhiều unwrap, bạn sẽ không biết nó panic ở đâu.

fn main() -> Result<(), &'static str> {
  let who = "ngan";
  let age = get_age(who).expect("could not get age");
  println!("{} is {}", who, age);

  Ok(())
}

Xem thêm mọi method khác của Result tại đây.

Convert Result sang Option

Đôi khi bạn sẽ cần convert từ

  • Ok(v) sang Some(v), hoặc
  • Err(e) sang Some(e)

.ok()

// .ok(v) = Some(v)
let x: Result<u32, &str> = Ok(2);
assert_eq!(x.ok(), Some(2));

let x: Result<u32, &str> = Err("Nothing here");
assert_eq!(x.ok(), None);

.err()

// .err()
let x: Result<u32, &str> = Ok(2);
assert_eq!(x.err(), None);

let x: Result<u32, &str> = Err("Nothing here");
assert_eq!(x.err(), Some("Nothing here"));

Toán tử ?

Khi viết code mà có quá nhiều functions trả về Result, việc handle Err sẽ khá nhàm chán. Toán tử chấm hỏi ? cho phép dừng function tại vị trí đó và return cho function cha nếu Result ở vị trí đó là Err.

Nó sẽ thay thế đoạn code sau:

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::prelude::*;
use std::io;

struct Info {
  name: String,
  age: i32,
  rating: i32,
}

fn write_info(info: &Info) -> io::Result<()> {
  // Early return on error
  let mut file = match File::create("my_best_friends.txt") {
    Err(e) => return Err(e),
    Ok(f) => f,
  };
  if let Err(e) = file.write_all(format!("name: {}\n", info.name).as_bytes()) {
    return Err(e)
  }
  if let Err(e) = file.write_all(format!("age: {}\n", info.age).as_bytes()) {
    return Err(e)
  }
  if let Err(e) = file.write_all(format!("rating: {}\n", info.rating).as_bytes()) {
    return Err(e)
  }
  Ok(())
}
}

thành

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::prelude::*;
use std::io;

struct Info {
  name: String,
  age: i32,
  rating: i32,
}

fn write_info(info: &Info) -> io::Result<()> {
  let mut file = File::create("my_best_friends.txt")?;
  // Early return on error
  file.write_all(format!("name: {}\n", info.name).as_bytes())?;
  file.write_all(format!("age: {}\n", info.age).as_bytes())?;
  file.write_all(format!("rating: {}\n", info.rating).as_bytes())?;
  Ok(())
}
}

Gọn đẹp hơn rất nhiều.

Toán tử ? sẽ unwrap giá trị Ok, hoặc return giá trị Err ở vị trí gần toán tử đó.

? chỉ có thể được dùng trong function có kiểu dữ liệu trả về là Result.

Result map

Ta có thể xử lý giá trị bên trong Result mà không cần xử lý Err, trong trường hợp bạn muốn trả Err cho hàm bên trên đó tự lý.

use std::num::ParseIntError;

fn multiply(a: &str, b: &str) -> Result<i32, ParseIntError> {
  match a.parse::<i32>() {
    Ok(first) => {
      match b.parse::<i32>() {
        Ok(second) => Ok(first * second),
        Err(e) => Err(e),
      }
    },
    Err(e) => Err(e),
  }
}

fn print(result: Result<i32, ParseIntError>) {
  match result {
    Ok(n)  => println!("n is {}", n),
    Err(e) => println!("Error: {}", e),
  }
}

fn main() {
  let twenty = multiply("10", "2");
  print(twenty);

  let tt = multiply("t", "2");
  print(tt);
}

Thay vào đó ta sử dụng .map(), .and_then() để đoạn code trên hiệu quả và dễ đọc hơn.

use std::num::ParseIntError;

fn multiply(a: &str, b: &str) -> Result<i32, ParseIntError> {
  a.parse::<i32>().and_then(|first| {
    b.parse::<i32>().map(|second| first * second)
  })
}

fn print(result: Result<i32, ParseIntError>) {
  match result {
    Ok(n)  => println!("n is {}", n),
    Err(e) => println!("Error: {}", e),
  }
}

fn main() {
    let twenty = multiply("10", "2");
    print(twenty);

    let tt = multiply("t", "2");
    print(tt);
}

Result alias

Rust cho phép chúng ta tạo alias. Việc alias Result sẽ tiết kiệm chúng ta rất nhiều thời gian, nhất là trong cùng một module và ta đang cố reuse Result nhiều lần.

use std::num::ParseIntError;

// Define a generic alias for a `Result` with the error type `ParseIntError`.
type AliasedResult<T> = Result<T, ParseIntError>;

// Use the above alias to refer to our specific `Result` type.
fn multiply(a: &str, b: &str) -> AliasedResult<i32> {
  a.parse::<i32>().and_then(|first| {
    b.parse::<i32>().map(|second| first * second)
  })
}

fn print(result: AliasedResult<i32>) {
  match result {
    Ok(n)  => println!("n is {}", n),
    Err(e) => println!("Error: {}", e),
  }
}

fn main() {
  print(multiply("10", "2"));
  print(multiply("t", "2"));
}

Viết Tests

Rust được thiết kế để đưa tính đúng đắn của chương trình lên hàng đầu. Như bạn đã thấy với những gì mà borrow checkers của compiler hay hệ thống type đã làm. Nhưng tính đúng đắn thì cực kỳ phức tạp và Rust không thể đảm bảo hết điều này.

Tuy nhiên, testing lại là một kỹ năng phức tạp. Mục tiêu của phần này mình sẽ không bàn đến việc viết good tests như thế nào, chúng ta sẽ bàn về những gì mà Rust cung cấp để giúp chúng ta viết tests, những công cụ, macros, chạy tests, cách tổ chức unit tests và integration tests.

Tổ chức Tests

Testing là một kỹ năng phức tạp. Nhiều lập trình viên sử dụng nhiều thuật ngữ khác nhau và tổ chức code tests khác nhau. Cộng đồng Rust đặt ra hai loại tests:

  • Unit tests: nhỏ và tập trung vào một function, module độc lập tại một thời điểm.
  • Integration tests: tests nằm ngoài thư viện của bạn, sử dụng thư viện của bạn để tests như thể là một chương trình thực tế sẽ thực sự sử dụng. Chỉ test trên public interface và tập trung vào cả 1 module cho một test.

Unit Tests

Unit tests được đặt trong thư mục src trong mỗi file code mà bạn đang test. Có một convention là tạo một module tên tests trong mỗi file chứa function cần test, annotate module này với attribute #[cfg(test)].

#[cfg(test)]

#[cfg(test)] báo cho compiler biết module này được dùng để compile thành test, chỉ được dùng khi chạy cargo test, không dùng khi cargo build.

File: src/lib.rs

#![allow(unused)]
fn main() {
pub fn adder(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn test_adder() {
    let expected = 4;
    let actual = adder(2, 2);

    assert_eq!(expected, actual);
  }
}
}

Trong module tests có thể sẽ có một vài helper function khác, do đó những function nào là function test sẽ được đánh dấu là #[test] để compiler nhận biết.

Test private function

Ở ví dụ trên thì public function adder được import vào trong module tests theo đúng rule của Rust. Vậy còn private function thì sao? Có một cuộc tranh cãi trong cộng đồng về việc này. Cuối cùng thì Rust cho phép import private function vào tests module.

#![allow(unused)]
fn main() {
pub fn adder(a: i32, b: i32) -> i32 {
    adder_internal(a, b)
}

fn adder_internal(a: i32, b: i32) -> i32 {
    a + b
}
 
#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn test_adder() {
    let expected = 4;
    let actual = adder_internal(2, 2);

    assert_eq!(expected, actual);
  }
}
}

Integration Tests

Integration tests nằm hoàn toàn bên ngoài thư viện của bạn. Mục đích của integration tests nhằm kiểm tra các thành phần của thư viện của bạn hoạt động cùng với nhau có chính xác không.

Để bắt đầu viết integration tests, tạo thư mục tests nằm cùng cấp với src.

Trong thư mục tests, cargo sẽ compile mỗi file thành một thành một crate độc lập.

File: tests/integration_test.rs

#![allow(unused)]
fn main() {
use adder;

#[test]
fưn it_adds_two() {
  assert_eq!(4, adder::adder(2, 2));
}
}

Ở đây chúng ta import use adder thay vì use crate:: do file integration test này là một crate độc lập.

Chạy cargo test

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 1.31s
     Running unittests (target/debug/deps/adder-1082c4b063a8fbe6)

running 1 test
test tests::test_adder ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/integration_test.rs (target/debug/deps/integration_test-1082c4b063a8fbe6)

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sđ

Do integration tests phải import thư viện để chạy test code, crate bắt buộc phải có src/lib.rs. Các binary crates không thể test theo cách này.

Do đó các project Rust họ thường tổ chức theo kiểu build binary từ src/main.rs và import trực tiếp logic từ src/lib.rs.

References

Doc Tests

Rust cũng hỗ trợ execute code ví dụ trên document như là một test. Đây là giải pháp cực kỳ thông minh giúp đảm bảo example code luôn up to date và nó có hoạt động.

#![allow(unused)]
fn main() {
/// Function that adding two number
///
/// # Example
///
/// ```
/// use adder::adder;
/// 
/// assert_eq!(4, adder(2, 2));
/// ```
pub fn adder(a: i32, b: i32) -> i32 {
    a + b
}
}

Khi chạy cargo test hoặc cargo test --doc, cargo sẽ compile phần example code này thành một crate test và thực thi nó.

Mặc định nếu không chỉ định ngôn ngữ cho block code thì rustdoc sẽ ngầm định nó là Rust code. Do đó

```
let x = 5;
```

sẽ tương đương với

```rust
let x = 5;
```

Ẩn một phần của example code

Đôi lúc bạn sẽ cần example code gọn hơn, ẩn bớt một số logic mà bạn chuẩn bị để code có thể chạy được, tránh làm distract của người xem.

/// ```
/// /// Some documentation.
/// # fn foo() {} // this function will be hidden
/// println!("Hello, World!");
/// ```

Chúng ta thêm # ở phần đầu của dòng code muốn ẩn đi trong generate doc, nó vẫn sẽ được compile như mình thường.

Sử dụng ? trong doc tests

? chỉ có thể được sử dụng khi function trả về Result<T, E>. Hãy sử dụng cách sau:

/// A doc test using ?
///
/// ```
/// use std::io;
/// fn main() -> io::Result<()> {
///     let mut input = String::new();
///     io::stdin().read_line(&mut input)?;
///     Ok(())
///  }
/// ```

Cùng với việc sử dụng # như ở trên, chúng ta có thể ẩn đi bớt logic.

/// A doc test using ?
///
/// ```
/// # use std::io;
/// # fn main() -> io::Result<()> {
/// let mut input = String::new();
/// io::stdin().read_line(&mut input)?;
/// # Ok(())
/// # }
/// ```

References

Unit Tests

Unit tests được đặt trong thư mục src trong mỗi file code mà bạn đang test. Có một convention là tạo một module tên tests trong mỗi file chứa function cần test, annotate module này với attribute #[cfg(test)].

#[cfg(test)]

#[cfg(test)] báo cho compiler biết module này được dùng để compile thành test, chỉ được dùng khi chạy cargo test, không dùng khi cargo build.

File: src/lib.rs

#![allow(unused)]
fn main() {
pub fn adder(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn test_adder() {
    let expected = 4;
    let actual = adder(2, 2);

    assert_eq!(expected, actual);
  }
}
}

Trong module tests có thể sẽ có một vài helper function khác, do đó những function nào là function test sẽ được đánh dấu là #[test] để compiler nhận biết.

Test private function

Ở ví dụ trên thì public function adder được import vào trong module tests theo đúng rule của Rust. Vậy còn private function thì sao? Có một cuộc tranh cãi trong cộng đồng về việc này. Cuối cùng thì Rust cho phép import private function vào tests module.

#![allow(unused)]
fn main() {
pub fn adder(a: i32, b: i32) -> i32 {
    adder_internal(a, b)
}

fn adder_internal(a: i32, b: i32) -> i32 {
    a + b
}
 
#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn test_adder() {
    let expected = 4;
    let actual = adder_internal(2, 2);

    assert_eq!(expected, actual);
  }
}
}

Integration Tests

Integration tests nằm hoàn toàn bên ngoài thư viện của bạn. Mục đích của integration tests nhằm kiểm tra các thành phần của thư viện của bạn hoạt động cùng với nhau có chính xác không.

Để bắt đầu viết integration tests, tạo thư mục tests nằm cùng cấp với src.

Trong thư mục tests, cargo sẽ compile mỗi file thành một thành một crate độc lập.

File: tests/integration_test.rs

#![allow(unused)]
fn main() {
use adder;

#[test]
fưn it_adds_two() {
  assert_eq!(4, adder::adder(2, 2));
}
}

Ở đây chúng ta import use adder thay vì use crate:: do file integration test này là một crate độc lập.

Chạy cargo test

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 1.31s
     Running unittests (target/debug/deps/adder-1082c4b063a8fbe6)

running 1 test
test tests::test_adder ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/integration_test.rs (target/debug/deps/integration_test-1082c4b063a8fbe6)

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sđ

Do integration tests phải import thư viện để chạy test code, crate bắt buộc phải có src/lib.rs. Các binary crates không thể test theo cách này.

Do đó các project Rust họ thường tổ chức theo kiểu build binary từ src/main.rs và import trực tiếp logic từ src/lib.rs.

References

Doc Tests

Rust cũng hỗ trợ execute code ví dụ trên document như là một test. Đây là giải pháp cực kỳ thông minh giúp đảm bảo example code luôn up to date và nó có hoạt động.

#![allow(unused)]
fn main() {
/// Function that adding two number
///
/// # Example
///
/// ```
/// use adder::adder;
/// 
/// assert_eq!(4, adder(2, 2));
/// ```
pub fn adder(a: i32, b: i32) -> i32 {
    a + b
}
}

Khi chạy cargo test hoặc cargo test --doc, cargo sẽ compile phần example code này thành một crate test và thực thi nó.

Mặc định nếu không chỉ định ngôn ngữ cho block code thì rustdoc sẽ ngầm định nó là Rust code. Do đó

```
let x = 5;
```

sẽ tương đương với

```rust
let x = 5;
```

Ẩn một phần của example code

Đôi lúc bạn sẽ cần example code gọn hơn, ẩn bớt một số logic mà bạn chuẩn bị để code có thể chạy được, tránh làm distract của người xem.

/// ```
/// /// Some documentation.
/// # fn foo() {} // this function will be hidden
/// println!("Hello, World!");
/// ```

Chúng ta thêm # ở phần đầu của dòng code muốn ẩn đi trong generate doc, nó vẫn sẽ được compile như mình thường.

Sử dụng ? trong doc tests

? chỉ có thể được sử dụng khi function trả về Result<T, E>. Hãy sử dụng cách sau:

/// A doc test using ?
///
/// ```
/// use std::io;
/// fn main() -> io::Result<()> {
///     let mut input = String::new();
///     io::stdin().read_line(&mut input)?;
///     Ok(())
///  }
/// ```

Cùng với việc sử dụng # như ở trên, chúng ta có thể ẩn đi bớt logic.

/// A doc test using ?
///
/// ```
/// # use std::io;
/// # fn main() -> io::Result<()> {
/// let mut input = String::new();
/// io::stdin().read_line(&mut input)?;
/// # Ok(())
/// # }
/// ```

References

Xung đột biến môi trường

Các unit tests trong cùng một module được thực thi song song nhau, trong cùng một process. Một trường hợp mà mình đã gặp phải là xung đột do 2 tests cùng sử dụng một biến môi trường hoặc biến môi trường ảnh hưởng đến kết quả của tests khác.

#![allow(unused)]
fn main() {
use std::env;

#[test]
fn test_one() {
  env::set_var("KEY", "value_one");
  assert_eq!(env::var("KEY"), Ok("value_one".to_string()));
}

#[test]
fn test_one() {
  env::set_var("KEY", "value_two");
  assert_eq!(env::var("KEY"), Ok("value_two".to_string()));
}
}

Giải pháp ổn nhất hiện tại mình dùng để giải quyết là serial_test

File: Cargo.toml

[dev-dependencies]
serial_test = "0.9"
#![allow(unused)]
fn main() {
use std::env;

#[test]
#[serial]
fn test_one() {
  env::set_var("KEY", "value_one");
  assert_eq!(env::var("KEY"), Ok("value_one".to_string()));
}

#[test]
#[serial]
fn test_one() {
  env::set_var("KEY", "value_two");
  assert_eq!(env::var("KEY"), Ok("value_two".to_string()));
}
}

Các tests có #[serial] sẽ được thực hiện tuần tự nhau, tránh trường hợp xung đột như trước.

References

Viết Docs

Rust mặc định được phát hành cùng với một công cụ gọi là rustdoc hay cargo doc. Giúp generate document cho Rust project.

Cơ bản

Thử viết 1 crate đơn giản và document nó:

cargo new duyet_lib --lib
cd duyet_lib

Trong src/lib.rs, Cargo đã generate sẵn 1 đoạn code, hãy xóa và thay nó bằng:

#![allow(unused)]
fn main() {
/// foo is a function
fn foo() {}
}

Bây giờ hãy dùng lệnh sau để generate document và xem nó trên trình duyệt:

cargo doc --open

rustdoc đọc mọi comment ///, //! và generate document theo cấu trúc của project. Nôi dung của document được viết bằng Markdown.

Hãy xem trang viết comment sao cho đúngDoc comments để biết thêm về cách viết comment doc sao cho chuẩn.

References

Doc comments

Doc comments sẽ được Compiler parse thành HTML documentation khi render document bằng cargo doc.

#![allow(unused)]
fn main() {
/// Generate library docs for the following item.
//! Generate library docs for the enclosing item.
}

Doc comments sẽ cực kỳ hữu ích cho project lớn và cần một hệ thống document chính xác và up to date.

//! sẽ generate doc cho crate/mod trong file hiện tại.

#![crate_name = "playground"]

/// A human being is represented here
pub struct Person {
    /// A person must have a name, no matter how much Juliet may hate it
    name: String,
}

impl Person {
    /// Returns a person with the name given them
    ///
    /// # Arguments
    ///
    /// * `name` - A string slice that holds the name of the person
    ///
    /// # Examples
    ///
    /// ```
    /// // You can have rust code between fences inside the comments
    /// // If you pass --test to `rustdoc`, it will even test it for you!
    /// use doc::Person;
    /// let person = Person::new("name");
    /// ```
    pub fn new(name: &str) -> Person {
        Person {
            name: name.to_string(),
        }
    }

    /// Gives a friendly hello!
    ///
    /// Says "Hello, [name]" to the `Person` it is called on.
    pub fn hello(&self) {
        println!("Hello, {}!", self.name);
    }
}

fn main() {
    let john = Person::new("John");

    john.hello();
}

Chúng ta có thể thậm chí comment lại example code hoặc cách sử dụng một function nào đó, code này cũng sẽ được compile và test, đảm bảo được code và document luôn luôn chính xác với nhau, một giải pháp khá thông minh.

#![allow(unused)]
fn main() {
/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
    x + 1
}
}

Sử dụng README.md làm crate document

Nội dung comment dưới đây trong src/lib.rs được gọi là crate document, được render thành nội dung trong trang chủ doc của library.

File: src/lib.rs

#![allow(unused)]
fn main() {
//! This is crate document
//!
//! # Usage
//!
//! ```
//! [dependencies]
//! duyet_lib = "0.9"
//! ```
//! ...
}

Và nội dung này cũng thường sẽ giống với README.md để hiển thị trên Github. Một cách để tránh lặp lại nội dung và đồng bộ với nhau là rustdoc render trực tiếp nội dung từ README.md.

File: src/lib.rs

#![allow(unused)]
fn main() {
#[doc = include_str!("../README.md")]
}

File: README.md

This is crate document

# Usage

```
[dependencies]
duyet_lib = "0.9"
```
...

Behavioural Patterns

Theo Wikipedia:

Behavioural Patterns: Design patterns that identify common communication patterns among objects. By doing so, these patterns increase flexibility in carrying out communication.

Một số Behavioural Patterns trong Rust

Strategy Pattern

Strategy design pattern  là một technique nhằm mục đích phân tách nhiều vấn đề, tách software modules thông qua Dependency Inversion.

Ý tưởng cơ bản của Strategy pattern là chỉ cần define skeleton ở abstract level, chúng ta tách biệt phần implementation của logic thành nhiều phần. Client sử dụng có thể tự implement 1 số method theo cách riêng của nó nhưng vẫn giữ được cấu trúc của logic workflow gốc.

Abstract class không không phụ thuộc vào implementation của lớp dẫn xuất (derived class), nhưng implementation của lớp dẫn xuất phải tuân thủ theo đặc tả của lớp abstract. Cho nên chúng có tên gọi là Dependency Inversion.

Một thứ mình thấy rõ là các project Rust rất hay sử dụng Strategy Design Pattern này.

Ví dụ, chúng ta có 1 struct Data và implement một số phương thức để generate ra nhiều dạng format khác nhau (ví dụ JSON, YAML, Plain Text, ...). Ta gọi mỗi format ở đây là một strategy.

use std::collections::HashMap;

type Data = HashMap<String, u32>;

impl Data {
  fn generate(&self, format: &str) {
    match format {
      "json" => { ... }
      "yaml" => { ... }
      "text" => { ... }
      _      => { ... }
    }
  }
}

Mọi thứ thay đổi theo thời gian, và khó đoán được trong tương lai chương trình của chúng ta có thể sửa đổi hoặc bổ sung thêm các loại format nào nữa trong tương lai hay không (ví dụ JSONLine, CSV, Parquet, ...)

Nếu thiết kế sử dụng Strategy Pattern:

use std::collections::HashMap;

// Data
type Data = HashMap<String, u32>;
impl Data {
  // f: T chap nhan moi struct co impl Formatter
  fn generate<T: Formatter>(f: T) -> String {
    f.format(&self)
  }
}

// Formatter
trait Formatter {
  fn format(&self, data: &Data) -> String;
}

// Formatter -> Json
struct Json;
impl Formatter for Json {
  fn format(&self, data: &Data) -> String {
    // res = { "a": 1, "b": 2. /// }
    res
  }
}

// Formatter -> Text
struct Text;
impl Formatter for Text {
  fn format(&self, data: &Data) -> String {
    // res = "a = 1, b = 2, ..."
    res
  }
}

fn main() {
  let mut data = Data::new();
  data.insert("a".to_string(), 1);
  data.insert("b".to_string(), 2);

  let s = data.generate(Text);
  assert!(s.contains("a = b, b = 2"));

  let s = data.generate(Json);
  assert!(s.contains(r#"{"a":1, "b":2}"#));
}

Theo chúng ta có thể thấy, Data::generate có thể không cần quan tâm implementation của f: T. Chỉ cần biết nó là một dẫn xuất của trait Formatter và có method format.

Nhược điểm là mỗi strategy cần được implement ít nhất một module, vì thế số lượng module có thể tăng cùng với số lượng strategy. Có quá nhiều strategy đòi hỏi user phải biết sự khác nhau giữa các strategy để sử dụng.

Ưu điểm là chúng ta có thể tách việc xử lý Json, Text, ... ra thành nhiều bài toán (strategy) nhỏ hơn theo như ví dụ trên.

Ở ví dụ trên các strategy được đặt chung ở một file, thực tế người ta thưởng đặt ở nhiều module khác nhau hoặc mỗi strategy một file (formatter::json, formatter::csv, ...). Việc tách này còn cho phép sử dụng compiler feature flags.

Còn nếu chúng ta đang implement một crate, thì crate ở ví dụ trên user có thể dễ dàng custom một Formatter mới:

use crate::example::{Data, Formatter};

struct CustomFormatter;

impl Formatter for CustomFormatter {
  fn format(&self, data: &Data) -> String {
    ...
  }
}

serde là một ví dụ hay của Strategy pattern, serde cho phép full customization serialization behavior bằng cách implement Serialize và Deserialize traits cho kiểu dữ liệu riêng của chúng ta.

Command Pattern

Ý tưởng cơ bản của Command Pattern là tách các actions thành các object riêng và gọi chúng thông qua parameters.

Khi nào dùng

Giả sử ta có một chuỗi các actions hoặc transactions. Chúng ta muốn các actions hoặc commands được thực thi theo thứ tự khác nhau. Các commands có thể được trigger bởi kết quả của một event nào đó. Ví dụ, khi user nhấn 1 nút, hoặc khi nhận được 1 data event nào đó. Ngoài ra thì các commands này có thể khôi phục (undo). Ví dụ như ta store các chuỗi thực thi (executed) của các commands, khi hệ thống gặp vấn đề ta có thể phục hồi lại bằng cách chạy lại từng commands một.

Ví dụ

Ta define hai database operations create table và add field. Mỗi operation là một command. Các command này có thể undo được, ví dụ drop table, drop field.

Khi user invoke database migration, mỗi command được thực thi theo thứ tự, khi user muốn rollback, tất cả command được undo theo thứ tự ngược lại.

Cách 1: sử dụng trait objects

Chúng ta định nghĩa một common trait cho command với hai operation là execrollback. Các struct command phải được implement trait này.

pub trait Migration {
  fn execute(&self) -> &str;
  fn rollback(&self) -> &str;
}

pub struct CreateTable;
impl Migration for CreateTable {
  fn execute(&self) -> &str {
    "create table"
  }
  fn rollback(&self) -> &str {
    "drop table"
  }
}

pub struct AddField;
impl Migration for AddField {
  fn execute(&self) -> &str {
    "add field"
  }
  fn rollback(&self) -> &str {
    "remove field"
  }
}

struct Schema {
  commands: Vec<Box<dyn Migration>>,
}

impl Schema {
  fn new() -> Self {
    Self { commands: vec![] }
  }

  fn add_migration(&mut self, cmd: Box<dyn Migration>) {
    self.commands.push(cmd);
  }

  fn execute(&self) -> Vec<&str> {
    self.commands.iter().map(|cmd| cmd.execute()).collect()
  }
  fn rollback(&self) -> Vec<&str> {
    self.commands
      .iter()
      .rev() // reverse iterator's direction
      .map(|cmd| cmd.rollback())
      .collect()
  }
}

fn main() {
  let mut schema = Schema::new();

  let cmd = Box::new(CreateTable);
  schema.add_migration(cmd);
  let cmd = Box::new(AddField);
  schema.add_migration(cmd);

  assert_eq!(vec!["create table", "add field"], schema.execute());
  assert_eq!(vec!["remove field", "drop table"], schema.rollback());
}

Cách 2: sử dụng function pointers

Chúng ta có thể thực hiện theo một cách khác là tách mỗi command thành một function và lưu lại function pointer để thực thi sau.

type FnPtr = fn() -> String;

struct Command {
  execute: FnPtr,
  rollback: FnPtr,
}

struct Schema {
  commands: Vec<Command>,
}

impl Schema {
  fn new() -> Self {
    Self { commands: vec![] }
  }
  fn add_migration(&mut self, execute: FnPtr, rollback: FnPtr) {
    self.commands.push(Command { execute, rollback });
  }
  fn execute(&self) -> Vec<String> {
    self.commands.iter().map(|cmd| (cmd.execute)()).collect()
  }
  fn rollback(&self) -> Vec<String> {
    self.commands
      .iter()
      .rev()
      .map(|cmd| (cmd.rollback)())
      .collect()
  }
}

fn add_field() -> String {
  "add field".to_string()
}

fn remove_field() -> String {
  "remove field".to_string()
}

fn main() {
  let mut schema = Schema::new();
  schema.add_migration(|| "create table".to_string(), || "drop table".to_string());
  schema.add_migration(add_field, remove_field);

  assert_eq!(vec!["create table", "add field"], schema.execute());
  assert_eq!(vec!["remove field", "drop table"], schema.rollback());
}

Cách 3: sử dụng Fn trait objects

Thay vì định nghĩa một command trait theo cách 1, ta có thể lưu tất cả command được implement trait Fn trong một vector.

type Migration<'a> = Box<dyn Fn() -> &'a str>;

struct Schema<'a> {
  executes: Vec<Migration<'a>>,
  rollbacks: Vec<Migration<'a>>,
}

impl<'a> Schema<'a> {
  fn new() -> Self {
    Self {
        executes: vec![],
        rollbacks: vec![],
    }
  }

  fn add_migration<E, R>(&mut self, execute: E, rollback: R)
  where
    E: Fn() -> &'a str + 'static,
    R: Fn() -> &'a str + 'static,
  {
    self.executes.push(Box::new(execute));
    self.rollbacks.push(Box::new(rollback));
  }

  fn execute(&self) -> Vec<&str> {
    self.executes.iter().map(|cmd| cmd()).collect()
  }

  fn rollback(&self) -> Vec<&str> {
    self.rollbacks.iter().rev().map(|cmd| cmd()).collect()
  }
}

fn add_field() -> &'static str {
  "add field"
}

fn remove_field() -> &'static str {
  "remove field"
}

fn main() {
  let mut schema = Schema::new();
  schema.add_migration(|| "create table", || "drop table");
  schema.add_migration(add_field, remove_field);

  assert_eq!(vec!["create table", "add field"], schema.execute());
  assert_eq!(vec!["remove field", "drop table"], schema.rollback());
}

Thảo luận

Trong các ví dụ trên thì command của chúng ta khá nhỏ, nên thường được define dưới dạng function hoặc closure rồi bỏ thẳng function pointer vào Vec, rồi thực thi theo thứ tự. Trong thực tế các command có thể phức tạp hơn, có thể là một struct với hàng loạt các function và variable trong các module khác nhau, việc sử dụng traitBox ở cách 1 sẽ hiệu quả hơn.

References

Creational Patterns

Theo Wikipedia:

Creational Patterns: Design patterns that deal with object creation mechanisms, trying to create objects in a manner suitable to the situation. The basic form of object creation could result in design problems or in added complexity to the design. Creational design patterns solve this problem by somehow controlling this object creation.

Một số Creational Patterns trong Rust

Rust Builder Design Pattern

Rust không có overloading, do đó bạn cần phải viết nhiều construct cho tất cả các trường hợp có thể có, với các method name khác nhau. Việc này sẽ cực kỳ mất thời gian nếu struct có quá nhiều fields hoặc constructor phức tạp.

impl Foo {
  pub fn new(a: String) -> Self {}
  pub fn new(a: String, b: String) -> Self {} // <-- không thể
  pub fn new(a: i32) -> Self {} // <-- không thể
}

// Thay vào đó
impl Foo {
  pub fn new(a: String) -> Self {}
  pub fn new_from_two(a: String, b: String) -> Self {}
  pub fn new_from_int(a: i32) -> Self {}
}

Do đó, builder được sử dụng cực kỳ phổ biến trong Rust so với các ngôn ngữ khác.

Builder cho phép construct một object bằng cách gọi build().

Ví dụ

#[derive(Debug, PartialEq)]
pub struct Foo {
  // Lots of complicated fields.
  bar: String,
}

impl Foo {
  // This method will help users to discover the builder
  pub fn builder() -> FooBuilder {
    FooBuilder::default()
  }
}

#[derive(Default)]
pub struct FooBuilder {
  // Probably lots of optional fields.
  bar: String,
}

impl FooBuilder {
  pub fn new(/* ... */) -> FooBuilder {
    // Set the minimally required fields of Foo.
    FooBuilder {
      bar: "x".to_string(),
    }
  }

  pub fn name(mut self, bar: String) -> FooBuilder {
    // Set the name on the builder itself, and return the builder by value.
    self.bar = bar;
    self
  }

  // If we can get away with not consuming the Builder here, that is an
  // advantage. It means we can use the FooBuilder as a template for constructing
  // many Foos.
  pub fn build(self) -> Foo {
    // Create a Foo from the FooBuilder, applying all settings in FooBuilder
    // to Foo.
    Foo { bar: self.bar }
  }
}

#[test]
fn builder_test() {
  let foo = Foo { bar: "y".to_string() };
  let foo_from_builder = FooBuilder::new().name("y".to_string()).build();

  assert_eq!(foo, foo_from_builder);
}

Khi nào dùng

Hữu ích khi bạn muốn có nhiều loại constructors khác nhau hoặc khi constructor có side effects.

Ưu điểm

  • Tách biệt các methods của builder và các method khác của object.
  • Không cần phải viết quá nhiều constructor nếu struct có quá nhiều fields hoặc quá nhiều cách để khởi tạo một object.
  • One-liner initialization: FooBuilder::new().a().b().c().build()

Nhược điểm

Phức tạp hơn so với việc init object trực tiếp, hoặc so với object có constructor đơn giản.

References

Structural Patterns

Theo Wikipedia:

Structual Patterns: Design patterns that ease the design by identifying a simple way to realize relationships among entities.

Một số Structural Patterns trong Rust

Prefer Small Crates

Không hẳn là một Design pattern, mình thấy đây là một tư tưởng khi viết các project bằng Rust.

Cargo và crates.io giúp quản lý crate cực kỳ dễ dàng. Hơn nữa, crate trên crates.io không thể sửa hoặc xóa được sau khi publish, bất kỳ bản build nào đang hoạt động chắc chắn sẽ hoạt động được tiếp trong tương lai. Điều này bắt buộc để có được sự hiệu quả, mọi crate phải được thiết kế tốt, lựa chọn dependencies kỹ càng và càng nhỏ càng tốt.

Prefer small crates that do one thing well.

Ưu điểm

  • Small crate sẽ giúp ta dễ hiểu và dễ sử dụng hơn, code dễ module hóa hơn.
  • Đơn vị compilation nhỏ nhất của Rust là crate, tách nhỏ project thành nhiều crate giúp code build parallel.
  • Crate giúp tái sử dụng giữa nhiều project khác nhau.
    • Ví dụ, crate url là một phần của Servo browser engine, nhưng được sử dụng cực kỳ rộng rãi ở các project khác, do nó độc lập và giải quyết một vấn đề cụ thể.
    • Ví dụ, AWS SDK Rust được tách thành rất nhiều crate nhỏ, và các crate nhỏ này được sử dụng ở khắp nơi không chỉ ở AWS SDK Rust.
      • aws-sdk-*
      • aws-config
      • aws_smithy_client
      • aws_types
  • Tách nhỏ crate độc lập giúp việc chia tasks trong một project lớn của team hiệu quả hơn.

Nhược điểm

  • Có dễ dẫn đến “dependency hell”, một project depends vào cùng 1 crate nhưng version khác nhau cùng một lúc. Và các versions này xung đột nhau.
  • Hai crate quá nhỏ có thể kém hiệu quả hơn một crate lớn, bởi vì compiler mặc định không thực hiện link-time optimization (LTO).

Một số small crates điển hình

anyhow

anyhow là thư viện giúp đơn giản hóa việc handle lỗi trong Rust application.

File: Cargo.toml

[dependencies]
anyhow = "1"

Cách sử dụng

  • Sử dụng anyhow::Result<T> thay cho Result của std. Ta không cần định nghĩa Error trả về. Trong function sử dụng ? để trả mọi error đã được impl std::error::Error lên level cao hơn.

    #![allow(unused)]
    fn main() {
    use anyhow::Result;
    
    fn get_cluster_info() -> Result<ClusterMap> {
        let config = std::fs::read_to_string("cluster.json")?;
        let map: ClusterMap = serde_json::from_str(&config)?;
        Ok(map)
    }
    }
  • Thêm context để debug dễ hơn:

    use anyhow::{Context, Result};
    
    fn main() -> Result<()> {
        ...
        it.detach().context("Failed to detach the important thing")?;
    
        let content = std::fs::read(path)
            .with_context(|| format!("Failed to read instrs from {}", path))?;
        ...
    }
    Error: Failed to read instrs from ./path/to/instrs.json
    
    Caused by:
        No such file or directory (os error 2)
    
  • Return lỗi nhanh hơn với macros anyhow!, bail!, ensure!

    #![allow(unused)]
    fn main() {
    return Err(anyhow!("Missing attribute: {}", missing));
    }
    #![allow(unused)]
    fn main() {
    bail!("Missing attribute: {}", missing);    
    }
    #![allow(unused)]
    fn main() {
    ensure!(user == 0, "only user 0 is allowed");
    }

References

log

log là thư viện logging lightweight cho Rust application.

File: Cargo.toml

[dependencies]
log = "0.4"

Cách sử dụng

#![allow(unused)]
fn main() {
use log::{info, trace, warn};

pub fn shave_the_yak(yak: &mut Yak) {
    trace!("Commencing yak shaving");

    loop {
        match find_a_razor() {
            Ok(razor) => {
                info!("Razor located: {}", razor);
                yak.shave(razor);
                break;
            }
            Err(err) => {
                warn!("Unable to locate a razor: {}, retrying", err);
            }
        }
    }
}
}

env_logger

log thường được sử dụng với env_logger để cấu hình logging thông qua biến môi trường.

Mặc định, env_logger ghi log ra stderr.

File: Cargo.toml

[dependencies]
log = "0.4.0"
env_logger = "0.8.4"

Ví dụ:

File: src/main.rs

fn main() {
  env_logger::init();

  info!("starting up");
  error!("this is error!");
  debug!("this is debug {}!", "message");
}
$ RUST_LOG=error cargo run
[2022-07-11T02:12:24Z ERROR main] this is error
$ RUST_LOG=info cargo run
[2022-07-11T02:12:24Z INFO main] starting up
[2022-07-11T02:12:24Z ERROR main] this is error
$ RUST_LOG=debug cargo run
[2022-07-11T02:12:24Z INFO main] starting up
[2022-07-11T02:12:24Z ERROR main] this is error
[2022-07-11T02:12:24Z DEBUG main] this is debug message!

Filter log theo module name:

$ RUST_LOG=main=info cargo run
[2022-07-11T02:12:24Z INFO main] starting up
[2022-07-11T02:12:24Z ERROR main] this is error

Hiện mọi log level cho module main:

$ RUST_LOG=main cargo run
[2022-07-11T02:12:24Z INFO main] starting up
[2022-07-11T02:12:24Z ERROR main] this is error
[2022-07-11T02:12:24Z DEBUG main] this is debug message!

References

References

env_logger

log thường được sử dụng với env_logger để cấu hình logging thông qua biến môi trường.

Mặc định, env_logger ghi log ra stderr.

File: Cargo.toml

[dependencies]
log = "0.4.0"
env_logger = "0.8.4"

Ví dụ:

File: src/main.rs

fn main() {
  env_logger::init();

  info!("starting up");
  error!("this is error!");
  debug!("this is debug {}!", "message");
}
$ RUST_LOG=error cargo run
[2022-07-11T02:12:24Z ERROR main] this is error
$ RUST_LOG=info cargo run
[2022-07-11T02:12:24Z INFO main] starting up
[2022-07-11T02:12:24Z ERROR main] this is error
$ RUST_LOG=debug cargo run
[2022-07-11T02:12:24Z INFO main] starting up
[2022-07-11T02:12:24Z ERROR main] this is error
[2022-07-11T02:12:24Z DEBUG main] this is debug message!

Filter log theo module name:

$ RUST_LOG=main=info cargo run
[2022-07-11T02:12:24Z INFO main] starting up
[2022-07-11T02:12:24Z ERROR main] this is error

Hiện mọi log level cho module main:

$ RUST_LOG=main cargo run
[2022-07-11T02:12:24Z INFO main] starting up
[2022-07-11T02:12:24Z ERROR main] this is error
[2022-07-11T02:12:24Z DEBUG main] this is debug message!

References

async_trait

Rust chưa hỗ trợ async cho trait. Trait dưới đây sẽ báo lỗi:

#![allow(unused)]
fn main() {
trait MyTrait {
    async fn f() {}
}
}
error[E0706]: trait fns cannot be declared `async`
 --> src/main.rs:4:5
  |
4 |     async fn f() {}
  |     ^^^^^^^^^^^^^^^

async_trait cung cấp attribute macro để giúp async có thể hoạt động với trait.

File: Cargo.toml

[dependencies]
async_trait = "0.1"

Ví dụ:

#![allow(unused)]
fn main() {
use async_trait::async_trait;

#[async_trait]
trait Advertisement {
    async fn run(&self);
}

struct Modal;

#[async_trait]
impl Advertisement for Modal {
    async fn run(&self) {
        self.render_fullscreen().await;
        for _ in 0..4u16 {
            remind_user_to_join_mailing_list().await;
        }
        self.hide_for_now().await;
    }
}

struct AutoplayingVideo {
    media_url: String,
}

#[async_trait]
impl Advertisement for AutoplayingVideo {
    async fn run(&self) {
        let stream = connect(&self.media_url).await;
        stream.play().await;

        // Video probably persuaded user to join our mailing list!
        Modal.run().await;
    }
}
}

References

lazy_static

lazy_static là một macro cho phép khởi tạo biến static nhưng chứa giá trị được thực thi lúc runtime. Các giá trị này có thể là bất kỳ cái gì cần heap allocations, ví dụ như Vector, HashMap hoặc function call.

File: Cargo.toml

[dependencies]
lazy_static = "1"

Ví dụ:

use lazy_static::lazy_static;
use std::collections::HashMap;

lazy_static! {
    static ref HASHMAP: HashMap<u32, &'static str> = {
        let mut m = HashMap::new();
        m.insert(0, "foo");
        m.insert(1, "bar");
        m.insert(2, "baz");
        m
    };
    static ref COUNT: usize = HASHMAP.len();
    static ref NUMBER: u32 = times_two(21);
}

fn times_two(n: u32) -> u32 { n * 2 }

fn main() {
    println!("The map has {} entries.", *COUNT);
    println!("The entry for `0` is \"{}\".", HASHMAP.get(&0).unwrap());
    println!("A expensive calculation on a static results in: {}.", *NUMBER);
}

References

indoc

indoc là một crate nhỏ nhưng hữu ích giúp canh lề (indented documents). indoc!() macro nhận multiline string và un-indents lúc compile time, xoá tất cả khoảng trắng đầu tiên trên cách dòng dựa theo dòng đầu tiên.

File: Cargo.toml

[dependencies]
indoc = "1"

Ví dụ:

use indoc::indoc;

fn main() {
    let testing = indoc! {"
        def hello():
            print('Hello, world!')

        hello()
    "};

    let expected = "def hello():\n    print('Hello, world!')\n\nhello()\n";
    assert_eq!(testing, expected);
}

indoc cũng hoạt động với raw string r# ... # và byte string b" ... ".

References

rayon

rayon là thư viện data-parallelism cho Rust, gọn nhẹ và dễ dàng convert từ code tính toán tuần tự sang song song mà vẫn đảm bảo không lỗi data-race.

File: Cargo.toml

[dependencies]
rayon = "1.5"

Ví dụ:

#![allow(unused)]
fn main() {
use rayon::prelude::*;

fn sum_of_squares(input: &[i32]) -> i32 {
    input.par_iter() // <-- chỉ cần sử dụng `par_iter()` thay vì `iter()`!
         .map(|&i| i * i)
         .sum()
}
}

Parallel iterators sẽ phụ trách việc chia data thành nhiều tasks nhỏ như thế nào và sẽ đáp ứng linh hoạt để đạt maximum performance. Ngoài ra, Rayon cũng cung cấp 2 function joinscope để bạn có thể chủ động điều khiển việc parallel tasks.

Để tìm hiểu thêm về cách rayon hoạt động bạn có thể đọc thêm bài blog từ tác giả: https://smallcultfollowing.com/babysteps/blog/2015/12/18/rayon-data-parallelism-in-rust/

demo & bench

Trong repo của rayon có rất nhiều demo và bench, để xem danh sách demo hoặc bench:

$ git clone https://github.com/rayon-rs/rayon && cd rayon/rayon-demo 
$ cargo run --release -- --help

Usage: rayon-demo bench
       rayon-demo <demo-name> [ options ]
       rayon-demo --help

A collection of different benchmarks of Rayon. You can run the full
benchmark suite by executing `cargo bench` or `rayon-demo bench`.

Alternatively, you can run individual benchmarks by running
`rayon-demo foo`, where `foo` is the name of a benchmark. Each
benchmark has its own options and modes, so try `rayon-demo foo
--help`.

Benchmarks:

  - life : Conway's Game of Life.
  - nbody: A physics simulation of multiple bodies attracting and repelling
           one another.
  - sieve: Finding primes using a Sieve of Eratosthenes.
  - matmul: Parallel matrix multiplication.
  - mergesort: Parallel mergesort.
  - noop: Launch empty tasks to measure CPU usage.
  - quicksort: Parallel quicksort.
  - tsp: Traveling salesman problem solver (sample data sets in `data/tsp`).

Quicksort benchmark:

$ cargo run --release quicksort bench

seq: sorted 250000000 ints: 22.268583 s
par: sorted 250000000 ints: 4.172599 s
speedup: 5.34x

Demo nbody visualize: gõ s để chạy tuần tự và p để parallel

cargo run --release -- nbody visualize

References

Using Rust for efficient data processing and analysis

TBU

First high-performance data pipelines in Rust

Building scalable data-driven applications using Rust

TBU

Rust as an alternative to Python for data engineering tasks

TBU

Ideas: