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

Tin ngắn hàng ngày dành cho bạn
  • Bắt đầu kỳ nghỉ tết rồi nên mình cũng không đăng bài nữa. Hẹn gặp lại các bạn qua tết nha 😁

    » Xem thêm
  • Tiếp tục về jj. Đang thắc mắc là nó mới thế liệu có ai làm mấy phần mềm dạng GUI cho dễ nhìn chưa. Kiểu giống như git thì có quá nhiều rồi không đếm xuể.

    May quá, tác giả tổng hợp lại luôn rồi Community-built tools around Jujutsu 🥳

    » Xem thêm
  • Turso thông báo rằng họ đang viết lại SQLite bằng Rust. Thế là lại có thêm một bằng chứng nữa cũng cố cho câu nói Rust đang "tái định nghĩa" lại nhiều thứ.

    Nhưng nguyên nhân sâu xa mới thú vị. Tại sao họ lại làm vậy? Ai cũng biết SQLite là nguồn mở, ai cũng có thể tạo bản sao (fork) để chỉnh sửa lại theo ý mình. Lẽ nào nhóm của Turso không thích hoặc không tin vào C - vốn là ngôn ngữ dùng để cấu thành SQLite.

    Mình xin kể chuyện một chút. Turso là một bên cung cấp dịch vụ máy chủ cơ sở dữ liệu dựa trên SQLite, họ đã thực hiện một vài tùy chỉnh trên bản sao của SQLite để phục vụ cho mục đích của mình, gọi nó là libSQL. Họ "hào phóng" cho cộng đồng đóng góp thoải mái.

    Quay trở lại SQLite là mã nguồn mở chứ không phải là đóng góp mở. Chỉ có một nhóm người đứng đằng sau duy trì mã nguồn này, và họ không tiếp nhận yêu cầu kéo (pull request) từ những người khác. Đồng nghĩa mọi thay đổi hoặc tính năng đều là của nhóm người này tạo ra. Có vẻ như SQLite rất phổ biến nhưng cộng đồng không thể làm điều mà họ muốn là đóng góp cho sự phát triển của nó.

    Chúng ta biết rằng hầu hết ứng dụng mã nguồn mở thường đi kèm với một thư mục "tests" với các bài kiểm tra rất nghiêm ngặt. Điều đó giúp cho sự cộng tác trong phát triển trở nên dễ dàng hơn. Nếu muốn chỉnh sửa hoặc thêm một tính năng mới, trước hết bạn cần phải đảm bảo sự thay đổi vượt qua được tất cả bài kiểm tra. Nhiều thông tin cho rằng SQLite không công khai bộ kiểm tra này. Điều này vô tình gây khó khăn cho những ai muốn chỉnh sửa mã nguồn. Vì họ không chắc chắn rằng liệu triển khai mới của mình có phù hợp với những tính năng cũ hay không.

    tursodatabase/limbo là dự án viết lại SQLite bằng Rust đã nhắc đến ở đầu bài. Họ nói rằng nó hoàn toàn tương thích với SQLite và nguồn mở hoàn toàn. limbo đang trong giai đoạn hoàn thiện. Chúng ta hãy chờ xem kết quả trong tương lai thế nào nhé. Bài viết chi tiết tại Introducing Limbo: A complete rewrite of SQLite in Rust.

    » Xem thêm

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ứ? @@.

Lý thuyết và thực tế

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é!

Cao cấp
Hello

5 bài học sâu sắc

Mỗi sản phẩm đi kèm với những câu chuyện. Thành công của người khác là nguồn cảm hứng cho nhiều người theo sau. 5 bài học rút ra được đã thay đổi con người tôi mãi mãi. Còn bạn? Hãy bấm vào ngay!

Mỗi sản phẩm đi kèm với những câu chuyện. Thành công của người khác là nguồn cảm hứng cho nhiều người theo sau. 5 bài học rút ra được đã thay đổi con người tôi mãi mãi. Còn bạn? Hãy bấm vào ngay!

Xem tất cả

Đăng ký nhận thông báo bài viết mới

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.

Bình luận (1)

Nội dung bình luận...
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
Bấm hoặc cuộn mạnh để sang bài mới