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

Rust – Array, Vector và Slice

Trong phần này chúng ta tìm hiểu về 3 kiểu dữ liệu Array, Vector và Slice trong Rust.

Array

Array (hay có tên khác là mảng) là kiểu dữ liệu lưu trữ theo dạng danh sách, tức là giá trị sẽ là một dãy các giá trị chứ không còn là các giá trị đơn độc nữa. Ví dụ:

fn main() {
let langs = ["Rust", "C++", "Java", "Python"];
println!("{:?}", langs);
}

Để tạo một array thì chúng ta mở một cặp dấu ngoặc vuông [], bên trong chúng ta ghi danh sách phần tử bằng các giá trị và cách nhau bởi dấu phẩy. Các phần tử có thể có kiểu dữ liệu nào cũng được, chẳng hạn như i32, f32…v.v nhưng các phần tử phải có kiểu dữ liệu giống nhau. Khi đã khởi tạo một array thì kích thước của array này không thể thay đổi được nữa.

Mặc định thì các phần tử bên trong cũng không thể thay đổi được giá trị, nếu muốn thay đổi giá trị các phần tử thì chúng ta khai báo array với từ khóa mut, tuy nhiên số lượng phần tử của array thì vẫn luôn không đổi.

Khi tạo mảng chúng ta có thể quy định số phần tử cho array thông qua cú pháp như sau:

fn main() {
let langs: [&str; 4] = ["Rust", "C++", "Java", "Python"];
println!("{:?}", langs);
}

Phía sau tên array là một cặp dấu ngoặc vuông, bên trong là tên kiểu dữ liệu và số lượng phần tử. Ở trên chúng ta khai báo array có kiểu dữ liệu &str và số lượng phần tử là 4.

Chúng ta cũng có thể tạo một mảng với tất cả các phần tử có cùng chung giá trị, ví dụ:

fn main() {
let names = ["PhoCode"; 3];
println!("{:?}", names); // ["PhoCode", "PhoCode", "PhoCode"]
}

Để tạo một mảng rỗng thì chúng ta không đưa giá trị vào mảng ví dụ:

fn main() {
let arr: [i32; 0] = [];
println!("{:?}", arr);
}

Chúng ta có thể lấy số lượng phần tử của mảng thông qua hàm len() như sau:

fn main() {
let arr: [i32; 3] = [23, 856, 9302];
println!("arr has {} elements", arr.len());
}

Để truy xuất vào các phần tử của mảng thì chúng ta sử dụng toán tử [] và truyền vào số thứ tự của phần tử, ví dụ:

fn main() {
let arr: [i32; 3] = [23, 856, 9302];
println!("1st element in arr is {}", arr[0]);
println!("2nd element in arr is {}", arr[1]);
println!("3rd element in arr is {}", arr[2]);
}

Số thứ tự của các phần tử trong mảng luôn luôn bắt đầu từ 0. Và do đó phần tử cuối cùng trong mảng luôn luôn có số thứ tự là len() - 1. Nếu chỉ số mảng vượt ra ngoài phạm vi cho phép của mảng thì Rust sẽ báo lỗi.

Nếu chúng ta cố gắng truy xuất phần tử nằm ngoài phạm vi mảng thì Rust sẽ in một câu cảnh báo khi biên dịch.

fn main() {
let arr: [i32; 3] = [23, 856, 9302];
println!("5th element is {}", arr[5]);
}

warning: this expression will panic at run-time
 println!("5th element is {}", arr[5]);
                               ^^^^^^ index out of bounds: the len is 3 but the index is 5

Ngoài ra khi chạy thì Rust sẽ báo lỗi runtime, chúng ta sẽ tìm hiểu về lỗi runtime sau.

thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 5'

Chúng ta có thể sử dụng câu lệnh for để lặp qua từng phần tử trong mảng như sau:

fn main() {
let arr: [i32; 3] = [23, 856, 9302];
for i in 0..arr.len() {
println!("{}", arr[i]);
}
}

23 
856 
9302

Với kiểu lặp ở trên thì chúng ta sử dụng số thứ tự của mỗi phần tử trong mỗi lần lặp, và mỗi lần lặp như vậy Rust cũng sẽ kiểm tra xem số thứ tự có nằm trong phạm vi cho phép của mảng hay không. Tuy nhiên chúng ta có thể lặp theo kiểu iteration sau đây:

fn main() {    
let arr: [i32; 3] = [23, 856, 9302]; 
for element in arr.iter() {
println!("{}", element);
}
}

Vòng lặp theo kiểu trên hiệu quả hơn so với lặp theo chỉ số. Chúng ta sẽ tìm hiểu về iteration sau.

Vector

Kiểu dữ liệu array ở trên có một nhược điểm là kích thước không thể thay đổi được, kiểu vector ra đời là để thay thế tính chất trên.

Tương tự với array thì vector cũng lưu trữ danh sách các phần tử, các phần thử có thể có kiểu dữ liệu gì cũng được nhưng phải giống nhau.

Để tạo một vector thì chúng ta dùng hàm new() hoặc macro vec!, ví dụ:

fn main() {
let vec_number: Vec<i32> = Vec::new();
let vec_str = vec!["Lorem", "Ipsum"];
}

Khi dùng hàm new() thì chúng ta tạo khai báo một biến vector và phải chỉ rõ kiểu dữ liệu trong vector theo cú pháp Vec<tên_kiểu>. Còn khi chúng ta khai báo và khởi tạo vector theo cú pháp vec! thì chúng ta không cần khai báo kiểu, mà thay vào đó chúng ta cung cấp các phần tử cho vector và vector sẽ tự động suy ra kiểu dựa theo các giá trị mà chúng ta truyền vào.

Chúng ta cũng có thể dùng hàm with_capacity() để tạo vector với số lượng định sẵn:

fn main() {
let numbers : Vec<i32> = Vec::with_capacity(10);
}

Trong đoạn code trên biến numbers sẽ được tạo ra với 10 phần tử rỗng.

Việc lặp qua các phần tử trong vector cũng giống như trong array:

fn main() {
let numbers = vec![423, 943, 192];
for element in numbers {
println!("{}", element);
}
}

423 
943 
192

Chúng ta cũng có thể thêm một phần tử vào vector bằng hàm push(), xóa một phần tử ra khỏi vector với hàm pop():

fn main() {
let mut numbers = vec![423, 943, 192];
numbers.push(13);
numbers.push(52);
println!("{:?}", numbers); // [423, 943, 192, 13, 52]
numbers.pop();
println!("{:?}", numbers); // [423, 943, 192, 13]
}

 Hàm push() nhận vào giá trị có kiểu dữ liệu giống với kiểu của vector, và tạo ra phần tử mới rồi đưa vào cuối vector, hàm pop() sẽ xóa phần tử cuối cùng trong vector. Lưu ý biến phải được khai báo mut.

Slice

Slice là một cơ chế cho phép chúng ta lấy mảng con từ một mảng cha. Cú pháp như sau:

&<tên_mảng>[m..n]

Cú pháp trên sẽ tạo ra một mảng con từ mảng cha với các phần tử từ m đến n 1

VÍ dụ:

fn main() {
let numbers = vec![423, 943, 192, 13, 52];
let sub_numbers = &numbers[1..3];
println!("{:?}", sub_numbers);
}

Trong đoạn code trên chúng ta có mảng numbers có 5 phần tử, sau đó chúng ta tạo ra mảng con sub_numbers lấy bằng phần tử thứ 1đến phần tử thứ 2.

[943, 192]

Rust – String

Trong phần này chúng ta sẽ tìm hiểu về một kiểu dữ liệu đặc biệt là kiểu String – chuỗi kí tự.

String trong Rust hơi khác so với trong các ngôn ngữ khác, các chuỗi trong Rust được mã hóa bằng định dạng UTF-8, các kí tự có thể có kiểu null, tuy nhiên bản thân chuỗi trong Rust không được kết thúc bằng kí tự null như trong C. Trong Rust có 2 loại chuỗi kí tự:

  • Chuỗi tĩnh: tức là các chuỗi có độ dài cố định, và các kí tự trong chuỗi không thể thay đổi. Kiểu dữ liệu của chuỗi này trong Rust là &str. Kí tự & cho biết biến thuộc kiểu này là biến tham chiếu (chúng ta sẽ tìm hiểu sau). Ví dụ:

fn main() {
let blog = "Pho Code";
let url: &str = "phocode.com";
}

Lưu ý là các chuỗi này là không thể thay đổi được, nếu bạn viết các câu lệnh như:

fn main() {
let mut url = "phocode.com";
url = "phocode.net";
}

Thì ở đây chúng ta đã cho biến url tham chiếu tới một chuỗi khác có giá trị là “phocode.net”, còn bản thân chuỗi “phocode.com” vẫn tồn tại, nhưng không được biến url tham chiếu tới nữa mà thôi.

  • Chuỗi động: tức là các chuỗi có thể thay đổi các kí tự và do đó có thể thay đổi độ dài của nó. Trong Rust thì kiểu dữ liệu của chuỗi này là String. Chúng ta có thể khởi tạo chuỗi String rỗng cũng được. Ví dụ:

fn main() {
let url = String::new();
}

Khi một biến String được tạo ra thì biến này sẽ được hệ điều hành cấp bộ nhớ trong máy tính, mỗi khi chúng ta muốn thay đổi độ dài của chuỗi trong String thì chúng ta phải xin cấp lại lượng bộ nhớ mới. Chẳng hạn như chúng ta muốn lưu trữ 10 kí tự thì chúng ta phải xin hệ điều hành cấp 10 byte, để xin cấp bộ nhớ thì chúng ta gọi hàm with_capacity() và truyền vào hàm số byte như sau:

fn main() {
let url = String::with_capacity(10);
}

Chuyển kiểu String thành &str

Chúng ta cũng có thể chuyển một biến thuộc kiểu String thành kiểu &str bằng hàm to_string() trong lớp String như sau:

fn main() {
let url = String::with_capacity(10);
let urlStr = url.to_string();
}

Ngoài hàm to_string() chúng ta cũng có thể sử dụng kí tự & như sau:

fn main() {
let mut url = String::new();
let urlStr = &url;
}

Sự khác nhau của kí tự & và hàm to_string() là kí tự & không tạo ra một chuỗi kí tự mới mà chỉ tham chiếu đến chuỗi kí tự gốc mà thôi, nhưng khi sử dụng thì biến tham chiếu kiểu này vẫn được coi là kiểu &str chứ không phải String, do đó kí tự & tiết kiệm bộ nhớ hơn so với hàm to_string().

Đưa giá trị vào chuỗi

Để đưa các kí tự vào String thì chúng ta có thể sử dụng hàm push() hoặc push_str() của String như sau:

fn main() {
let mut url = String::new();
url.push(‘p’);
url.push_str("hocode.com");
println!("{}", url);
}

Đối với push() thì chúng ta chỉ đưa vào từng kí tự thôi, còn push_str() thì có thể đưa nhiều kí tự hơn.

phocode.com

Lấy độ dài chuỗi

Chúng ta có thể dùng hàm len() trong String để lấy độ dài của một chuỗi, ví dụ:

fn main() {
let mut url = String::new();
url.push_str("https://phocode.com");
println!("URL Length: {}", url.len());
}

URL Length: 18

Tách từng kí tự

Chúng ta có thể lấy danh sách các kí tự của String thông qua hàm chars(), thực chất hàm này trả về một kiểu dữ liệu là Iterator (chúng ta sẽ tìm hiểu sau) và chúng ta có thể dùng vòng lặp for để lặp qua các kí tự của kiểu này, ví dụ:

fn main() {
let mut url = String::new();
url.push(‘p’);
url.push_str("hocode.com");

for character in url.chars() {
println!("{}", character);
}
}

Đoạn code trên sẽ cho ra kết quả như sau:

p
h
o
c
o
d
e
.
c
o
m

Tách chuỗi

Chúng ta cũng có thể tách một chuỗi thành các chuỗi nhỏ hơn bằng hàm split(), hàm này nhận vào kí tự phân tách và cũng trả về một biến Iterator, ví dụ:

fn main() {
let mut str = String::new();
str.push_str("Lorem ipsum dolor sit amet, sententiae suscipiantur mel an");
for sentence in str.split(" ") {
println!("{}", sentence);
}
}

Trong đoạn code trên, chúng ta gọi hàm split() và truyền vào chuỗi có 1 dấu cách, tức là chúng ta muốn tách chuỗi str thành các chuỗi nhỏ hơn thông qua dấu cách.

Lorem
ipsum
dolor
sit
amet,
sententiae
suscipiantur
mel
an

Thay thế chuỗi

Trong String có hàm replace() dùng để thay thế một chuỗi con thành một chuỗi khác.

fn main() {
let mut url = String::new();
url.push_str("Rust Programming");
println!("{}", url);

let url2 = url.replace("Programming", "Language");
println!("{}", url2);
}

Hàm replace() nhận vào 2 tham số, tham số đầu tiên là chuỗi cần tìm, tham số thứ 2 là chuỗi sẽ được thay thế. Trong đoạn code trên chúng ta thay thế tất cả chuỗi con ".com" thành ".net". Hàm này sẽ trả về một chuỗi mới chứ không thực hiện thay đổi các kí tự trong chuỗi gốc.

Rust Programming
Rust Language

Tham số hàm

Khi chúng ta viết hàm mà có nhận vào tham số là chuỗi thì chúng ta nên dùng kiểu &str thay cho String, lý do là vì dùng String sẽ tốn nhiều bộ nhớ hơn so với dùng &str vì &str chỉ là kiểu tham chiếu lại, không phải tạo mới chuỗi.

fn main() {    
let mut url = String::new();    
url.push_str("Lorem ipsum dolor sit amet, sententiae suscipiantur mel an");    
getStringLength( &url );
}

fn getStringLength(str: &str) {
println!("String Length: {}", str.len());
}

String Length: 58

Rust – Hàm

Điểm khởi đầu của bất cứ một chương trình Rust nào cũng là đoạn fn main(), đây là một hàm trong Rust, hàm được định nghĩa bởi từ khóa fn, phía sau là tên hàm rồi kèm thêm cặp dấu () và cuối cùng là một khối lệnh. Hàm là một nhóm các đoạn code thực hiện một công việc nào đó và có thể được gọi để sử dụng ở nhiều nơi khác nhau. Trong một chương trình Rust có thể viết bao nhiêu hàm cũng được.

Ví dụ:

fn main() {
say_hello();
hello_name("Pho Code");
}

fn say_hello() {
println!("Hello from sayHello()");
}

fn hello_name(name: &str) {
println!("Hello {}", name);
}

Đoạn code trên sẽ cho ra kết quả như sau:

Hello from sayHello()
Hello Pho Code

Trong đoạn code trên chúng ta viết 2 hàm là say_hello()hello_name(), chúng ta gọi các hàm này bằng cách ghi tên chúng ra, ở đây chúng ta gọi 2 hàm này trong hàm main() với các câu lệnh say_hello();hello_name("Pho Code");. Bên trong hàm say_hello() là câu lệnh println!() để in một chuỗi lên màn hình. Còn hàm hello_name() thì có nhận thêm cả tham số và in chuỗi có kèm tham số đó lên màn hình.

Tham số là các giá trị được gửi kèm khi gọi hàm, và các hàm có thể sử dụng các tham số này như các biến bình thường. Trong hàm hello_name(), tham só được đặt tên là name và có kiểu dữ liệu là &str. Các hàm có thể nhận bao nhiêu tham số cũng được.

Hàm trả giá trị

Các hàm cũng có thể thực hiện các công việc tính toán và cho ra kết quả chứ không đơn giản chỉ các thực hiện các công việc như println!().

Khi muốn một hàm cho ra kết quả nào thì chúng ta phải thêm kí tự -> <kiểu dữ liệu> sau tên hàm, và hàm sẽ cho ra kết quả là biểu thức cuối cùng có trong hàm đó, lưu ý biểu thức cuối cùng không được có dấu chấm phẩy. Ví dụ:

fn main() {
let n: i32 = 8;
println!("n * n = {}", power(n));
}

fn power(n: i32) -> i32 {
n * n
}

Trong đoạn code trên chúng ta khai báo hàm power() nhận vào một tham số có tên là n, có kiểu dữ liệu là i32, hàm này cho ra kết quả là một giá trị có kiểu dữ liệu i32 vì chúng ta đã khai báo -> i32, bên trong hàm có một câu biểu thức là n*n, và hàm này sẽ cho ra kết quả là giá trị của biểu thức n*n này.

Trong hàm main() chúng ta khai báo một biến n có giá trị là 8 và chúng ta truyền vào hàm power() biến n này, hàm power() sẽ cho ra kết quả là 64. Lưu ý là biến n trong hàm main() và tham số n trong hàm power() là 2 biến khác nhau mặc dù chúng trùng tên.

n * n = 64

Trong trường hợp chúng ta muốn trả về giá trị không phải ở cuối hàm thì chúng ta phải dùng câu lệnh return, phía sau câu lệnh return là một giá trị hoặc một câu lệnh, hoặc biểu thức có trả về giá trị. Ví dụ:

fn main() {
let n: i32 = 1_234_567_890;
println!("{}", power(n));
}

fn power(n: i32) -> i32 {
if n >= 1_000_000_000 {
println!("n is too big");
return -1;
}
n * n
}

Trong đoạn code trên chúng ta viết lại hàm power(), ở đây chúng ta kiểm tra xem nếu n lớn hơn 1 tỉ thì in ra một dòng thông báo và cho trả về kết quả là -1.

n is too big
-1

Hàm lồng nhau

Chúng ta cũng có thể định nghĩa một hàm bên trong một hàm khác, và các hàm đó được gọi là nested function – tức hàm lồng nhau. Ví dụ:

fn main() {
outer_fn();
}

fn outer_fn() {
println!("Enter outer_fn()");
nested_fn();

fn nested_fn() {
println!("Enter nested_fn()");
}
}

Enter outer_fn()
Enter nested_fn()

Rust – Rẽ nhánh và vòng lặp

Trong phần này chúng ta sẽ tìm hiểu về cú pháp rẽ nhánh và lặp trong Rust.

Rẽ nhánh

Cú pháp rẽ nhánh là các từ khóa if, if-elseif-else if-else, ví dụ:

fn main() {
let lose: bool = true;
if lose == true {
println!("You lose");
}

let age: i32 = 24;
if age < 18 {
println!("Young");
} else {
println!("Adult");
}

let grade: f32 = 8.2;
if grade >= 8.5 {
println!("Excellent");
} else if grade >= 6.5 {
println!("Good");
} else if grade >= 5.0 {
println!("Avarage");
} else {
println!("Bad");
}
}

Trong đoạn code trên chúng ta sử dụng 3 dạng của câu lệnh if. Câu lệnh if là câu lệnh rẽ nhánh, câu lệnh if sẽ nhận vào một biểu thức và một khối lệnh {}, nếu biểu thức cho ra kết quả là true thì đoạn code trong khối lệnh {} sẽ được thực thi.

You lose
Adult
Good

Trong câu lệnh if đầu tiên chúng ta kiểm tra xem biến lose có bằng giá trị true hay không, nếu có thì in chuỗi "You lose" ra. Trong câu lệnh if thứ 2 chúng ta kiểm tra biến age có bé hơn 18 hay không, nếu có thì in chuỗi "Youth", từ khóa else có tác dụng thực thi đoạn code trong trường hợp ngược lại là in chuỗi "Adult" ra. Nếu có nhiều trường hợp cần kiểm tra thì chúng ta dùng cú pháp if-else if-else else như trong ví dụ thứ 3.

Trong Rust chúng ta có thể dùng câu lệnh if để thực hiện việc gán giá trị cho biến như sau:

fn main() {
let age: i32 = 24;
let result = 
if age < 18 {
"Young"
} else {
"Adult"
};
println!("You are {}", result);
}

Giá trị được gán sẽ là giá trị của câu lệnh cuối cùng trong khối lệnh, ngoài ra cuối câu lệnh if phải có dấu chấm phẩy ;. Tất cả các khối lệnh đều phải trả về giá trị có cùng kiểu dữ liệu (chẳng hạn cùng là số hoặc cùng là chuỗi… không được một số một chuỗi).

You are Adult

Nếu bạn đã từng lập trình C++ hay Java thì bạn có thể dùng cú pháp trên thay cho câu lệnh (?:) cũng được:

let result = if age < 18 { "Young" } else { "Adult" };

Vòng lặp

Vòng lặp là câu lệnh thực thi các câu lệnh khác nhiều lần. Rust có 3 câu lệnh lặp là while, forloop. Ví dụ với while:

fn main() {   
let mut i: i32 = 0;
while i < 10 {
println!("i = {}", i); i = i + 1;
}
}

Sau từ khóa while là một biểu thức và một khối lệnh, câu lệnh này sẽ liên tục kiểm tra xem biểu thức có trả về giá trị true hoặc false, nếu true thì các câu lệnh trong khối lệnh phía sau sẽ được thực thi, cứ thế cho đến khi biểu thức cho ra kết quả false thì dừng lại.

i = 0
i = 1
i = 2
i = 3
i = 4
i = 5
i = 6
i = 7
i = 8
i = 9

Trong đoạn code trên chúng ta để ý bên trong khối lệnh có câu lệnh i = i + 1, nếu không có câu lệnh này thì biến i sẽ luôn luôn có giá trị là 0 và vòng lặp sẽ chạy mãi không dừng (mà người ta hay gọi là lặp vô tận).

Tuy nhiên bản thân Rust lại có câu lệnh lặp loop mang ý nghĩa lặp vô tận:

fn main() {
let mut i: i32 = 0;   
loop {
i = i + 1;
if i == 1993 {
println!("Continue…");
continue;
}

if i == 2017 {
println!("Break");
break;
}
println!("Increasing…");
}
}

Phía sau từ khóa loop không có biểu thức nào cả mà chỉ có một khối lệnh và khối lệnh này sẽ được chạy liên tục không bao giờ ngừng. Để có thể dừng vòng lặp này lại thì chúng ta phải dùng đến một câu lệnh có tên là break, trong ví dụ trên chúng ta cho biến i tăng dần, đến khi biến này có giá trị là 2017 thì chúng ta dừng bằng câu lệnh break. Ngoài ra bên trong còn có câu lệnh continue, khi gặp câu lệnh này thì vòng lặp hiện tại sẽ dừng và chuyển sang vòng lặp tiếp theo, các câu lệnh phía sau continue sẽ không được thực thi, trong đoạn code trên, số 1993 sẽ không được hiển thị và thay vào đó là chuỗi "Continue..." vì câu lệnh continue đã chặn câu lệnh in ở dưới.

i = 1
i = 2
i = 3
...
i = 1991
i = 1992
Continue...
i = 1994
i = 1995
...
i = 2015
i = 2016
Break

Chúng ta cũng có thể lồng các câu lệnh loop vào nhau và đặt tên cho mỗi vòng loop, khi đặt tên cho loop thì chúng ta có thể chỉ định câu lệnh break ngắt vòng loop nào bằng tên của loop, ví dụ:

fn main() {
let mut i: i32 = 0;
println!("Start outer loop");
‘outer: loop {
println!("Start inner loop");
‘inner: loop {
i = i + 1;
if i == 5 {
break ‘outer;
}
println!("i = {}", i);
}
}
println!("Exit outer loop!");
}

Trong đoạn code trên chúng ta có 2 vòng loop lồng nhau và được đặt tên là 'outer'inner. Cú pháp đặt tên loop là phải có dấu nháy đơn, sau đó là tên rồi dấu 2 chấm và đến từ khóa loop. Khi sử dụng break chúng ta cũng ghi tên loop giống như lúc đặt tên.

Start outer loop
Start inner loop
i = 1
i = 2
i = 3
i = 4
Exit outer loop!

Nếu muốn lặp trong một số lần nhất định thì chúng ta sử dụng câu lệnh for-in như ví dụ sau:

fn main() {
for n in 1..10 {
println!("{}^{} = {}", n, n, n * n);
}
}

Cú pháp như sau:

for <tên biến lặp> in <dãy> 

Trong đó tên biến lặp là do chúng ta tự đặt, ở đây chúng ta đặt tên là n, còn dãy là một kiểu dữ liệu dạng danh sách (chúng ta sẽ tìm hiểu thêm trong các bài sau), ở đây chúng ta ghi 1..10 tức là một dãy số từ 1 đến 10, và vòng lặp for sẽ lặp 10 lần, mỗi lần lặp thì n sẽ có giá trị từ 1 đến 10. Bên trong vòng lặp chúng ta in giá trị của nn^n.

1^1 = 1
2^2 = 4
3^3 = 9
4^4 = 16
5^5 = 25
6^6 = 36
7^7 = 49
8^8 = 64
9^9 = 81

Nếu chúng ta không cần đến biến n mà chỉ quan tâm đến số vòng lặp thì có thể đặt tên biến lặp là dấu gạch dưới:

for _ in 1..10

Rust – Biểu thức và câu lệnh

Một trong những thành phần của Rust là các câu biểu thức, đây là các câu lệnh thực hiện các công việc tính toán và cho ra các kết quả là các giá trị. Tuy nhiên biểu thức đứng một mình thì sẽ không có ý nghĩa nên thường sẽ đi với các câu lệnh.

Ví dụ:

fn main() {
let a = 2;
let b = 5;
let n = a + b;
}

Trong đoạn code trên, let a = 2, let b = 5 là các câu lệnh, còn a + b là biểu thức, kết quả của biểu thức a + b sẽ cho ra giá trị 5, sau đó giá trị này sẽ được gán cho biến n trong câu lệnh

let n = a + b

let n =       a + b;
^^^^^^^       ^^^^^^
Câu lệnh      Biểu thức

Thông thường thì biểu thức hay được dùng để trả kết quả trong các hàm (chúng ta sẽ tìm hiểu sau). Các biểu thức và câu lệnh phải được kết thúc bằng dấu chấm phẩy ở cuối câu, nếu không có thì Rust sẽ báo lỗi.

fn main() {
let a = 5;
let b = 10 // Lỗi thiếu dấu chấm phẩy
}

Một đoạn code chương trình sẽ bao gồm nhiều câu lệnh, từng câu sẽ được thực hiện từ trên xuống dưới.

Các câu lệnh gán nối nhau không được hỗ trợ trong Rust, ví dụ như:

let a = b = c = 3; // Lỗi

Cách duy nhất là ghi từng câu lệnh tiếp nối nhau, ví dụ:

fn main() {
let mut n = 0;
let mut m = 1;
let t = m; m = n; n = t;
println!("{} {} {}", m, n, t); // OK: 1 0 1
}

Các khối lệnh trong Rust cũng được coi là các câu lệnh và có trả về giá trị, giá trị được trả về sẽ là giá trị của biểu thức cuối cùng trong khối lệnh, ví dụ:

fn main() {
let n1 = {
let a = 2;
let b = 5;
a + b // Không cần dấu chấm phẩy, chỉ cần có biểu thức là được
};
println!("{}", n1);
}

Câu lệnh cuối cùng trong khối lệnh không cần có dấu chấm phẩy, Rust chỉ quan tâm xem nó có phải là biểu thức có trả về giá trị nào không.

7

Rust – Giá trị và kiểu dữ liệu

Các hằng số được khởi tạo luôn luôn có một giá trị, giá trị cũng có nhiều kiểu dữ liệu khác nhau, chẳng hạn giá trị 70 có kiểu số nguyên, giá trị 3.14 có kiểu số thực, kí tự z, q là kiểu kí tự… Bảng dưới đây tổng hợp các kiểu dữ liệu cơ bản của Rust:

TÊN Ý NGHĨA VÍ DỤ
u8 Số nguyên không dấu 8 bit 2017
u16 Số nguyên không dấu 16 bit 0x46
u32 Số nguyên không dấu 32 bit 0o106
u64 Số nguyên không dấu 64 bit 0b1000110
i8 Số nguyên có dấu 8 bit 1_000_000
i16 Số nguyên có dấu 16 bit -2823
i32 Số nguyên có dấu 32 bit
i64 Số nguyên có dấu 64 bit
f32 Số thực 32 bit 3.14
f64 Số thực 64 bit
bool Kiểu luận lí (đúng hoặc sai) true, false
char Kí tự a, b, c
&str Chuỗi kí tự theo định dạng Unicode UTF8 “phocode.com”

Ngoài ra trong Rust còn có một giá trị biểu diễn bằng cặp dấu ngoặc đơn như sau: (), giá trị này có nghĩa là “không có giá trị”. Lưu ý là giá trị () trong Rust không giống như kiểu null trong các ngôn ngữ khác.

Các kiểu số nguyên có thể được viết dưới nhiều dạng khác nhau, chẳng hạn:

  • 2017: dạng hệ thập phân (hệ 10)
  • 0x46: hệ thập lục phân (hệ 16)
  • 0o106: hệ bát phân (hệ 8)
  • 0b1000101: hệ nhị phân (hệ 2)

Và có thể thêm dấu gạch ngang để ngăn cách các số 0 nhằm mục đích dễ đọc: 1_000_000

Rust – Các thành phần của Rust – Phần 2

Trong phần này chúng ta tiếp tục tìm hiểu các thành phần cấu tạo nên Rust.

Biến

Trong phần trước chúng ta đã tìm hiểu về hằng số, giá trị được gán cho hằng số là không thể thay đổi được, để có thể thay đổi được giá trị thì chúng ta gán cho các biến, để khai báo một biến thì chúng ta dùng từ khóa let, tiếp theo là dấu bằng =, rồi đến giá trị và dấu chấm phẩy, ví dụ:

fn main() {
let name = "phocode.com"; // Gán chuỗi "phocode.com" cho biến name
println!("Blog URL: {}", name);
}

Đoạn code trên sẽ không báo lỗi nhưng Rust sẽ in câu cảnh báo biến name được khai báo nhưng không được sử dụng (nên sẽ gây lãng phí).

warning: unused variable: `name`

Chúng ta có thể loại bỏ câu thông báo bằng cách đặt tên biến có dấu gạch ngang đầu tiên, Rust sẽ hiểu là biến này được khai báo nhưng có thể không được sử dụng, ví dụ:

let _name = 5;

Khác với khai báo hằng, đối với khai báo biến thì chúng ta không cần chỉ ra kiểu dữ liệu, thay vào đó Rust sẽ nhìn vào giá trị và tự gấn kiểu dữ liệu phù hợp cho biến, chẳng hạn như ở trên Rust sẽ cho biến name kiểu dữ liệu là &str.

Giá trị bất biến

Giá trị bất biến là các giá trị không thể thay đổi được. Tức là đoạn code sau sẽ báo lỗi:

fn main() {
let num = 1;
num = 2017; // Báo lỗi
}

Khi chúng ta khai báo num có giá trị 1, biến trong Rust có giá trị luôn luôn bất biến, chúng ta không thể thay đổi được giá trị.

error[E0384]: re-assignment of immutable variable `num`

Nếu chúng ta muốn biến có thể thay đổi giá trị được thì chúng ta phải khai báo thêm từ khóa mut phía trước tên biến như sau:

fn main() {
let mut num = 1;
num = 2017; // OK
}

Một điều nữa là tuy chúng ta không cần chỉ định kiểu dữ liệu khi khai báo biến vì Rust có thể tự nội suy kiểu, nhưng chúng ta cũng nên khai báo kiểu, chẳng hạn như trong trường hợp chúng ta chỉ khai báo tên biến chứ không gán dữ liệu thì Rust không thể nào biết được biến này nên dùng kiểu dữ liệu gì, ví dụ:

fn main() {
let num1; // Lỗi error[E0282]: type annotations needed
let num2: i32; // OK
let num3: i32 = 2017; // OK
}

Vậy cú pháp đầy đủ để khai báo và gán giá trị cho biến là:

let <tên biến>: <tên kiểu dữ liệu> = <giá trị>;

Phạm vi hoạt động của biến

Tất cả các biến đều có phạm vi hoạt động, phạm vi hoạt động có thể hiểu là khu vực mà chúng ta có thể thao tác với các biến, thông thường là nằm bên trong một cặp dấu {...}, chẳng hạn như với ví dụ phía trên là các biến num1, num2num3 có phạm vi hoạt động trong hàm main(). Ở ngoài dấu {} thì các biến này không tồn tại.

Chúng ta cũng có thể khai báo các cặp dấu {...} lồng nhau, ví dụ:

fn main() {    
let num1: i32 = 2000;   
         
{
let num2: i32 = 17;
println!("num1 + num2 = {}", num1 + num2);
}
}

Trong đoạn code trên chúng ta có phạm vi của hàm main(), trong này khai báo biến num1, sau đó chúng ta lồng thêm một cặp {} nữa, bên trong cặp này chúng ta khai báo biến num2 và in ra giá trị của num1 + num2.

num1 + num2 = 2017

Biến num2 chỉ tồn tại bên trong cặp dấu {} được lồng bên trong hàm main(), tức là nếu chúng ta sử dụng biến num2 như sau thì sẽ bị lỗi:

fn main() {    
let num1: i32 = 2000;             
{
let num2: i32 = 17;
}
println!("num1 + num2 = {}", num1 + num2);
}

Trong đoạn code trên chúng ta sử dụng num2 ở hàm main() nhưng num2 không được khai báo trong hàm main() nên biến này không tồn tại và do đó Rust sẽ báo lỗi.

error[E0425]: cannot find value `num2` in this scope

Typesafe

Rust là một ngôn ngữ typesafe, nói đơn giản thì biến nào đã được quy định kiểu dữ liệu nào thì luôn luôn phải được gán giá trị có kiểu dữ liệu đó, không thể thay đổi được (khác với các ngôn ngữ thông dịch như Python, Javascript…). Đoạn code sau sẽ báo lỗi:

fn main() {    
let num1: i32 = 10;
num1 = "phocode.com"; // Lỗi error[E0308]: mismatched types
}

Tuy nhiên Rust lại cho phép chúng ta tái định nghĩa biến (trùng tên) bằng từ khóa let, và biến trùng tên đã được định nghĩa trước đó sẽ được giải phóng khỏi bộ nhớ, ví dụ:

fn main() {    
let num1: i32 = 10;
let num1 = "phocode.com";
}

Có một điểm lưu ý là trong Rust thì toán tử + không thể dùng được với kiểu str để nối chuỗi, đoạn code sau sẽ báo lỗi:

fn main() {    
let str1 = "phocode";
let str2 = ".com";
let str3 = str1 + str2;
println!("{}", str3);
}

error[E0369]: binary operation `+` cannot be applied to type `&str`

Tuy nhiên Rust lại cho chúng ta một hàm có tên to_string() để chuyển sang một kiểu dữ liệu khác là String (khác với str) và chúng ta lại có thể thực hiện nối chuỗi với kiểu này:

fn main() {
let str1 = "phocode";
let str2 = ".com";
let str3 = str1.to_string() + str2;
println!("{}", str3);
}

phocode.com

Ép kiểu

Giả sử chúng ta có đoạn code sau:

fn main() {
let _num1: i32 = 10;
let mut _num2: u32 = 0;
_num2 = _num1; // Lỗi error[E0308]: mismatched types
}

Chúng ta định nghĩa 2 biến _num1 có kiểu i32_num2 có kiểu u32, cả 2 kiểu này đều là kiểu số nguyên (chúng ta sẽ tìm hiểu về kiểu dữ liệu sau), tuy nhiên chúng khác nhau về mặt độ lớn, do đó chúng ta cũng không thể nào gán giá trị của 2 biến này cho nhau được.

Rust cho phép chúng ta ép kiểu đối với các kiểu dữ liệu tương đồng với nhau bằng cách dùng từ khóa as, ví dụ:

fn main() {
let _num1: i32 = 10;
let mut _num2: u32 = 0;
_num2 = _num1 as u32;
}

Ở đây Rust sẽ chuyển đổi kiểu dữ liệu của giá trị trong biến _num1 thành u32 trước rồi mới gán vào biến _num2.

Chỉ có các kiểu dữ liệu tương đồng với nhau mới có thể ép kiểu được chẳng hạn như i32u32, còn những kiểu không giống nhau như i32str thì không thể nào “ép” được.

Alias

Alias là tính năng cho phép chúng ta đặt tên mới cho các kiểu dữ liệu, bởi vì đôi khi tên các kiểu dữ liệu trong Rust khá khó hiểu, để đặt tên alias thì chúng ta khai báo với từ khóa type, ví dụ:

type PhoCodeInteger = i32;

fn main() {
let _n: PhoCodeInteger = 9999;
}

Trong đoạn code trên chúng ta định nghĩa một tên mới cho kiểu i32PhoCodeInteger.

Rust – Các thành phần của Rust – Phần 1

Trong phần này chúng ta sẽ tìm hiểu các thành phần cấu tạo nên một chương trình Rust.

Bình luận – Comment

Bình luận là các câu chữ chỉ có tác dụng giải thích code chứ không được biên dịch và chạy. Cú pháp bình luận trong Rust giống hoàn toàn với trong C:

  • Bình luận 1 dòng: thêm dấu // vào trước dòng đó
  • Bình luận nhiều dòng: bọc câu bình luận trong cặp dấu /* */

// Bình luận một dòng
fn main() {
/*
Bình luận
nhiều dòng
*/
println!("Hello world");
}

Ngoài ra Rust còn có cú pháp 3 dấu chéo /// dùng để bình luận theo dạng tài liệu, thường dùng khi số lượng câu bình luận rất lớn, có thể kéo theo thành các đoạn văn. Cách dùng cũng tương tự:

/// Bình luận tài liệu
fn main() {
// ..
}

Trình rustdoc của Rust có thể biên dịch các câu bình luận dạng tài liệu sang các dạng văn bản thường dùng.

Hằng số

Hằng số là các biến (chúng ta sẽ tìm hiểu về biến sau) chứa giá trị không thể thay đổi được. Chẳng hạn như tên chương trình, số PI…v.v Hằng có thể được truy xuất ở bất cứ đâu. Để khai báo một biến là hằng số thì chúng ta khai báo từ khóa static như sau:

static PI: f32 = 3.14;
static NAME: &’static str = "Constant";
fn main() {

}

Khi khai báo hằng, chúng ta nên đặt tên chúng viết HOA tất cả các kí tự, sau đó là dấu 2 chấm : và tên kiểu dữ liệu (chúng ta sẽ tìm hiểu kiểu dữ liệu sau), ở đây kiểu dữ liệu là f32 và str, rồi đến dấu bằng = và tiếp theo là giá trị (ở đây giá trị là 3.14 và "Constant")

Ở đây chúng ta chưa cần quan tâm đến đoạn &'static trong đoạn code trên, đây là cú pháp phải có trong Rust vì Rust là một ngôn ngữ lập trình cấp thấp, mọi thứ phải được khai báo rõ ràng. Ở đây mình chỉ giải thích đơn giản là &'static sẽ quy định cách Rust quản lý bộ nhớ cho hằng số đó.

Đoạn code trên sẽ biên dịch thành công nhưng Rust sẽ in một số câu thông báo với mục đích báo cho chúng ta biết là các hằng số được khai báo nhưng không được sử dụng ở bất kì chỗ nào, do đó gây lãng phí bộ nhớ:

warning: static item is never used: `PI`
...
warning: static item is never used: `NAME`
...

Ngoài từ khóa static thì chúng ta có thể dùng thêm từ khóa const nữa, chẳng hạn:

const PI: f32 = 3.14;

Điểm khác của const là các hằng số const có phạm vi truy xuất nhỏ hơn so với hằng số static.

Truyền giá trị vào chuỗi

Chúng ta có thể in giá trị của các hằng số bằng cách đặt bên trong các cặp dấu {} bên trong các chuỗi, các giá trị của hằng số sẽ được truyền vào các cặp dấu {} này. Ví dụ:

static PI: f32 = 3.14;
static NAME: &’static str = "Constant";
fn main() {
println!("Program Name: {}", NAME);
println!("PI = {}", PI);
}

Đoạn code trên sẽ cho ra kết quả như sau:

Program Name: Constant
PI = 3.14

Ngoài các hằng số mà chúng ta tự đặt, trong Rust cũng có rất nhiều hằng số nằm trong các thư viện có sẵn, trong đó có cả hằng số PI, để sử dụng hằng số PI của Rust thì chúng ta phải sử dụng bộ thư viện của Rust như sau:

use std::f32::consts;

static PI: f32 = 3.14;
static NAME: &’static str = "Constant";

fn main() {
println!("Program Name: {}", NAME);
println!("PI = {}", PI);
println!("Rust’s PI = {}", consts::PI);
}

Dòng use std::f32::consts; có nghĩa là chúng ta sử dụng module std::f32::consts, bên trong module này có một hằng số là PI, chúng ta gọi đến PI của module này bằng cách ghi consts::PI.

Chúng ta cũng có thể đặt tên trước cho các cặp dấu {} nếu trong hàm macro println!() có nhiều cặp dấu, ví dụ:

fn main() {
println!("Today is {month} {day}, {year}", year=2017, month="September", day=28);
}

Trong đoạn code trên, các cặp dấu {} sẽ lấy giá trị của các tham số có tên tương ứng, đoạn code sẽ cho kết quả như sau:

Today is September 28, 2017

Ngoài đặt tên cho các cặp dấu {} thì chúng ta có thể truyền vào một số từ khóa định dạng như sau:

static NUMBER_1: i32 = 2017;
static NUMBER_2: f32 = 1.234e+10;

fn main() {
println!("Octal: {:o}", NUMBER_1); // Hệ bát phân
println!("Lower Hexa: {:x}", NUMBER_1); // Hệ thập lục phân
println!("Upper Hexa: {:X}", NUMBER_1); // Hệ thập lục phân
println!("Pointer: {:p}", &NUMBER_1); // Địa chỉ con trỏ
println!("Binary: {:b}", NUMBER_1); // Hệ nhị phân
println!("Lower Exponential: {:e}", NUMBER_2); // Số mũ
println!("Upper Exponential: {:E}", NUMBER_2); // Số mũ
println!("Debugging: {:?}", NUMBER_1); // Giá trị
}

Chúng ta truyền vào dấu 2 chấm : kèm theo tên của kiểu định dạng.  Đoạn code trên sẽ in ra kết quả như sau:

Octal: 3741
Lower Hexa: 7e1
Upper Hexa: 7E1
Pointer: 0x7ff696fff300
Binary: 11111100001
Lower Exponential: 1.234e10
Upper Exponential: 1.234E10
Debugging: 2017

Rust – Cargo

Cargo là phần mềm quản lý thư viện của Rust, nếu bạn đã từng làm việc với các ngôn ngữ hoặc công nghệ như Python, Ruby, Node.js… thì Cargo cũng tương tự như Bundler, npm, pub, pip… trong các ngôn ngữ hay công nghệ trên. Khi chúng ta cài đặt Rust thì trình cài đặt của Rust cũng đã cài đặt sẵn luôn phần mềm Cargo cho chúng ta.

Cargo có các chức năng sau:

  • Tạo và quản lý project
  • Biên dịch project
  • Chạy project
  • Chạy unit test
  • Cài đặt các thư viện cần thiết

Tài liệu hướng dẫn sử dụng Cargo nằm tại địa chỉ: http://doc.crates.io/guide.html

Ví dụ

Chúng ta sẽ tìm hiểu một số lệnh thường dùng trong Cargo.

Tạo project Rust

Để tạo project Rust bằng Cargo thì chúng ta chạy lệnh cargo new <tên project> --bin, ví dụ:

Tham số --bin cho Cargo biết chúng ta muốn tạo một project có thể chạy được, tức là có thể biên dịch thành file .exe trên Windows, nếu không đưa tham số này vào thì Cargo sẽ tạo một project thư viện, tức là code viết ra sẽ biên dịch thành các file thư viện.

Trong ví dụ trên chúng ta tạo project với tên hello, Cargo sẽ tạo một thư mục có tên hello và tạo các file và thư mục cần thiết trong đó, trong số đó có một thư mục tên là src chứa một file Rust có tên main.rs có nội dung như sau:

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

Đây chỉ là đoạn code in chuỗi “Hello, world!” tương tự như trong bài trước chúng ta đã làm.

Bên ngoài thư mục gốc của project có một file tên Cargo.toml, đây là file chứa code cấu hình project được viết dưới định dạng TOML (xem thêm tại https://github.com/toml-lang/toml) có nội dung như sau:

[package]
name = "hello"
version = "0.1.0"
authors = ["tên <địa chỉ email>"]

[dependencies]

Chúng ta sẽ tìm hiểu về file này sau.

Biên dịch project

Để biên dịch project thì chúng ta chạy lệnh cargo build:

Cargo sẽ biên dịch thành file hello.exe và đặt trong một thư mục mới có tên target/debug.

Chạy project

Để chạy project thì chúng ta chỉ cần chạy lệnh cargo run, cargo sẽ chạy file hello.exe trên và in một số câu thông báo, chẳng hạn như thời gian chạy…

Ngoài ra khi biên dịch thì Cargo cũng sẽ tạo một file có tên Cargo-lock nằm ở thư mục gốc của project, mục đích của file này là để theo dõi các thư viện được dùng trong project, hiện tại file này có nội dung như sau:

[root]
name = "hello"
version = "0.1.0"

Lý do là bởi vì các thư viện luôn luôn được phát triển và được cập nhật phiên bản mới, project sẽ luôn chỉ dùng phiên bản được chỉ định trong file Cargo-lock này nhằm đảm bảo quá trình biên dịch đươc ổn định. 

Rust – Hello World

Trong phần này chúng ta sẽ tìm hiểu cách viết một chương trình bằng Rust. File code Rust có phần mở rộng là .rs nên đầu tiên chúng ta tạo một file có tên hello_world.rs và gõ vào đoạn code sau:

fn main() {
println!("Hello World");
}

Tiếp theo chúng ta biên dịch file hello_world.rs này bằng cách mở command prompt (cmd) lên, chuyển thư mục hiện hành (bằng lệnh cd) đến thư mục chứa file hello_world.rs và gõ lệnh rustc hello_world.rs để biên dịch:

Lệnh rustc sẽ biên dịch và nếu biên dịch thành công thì tạo ra 2 file có tên hello_world.pdbhello_world.exe trên Windows hoặc hello_world trên Linux. Nếu có lỗi thì Rust sẽ báo lỗi biên dịch và không tạo 2 file trên.

Sau đó chúng ta có thể chạy bằng cách gõ tên file hello_world hoặc ./hello_world (trên Linux) là được:

Chúng ta sẽ nhận được kết quả là một dòng chữ “Hello World”. 

Nếu bạn đã từng làm việc với C/Java/C# thì bạn sẽ thấy đoạn code trên khá quen thuộc. Một chương trình viết bằng Rust bắt đầu chạy ở một điểm xuất phát có tên là hàm main(), tất cả mọi thứ nằm trong cặp dấu {} phía sau fn main() sẽ chạy trước:

fn main() {

}

Chúng ta sẽ tìm hiểu thêm về hàm sau.

Bên trong hàm main() này chỉ có một dòng code duy nhất là println!("Hello World"); chuỗi “Hello World” được đặt bên trong một macro có tên println!(); lưu ý dấu chấm than ! ngay phía sau println cho biết đây là một macro chứ không phải một hàm, chúng ta sẽ tìm hiểu về macro sau, macro printlln!() có chức năng in chuỗi nằm bên trong nó ra màn hình console.