Category Archives: Rust – Lập trình Rust

Rust – Trao đổi dữ liệu luồng qua Channel

Chúng ta có thể trao đổi dữ liệu giữa các luồng thông qua cơ chế channel của Rust.

Channel có thể hiểu như các ống truyền dữ liệu giữa 2 luồng bất kì, dữ liệu nào được gửi trước sẽ được xử lý trước, 2 luồng giữa 2 đầu channel sẽ đóng vai trò một bên gửi và một bên nhận, bên gửi sẽ mang kiểu Sender<T>, còn bên nhận sẽ là kiểu Receiver<T>, đây là các kiểu generic, do đó kiểu dữ liệu T của bên gửi và nhận phải giống nhau.

Khi truyền dữ liệu giữa các luồng thì dữ liệu sẽ được tạo thành các bản copy để luồng Receiver làm việc, do đó chúng ta chỉ nên truyền các dữ liệu nhỏ, nếu dữ liệu lớn sẽ ngốn rất nhiều bộ nhớ.

Tạo channel

Để tạo một channel thì chúng ta gọi hàm channel() trong module std::sync::mpsc, ví dụ:

use std::thread;
use std::sync::mpsc::channel;
use std::sync::mpsc::{ Sender, Receiver };
fn main() {
let (mySender, myReceiver): (Sender<&’static str>, Receiver<&’static str>) = channel();
}

Ngoài ra chúng ta import thêm lớp SenderReceiver từ module mpsc. Hàm channel() sẽ trả về một tuple gồm một đối tượng Sender và một đối tượng Receiver.

Gửi và nhận dữ liệu

Để gửi dữ liệu thì chúng ta gọi hàm send() từ lớp Sender, và nhận dữ liệu thông qua hàm recv(). Ví dụ:

use std::thread;
use std::sync::mpsc::channel;
use std::sync::mpsc::{ Sender, Receiver };
fn main() {
let (mySender, myReceiver): (Sender<&’static str>, Receiver<&’static str>) = channel();
thread::spawn(move || {
mySender.send("A message from phocode");
});
let res = myReceiver.recv().unwrap();
println!("{}", res);
}

Trong đoạn code trên, chúng ta gửi một chuỗi kí tự trong luồng con thông qua hàm send(), và nhận lại chuỗi kí tự đó trong luồng cha (là luồng main()). Hàm unwrap() sẽ chuyển dữ liệu lại thành đối tượng Receiver.

A message from phocode

Nếu chúng ta không gọi hàm send() mà chỉ gọi hàm recv(), tức là chỉ nhận dữ liệu từ channel mà không gửi bất kì thứ gì vào channel thì luồng luồng đó sẽ bị block, tức là bị tạm dừng, và luồng sẽ đợi cho đến khi có dữ liệu trong channel để đọc, nếu không muốn bị block thì chúng ta dùng hàm try_recv().
Cả 2 hàm send()recv() đều trả về kiểu Result, tức là một trong 2 kiểu Ok hoặc Err. Tuy nhiên khi trả về Err thì channel sẽ không làm việc nữa, nên cho dù chúng ta có bắt lỗi Err thì ứng dụng cũng ngừng hoạt động.

Trên thực tế thì chúng ta sẽ cho các luồng con làm các công việc mang nặng tính toán, rồi nhận kết quả trong luồng cha.

Trao đổi dữ liệu đồng bộ và bất đồng bộ

Cách thức gửi và nhận dữ liệu ở đoạn code trên là bất đồng bộ, có nghĩa là các luồng không bị tạm dừng khi chạy. Tuy nhiên Rust cũng có cơ chế đồng bộ các luồng với nhau. Cơ chế này hoạt động bằng cách yêu cầu luồng con gửi dữ liệu bị tạm dừng khi dữ liệu được gửi vô channel đã đầy, và luồng này sẽ đợi cho đến khi luồng cha lấy dữ liệu ra khỏi channel.

Để làm điều này thì thay vì dùng hàm channel(), chúng ta sẽ dùng hàm sync_channel(), chúng ta cũng sẽ đùng lớp SyncSender thay cho lớp Sender. Ví dụ:

use std::thread;
use std::sync::mpsc::{SyncSender, Receiver};
use std::sync::mpsc::sync_channel;
fn main() {
let (mySender, myReceiver): (SyncSender<&’static str>, Receiver<&’static str>) = sync_channel(1);
thread::spawn(move || {
mySender.send("Message 1").unwrap();
println!("Message 1 sent into the buffer");
mySender.send("Message 2").unwrap();
println!("Message 2 sent into the buffer");
});
thread::sleep_ms(3000);
let data = myReceiver.recv().unwrap();
println!("The data: {}", data);
}

Trong đoạn code trên, chúng ta tạo ra môt tuple (SyncSender, Receiver) bằng hàm sync_channel(), hàm này nhận vào tham số là 1, tức là chỉ nhận send() 1 lần là đầy bộ nhớ channel, sau đó chúng ta tạo một luồng và gọi hàm send() 2 lần, rồi ở bên ngoài luồng cha chúng ta cho đợi 3 giây, sau đó lấy dữ liệu từ ra. Chúng ta sẽ được output như sau:

Message 1 sent into the buffer
The data: Message 1
Message 2 sent into the buffer

Lý do chuỗi “Message 2 sent into the buffer” được hiện ra sau cùng là vì khi chúng ta gọi send() lần 1, thì channel đã đầy, nên luồng này bị đóng băng lại, đợi cho luồng cha lấy dữ liệu đó ra rồi lần send() thứ 2 mới có thể gọi được.

Rust – Chia sẻ dữ liệu trên luồng

Rust cung cấp một số kiểu dữ liệu với tên gọi là kiểu atomic, được định nghĩa trong module std::sync::atomic có thể hỗ trợ chúng ta làm việc với dữ liệu trong các luồng một cách an toàn. Để có thể chia sẻ dữ liệu giữa các luồng thì chúng ta phải đưa dữ liệu đó vào một trong các kiểu atomic có trong Rust như Arc, Mutex, RwLock, AtomicUSize

Về cơ bản thì các kiểu dữ liệu này sẽ thực hiện một cơ chế khóa, tương tự như cơ chế khóa của hệ điều hành, tức là sẽ chỉ cung cấp tài nguyên cho luồng nào nhắm giữ ổ khóa của tài nguyên đó. Một ổ khóa chỉ có thể được nắm giữ bởi một luồng tại một thời điểm, do đó không thể có khả năng có 2 luồng cùng nhau đọc/ghi dữ liệu trên một tài nguyên tại cùng một thời điểm. Các tài nguyên sẽ bị khóa khi cần thiết. Khi một luồng đang nắm giữ ổ khóa của một tài nguyên và hoàn thành công việc của nó, thì ổ khóa sẽ được gỡ ra khỏi tài nguyên đó và một luồng khác có thể bắt đầu làm việc với tài nguyên đó.

Trong Rust thì chúng ta làm điều này với kiểu Mutex<T> trong module std::sync. Mutex sẽ thực hiện cơ chế khóa vừa được giải thích ở trên, chúng ta chỉ cần tạo một đối tượng Mutex và truyền vào tài nguyên của chúng ta là được, ví dụ:

use std::sync::Mutex;
use std::thread;
fn main() {
let myData = "phocode.com";
let myMutexData = Mutex::new(myData);
thread::spawn(move || {
let myThreadData = myMutexData.lock().unwrap();
println!("{}", myThreadData);
});
}

Để tạo một đối tượng Mutex thì chúng ta chỉ cần gọi hàm Mutex::new() và truyền vào dữ liệu mà chúng ta muốn.

phocode.com

Hàm lock() sẽ “khóa” dữ liệu lại và trả về một tham chiếu tới biến mutex đó, luồng này sẽ trả lại khóa cho mutex khi kết thúc hàm closure của nó trong cặp dấu {}. Trong đoạn code trên, chúng ta tạo một luồng và gọi hàm lock() trong đó, nếu chúng ta tạo thêm một luồng nữa và trong luồng này cũng gọi tới hàm lock() như sau:

let myData = "phocode.com";
let myMutexData = Mutex::new(myData);
thread::spawn(move || {
let myThreadData = myMutexData.lock().unwrap();
println!("Thread 1: {}", myThreadData);
});
thread::spawn(move || {
let myThreadData = myMutexData.lock().unwrap();
println!("Thread 2: {}", myThreadData);
});

thì Rust sẽ báo lỗi:

captured of moved value: `myMutexData`

Để có thể dùng được dữ liệu giữa các luồng thì chúng ta cần phải bọc kiểu Mutex trong một kiểu dữ liệu atomic khác là kiểu Arc<T>. Ví dụ như sau:

use std::sync::Mutex;
use std::sync::Arc;
use std::thread;

fn main() {
let myData ="phocode.com";
let myArcData = Arc::new(Mutex::new(myData)); 

let myMutex1 = myArcData.clone();
thread::spawn(move || {
println!("Thread 1: {}", myMutex1.lock().unwrap());
});

let myMutex2 = myArcData.clone();
thread::spawn(move || {
println!("Thread 2: {}", myMutex2.lock().unwrap());
});
thread::sleep_ms(50);
}

Chúng ta tạo một đối tượng Arc bằng cách gọi Arc::new() và truyền vào đối tượng Mutex, sau đó mỗi lần cần sử dụng tới dữ liệu thì chúng ta gọi hàm clone() từ đối tượng Arc để tạo ra một bản copy của đối tượng Mutex và từ đó có thể lock() dữ liệu như bình thường.

Thread 1: phocode.com
Thread 2: phocode.com

Trên thực tế, sau khi tạo ra bản copy Mutex và gọi lock() thì hàm lock() có thể sẽ trả về lỗi Result<T, E>khi số lượng luồng được tạo ra khá nhiều, chứ không phải lúc nào cũng có thể khóa tất cả các mutex lại được. Nếu lock() hoạt động bình thường thì chúng ta mới có thể gọi unwrap() được, nếu có lỗi thì Rust sẽ phát sinh một panic và chúng ta nên bắt lỗi này. Ví dụ:

use std::sync::Mutex;
use std::sync::Arc;
use std::thread;

fn main() {
let myData = "phocode.com";
let myArcData = Arc::new(Mutex::new(myData)); 
for i in 0..50 {
let myMutex = myArcData.clone();
thread::spawn(move || {
let lockResult = myMutex.lock();
match lockResult {
Ok(myData) => {
println!("Locking OK, here is the data: {}", myData)
},
Err(error) => {
println!("Locking failed: {}", error);
}
}
});
}
}

Trong đoạn code trên chúng ta lưu lại kết quả sau khi gọi hàm lock(), nếu lock() thành công thì chúng ta in ra đoạn text “Locking OK…”, ngược lại thì in ra chuỗi “Locking failed…”.

Rust – Concurrency và thread

Concurrency là khả năng chạy nhiều tác vụ song song tại một thời điểm và những tác vụ này có thể tương tác với nhau.

Một ứng dụng Rust khi chạy thì sẽ là tập hợp của rất nhiều luồng (thread) của hệ điều hành mà ứng dụng đang chạy. Bản thân mỗi ứng dụng Rust cũng có thể có các luồng của riêng nó.

Trong các bài trước thì chúng ta chủ yếu làm việc với một luồng chính là main(). Trong bài này chúng ta sẽ tìm hiểu cách tạo ra các luồng khác và các luồng có thể chia sẻ dữ liệu với nhau.

Tạo luồng

Để có thể tạo luồng thì chúng ta phải dùng tới module std::thread, và để tạo luồng thì chúng ta sẽ gọi hàm spawn(). Ví dụ:

use std::thread;
fn main() {
thread::spawn(move || {
println!("Hello World printed from another thread");
});
}

Tham số của hàm spawn() là một hàm closure, nếu bạn chưa biết hàm closure là gì thì có thể giải thích đơn giản là đây là các hàm được định nghĩa bên trong một hàm khác. Trong trường hợp này là macro println!() được gọi bên trong cặp dấu {}, tham số sẽ được truyền trong cặp dấu ||, ở đây chúng ta không truyền vào tham số nào (nên 2 dấu | đứng một mình).

Khi chạy thì chúng ta sẽ không thấy đoạn text trong println!() được in ra, lí do là vì luồng con được tạo ra có thể vẫn còn chạy trong khi luồng cha là main() đã kết thúc trước đó. Chúng ta có thể cho luồng main() tạm dừng một khoảng thời gian ngắn để thấy output của luồng con như sau:

use std::thread;
fn main() {
thread::spawn(move || {
println!("Hello World printed from another thread");
});
thread::sleep_ms(50);
}

Hàm sleep_ms() sẽ làm cho luồng main() tạm dừng 50 mili giây và chúng ta có thể thấy được đoạn text in ra của luồng con.

Hello World printed from another thread

Một cách khác là chúng ta có thể dùng gán giá trị trả về của hàm spawn() cho một biến, sau đó dùng hàm join() để kết nối biến đó với luồng của nó, rồi dùng hàm unwrap() để lấy kết quả. Ví dụ:

use std::thread;

fn main() {
let wait = thread::spawn(move || {
println!("Hello World printed from another thread");
}); 
let result = wait.join().unwrap();
println!("{:?}", result);
}

Khi chúng ta gọi hàm join() để liên kết thì luồng main() sẽ bị buộc phải đợi cho luồng con chạy xong rồi mới chạy tiếp, sau đó hàm này sẽ trả về một đối tượng Result. Hàm unwarp() sẽ chuyển đối tượng này thành một đối tượng Ok hoặc Err tùy trường hợp có lỗi hay không.

 Hello World printed from another thread 

Bạn cũng có thể gọi thread::spawn(...).join(), làm thế này thì luồng chương trình sẽ là tuyến tính, tức là main() sẽ tạo luồng con, rồi đợi luồng con kết thúc mới tiếp tục.

Tạo nhiều luồng

Luồng trong Rust rất nhẹ, chúng ta có thể tạo ra khoảng 10.000 luồng chỉ trong vòng vài giây. Ví dụ:

use std::thread;

fn main() {
for i in 0..10000 {
thread::spawn(move || {
println!("Creating thread {}", i);
});
}
}

Bạn có thể thấy output được in ra rất lộn xộn, không có thứ tự vì các luồng có tốc độ chạy khác nhau.

Creating thread 4
Creating thread 23
Creating thread 48
Creating thread 13
Creating thread 293
...

Chúng ta cũng có thể tạo nhiều luồng bằng một crate có tên là threadpool. Đầu tiên chúng ta khai báo crate này trong Cargo.toml như sau:

[dependencies]
threadpool = "*"

Chúng ta sử dụng crate này trong main.rs như sau:

extern crate threadpool;

use std::thread;
use threadpool::ThreadPool;

fn main() {
let pool = ThreadPool::new(10);
for i in 0..10 {
pool.execute(move || {
println!("Creating thread {} using pool", i);});
}
thread::sleep_ms(50);
}

Chúng ta khai báo module ThreadPool trong crate threadpool, rồi gọi hàm new(10) để khai báo 10 luồng. Sau đó thay vì gọi spawn() như trong module thread thì chúng ta gọi hàm execute() để tạo luồng.

Creating thread 0 using pool
Creating thread 2 using pool
Creating thread 5 using pool
Creating thread 3 using pool
Creating thread 4 using pool
Creating thread 1 using pool
Creating thread 8 using pool
Creating thread 7 using pool
Creating thread 8 using pool
Creating thread 6 using pool

Panic trong các luồng

Như đã nói ở trên, các luồng độc lập với nhau, không liên quan gì với nhau cả, do đó khi một luồng panic thì các luồng khác vẫn chạy bình thường như không có gì xảy ra. Tuy nhiên luồng cha có thể kiểm tra xem một luồng con của nó có panic hay không bằng cách sử dụng hàm is_err(), ví dụ:

use std::thread;

fn main() {
let result = thread::spawn(move|| {
panic!("This thread is panicked");
}).join();

if result.is_err() {
println!("The child thread is panicked");
}
}

Đoạn code trên sẽ cho ra output như sau:

 
thread '<unnamed>' panicked at 'This thread is panicked', src\main.rs:5:8 
note: Run with `RUST_BACKTRACE=1` for a backtrace. 
The child thread is panicked

Bảo mật luồng

Khi làm việc với luồng trong các ngôn ngữ như C++ thì việc điều khiển luồng là rất khó khi các luồng có tương tác với nhau. Khi có 2, 3 hay nhiều luồng cùng chỉnh sửa dữ liệu thì dữ liệu rất dễ bị phá hỏng. Các ngôn ngữ khác thường không cho phép làm việc với luồng để tránh điều này.

Trong Rust thì các luồng sẽ làm việc với cơ chế ownership mà chúng ta đã tìm hiểu. Ví dụ:

use std::thread;

fn main() {
let mut x = 12;
for i in 2..5 {
thread::spawn(move || {
x *= i;
});
}
thread::sleep_ms(50);
println!("x = {}", x);
}

Trong đoạn code trên chúng ta tạo ra 3 thread cùng có thể chỉnh sửa giá trị của biến x, nhưng khi in giá trị của x trong hàm main() thì chúng ta vẫn sẽ được giá trị cũ của x, tức là x không bị thay đổi bởi các luồng, lý do là vì các luồng chỉ làm việc với một bản copy của x chứ không thay đổi trực tiếp lên x.

 x = 12

Rust – Macro

Trong các bài trước chúng ta đã làm việc rất nhiều với macro. Mỗi lần chúng ta gọi một hàm mà có dấu chấm than ! ở sau tên thì đó chính là một macro có sẵn trong Rust. Những macro mà chúng ta đã sử dụng là println!, panic!, vec!, assert!… Kể từ bài này mình sẽ gọi chúng ta macro chứ không gọi là hàm như những bài trước nữa.

Tại sao cần đến macro?

Macro cho phép chúng ta tạo nên các cú pháp gọn nhẹ nhưng mạnh mẽ, nhờ đó mà việc lập trình trở nên dễ dàng hơn rất nhiều. Chẳng hạn như trong Rust có macro regex! cho phép chúng ta định nghĩa các biểu thức chính quy, các biểu thức chính quy này sẽ được dịch và sử dụng.

Macro giúp giảm đi việc lặp lại code nhiều lần, chúng ta có thể định nghĩa một macro thực hiện một công việc nào đó và gọi mỗi khi cần.

Giống như với các ngôn ngữ khác, chúng ta cũng có thể định nghĩa macro của riêng chúng ta trong Rust.

Cú pháp

Chúng ta định nghĩa một macro theo như mẫu dưới đây:

macro_rules! mac1 {
(pattern) => (expansion);
(pattern) => (expansion);

}

Đầu tiên chúng ta ghi từ khóa macro_rules!, sau đó là tên macro, trong đoạn code trên thì tên macro là mac1, rồi đến cặp dấu {}, và danh sách các quy luật, mỗi luật kết thúc bằng dấu chấm phẩy ;

Quy luật mà chúng ta nói tới ở đây có thể hiểu như trong câu lệnh match mà chúng ta đã học (hay câu lệnh switch của C++, Java…), tức là chúng ta có thể truyền vào các tham số, và Rust sẽ so sánh xem tham số nào phù hợp và chạy đoạn code phía sau đó. Ví dụ:

fn main() {
macro_rules! hello {
() => {
println!("Hello World");
}
}
hello!();
}

Trong đoạn code trên có một macro tên là hello, bên trong chúng ta định nghĩa một luật, luật này không nhận vào tham số nào. Khi chúng ta gọi macro này thì chúng ta ghi là hello!() và luật đầu tiên khớp sẽ chạy, ở đây chuỗi Hello World sẽ được in ra.

Hello World

Khi muốn đưa tham số vào thì chúng ta đưa với cấu trúc sau:

$arg:frag

Trong đó $arg chỉ là tên biến bình thường như khi định nghĩa hàm, đối với tham số cho macro thì phải có dấu $. Còn frag có thể là một trong những giá trị sau:

  • expr: biểu thức cho ra một giá trị (vd 2+2, ‘phocode’)
  • item: một thành phần của một crate (chẳng hạn struct Blog)
  • block: một khối lệnh (vd { println!(); return 12; } )
  • stmt: một câu lệnh (vd let x = 3)
  • pat: mẫu hay patter (vd (17, 'a') )
  • ty: biến theo sau bởi dấu => hoặc : => as
  • ident: tên một biến (vd x; str)
  • path: tên một struct, interface… hợp lệ (vd Blog::PhoCode)
  • tt: token tree (bạn có thể tự tìm hiểu thêm)

Ví dụ:

fn main() {
macro_rules! hello_name {
($arg:expr) => {
println!("Hello {}", $arg);
}
}
hello_name!("Pho Code");
}

Trong đoạn code trên, macro hello_name nhận vào một luật là nhận một biến kiểu expr, tức là chúng ta có thể truyền vào một biểu thức hay một giá trị. Bên trong chúng ta cho in giá trị của biểu thức đó ra.

Hello Pho Code

Macro nhiều tham số

Chúng ta có thể truyền vào nhiều tham số cho một macro, ví dụ:

fn main() {
macro_rules! arguments_list {
(
$($arg:expr), *) => (
{$
( println!("{}", $arg); );
*}
);
}
arguments_list!("Pho Code", 2017, 3.14);
}

Cú pháp này của Rust hơi lạ mắt và khó đọc, bạn có thể tự suy ra, ở đây mình sẽ không giải thích. Bạn có thể viết liền trên một hàng chứ không nhất thiết phải xuống dòng như mình.

Pho Code
2017
3.14

Tạo hàm mới

Chúng ta có thể dùng macro để tạo một hàm mới như sau:

fn main() {
macro_rules! create_a_function {
($arg:ident) => {
fn $arg() {
println!("Calling the {} function", stringify!($arg));
}
}
}
create_a_function!(phocode);
phocode();
}

Trong đoạn code trên, chúng ta tạo một macro có tên create_a_function, macro này nhận vào tên hàm, bên trong chúng ta định nghĩa một hàm mới với tên lấy từ $arg. Khi gọi create_a_function!() là chúng ta đã tạo được một hàm mới và có thể gọi một cách bình thường.

Ngoài ra ở đây chúng ta còn dùng đến macro stringify để chuyển $arg thành một chuỗi.

Calling the phocode function

Gọi macro của crate khác

Đây là trường hợp đã được đề cập trong bài trước, khi chúng ta gọi macro error!() của crate log, chúng ta phải thêm dòng #[macro_use] vào đầu file.

#[macro_use]
extern crate log;
extern crate env_logger;

fn main() {
env_logger::init();
error!("There are some errors");
}

Trong trường hợp chúng ta chỉ muốn sử dụng một số macro nào đó của crate thì chúng ta có thể viết như sau:

#[macro_use(error, info)] // Chỉ sử dụng macro errror!() và info!()
extern crate log;
extern c<span data-mce-type="bookmark" style="display: inline-block; width: 0px; overflow: hidden; line-height: 0;" class="mce_SELRES_start"></span>rate env_logger;

fn main() {
env_logger::init();
error!("There are some errors");
}

Nếu không thì chúng ta sẽ không thể sử dụng được macro nào của crate đó. Hoặc có một cách tiếp cận khác là trong crate đó chúng ta khai báo #[macro_export] vào đầu file, và chúng ta sẽ có thể sử dụng macro trong file khác mà không cần khai báo lại #[macro_use] nữa.

Rust – Module và Crate

Trong các bài viết trước đây thì chúng ta chủ yếu viết code trong một file duy nhất, khi làm việc với những dự án thực tế thì số lượng file sẽ nhiều hơn. Có nhiều file chứa các struct, hàm… khác nhau nhưng có liên quan đến nhau, khi đo chúng ta phải gộp nhóm chúng lại với nhau. Trong Rust thì chúng ta gộp nhóm các file lại với nhau thông qua module.

Crate

Nếu bạn đã từng lập trình Java thì chắc hẳn bạn đã biết qua từ khóa package, trong Rust thì chúng ta gọi là crate.

Khi chúng ta viết các app có hàm main(), thì khi chúng ta biên dịch thì Rust sẽ tạo ra một file khả thi – tức file .exe (trên Windows) hay .sh (trên Linux) có thể chạy được.

Trong trường hợp chúng ta muốn viết các file .rs có chứa các biến, hằng, hàm, struct…. có thể được gọi từ các file khác thì khi biên dịch chúng ta sẽ có các file .dll (Windows), .so (Linux)… Đây là các file thư viện liên kết động, để project Rust biên dịch ra file này thì khi biên dịch, thay vì chúng ta đưa vào tham số --bin, chúng ta thay bằng tham số --crate-type=lib, ví dụ:

Lúc này thay vì tạo một file .exe thì Rust sẽ tạo một file .rlib, ngoài ra tên trước phần mở rộng sẽ là tên file kèm theo từ “lib”, tức ở đây chúng ta được file libmain.rlib.

Ngoài tham số lib thì chúng ta còn có thể yêu cầu Rust tạo ra các loại file thư viện khác như sau:

  • --crate-type=bin: tạo file .exe
  • --crate-type=lib: tạo file .rlib
  • --crate-type=rlib: tương tự tạo file .rlib
  • --crate-type=dylib: tạo file .dll
  • --crate-type=staticlib: tạo file .lib

Ngoài cách chỉ định trong câu lệnh rustc thì chúng ta cũng có thể chỉ định bằng cách ghi dòng này ở đầu file:

#![crate_type = "lib"]
fn main() {
println!("Hello World");
}

Module

Crate là các thực thể đã được biên dịch và phân phối trên máy tính để có thể chạy được. Mặc định Rust sẽ quy định các đoạn code một crate được “chứa” trong một module có tên là root một cách ngầm định. Sau đó các code này có thể được các coder phân chia thành các module nhỏ hơn, gọi là submodule nằm bên dưới module root.

Module cũng có thể được định nghĩa bên trong một module khác, lúc này module đó được gọi là nested. Khi biên dịch thì tất cả các module nằm trong một crate sẽ được biên dịch cùng với nhau.

Trong các bài trước chúng ta đã dùng qua một số module có sẵn trong Rust như io, str, vec… từ crate std. Crate này chứa rất nhiều module và hàm có ích trong các dự án thực tế.

Thông thường một module sẽ chứa tập hợp các đoạn code định nghĩa các trait, struct, phương thức, hàm có liên quan với nhau. Khi chúng ta đặt tên một module thì tên module đó được gọi là namespace. Để định nghĩa một module thì chúng ta dùng từ khóa mod {...}, ví dụ như sau:

mod CMS {
fn func1() { }
fn func2() { }
}

 

Tương tự với Java, mỗi file code của Rust đều được định nghĩa một module ngầm định có tên là root, kể cả khi chúng ta không khai báo từ khóa mod nào trong file. Trong đoạn code trên chúng ta định nghĩa module CMS, bên trong có 2 hàm là hàm func1()func2(). Để gọi đến 2 hàm này thì chúng ta dùng cú pháp :: như sau: CMS::func1(), CMS::fun2().

Mặc định thì các thành phần được định nghĩa bên trong một module thì chỉ có thể được gọi bên trong module đó thôi, để có thể gọi từ bên ngoài thì chúng ta phải ghi thêm từ khóa pub (viết tắt của public) trước mỗi phần tử bên trong module. Ví dụ:

mod CMS {
fn func1() {
println!("Func1 called");
}
pub fn func2() {
println!("Func2 called");
}
}

fn main() {
CMS::func1(); // lỗi
CMS::func2(); // OK
}

Lời gọi tới hàm CMS::func1() sẽ báo lỗi thông báo func1()private, còn hàm CMS::func2() thì chạy bình thường.

Func2 called

Đối với các nested module thì các thành phần bên trong đó phải được khai báo pub thì mới có thể gọi được.

Khi định nghĩa struct bên trong module thì các struct này cũng phải được khai báo với từ khóa pub thì mới có thể đọc được, tuy nhiên các phần tử của struct thì lại là private, do đó nếu chúng ta muốn đọc luôn cả các thuộc tính của struct thì chúng ta cũng phải khai báo pub cho các thuộc tính đó. Ví dụ:

pub mod CMS {
pub struct Blog {
pub name: &’static str, // public
year: i32 // private
}
}
fn main() {
let phocode = CMS::Blog {
name : "phocode.com",
year: 2017 // lỗi
};
}

Đoạn code trên sẽ báo lỗi vì thuộc tính year trong struct Blog là private và không thể đọc/ghi được.

field 'year' of struct 'CMS::Blog' is private

Import module

Chúng ta có thể sử dụng từ khóa use để chỉ định là chúng ta sẽ dùng thành phần nào trong một module mà không cần phải ghi tên module ra. Ví dụ:

use module1::fun1;
use module1::func2 as gf2;
use module2::*;
pub mod module1 {
pub fn func1() {
println!("Calling Module1.func1");        
}    
pub fn func2() {
println!("Calling Module1.func2");        
}
}
pub mod module2 {
pub fn func1() {
println!("Calling Module2.func1");        
self::func2();
}    
fn func2() {        
println!("Calling Module2.func2");    
}
}
fn main() {
module1::func1();
gf2();
func1();
}

Trong đoạn code trên, chúng ta định nghĩa 2 module là module1module2, cả 2 module này đều chứa 2 hàm là func1()func2().

Ở dòng đầu tiên chúng ta ghi câu lệnh use module1::func2 as gf2. Tức là chúng ta cho Rust biết rằng chúng ta muốn dùng hàm func2() trong module1 mà không cần phải ghi phần module1:: phía trước, bằng cách này chúng ta chỉ cần gọi func2() là được, phần as gf2 tức là chúng ta đặt tên viết tắt cho funf2 là gf2, và thay vào đó thay vì ghi func2() thì chúng ta ghi là gf2().

Nếu muốn use nhiều phần tử thì chúng ta có thể ghi như sau:

use module1::{func1, func2};

Ngoài ra trong đoạn code trên chúng ta còn có câu lệnh use module2::* có nghĩa là import toàn bộ hàm public có trong module.

Bên trong một module chúng ta có thể dùng self:: để chỉ tới module chứa hàm đó.

Calling Module1.func1
Calling Module1.func2
Calling Module2.func1
Calling Module2.func2

Import module khác file

Chúng ta có thể code các module ở trong các file khác và dùng ở một file khác. Ví dụ:

Chúng ta viết file CMSModule.rs như sau:

pub mod CMS {
pub struct Blog {
pub name: &’static str,
pub year: i32
}
}

pub fn setup() {
println!("Start setting things up");
}

File này chứa một module tên CMS và một struct là Blog.

Để có thể đọc được module này từ một file khác thì chúng ta sử dụng từ khóa mod như sau:

mod CMSModule;

fn main() {
let phocode = CMSModule::CMS::Blog{
name: "phocode.com",
year: 2017
};
println!("{}", phocode.name);
CMSModule::setup();
}

Trong đoạn code trên, câu lệnh mod CMSModule sẽ tìm tất cả các file có tên CMSModule.rs nằm trong thư mục cùng với thư mục của file hiện tại rồi đọc code trong các file đó. Nếu thư mục hiện tại không có thì Rust sẽ tiếp tục tìm trong các thư mục con cho đến hết.

phocode.com
Start setting things up

Import crate từ file khác

Như đã nói ở trên, crate là các file thư viện nhị phân được tạo ra, chứa các hàm, module, struct… đã được biên dịch và có thể sử dụng được.

Bây giờ chúng ta mở command prompt (hoặc terminal trong Linux) lên và tạo một project library có tên myLib và biên dịch thành file thư viện như sau:

cargo new myLib
cd myLib

Lệnh cargo new sẽ tạo một thư mục project có tên mylib và tạo file có tên lib.rs nằm trong thư mục src.

Chúng ta sửa file này lại như sau:

pub fn myFunc() {
println!("Calling myFunc");
}

Tiếp theo chúng ta dùng lệnh cd để di chuyển vào thư mục myLib. Sau đó chúng ta biên dịch bằng lệnh cargo build. Cargo sẽ tạo một thư mục có tên target/debug, bên trong thư mục này sẽ có một file có tên là libmyLib.rlib.

Để có thể sử dụng file thư viện này thì chúng ta sử dụng câu lệnh extern crate.

Bây giờ chúng ta tạo file main.rs trong thư mục src để sử dụng file thư viện này:

extern crate myLib;
fn main() {
myLib::myFunc();
}

Câu lệnh extern crate myLib sẽ đọc file thư viện libmyLib.rlib, và chúng ta có thể gọi tới hàm myFunc() một cách dễ dàng.

Lưu ý là câu lệnh extern chỉ đọc các thành phần public được khai báo với từ khóa pub thôi.

Bây giờ chúng ta có thể chạy lệnh cargo run để chạy file .exe từ main.rs

Calling myFunc

Có một lưu ý là chúng ta không bao giờ dùng câu lệnh extern crate std; nhưng vẫn có thể sử dụng được các hàm như println!(), lý do là vì crate std được import một cách ngầm định bởi Rust.

crates.io

Rust cũng có một cộng đồng thư viện mở rất lớn có địa chỉ tại https://crates.io, đây là nơi tập hợp rất nhiều các crate do các coder trên toàn thế giới đóng góp, chúng ta có thể tải về và sử dụng.

Để có thể tải một crate về và sử dụng thì chúng ta mở file Cargo.toml lên, sau đó thêm tên crate vào sau phần [dependencies] với cú pháp sau:

<tên_crate> = "<số_phiên_bản>"

Ví dụ, trên crates.io có một crate tên là log có chức năng giúp chúng ta debug dễ dàng, chúng ta có thể lên trang chủ và tìm kiếm với từ khóa “log”, trang web sẽ trả về danh sách các crate liên quan, chúng ta click vào để đọc, bên trong sẽ có đoạn code để chúng ta copy vào file .toml.

Ở thời điểm khi viết bài này thì crate log có phiên bản là 0.4.1, ngoài ra crate log thường hay được dùng kèm với một crate khác là env_logger, do đó chúng ta sửa lại file Cargo.toml để cài 2 crate này như sau:

[package]
name = "Example"
version = "0.1.0"
authors = ["phocode"]

[dependencies]
log = "0.4.1"
env_log = "*"

Và sau đó chúng ta chạy lệnh cargo build thì Rust sẽ tải về mã nguồn và biên dịch crate này.

> cargo build
  Blocking waiting for file lock on the registry index 
  Updating registry `https://github.com/rust-lang/crates.io-index` 
  Downloading log v0.4.1
  Downloading cfg-if v0.1.2
  Compiling cfg-if v0.1.2
  Compiling log v0.4.1
  Compiling winapi v0.2.8
  Compiling winapi-build v0.1.1
  Compiling utf8-ranges v1.0.0
  Compiling void v1.0.2
  Compiling libc v0.2.36
  Compiling lazy_static v1.0.0
  Compiling regex-syntax v0.4.2
  Compiling winapi v0.3.4
  Compiling cfg-if v0.1.2
  Compiling num-traits v0.1.41
  Compiling kernel32-sys v0.2.2
  Compiling unreachable v1.0.0
  ...

Chúng ta cũng có thể khai báo phiên bản là log = "*" thì Rust sẽ tải phiên bản mới nhất về cho chúng ta sử dụng.

Sau đó để sử dụng thì chúng ta extern crate như bình thường và gọi các hàm hoặc dùng các struct mà crate đó cung cấp, ví dụ;

#[macro_use]
extern crate log;
extern crate env_logger;

fn main() {
env_logger::init();
error!("There are some errors");
}

Trên đây chỉ là đoạn code ví dụ được mình sử dụng từ trang chủ của 2 crate này, mình sẽ không giải thích ở đây, bạn có thể tìm hiểu thêm trên mạng. Ngoài ra trong đoạn code trên chúng ta có sử dụng macro là #[macro_use], mình sẽ giải thích trong bài sau.

ERROR 2018-01-18T11:24:10Z: Test: There are some errors

Rust – Box

Trong Rust còn có một loại con trỏ khác có tên là con trỏ Box, đây là con trỏ dùng cho kiểu generic T, tức là dùng cho bất kì kiểu dữ liệu nào. Khi dùng con trỏ Box thì dữ liệu sẽ được lưu trên bộ nhớ heap.

Để tạo một biến con trỏ Box thì chúng ta dùng hàm Box::new, ví dụ như sau:

struct Blog {
year: i32,
name: &’static str

fn main() {
let mut a1 = Box::new(Blog {
year: 2017,
name: "phocode.com"
});
println!("{}", a1.year);
}

Trong đoạn code trên chúng ta khai báo biến a1 là một biến con trỏ Box chứa dữ liệu là một biến Blog.

2017

Cơ chế borrowingownership trong con trỏ Box cũng hoàn toàn giống như với con trỏ bình thường.

Ngoài các struct thì Box cũng hỗ trợ các kiểu dữ liệu cơ bản, ví dụ:

fn main() {
let a = Box::new(2017);
let b = Box::new("phocode.com");
let mut c = Box::new("3.14");
*c = 3.14159;
}

Mặc định thì nếu không chỉ định là biến mut thì các biến sẽ không thể thay đổi giá trị được, giống như với các con trỏ bình thường.

Rust – Borrowing và Ownership

Mỗi ứng dụng được viết, cho dù là làm gì đi nữa, từ đọc dữ liệu database, hay thực hiện các công việc tính toán phức tạp… thì cũng đều là làm việc với một nguồn tài nguyên nào đó. Nguồn tài nguyên mà chúng ta thường thấy đối với một ứng dụng là vùng bộ nhớ trong RAM chứa tất cả các biến có trong ứng dụng đó. Các nguồn tài nguyên khác có thể có là các file, các kết nối mạng, kết nối database…

Trong Rust thì mỗi một tài nguyên đều được đặt một cái tên, đó là khi chúng ta thực hiện câu lệnh let, và khi đó tài nguyên đó được gọi là có chủ (hay có owner trong tiếng Anh). Ví dụ:

struct Blog {
year: i32,
name: &’static str
}
fn main() {
let phocode: Blog = Blog{
year: 2017,
name: "phocode.com"
};
}

Trong đoạn code trên, biến phocode sở hữu một vùng nhớ trong RAM có độ lớn bằng độ lớn của 1 biến i32 cộng với một biến str.

Và chỉ có duy nhất biến phocode được quyền thay đổi giá trị của vùng bộ nhớ đó thôi, và tại một thời điểm thì chỉ có duy nhất một biến được quyền sở hữu một vùng bộ nhớ đó thôi. Lúc này biến phocode là chủ của vùng bộ nhớ đó.

Một vùng bộ nhớ có thể được thay đổi chủ khi chúng ta dùng câu lệnh let như sau:

let phocode2 = phocode;

Lúc này thì chủ mới của vùng bộ nhớ là phocode2 chứ không phải phocode nữa. Tuy nhiên các giá trị bên trong vùng bộ nhớ đó thì vẫn giữ nguyên, không thay đổi.

Lúc này nếu chúng ta cố gắng truy xuất giá trị từ phocode hay thay đổi giá trị của phocode2 thì trình biên dịch sẽ báo lỗi:

struct Blog {
year: i32,
name: &’static str
}

fn main() {
let phocode: Blog = Blog{
year: 2017,
name: "phocode.com"
};
let phocode2 = phocode;
phocode2.year = 2016; // lỗi: cannot assign to immutable field `phocode2.year`
println!("{}", phocode.name); // lỗi: use of moved value `phocode.com`
}

Lý do là vì vùng bộ nhớ này là không thể thay đổi giá trị được (mặc định trong Rust). Và bởi vì phocode không còn là chủ nhân của vùng tài nguyên đó nữa nên không có quyền đọc giá trị trong vùng tài nguyên này.

Tuy nhiên, khi chúng ta gán tên biến mới theo kiểu tham chiếu đã học trong bài trước như sau:

let phocode2 = &mut phocode;

Thì lúc này biến phocode2 đang mượn tài nguyên của ông chủ là biến phocode.  Lúc này chúng ta có thể đọc/ghi tài nguyên của phocode thông qua biến phocode2 như bình thường, nhưng nếu chúng ta thao tác thông qua phocode thì Rust lại báo lỗi, ví dụ:

struct Blog {
year: i32,
name: &’static str
}
fn main() {
let mut phocode: Blog = Blog{
year: 2017,
name: "phocode.com"
};
let phocode2 = &mut phocode;
phocode2.name = "https://phocode.com"; // OK
println!("{}", phocode2.name); // OK: https://phocode.com
phocode.year = 2016; // lỗi: cannot assign to `phocode.year` because it is borrowed
}

Giải thích ngắn gọn là, phocode2 đang mượn tài nguyên chủ phocode, vì tài nguyên của phocode đang được cho mượn nên phocode không thể làm gì trên tài nguyên của mình được.

Biến phocode chỉ có thể truy cập lại được tài nguyên của mình khi các biến đang mượn tài nguyên của mình đã dùng xong (và tất nhiên là trả lại cho chủ), ví dụ:

struct Blog {
year: i32,
name: &’static str
}
fn main() {
let mut phocode: Blog = Blog{
year: 2017,
name: "phocode.com"
};
if phocode.year > 0 {
let phocode2 = &mut phocode;
phocode2.name = "phocode.net";
println!("{}", phocode2.name);
}
phocode.year = 2016;
println!("{}", phocode.year);
}

Trong đoạn code trên thì biến phocode2 chỉ mượn tài nguyên trong phạm vi là câu lệnh if thôi, sau khi cậu lệnh if kết thúc thì biến phocode2 được giải phóng khỏi bộ nhớ và trả lại tài nguyên cho biến phocode.

phocode.net
2016

Nhờ có cơ chế borrowingownership này mà các ứng dụng Rust ít gặp vấn đề với lỗi memory leak hơn so với trong C++.

Rust – Tham chiếu

Trong bài trước chúng ta đã biết về toán tử & dùng để lấy địa chỉ bộ nhớ của một biến. Những biến có giá trị được lấy từ toán tử & được gọi là con trỏ, và phép lấy địa chỉ này được gọi là tham chiếu.

Khi một biến có kiểu dữ liệu là T thì con trỏ trỏ tới biến đó sẽ có kiểu dữ liệu là &T. Một biến có thể được tham chiếu bởi nhiều con trỏ, ví dụ:

fn main() {
let a: u8 = 10;
let b = &a;
let c = &a;
println!("{:p}", b);
println!("{}", *c);
}

Kết quả:

0x8d0e0ffa07
10

Có thể xem trong hình minh họa dưới đây:

Nếu bạn đã từng lập trình với con trỏ và tham chiếu trong C++ thì bạn sẽ biết là chúng ta có thể thay đổi giá trị mà con trỏ đang tham chiếu tới.

Đối với Rust thì mặc định là không được phép làm điều này:

fn main() {
let a: u8 = 10;
let b =&a;
*b = 11; // lỗi: cannot assign to immutable borrowed content `*b`
}

Bởi vì làm như thế sẽ phát sinh nhiều vấn đề nghiêm trọng, tuy nhiên Rust cũng không hoàn toàn cấm chúng ta làm điều đó, chỉ cần chúng ta khai báo với Rust là biến được tham chiếu là mut và kiểu tham chiếu là &mut như trong ví dụ sau là được:

fn main() {
let mut a: u8 = 10;
let b = &mut a;
*b =11; // OK
println!("{}", *b); // 11
println!("{}", a); // lỗi: cannot borrow `a` as immutable because it is also borrowed as mutable
a = 12; // lỗi: cannot assign to `a` because it is borrowed
}

Trong ví dụ trên, chúng ta có thể thay đổi giá trị bằng cách sử dụng *b, nhưng khi in giá trị trong a thì Rust vẫn báo lỗi, hay khi chúng ta cố thay đổi giá trị của a thì cũng bị báo lỗi. Chúng ta sẽ tìm hiểu trong khái niệm borrowing ở bài sau, ở đây chúng ta chỉ cần hiểu là nếu một biến có các con trỏ tham chiếu tới nó thì biến đó sẽ bị Rust khóa lại và chúng ta không thể đọc/ghi giá trị cho biến đó được.

Ngoài ra thì Rust chỉ cho phép một con trỏ được tham chiếu theo kiểu &mut mà thôi, ví dụ:

fn main() {
let mut a: u8 = 10;
let b = &mut a;
let c = &mut a; // lỗi: cannot borrow `a` as mutable more than once at a time
}

Rust cho phép chúng ta truyền con trỏ &mut vào hàm, ví dụ như sau:

fn square(n: &mut i32) -> i32 {
return *n * *n;
}
fn main() {
let mut a: i32 = 10;
println!("{}", square(&mut a));
}

Trong đoạn code trên chúng ta viết hàm square() nhận vào một con trỏ &mut kiểu i32, và bên trong chúng ta cho bình phương giá trị của con trỏ đó.

100

Từ khóa ref

Trong câu lệnh match, chúng ta có thể dùng từ khóa ref để lấy nhanh một con trỏ tới biến khóa, ví dụ:

fn main() {
let n = 42;
match n {
ref p => {
println!("p is a pointer to n, *p = {}", *p);
},
_ => println!("")
}
}

p sẽ là biến con trỏ tham chiếu tới biến n.

p is a pointer to n, *p = 42

Nếu muốn tham chiếu theo kiểu &mut thì chúng ta khai báo ref mut như sau:

fn main() {
let mut n = 42;
match n {
ref mut p => {
println!("p is a pointer to n, *p = {}", *p);},
_ => println!("")
}
}

Rust – Con trỏ

Trong phần này chúng ta sẽ tìm hiểu về con trỏ trong Rust.

Bộ nhớ stack và bộ nhớ heap

Khi một ứng dụng khởi chạy thì ứng dụng này sẽ được hệ điều hành cấp 2MB bộ nhớ stack. Đây là nơi ứng dụng lưu trữ các biến cục bộ và các tham số hàm, chẳng hạn như các biến i32 (chiếm 4 byte trong bộ nhớ stack). Khi ứng dụng gọi một hàm, một phần trong stack sẽ được dùng để lưu trữ lời gọi này. Nhờ có cơ chế này mà vùng nhớ stack biết được hàm nào được gọi trước, hàm nào được gọi sau rồi sau đó trả về theo thứ tự đó cho đúng.

Các biến có kiểu dữ liệu động như string hay mảng thì không được lưu trữ trong bộ nhớ stack. Với những loại giá trị này thì ứng dụng sẽ yêu cầu một vùng nhớ trong một bộ nhớ khác là heap, do đó thông thường bộ nhớ heap có dung lượng dữ liệu lưu trữ lớn hơn bộ nhớ stack.

Thời gian tồn tại của biến

Tất cả các biến trong Rust đều có thời gian tồn tại hữu hạn. Giả sử chúng ta khai báo biến n và gán giá trị 42, thì biến này sẽ tồn tại cho đến khi không còn có câu lệnh nào có gọi tới biến này nữa, đây cũng là thời gian tồn tại của biến. Ví dụ chúng ta có đoạn code sau đây:

fn main() {
let n: u32 = 42;
let n2 = n;
life(n);
println!("{}", m); // lỗi: unresolved name `m`.
println!("{}", o); // lỗi: unresolved name `o`.
}

fn life(m: u32) -> u32 {
let o = m;
o
}

Trong đoạn code trên thì biến n không còn tồn tại cho đến khi kết thúc hàm main(), tương tự biến mo sẽ chỉ tồn tại khi bắt đầu và kết thúc hàm life(). Cơ chế này giống hệt với các ngôn ngữ khác.

Và cũng tương tự với các ngôn ngữ khác, các biên được khai báo trong một khối lệnh – tức là trong một cặp dấu ngoặc nhọn {} thì chỉ tồn tại trong cặp dấu đó mà thôi. Ví dụ;:

{
let PI = 3.14;
}
println!("The value of PI is {}", PI); // lỗi: unresolved name ‘PI’

Ngoài ra trong Rust chúng ta còn có thể chỉ định thời gian tồn tại cho các biến bằng cách ghi các chú thích, một ví dụ về cách chú thích thời gian tồn tại kiểu này là 'static mà chúng ta đã dùng rất nhiều với kiểu dữ liệu str, 'static sẽ khiến cho một biến là biến toàn cục, tức là sẽ tồn tại mãi mãi cho đến khi ứng dụng kết thúc.

Địa chỉ bộ nhớ

Hệ điều hành lưu trữ dữ liệu trong bộ nhớ RAM, bộ nhớ này được quản lý bằng cách phân chia thành các ô nhớ có độ lớn là 1 byte, mỗi ô nhớ sẽ được đánh địa chỉ bắt đầu từ 0 và được biểu diễn bằng số hexa.

Các biến trong Rust khi được khai báo và khởi tạo thì cũng sẽ được cấp một vùng bộ nhớ bằng với độ lớn của kiểu dữ liệu trong RAM và cũng được cấp địa chỉ. Đoạn code sau đây sẽ lấy địa chỉ bộ nhớ của biến:

fn main() {
let a: u8 = 10;
let b: u8 = 10;
let c: u8 = 10;
let d: u32 = 10;
let e: u32 = 10;
let f = &e;
println!("{:?}", &a as *const u8);
println!("{:?}", &b as *const u8);
println!("{:?}", &c as *const u8);
println!("{:?}", &d as *const u32);
println!("{:?}", &e as *const u32);
println!("{:?}", f as *const u32);
}

Chúng ta lấy địa chỉ bộ nhớ của biến bằng cách ghi kí tự & trước tên biến, rồi thêm đoạn as *const <tên_kiểu>.Chúng ta sẽ tìm hiểu về hàm *const sau, đoạn code trên sẽ cho ra output dạng như sau:

0x93723ff9e5
0x93723ff9e6
0x93723ff9e7
0x93723ff9e8
0x93723ff9ec
0x93723ff9ec

Đó là các địa chỉ trong bộ nhớ của từng biến được viết dưới dạng số hexa. Bạn có thể để ý thấy là địa chỉ của tất cả các biến chỉ khác nhau kí tự cuối cùng, lý do là vì các biến được cấp bộ nhớ một cách liên tục – tức là chúng đứng cạnh nhau trong RAM.

Số cuối cùng của 4 biến a, b, cd tăng dần theo 1 đơn vị, lý do là vì u8 là số nguyên 8 bit – tức chỉ có 1 byte, và 3 biến a, b, c này chiếm đúng 1 byte trong bộ nhớ.

Ngoài ra chúng ta cũng khai báo biến f có giá trị là &e, tức là chứa địa chỉ bộ nhớ của biến e. Lúc này f là một biến con trỏ.

Con trỏ

Con trỏ là một biến chứa giá trị là địa chỉ bộ nhớ của một giá trị nào đó. Để lấy giá trị mà con trỏ trỏ tới thì chúng ta dùng toán tử *.  Ví dụ:

fn main() {
let a: u8 = 10;
let b = &a;
println!("a = {}", a);
println!("&a = {:?}", &a as *const u8);
println!("b = {:?}", b as *const u8);
println!("*b = {}", *b);
}

Trong đoạn code trên, chúng ta khai báo biến a kiểu u8 có giá trị bằng 10, biến b là một biến con trỏ trỏ tới địa chỉ của biến a, khi chúng ta ghi b bình thường thì chúng ta sẽ có được địa chỉ của biến a, còn nếu ghi *b thì có được giá trị mà biến b đang trỏ tới, tức là giá trị của biến a.

a = 10
&a = 0x3010f5f74f
b = 0x3010f5f74f
*b = 10

Việc dùng con trỏ có ích lợi là chúng ta có quyền kiểm soát bộ nhớ một cách linh hoạt hơn, do đó chúng ta có thể viết cách ứng dụng có hiệu suất cao hơn. Tuy nhiên quản lý bộ nhớ là một công việc không dễ dàng nên thông thường ứng dụng sẽ được phát triển lâu hơn.

Chúng ta cũng có thể truyền con trỏ làm tham số cho các hàm, ví dụ:

fn main() {
let q = 42;
println!("{}", square(&q)); // 1764
}

fn square(k: &i32) -> i32 {
return *k * *k;
}

Con trỏ trong Rust giống hoàn toàn với con trỏ trong C++, và ở ngôn ngữ nào đi nữa thì chúng ta cũng nên làm công việc dọn dẹp – tức giải phóng tải nguyên, mặc dù cả Rust và C++ đều có bộ phần dọn dẹp tài nguyên tự động. Tuy nhiên nếu không chủ động giải phóng tài nguyên, đôi khi chúng ta sẽ gặp phải các lỗi ngoại lệ rất khó tìm và sửa.

Rust có một ưu điểm là tự động dò tìm được các lỗi ngoại lệ này trong quá trình biên dịch.

Rust – Trait

Trait chính là tên gọi khác của interface trong các ngôn ngữ khác như Java, C#…

Trait cho phép chúng ta định nghĩa tên các phương thức nhưng không có phần thân phương thức, mục đích để làm phương thức mẫu cho các struct, rồi các struct có thể code lại (hay implement như trong các ngôn ngữ khác) trait này, tức là có thể code lại phương thức đó tùy theo từng struct, qua đó chúng ta có thể định các phương thức trùng tên cho nhiều struct khác nhau nhưng có ý nghĩa tương đồng với nhau.

Để định nghĩa một trait thì chúng ta dùng cú pháp như sau

trait <tên_trait> {

fn <tên_phương_thức_1>();

fn <tên_phương_thức_2>();

...

}

Lưu ý chúng ta không để cặp dấu ngoặc nhọn {} sau tên phương thức mà để một dấu chấm phẩy ;

Để code một trait cho một struct thì chúng ta dùng cú pháp như sau:

impl <tên_trait> for <tên_struct> {

fn <tên_phương_thức_1>();

fn <tên_phương_thức_2>();

...

}

Ví dụ:

trait Web {
fn publish(&self);
}

struct Blog {}

impl Web for Blog {
fn publish(&self) {
println!("Publishing new post on blog");
}
}

struct Forum {}

impl Web for Forum {
fn publish(&self) {
println!("Publishing new post on forum");
}
}

fn main() {
let newBlog = Blog{};
let newForum = Forum{};
newBlog.publish();
newForum.publish();
}

Trong đoạn code trên chúng ta định một trait có tên là Web, bên trong có chứa tên một phương thức có tên là publish() nhưng không có phần thân hàm mà chỉ có dấu chấm phẩy ở phía sau. Sau đó chúng ta định nghĩa struct BlogForum (chưa có gì bên trong), rồi viết phần implement cho các struct này. Ở phần main() thì chúng ta chỉ đơn giản là gọi các câu lệnh println!() thôi.

Khi tạo các đối tượng struct này và gọi phương thức publish() thì tùy đối tượng thuộc struct nào mà phương thức publish() của struct đó sẽ được gọi.

Publishing new post on blog
Publishing new post on forum

Phương thức mặc định

Chúng ta có thể code phương thức mặc định cho trait, khi đó nếu struct nào implement trait mà không code lại phương thức của trait thì đoạn code mặc định của trait đó sẽ được gọi, ví dụ:

trait Web {
fn publish(&self);
fn deletePost(&self) {
println!("Deleting post on this Web");
}
}

struct Blog {}
impl Web for Blog {
fn publish(&self) {
println!("Publishing new post on blog");
}
fn deletePost(&self) {
println!("Deleting post on this Blog");
}
}

struct Forum {}
impl Web for Forum {
fn publish(&self) {
println!("Publishing new post on forum");
}
}

fn main() {
let newBlog = Blog{};
let newForum = Forum{};
newBlog.deletePost();
newForum.deletePost();
}

Trong đoạn code trên chúng ta dùng lại đoạn code ở đầu bài, ở đây chúng ta định nghĩa thêm phương thức deletePost() cho trait, và có cả phần định nghĩa code bên trong trait luôn, và chúng ta chỉ implement lại phương thức này cho struct Blog, còn Forum thì không. Do đó khi gọi phương thức deletePost() thì phương thức của struct Blog sẽ được gọi, còn đối với Forum thì phương thức mặc định của trait sẽ được gọi.

Deleting post on this Blog
Deleting post on this Web

Một số đặc điểm:

  • Một struct có thể implement bao nhiêu trait cũng được
  • Và một trait có thể được implement bởi bao nhiêu struct cũng được
  • Trait có thể được implement trên bất kì kiểu dữ liệu nào chứ không chỉ riêng struct