Hàm

Hàm phổ biến trong Rust. Bạn đã thấy một trong những hàm quan trọng nhất trong ngôn ngữ này: đó là hàm main. Bạn cũng thấy từ khóa fn giúp cho phép bạn khai báo hàm mới.

Rust sử dụng quy tắc con rắn (snake case) làm quy ước cách đặt tên hàm và biến, trong đó tất cả các chữ cái phải viết thường và phân cách các từ bằng dấu gạch dưới. Bên dưới là một chương trình ví dụ về cách đị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 hàm trong Rust bằng cách gõ từ khóa fn theo sau là tên của hàm và một cặp dấu ngoặc đơn. Cặp dấu ngoặc nhọn cho trình biên dịch biết 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 đã được định nghĩa bằng cách gõ tên hàm theo sau là cặp dấu ngoặc tròn. Do hàm another_function đã được định nghĩa trong chương trình, bạn có thể gọi hàm này từ bên trong hàm main. Lưu ý rằng chúng ta đã định nghĩa hàm another_function sau hàm main trong code; chúng 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 ở đâu, Rust chỉ quan tâm hàm đã được định hay chưa.

Hãy bắt đầu một dự án nhị phân có tên functions để khám phá thêm về hàm. Lấy ví dụ hàm another_function cho vào src/main.rs và khởi chạy chương trình. Bạn sẽ thấy kết quả như bên dưới:

$ 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 thực thi theo thứ tự xuất hiện trong hàm main. Đầu tiên, thông điệp “Hello, world!” được in ra, sau đó gọi hàm another_function và in thông điệp của nó ra.

Tham số

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

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

Filename: src/main.rs

fn main() {
    another_function(5);
}

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

Thử khởi chạy chương trình; bạn sẽ nhận được kết quả 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

Khi khai báo hàm another_function, chúng ta có một tham số tên x. Kiểu của x được chỉ định là i32. Khi chúng ta chuyển giá trị 5 vào hàm another_function, println! macro sẽ đặt 5 vào vị trí của cặp dấu ngoặc nhọn để định dạng thành string.

Trong chữ ký hàm, bạn buộc phải khai báo kiểu của mọi tham số. Đây là một quyết định có chủ ý trong thiết kế của Rust: việc yêu cầu chú thích kiểu dữ liệu trong định nghĩa hàm có nghĩa là trình biên dịch sẽ không bao giờ yêu cầu bạn khai báo kiểu dữ liệu ở bất kỳ nơi khác trong code để tìm ra kiểu dữ liệu mà bạn muốn nói đến.

Khi bạn định nghĩa hàm với nhiều tham số, phân cách việc khai báo tham số bằng dấu phẩy 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ố thứ nhất có tên value và có kiểu i32. Tham số thứ hai có tên unit_label và có kiểu char. Sau đó hàm in ra đoạn văn bản chứa cả valueunit_label.

Hãy thử khởi chạy code. Thay thế ví dụ vừa rồi vào file src/main.rs của dự án functions và khởi chạy nó với 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

Bởi vì chúng ta đã gọi hàm với giá trị của value5unit_label'h', đầu ra của chương trình sẽ chứa các giá trị đó.

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

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

Câu lệnh (Statements) là các hướng dẫn thực hiện một số hành động và không trả về giá trị. Biểu thức (Expressions) tính toán để đưa ra một giá trị kết quả. Hãy xem xét một vài ví dụ.

Chúng ta đã thực sự sử dụng câu lệnh và biểu thức. Tạo một biến và gán một 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.

Filename: src/main.rs

fn main() {
    let y = 6;
}

Listing 3-1: A main function declaration containing one statement

Việc định nghĩa một hàm cũng là một câu lệnh; toàn bộ ví dụ trước là một câu lệnh.

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 statement vào một biến khác, bạn sẽ gặp lỗi nếu cố gắng làm như đoạn code bên dưới:

Filename: src/main.rs

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

Khi bạn chạy chương trình, bạn sẽ bắt gặp lỗi như thế này:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
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 experimental
 --> 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
  = help: you can write `matches!(<expr>, <pattern>)` instead of `let <pattern> = <expr>`

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 2 previous errors; 1 warning emitted

Câu lệnh let y = 6 không trả về một giá trị, do đó không có bất kỳ giá trị nào được gán cho x. Điều này khác biệt với những gì xảy ra trong các ngôn ngữ khác, như C và Ruby, trong đó phép gán trả về giá trị của phép gán đó. Trong những ngôn ngữ này, bạn có thể viết x = y = 6, cả xy đều sẽ có giá trị là 6; nhưng Rust thì không hiểu như vậy.

Biểu thức tính toán đưa ra một giá trị và tạo nên gần như toàn bộ phần còn lại của code mà bạn viết bằng Rust. Xét về một phép toán số học như 5 + 6, đó là một biểu thức sẽ đưa ra kết quả là 11. Biểu thức có thể là một phần của câu lệnh: trong Listing 3-1, giá trị 6 trong câu lệnh let y = 6; là một biểu thức đưa ra giá trị là 6. Việc gọi một hàm cũng 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 bằng 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
}

trong trường hợp này là một khối sẽ đưa ra giá trị là 4. Giá trị đó được gán vào 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 câu, không giống như hầu hết các dòng code bạn đã thấy cho đến nay. Các biểu thức không kết thúc bằng dấu chấm phẩy. Nếu bạn thêm dấu chấm phẩy vào cuối biểu thức, nó sẽ trở thành một câu lệnh và câu lệnh thì không trả về giá trị. Hãy nhớ điều này khi bạn tìm hiểu về các hàm trả về giá trị và biểu thức ở phần kế tiếp.

Hàm trả về giá trị

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

$ 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 hàm five là giá trị trả về của hàm, đó là lý do tại sao kiểu dữ liệu trả về là i32. Hãy xem xét điều này chi tiết hơn. Có 2 bits quan trọng: đầu tiên, dòng lệnh let x = five(); cho biết rằng bạn đang sử dụng giá trị trả về của hàm để khởi tạo ra một biến. Bởi vì hàm five trả về giá trị 5, dòng lệnh đó tương đương với dòng lệnh bên dưới:

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

Thứ hai, hàm five không có tham số và hàm xác định kiểu dữ liệu của giá trị trả về, nhưng trong phần thân hàm chỉ có số 5 không có dấu chấm phẩy bởi vì nó là một biểu thức chứa giá trị mà chúng ta muốn trả về.

Hãy xem xét 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
}

Code này sẽ in ra The value of x is: 6. Nhưng nếu chúng ta đặt dấu chấm phẩy vào cuối dòng chứa x + 1, nó sẽ từ biểu thức chuyển thành câu lệnh, chúng ta sẽ gặp lỗi ngay.

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 code này sẽ gây 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: consider removing this semicolon

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 vấn đề cốt lõi trong code. Định nghĩa hàm plus_one nói rằng nó sẽ trả về một giá trị i32, nhưng câu lệnh không trả về giá trị được biểu thị bằng (), unit type. Do đó, không có gì để trả về, điều này mẫu thuẫn với định nghĩa của hàm và dẫn đến lỗi. Trong đầu ra này, Rust đưa ra một thông báo để có thể giúp khắc phục vấn đề này: Rust đề xuất loại bỏ dấu chấm phẩy để khắc phục lỗi.