Các Hàm

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

Mã Rust sử dụng snake case là quy ước phong cách cho tên hàm và tên biến, trong đó tất cả các chữ cái đều là chữ thường và dấu gạch dưới phân tách các từ. Đây là một chương trình chứa ví dụ về định nghĩa 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 nhập fn theo sau là tên hàm và một tập hợp dấu ngoặc đơn. Dấu ngoặc nhọn cho trình biên dịch biết nơi bắt đầu và kết thúc thân hàm.

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

Hãy bắt đầu một dự án nhị phân mới có tên là functions để khám phá hàm sâu hơn. Đặt ví dụ another_function trong src/main.rs và chạy nó. Bạn sẽ thấy kết quả sau:

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

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

Tham số

Chúng ta có thể định nghĩa hàm có tham số, đó là các biến đặc biệt là một phần của chữ ký hàm. Khi một hàm có tham số, bạn có thể cung cấp cho nó các giá trị cụ thể cho các tham số đó. Về mặt kỹ thuật, các giá trị cụ thể được gọi là đối số, nhưng trong cuộc trò chuyện thông thường, mọi người thường sử dụng từ tham sốđối số thay thế cho nhau cho cả biến trong định nghĩa một hàm hoặc các giá trị cụ thể được truyền vào khi bạn gọi một hàm.

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}");
}

Hãy thử chạy chương trình này; bạn sẽ nhận được kết quả sau:

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

Khai báo của another_function có một tham số có tên là x. Kiểu của x được xác định là i32. Khi chúng ta truyền 5 vào another_function, macro println! đặt 5 vào nơi có cặp dấu ngoặc nhọn chứa x trong chuỗi định dạng.

Trong chữ ký hàm, bạn phải khai báo kiểu của mỗi tham số. Đây là một quyết định có chủ đích trong thiết kế của Rust: yêu cầu chú thích kiểu trong định nghĩa hàm có nghĩa là trình biên dịch hầu như không cần bạn sử dụng chúng ở nơi khác trong mã để xác định kiểu bạn muốn. Trình biên dịch cũng có thể đưa ra thông báo lỗi hữu ích hơn nếu nó biết kiểu nào mà hàm mong đợi.

Khi định nghĩa nhiều tham số, hãy phân tách khai báo tham số bằng dấu phẩy, như thế này:

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 là print_labeled_measurement với hai tham số. Tham số đầu tiên có tên là value và là một i32. Tham số thứ hai có tên là unit_label và có kiểu char. Sau đó hàm in ra văn bản chứa cả valueunit_label.

Hãy thử chạy mã này. Thay thế chương trình hiện có trong tập tin src/main.rs của dự án functions bằng ví dụ trước đó và chạy nó bằng cargo run:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished `dev` profile [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 là giá trị cho value'h' là giá trị cho unit_label, kết quả chương trình chứa những giá trị đó.

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

Phần thân hàm được cấu thành từ một chuỗi các câu lệnh, tùy chọn kết thúc bằng một biểu thức. Cho đến nay, các hàm chúng ta đã đề cập chưa bao gồm một biểu thức kết thúc, nhưng bạn đã thấy một biểu thức như một phần của một câu lệnh. Bởi vì Rust là một ngôn ngữ dựa trên biểu thức, đây là một sự phân biệt quan trọng cần hiểu. Các ngôn ngữ khác không có sự phân biệt giống nhau, vì vậy hãy xem xét câu lệnh và biểu thức là gì và cách sự khác biệt của chúng ảnh hưởng đến phần thân của các hàm.

  • Câu lệnh là những chỉ thị thực hiện một số hành động và không trả về giá trị.
  • Biểu thức đánh giá thành một giá trị kết quả.

Hãy xem một số ví dụ.

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

fn main() {
    let y = 6;
}

Định nghĩa hàm cũng là câu lệnh; toàn bộ ví dụ trước đó là một câu lệnh tự nó. (Như chúng ta sẽ thấy dưới đây, gọi một hàm không phải là một câu lệnh, tuy nhiên.)

Câu lệnh không trả về giá trị. Do đó, bạn không thể gán một câu lệnh let cho một biến khác, như mã sau cố gắng làm; bạn sẽ gặp lỗi:

Filename: src/main.rs

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

Khi bạn chạy chương trình này, lỗi bạn sẽ nhận được trông như thế này:

$ 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);
  |              ^^^
  |
  = note: only supported directly in conditions of `if` and `while` expressions

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;
  |

warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` (bin "functions") due to 1 previous error; 1 warning emitted

Câu lệnh let y = 6 không trả về giá trị, vì vậy không có gì để x ràng buộc. Điều này khác với những gì xảy ra trong các ngôn ngữ khác, chẳng hạn như C và Ruby, nơi phép gán trả về giá trị của phép gán. Trong các ngôn ngữ đó, bạn có thể viết x = y = 6 và cả xy đều có giá trị 6; điều đó không đúng trong Rust.

Biểu thức đánh giá thành một giá trị và tạo nên phần lớn phần còn lại của mã mà bạn sẽ viết trong Rust. Hãy xem xét một phép toán toán học, chẳng hạn như 5 + 6, đó là một biểu thức đánh giá thành giá trị 11. Biểu thức có thể là một phần của câu lệnh: trong Listing 3-1, 6 trong câu lệnh let y = 6; là một biểu thức đánh giá thành giá trị 6. Gọi một hàm là một biểu thức. Gọi một macro là một biểu thức. Một khối phạm vi mới được tạo ra với dấu ngoặc nhọn là một biểu thức, ví dụ:

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 mà trong trường hợp này, đánh giá thành 4. Giá trị đó được ràng buộc với y như một phần của câu lệnh let. Lưu ý rằng dòng x + 1 không có dấu chấm phẩy ở cuối, khác với hầu hết các dòng mà bạn đã thấy cho đến nay. Biểu thức không bao gồm dấu chấm phẩy kết thúc. Nếu bạn thêm dấu chấm phẩy vào cuối một biểu thức, bạn biến nó thành một câu lệnh, và nó sẽ không trả về giá trị nữa. Hãy ghi nhớ điều này khi bạn khám phá giá trị trả về của hàm và biểu thức tiếp theo.

Hàm với Giá trị Trả về

Hàm có thể trả về giá trị cho mã gọi chúng. Chúng ta không đặt tên cho các giá trị trả về, nhưng chúng ta phải khai báo kiểu của chúng sau một 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 khối của phần thân hàm. Bạn có thể trả về sớm từ một hàm bằng cách sử dụng từ khóa return và chỉ định một giá trị, nhưng hầu hết các hàm đều trả về biểu thức cuối cùng một cách ngầm định. Đây là một ví dụ về một hàm trả về một 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ó lời gọi hàm, macro, hay thậm chí câu lệnh let trong hàm five—chỉ có số 5 đứng một mình. Đó là một hàm hoàn toàn hợp lệ trong Rust. Lưu ý rằng kiểu trả về của hàm cũng được chỉ định, là -> i32. Hãy thử chạy mã này; kết quả sẽ trông như thế này:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished `dev` profile [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 tại sao kiểu trả về là i32. Hãy xem xét điều này chi tiết hơn. Có hai phần quan trọng: đầu tiên, dòng let x = five(); cho thấy rằng chúng ta đang sử dụng giá trị trả về của một hàm để khởi tạo một biến. Bởi vì hàm five trả về 5, dòng đó giống 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ề, nhưng phần thân của hàm chỉ là một 5 cô đơn không có dấu chấm phẩy vì đó là một biểu thức mà chúng ta muốn trả về giá trị.

Hãy 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 mã này sẽ in ra The value of x is: 6. Nhưng nếu chúng ta đặt một dấu chấm phẩy ở cuối dòng chứa x + 1, biến nó từ một biểu thức thành một câu lệnh, chúng ta sẽ nhận được 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;
}

Biên dịch mã này tạo ra lỗi 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` (bin "functions") due to 1 previous error

Thông báo lỗi chính, mismatched types (không khớp kiểu), tiết lộ vấn đề cốt lõi với mã này. Định nghĩa của hàm plus_one nói rằng nó sẽ trả về một i32, nhưng câu lệnh không đánh giá thành một giá trị, được biểu thị bởi (), kiểu đơn vị. Do đó, không có gì được trả về, mâu thuẫn với định nghĩa hàm và dẫn đến lỗi. Trong kết quả này, Rust cung cấp một thông báo để có thể giúp khắc phục vấn đề này: nó đề xuất loại bỏ dấu chấm phẩy, điều đó sẽ khắc phục lỗi.