Hàm - Functions

Các hàm khá phổ biến trong Rust. Bạn đã gặp một trong những hàm quan quan trọng nhất của ngôn ngữ: hàm main, vốn là điểm bắt đầu cho nhiều chương trình. Bạn cũng đã thấy từ khóa fn, vốn cho phép bạn khai báo các hàm mới.

Mã Rust dùng snake case như quy ước đặt tên hàm và biến, trong đó tất cả các ký tự được viết chữ thường, các từ cách nhau bởi dấu gạch dưới. Đây là một chương trình chứa một ví dụ về cách khai báo hàm:

Filename: src/main.rs

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

    another_function();
}

fn another_function() {
    println!("Another function.");
}

Chúng ta định nghĩa một hàm trong Rust bằng cách dùng từ khóa fn theo sau bởi tên hàm và một tập các dấu ngoặc tròn. Cặp dấu ngoặc nhọn theo sau đó chỉ ra vị trí bắt đầu và kết thúc của thân hàm.

Chúng ta có thể gọi bất kỳ hàm nào ta đã định nghĩa bằng cách gọi tên hàm, theo sau bởi cặp dấu ngoặc tròn. Vì another_function được định nghĩa trong chương trình, nó có thể được gọi từ hàm main. Lưu ý là chúng ta định nghĩa another_function sau hàm main trong file mã nguồn; nhưng tất nhiên ta cũng có thể định nghĩa nó trước hàm main. Rust không quan tâm bạn định nghĩa hàm ở chỗ nào, miễn sao bạn viết nó trong phạm vi mà từ nơi gọi có thể truy cập đến được.

Hãy tạo một dự án có tên functions để khám phá thêm về các hàm. Đặt hàm ví dụ another_function trong file src/main.rs và chạy nó. Bạn sẽ thấy nội dung sau được xuất ra:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/functions`
Hello, world!
Another function.

Các dòng được thực thi theo thứ tự chúng xuất hiện trong hàm main. Đầu tiên, chuỗi "Hello, world!" được xuất ra màn hình, tiếp theo đó another_function được gọi và nội dung của nó được in ra.

Tham số

Chúng ta có thể định nghĩa hàm với các parameters (tham số), là những biến đặc biệt như là một phần của chữ ký của hàm. Nếu một hàm có tham số, bạn có thể cung cấp cho nó các giá trị cụ thể khi gọi. Về mặt kỹ thuật, những giá trị cụ thể mà bạn cung cấp sẽ được gọi là các argument (đối số), tuy nhiên trong thực tế sử dụng, chúng ta thường dùng hai từ này lẫn lộn với nhau.

Trong phiên bản này của another_function chúng ta thêm một tham số:

Filename: src/main.rs

fn main() {
    another_function(5);
}

fn another_function(x: i32) {
    println!("The value of x is: {x}");
}

Khi thử chạy chương trình; bạn sẽ nhận được kết xuất sau:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 1.21s
     Running `target/debug/functions`
The value of x is: 5

Trong phần khai báo hàm another_function ta có thêm một tham số được đặt tên x. Kiểu của xi32. Khi truyền giá trị 5 cho another_function, macro println! sẽ in ra giá trị 5 ngay tại vị trí chứa x được bao bởi cặp dấu ngoặc nhọn trong chuỗi định dạng.

Trong chữ ký của hàm (thành phần phân biệt các hàm với nhau), bạn phải khai báo kiểu của mỗi tham số. Đây là một quyết định cẩn trọng trong thiết kế của Rust: yêu cầu khai báo kiểu khi định nghĩa hàm đồng nghĩa với việc trình biên dịch sẽ không bao giờ cần bạn phải dùng một tham số để có thể xác định được kiểu dữ liệu của tham số đó. Trình dịch cũng sẽ có thể đưa ra các thông báo hữu ích hơn khi nó biết chính xác kiểu dữ liệu mà hàm đó cần.

Khi định nghĩa nhiều tham số, bạn cần phân cách chúng bằng dấu phẩy, giống như sau:

Filename: src/main.rs

fn main() {
    print_labeled_measurement(5, 'h');
}

fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("The measurement is: {value}{unit_label}");
}

Ví dụ này tạo ra một hàm có tên print_labeled_measurement với hai tham số. Tham số đầu tiên được đặt tên value và là một i32. Tham số thứ hai có tên unit_label và có kiểu char. Hàm này sau đó in ra một dòng văn bản chứa cả hai valueunit_label.

Hãy thử chạy đoạn code này. Thay thế chương trình src/main.rs hiện có trong dự án functions với ví dụ phía trên và chạy dùng cargo run:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/functions`
The measurement is: 5h

Vì chúng ta gọi hàm với 5 như là giá trị của value'h' là giá trị của unit_label, chương trình sẽ xuất ra các giá trị đó.

Các phát biểu (statement) và biểu thức (expression)

Thân hàm được tạo bởi một chuỗi các phát biểu, và có thể kết thúc bằng một biểu thức. Cho tới hiện tại, các hàm chúng ta đã làm qua chưa bao gồm biểu thức kết thúc, nhưng bạn cũng đã gặp các biểu thức là một phần của một phát biểu. Vì Rust là một ngôn ngữ dựa trên các phát biểu, đây là một sự phân biệt quan trọng để hiểu. Các ngôn ngữ khác không có sự phân biệt tương tự như vậy, do vậy ta hãy cùng xem qua các phát biểu và biểu thức cũng như sự khác biệt giữa chúng đã ảnh hưởng đến thân các hàm như thế nào.

Phát biểu là các câu lệnh mà nó sẽ thực hiện một số hành động nào đó và không trả về giá trị. Biểu thức có trả về giá trị. Hãy cũng xem qua một số ví dụ.

Chúng ta đã từng thực sự dùng đến các phát biểu và biểu thức. Việc tạo ra một biến và gán một giá trị cho nó với từ khóa let là một phát biểu. Trong Listing 3-1, let y = 6; là một phát biểu.

Filename: src/main.rs

fn main() {
    let y = 6;
}

Listing 3-1: Một hàm main đang khai báo một phát biểu

Các khai báo hàm cũng là các phát biểu; toàn bộ ví dụ trên bản thân nó cũng là một phát biểu.

Các phát biểu không trả về giá trị. Do vậy, bạn không thể gán một phát biểu let cho một biến khác, bạn sẽ gặp lỗi nếu thử làm như trong ví dụ sau:

Filename: src/main.rs

fn main() {
    let x = (let y = 6);
}

Khi chạy chương trình này, lỗi bạn gặp sẽ giống như sau:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found `let` statement
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^

error: expected expression, found statement (`let`)
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: variable declaration using `let` is a statement

error[E0658]: `let` expressions in this position are unstable
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: see issue #53667 <https://github.com/rust-lang/rust/issues/53667> for more information

warning: unnecessary parentheses around assigned value
 --> src/main.rs:2:13
  |
2 |     let x = (let y = 6);
  |             ^         ^
  |
  = note: `#[warn(unused_parens)]` on by default
help: remove these parentheses
  |
2 -     let x = (let y = 6);
2 +     let x = let y = 6;
  |

For more information about this error, try `rustc --explain E0658`.
warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` due to 3 previous errors; 1 warning emitted

Phát biểu let y = 6 không trả về một giá trị, do vậy chẳng có gì để đưa vào biến x. Điều này khác với các ngôn ngữ khác như C hoặc Ruby, khi các một câu lệnh gán sẽ trả về giá trị bằng giá trị được gán. Trong các ngôn ngữ đó, bạn có thể viết x = y = 6 và làm cho cả hai xy mang cùng giá trị 6; điều này không xảy ra trong Rust.

Các biểu thức xác định giá trị và tạo nên hầu hết các đoạn lệnh bạn viết trong Rust. Hãy xem qua một phép toán, như 5 + 6, đó là một biểu thức và trả về giá trị 11. Các biểu thức cũng có thể là một phần của các phát biểu: Trong Listing 3-1, giá trị 6 trong phát biểu let y = 6; là một biểu thức và trả về giá trị 6. Việc gọi một hàm cũng là một biểu thức, gọi một macro cũng là một biểu thức. Một khối lệnh mới được tạo bởi một cặp ngoặc nhọn cũng là một biểu thức, giống như trong ví dụ sau:

Filename: src/main.rs

fn main() {
    let y = {
        let x = 3;
        x + 1
    };

    println!("The value of y is: {y}");
}

Biểu thức này:

{
    let x = 3;
    x + 1
}

là một khối lệnh, mà trong trường hợp này trả về giá trị 4. Giá trị này được gán vào cho y như một phần của phát biểu let. Lưu ý là x + 1 line không có một dấu chấm phẩy (;) ở cuối, không như hầu hết các dòng lệnh bạn đã từng thấy. Các biểu thức không có dấu chấm phẩy kết thúc. Nếu bạn thêm dấu chấm phẩy, bạn sẽ biến nó thành một phát biểu, và nó sẽ không trả về giá trị. Hãy lưu ý điều này khi bạn xem đến phần kế tiếp nói về giá trị và biểu thức trả về của hàm.

Hàm với kết quả trả về

Các hàm có thể trả về các giá trị cho đoạn code gọi chúng. Chúng ta không cần đặt tên cho kết quả trả về, nhưng ta phải khai báo kiểu dữ liệu của chúng phía sau dấu mũi tên (->). Trong Rust, giá trị trả về của hàm đồng nghĩa với giá trị của biểu thức cuối cùng trong thân hàm. Bạn có thể dùng từ khóa return để trả về một giá trị sớm hơn, nhưng hầu hết các hàm sẽ lấy giá trị trả về là câu lệnh cuối cùng. Sau đây là một ví dụ về hàm có trả về giá trị:

Filename: src/main.rs

fn five() -> i32 {
    5
}

fn main() {
    let x = five();

    println!("The value of x is: {x}");
}

Không có các lời gọi hàm, macro, hay thậm chí phát biểu let trong hàm five ngoài chính số 5. Đó hoàn toàn là một hàm hợp lệ trong Rust. Lưu ý là kiểu trả về của hàm cũng được chỉ định, là -> i32. Thử chạy đoạn code; kết xuất sẽ giống như sau:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/functions`
The value of x is: 5

Số 5 trong five là giá trị trả về của hàm, đó là lý do vì sao hàm này phải có kiểu là i32. Hãy xem xét một cách chi tiết hơn. Có hai thông tin quan trọng: đầu tiên, dòng let x = five(); cho thấy chúng ta đang dùng kết quả trả về của một hàm để khởi tạo một biến. Vì hàm five trả về giá trị 5, do vậy dòng lệnh đó cũng tương tự như sau:

#![allow(unused)]
fn main() {
let x = 5;
}

Thứ hai, hàm five không có tham số và định nghĩa kiểu của giá trị trả về, mà thân hàm chỉ bao gồm một số 5 không có dấu chấm phẩy, vì đó chính là biểu thức chúng ta muốn trả về.

Hãy cùng xem một ví dụ khác:

Filename: src/main.rs

fn main() {
    let x = plus_one(5);

    println!("The value of x is: {x}");
}

fn plus_one(x: i32) -> i32 {
    x + 1
}

Chạy đoạn code này sẽ in ra The value of x is: 6. Nhưng nếu ta thêm một dấu chấm phẩy vào cuối dòng chứa x + 1, biến nó từ một biểu thức thành một phát biểu, chúng ta sẽ nhận được một lỗi.

Filename: src/main.rs

fn main() {
    let x = plus_one(5);

    println!("The value of x is: {x}");
}

fn plus_one(x: i32) -> i32 {
    x + 1;
}

Dịch đoạn code sinh ra lỗi, giống như sau:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
 --> src/main.rs:7:24
  |
7 | fn plus_one(x: i32) -> i32 {
  |    --------            ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
8 |     x + 1;
  |          - help: remove this semicolon to return this value

For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions` due to previous error

Thông báo lỗi chính, mismatched types, cho thấy gốc rễ vấn đề của đoạn code. Định nghĩa hàm plus_one nói rằng nó sẽ trả về một i32, nhưng các phát biểu lại không trả về một giá trị, tương đương với việc trả về một giá trị rỗng (). Do không có giá trị nào được trả về, điều này trái ngược với định nghĩa của hàm và gây ra lỗi. Trong kết xuất này, Rust cũng cung cấp thêm một thông báo có thể giúp sửa được lỗi: nó đề xuất việc xóa dấu chấm phẩy, và có thể loại bỏ được lỗi.