1 tháng học Rust - Những điều cơ bản của ngôn ngữ lập trình Rust

1 tháng học Rust - Những điều cơ bản của ngôn ngữ lập trình Rust

Vấn đề

Một câu chuyện muôn thuở, cái cơ bản lúc nào cũng dễ học, cho đến khi chúng ta bắt tay vào áp dụng trong thực tế thì nhiều vấn đề phát sinh. Tại sao trong tài liệu chỉ hướng dẫn vài ba câu lệnh đơn giản, nhưng khi đọc một dự án thực tế thì lại…toàn cú pháp đến từ sao hỏa vậy chứ? @@.

banner

Thật ra là tài liệu phải hướng dẫn chúng ta từ cơ bản đến nâng cao chứ không tự nhiên giới thiệu ngay những cú pháp khó hiểu làm gì. Nếu nắm chắc được kiến thức cơ bản, chúng ta vẫn có thể viết được chương trình đầu tiên, sau này khi trở nên quá quen rồi hoặc tiếp xúc thêm nhiều dự án trong thực tế, tự nhiên cảm giác làm quen với những cái mới lại trở nên nhanh hơn. Vì thế kiến thức cơ bản là nền móng vững chắc cho chúng ta trước khi muốn làm gì đó lớn lao hơn.

Tuần vừa rồi, tôi đã lướt qua từ đầu đến cuối tài liệu của Rust, sau đó tập trung vào cú pháp cùng với khái niệm cơ bản đầu tiên.

Cài đặt

Rust hỗ trợ đa nền tảng. Bạn có thể cài đặt Rust thông qua câu lệnh duy nhất nếu đang sử dụng macOS, Linux.

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

Hoặc tham khảo thêm các cách khác tại Other Rust Installation Methods.

Chương trình Hello world

Một chương trình in ra dòng chữ "Hello world!" trong Rust rất đơn giản.

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

println! là một dạng macro, hiểu đơn giản nó là dạng viết tắt của một hàm, trong trường hợp này, nếu không sử dụng macro, có thể bạn sẽ phải viết một đoạn mã dài hơn. Trong Rust, bạn sẽ làm quen nhiều hơn với các macro khác để đẩy nhanh tốc độ viết mã. println! có chức năng tương tự như console.log trong JS, in mọi thứ ra màn hình console.

Cú pháp để "build" chương trình thành dạng binary.

$ rustc main.rs

Sau đó có thể chạy chương trình bằng cách

$ ./main
Hello world!

Để chạy được trương trình Rust, trước tiên chúng ta cần biên dịch chúng thành mã máy, không giống như JS, nơi chương trình được thực thi ngay lập tức mà không cần qua bước dịch.

Rust cũng cung cấp cơ chế chạy mã ngay lập tức, nhưng trong bài viết này tôi sẽ tạm thời chưa nói đến.

Biến, kiểu dữ liệu và hàm

Khai báo một biến trong Rust bằng từ khóa let hoặc const.

let a: i32 = 1;
# hoặc tự phát hiện kiểu dữ liệu
let a = 1; // a: i32

Vì là một ngôn ngữ định kiểu (static typed) nên biến trong Rust cần được khai báo kiểu khi khởi tạo, hoặc Rust tự động nhận ra kiểu cho lần khai báo đầu tiên.

Mặc định biến của Rust không thể thay đổi giá trị, sử dụng từ khóa mut để khai báo các biến có thể thay đổi. Thật thú vị đúng không!

let mut a: i32 = 1;
a = 2;

Hàm số trong Rust phải là một giá trị được không thay đổi được. Rust không cho phép khai báo hàm số có giá trị thay đổi theo biến.

const A: i32 = 1;
# hoặc
const A: i32 = 1 + 2;

# tính toán từ một biến sẽ bị lỗi
const A: i32 = a + 1; // lỗi

Các kiểu dữ liệu cơ bản của Rust bao gồm char, string, integer, float, boolean… và các cấu trúc dữ liệu như Tuple, Array… Để biết thêm chi tiết, bạn đọc có thể xem tại Data Types.

Hàm số trong Rust có thể trả về hoặc không trả về dữ liệu, nếu có dữ liệu, đồng nghĩa với việc bạn cần phải khai báo cả kiểu dữ liệu trả về.

# Hàm không trả về dữ liệu
fn another_function() {
    println!("Another function.");
}

# Hàm trả về dữ liệu
fn five() -> i32 {
    return 5;
}
# Hoặc viết ngắn gọn
fn five() -> i32 {
    5
}

# Hàm có tham số
fn five(n: i32) -> i32 {
    n
}

Kết thúc dòng trong Rust bắt buộc phải có dấu ;, tuy nhiên nếu không sử dụng return thì kết thúc hàm giá trị trả về là dòng không có dấu ;.

if…else và vòng lặp

if...else trong Rust đóng vai trò như là một biểu thức, có nghĩa là nó có thể trả về giá trị hoặc là không, giống như hàm.

# if...else không trả về giá trị
let number = 3;

if number < 5 {
    println!("condition was true");
} else {
    println!("condition was false");
}

# if...else trả về giá trị và có thể gán vào biến
let condition = true;
let number = if condition { 5 } else { 6 };

Khi có giá trị trả về, đồng nghĩa với việc bạn phải luôn luôn có else và kiểu dữ liệu trả về phải giống nhau.

Không giống như JS, biểu thức đánh giá trong if có thể là dữ liệu hoặc là các phép logic, trong Rust thì nó bắt buộc phải là biểu thức logic, có nghĩa là true/false.

Cú pháp cơ bản để lặp là sử dụng loop.

let mut counter = 0;

let result = loop {
    counter += 1;

    if counter == 10 {
        break counter * 2;
    }
};

Ngoài ra cũng có while.

let mut number = 3;

while number != 0 {
    println!("{number}!");

    number -= 1;
}

Và cả for nữa.

let a = [10, 20, 30, 40, 50];

for element in a {
    println!("the value is: {element}");
}

for có thể được sử dụng trong bất cứ kiểu dữ liệu nào "có thể lặp" tương tự như JS. Ví dụ như:

for number in (1..4) {
    println!("{number}!");
}

(1..4) là một cấu trúc mảng u32, vì thế for sẽ lặp qua từng phần tử và thực hiện các câu lệnh bên trong dấu móc.

Structs

Struct là một cấu trúc dữ liệu với các thuộc tính và phương thức kèm theo. Có thể hình dung struct giống như là Class trong lập trình hướng đối tượng.

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

let user = User {
    active: true,
    username: String::from("hoaitx"),
    email: String::from("[email protected]"),
    sign_in_count: 1,
};

Ngoài ra struct có cú pháp gần giống như Spread syntax như JS để ghi đè dữ liệu.

let user2 = {
    username: String::from("2coffee"),
    email: String::from("[email protected]"),
    ..user
};

Để khai báo phương thức cho struct, sử dụng cú pháp impl.

impl User {
    fn sayName(&self) -> String {
        self.username
    }

    fn sayEmail(&self) -> String {
        self.email
    }
}

let user = User {
    active: true,
    username: String::from("hoaitx"),
    email: String::from("[email protected]"),
    sign_in_count: 1,
};

println!("user say name {}", user.sayName);

self giống như this trong Class, sử dụng self để trỏ đến các thuộc tính và phương thức trong chính nó.

Khi không có self trong phương thức, các hàm đó được gọi là hàm liên kết. Chúng được sử dụng để tạo ra một phiên bản được xác định trước của struct.

impl User {
    fn default(size: u32) -> User {
        Self {
            active: false,
            username: String::from(""),
            email: String::from(""),
            sign_in_count: 0,
        }
    }
}

let defaultUser = User::default();

Điều này làm chúng ta liên tưởng đến phương thức String::from().

Giải thích một chút về bộ nhớ trong Rust

Có rất nhiều điều mới ở đây. Ví dụ cú pháp String::from() để tạo ra một chuỗi trong Rust, tại sao lại phải phức tạp như thế? Trong khi JS hoặc nhiều ngôn ngữ khác chỉ cần bỏ chúng vào dấu nháy đơn ('') hoặc nháy kép (""). Để làm rõ, chúng ta cần làm quen với bộ nhớ stack và heap trong Rust.

Bộ nhớ stack dùng để lưu giá trị của biến có kích thước cố định ví dụ như char, str, int32, int64… còn heap thì ngược lại, kích thước dữ liệu phụ thuộc vào thời gian chạy (runtime). Dữ liệu trong dấu nháy kép ("") được xác định là char Dữ liệu trong dấu nháy kép ("") là str, còn khi sử dụng String:from() nó là String và được lưu trữ trong heap.

Thế tóm lại là str với String chỉ khác nhau ở chỗ lưu trữ? Đúng vậy, nhưng sự khác biệt rõ ràng hơn ở cơ chế quản lý bộ nhớ của Rust. Rust không có bộ thu gom rác tự động cho nên nó phải sinh ra cơ chế quản lý bộ nhớ cho các biến, hay nói cách khác là vòng đời của biến trong chương trình.

Tất cả các biến trong Rust theo mặc định là bất biến!? Đúng vậy, nghĩa là chúng không thể thay đổi được sau khi khai báo. Đừng lo lắng, Rust vẫn cho phép chúng ta khai báo biến có thể thay đổi được, đổi lại là một cơ chế quản lý bộ nhớ "có 102". Chúng ta tiếp tục tìm hiểu nó trong bài viết tiếp theo nhé!

hoặc
* Bản tin tổng hợp được gửi mỗi 1-2 tuần, huỷ bất cứ lúc nào.
Author

Xin chào, tôi tên là Hoài - một anh Dev kể chuyện bằng cách viết ✍️ và làm sản phẩm 🚀. Với nhiều năm kinh nghiệm lập trình, tôi đã đóng góp một phần công sức cho nhiều sản phẩm mang lại giá trị cho người dùng tại nơi đang làm việc, cũng như cho chính bản thân. Sở thích của tôi là đọc, viết, nghiên cứu... Tôi tạo ra trang Blog này với sứ mệnh mang đến những bài viết chất lượng cho độc giả của 2coffee.dev.Hãy theo dõi tôi qua các kênh LinkedIn, Facebook, Instagram, Telegram.

Bạn thấy bài viết này có ích?
Không

Bình luận (1)

Avatar
Ẩn danh1 năm trước
''Dữ liệu trong dấu nháy kép ("") được xác định là char''. theo mình biết thì char là kiểu dữ liệu 4 bytes và trong rust nó hỗ trợ unicode. Còn char mà bạn nhắc đến trong bài viết được đặt trong "" thì nó là str
Trả lời
Avatar
Xuân Hoài Tống1 năm trước
Cảm ơn bạn đã chỉ ra sai sót, mình đã cập nhật lại bài viết :D