Mục lục
- Mục Tiêu Bài Học
- Parameter Bắt Buộc Type Annotation
- Nhiều Parameter Cùng Type — Vẫn Phải Tách
- Default Parameter — KHÔNG Có Trong Rust
- Variadic Parameter — KHÔNG Có Trừ Macro
- Return Type Annotation
- Return Bằng Expression Cuối — KHÔNG Semicolon
- Return Sớm Với return Keyword
- Bug Phổ Biến Semicolon
- Tổng Kết
- Bài Tập Củng Cố
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu vì sao parameter BẮT BUỘC annotation type trong Rust (khác
letđược infer) — lý do là API contract phải rõ ràng tại signature. - Biết nhiều parameter cùng type vẫn phải tách từng cái — không có shorthand kiểu
(a, b: i32)như TypeScript / Python type hint. - Biết Rust KHÔNG support default parameter value; nắm các workaround chính:
Option<T>, builder pattern, multiple function với tên khác nhau. - Biết function thường KHÔNG nhận variable count parameter (variadic); chỉ macro như
println!,vec!mới làm được. Workaround: dùngVec<T>hoặc&[T]. - Viết đúng return type qua
-> Tsau parameter list; biết khi không khai báo thì return type ngầm định là()(unit type). - Phân biệt expression cuối không semicolon (= trả giá trị) vs có semicolon (= statement, trả
()); và khi nào dùngreturnkeyword cho early-return. - Đọc và sửa được bug phổ biến nhất của newcomer Rust: thừa semicolon ở dòng cuối hàm — compiler báo
mismatched types: expected ..., found ().
Parameter Bắt Buộc Type Annotation
Khác với let x = 5 nơi compiler tự suy ra x: i32, tại signature của function bạn BẮT BUỘC viết type cho mọi parameter. Bỏ qua là compile error luôn — không có chuyện "Rust tự đoán":
// OK - annotation đầy đủ cho mỗi parameter
fn add(a: i32, b: i32) -> i32 {
a + b
}
// COMPILE ERROR - thiếu type cho a và b
// fn add(a, b) -> i32 { a + b }
//
// error: expected one of `:`, `@`, or `|`, found `,`
// |
// 1 | fn add(a, b) -> i32 { a + b }
// | ^ expected one of `:`, `@`, or `|`
//
// help: declare the type after the parameter binding
Vì sao Rust bắt buộc trong khi let không? Lý do gốc là API contract:
- Function signature là boundary giữa caller và implementation. Caller cần biết chính xác type cho từng vị trí mà không cần đọc body.
- Cho phép compile từng module độc lập — nếu inference xuyên qua signature, sửa body một hàm có thể đổi type signature, làm caller ở module khác fail bất ngờ.
- Cho recursive function, mutual recursion, generic — inference toàn cục sẽ rất phức tạp và thường không có lời giải duy nhất.
- Documentation: signature chính là tài liệu ngắn gọn cho user; thiếu type, signature mất giá trị tham chiếu.
So sánh với các ngôn ngữ khác để khắc sâu:
- TypeScript / Python: type hint là tuỳ chọn. Có thể viết
function add(a, b)chạy được (Python) hoặc bị suy raany(TS không strict). - Haskell / OCaml: có type inference toàn cục mạnh, viết
add a b = a + bcompiler tự suy. Nhưng đổi lại lỗi inference khó đọc, refactor lan rộng khó kiểm soát. - Rust: chọn middle ground — inference chỉ trong body (
let x = ...), nhưng biên (function signature) bắt buộc explicit. Đây cũng là quy tắc của struct field, const, static — tất cả "biên" đều cần annotation.
Nhiều Parameter Cùng Type — Vẫn Phải Tách
Khi nhiều parameter có cùng type, Rust không có cú pháp gộp — phải lặp lại annotation cho từng cái:
// ĐÚNG - lặp lại i32 cho cả 3 parameter
fn sum3(a: i32, b: i32, c: i32) -> i32 {
a + b + c
}
// SAI - không có shorthand "shared type" kiểu Python / TypeScript
// fn sum3(a, b, c: i32) -> i32 { a + b + c }
//
// error: expected one of `:`, `@`, or `|`, found `,`
// So sánh với cú pháp các ngôn ngữ khác cho thấy không tương đương:
//
// TypeScript: function sum3(a: number, b: number, c: number): number { ... }
// (vẫn phải lặp, nhưng cho phép trick destructure: ({a,b,c}: {a:number, ...}))
//
// Python type hint: def sum3(a: int, b: int, c: int) -> int: ...
// (cũng phải lặp)
//
// Pascal / Ada: procedure Sum3(a, b, c: Integer); -- gộp được
// Go: func sum3(a, b, c int) int { ... } -- gộp được
//
// Rust: KHÔNG gộp - design đơn giản, parser dễ.
Nếu cảm thấy verbose, đó là dấu hiệu cần refactor:
- Nhiều parameter cùng type → có thể nhóm vào struct (vd
Point { x: f64, y: f64 }thay vìfn dist(x1: f64, y1: f64, x2: f64, y2: f64)) — vừa rõ ràng, vừa tránh nhầm thứ tự (gọidist(x2, y1, x1, y2)nhầm vẫn compile). - Hoặc nhận slice:
fn sum(xs: &[i32]) -> i32linh hoạt hơn nhiều so vớifn sum(a: i32, b: i32, c: i32). - Hoặc dùng tuple nếu chỉ ngắn hạn:
fn dist(p1: (f64, f64), p2: (f64, f64)).
Nói cách khác: việc Rust ép bạn viết dài hơn ở vài trường hợp lại nhắc bạn refactor sang abstraction phù hợp — đó là chủ ý design.
Default Parameter — KHÔNG Có Trong Rust
Rust KHÔNG hỗ trợ default parameter value. Không có cú pháp fn greet(name: &str, greeting: &str = "Hello") như Python / C++ / Kotlin. Đây là quyết định có chủ ý: signature phải tường minh hoàn toàn, không có "magic" ẩn.
Có 3 workaround phổ biến — tuỳ ngữ cảnh chọn cái phù hợp:
Workaround 1: Option<T> — Đơn Giản Nhất
Param trở thành Option<T>; bên trong dùng unwrap_or để đặt default. Caller pass None khi muốn default, Some(value) khi muốn override.
fn greet(name: &str, greeting: Option<&str>) {
let g = greeting.unwrap_or("Hello");
println!("{g}, {name}!");
}
fn main() {
greet("Canh", None); // Hello, Canh!
greet("Canh", Some("Chào")); // Chào, Canh!
}
Ưu: ngắn, không thêm type mới. Nhược: caller vẫn phải gõ None — không "ẩn" được như default thực sự.
Workaround 2: Builder Pattern — Nhiều Optional
Khi có ≥3 param optional, builder rõ ràng hơn hẳn Option:
struct Greeter<'a> {
name: &'a str,
greeting: &'a str,
punctuation: char,
}
impl<'a> Greeter<'a> {
fn new(name: &'a str) -> Self {
// Default values nằm ở constructor
Self { name, greeting: "Hello", punctuation: '!' }
}
fn greeting(mut self, g: &'a str) -> Self { self.greeting = g; self }
fn punctuation(mut self, p: char) -> Self { self.punctuation = p; self }
fn say(&self) {
println!("{}, {}{}", self.greeting, self.name, self.punctuation);
}
}
fn main() {
Greeter::new("Canh").say(); // Hello, Canh!
Greeter::new("Canh").greeting("Chào").say(); // Chào, Canh!
Greeter::new("Canh").greeting("Hi").punctuation('?').say(); // Hi, Canh?
}
Workaround 3: Hai Function Tên Khác Nhau
Đơn giản nhất khi chỉ có 1 variant default — đặt tên new cho default, with_xxx cho biến thể:
fn greet(name: &str) { greet_with(name, "Hello"); }
fn greet_with(name: &str, greeting: &str) { println!("{greeting}, {name}!"); }
Trong stdlib bạn thấy pattern này khắp nơi: String::new() vs String::with_capacity(n), Vec::new() vs Vec::with_capacity(n).
Variadic Parameter — KHÔNG Có Trừ Macro
Function Rust không nhận variable count parameter. Không có cú pháp fn sum(...nums: i32) như JavaScript rest, hay fn sum(*args) như Python. Số parameter là cố định tại signature.
Điều này khác hẳn với một số function/macro bạn đã quen như println!, vec! — gọi với số lượng argument tuỳ ý:
fn main() {
println!("zero args");
println!("{}", 1);
println!("{} + {} = {}", 1, 2, 3);
let v = vec![1, 2, 3, 4, 5, 6, 7];
println!("{v:?}");
}
Lý do là println! và vec! không phải function — chúng là macro. Dấu ! phía sau là chỉ báo. Macro được expand tại compile-time thành code Rust thường, nên có thể "nhận" số argument tuỳ ý — compiler thấy code đã expand chứ không thấy variadic. Function thường thì không.
Trường hợp duy nhất function Rust được phép variadic là extern "C" để gọi C-API (vd printf), và chỉ là khai báo binding, không tự viết:
extern "C" {
// Khai báo binding cho C variadic - KHÔNG tự viết function variadic Rust
fn printf(fmt: *const i8, ...) -> i32;
}
Workaround cho function bình thường khi muốn nhận nhiều giá trị: dùng collection làm 1 parameter.
// Nhận slice - linh hoạt nhất, không lấy ownership
fn sum_slice(xs: &[i32]) -> i32 {
let mut total = 0;
for x in xs { total += x; }
total
}
// Nhận Vec - lấy ownership, owner cũ mất quyền dùng
fn sum_vec(xs: Vec<i32>) -> i32 {
xs.iter().sum()
}
fn main() {
// Caller "bó" số lượng tuỳ ý vào slice/vec
println!("{}", sum_slice(&[1, 2, 3])); // 6
println!("{}", sum_slice(&[1, 2, 3, 4, 5])); // 15
let v = vec![10, 20, 30];
println!("{}", sum_slice(&v)); // 60 (slice từ Vec)
println!("{}", sum_vec(vec![1, 2, 3, 4])); // 10
}
Idiom Rust: nhận &[T] cho hầu hết trường hợp. Caller vẫn pass được mảng literal (&[1, 2, 3]), Vec (&v), slice từ array (&arr[1..4]) — tất cả deref về cùng một type.
Return Type Annotation
Return type khai báo qua -> T đặt giữa parameter list và body. Khi không có -> T, Rust ngầm coi return type là () (unit — đã học ở Bài 39 phần Tuple Empty):
// Có return type rõ ràng
fn double(x: i32) -> i32 { x * 2 }
// Không có -> ... = return type ngầm định là ()
fn log_msg(msg: &str) {
println!("[LOG] {msg}");
// function trả về () - không cần viết gì cuối body
}
// Tương đương hoàn toàn với log_msg ở trên
fn log_msg_explicit(msg: &str) -> () {
println!("[LOG] {msg}");
}
// main thường có return () (mặc định)
// hoặc -> Result<(), Box<dyn std::error::Error>> khi cần dùng `?`
fn main() {
let x = double(21);
println!("{x}"); // 42
log_msg("hello");
log_msg_explicit("world");
}
Khi muốn trả nhiều giá trị, gói vào tuple (đã học Bài 39):
fn min_max(xs: &[i32]) -> (i32, i32) {
let mut lo = xs[0];
let mut hi = xs[0];
for &x in xs {
if x < lo { lo = x; }
if x > hi { hi = x; }
}
(lo, hi) // expression cuối, không semicolon - đây là giá trị trả về
}
fn main() {
let (lo, hi) = min_max(&[3, 7, 1, 9, 5]);
println!("min={lo}, max={hi}"); // min=1, max=9
}
Lưu ý: viết fn foo() -> () hợp lệ nhưng clippy sẽ cảnh báo (clippy::unused_unit) vì thừa — bỏ luôn để code idiomatic.
Return Bằng Expression Cuối — KHÔNG Semicolon
Rust có 2 cách trả giá trị từ function:
- Expression cuối, KHÔNG semicolon — idiom Rust, dùng cho happy path.
- Keyword
return— dùng cho early-return (xem bước 8).
Quy tắc cốt lõi: trong block { ... }, nếu dòng cuối không có semicolon thì nó là expression — block "evaluate" thành giá trị đó. Nếu có semicolon thì nó là statement — block evaluate thành ().
// Body là 1 expression - giá trị x * 2 là return value
fn double(x: i32) -> i32 {
x * 2 // KHÔNG semicolon - đây là expression cuối, return giá trị
}
// Body có nhiều statement, expression cuối là return value
fn double_verbose(x: i32) -> i32 {
let r = x * 2; // statement (có semicolon)
println!("doubled"); // statement
r // expression cuối - return value
}
// Block expression cũng theo cùng quy tắc
fn main() {
let y = {
let a = 10;
let b = 20;
a + b // expression cuối - block evaluate thành 30
};
println!("y = {y}"); // y = 30
let z = {
let a = 10;
a + 1; // có semicolon - statement, không phải expression cuối
// block kết thúc, không có expression cuối -> evaluate thành ()
};
let _: () = z; // OK - z có type ()
}
Cách nhớ: semicolon = "discard kết quả". Có semicolon nghĩa là "tôi không quan tâm giá trị trả về của expression này"; bỏ semicolon nghĩa là "giá trị này quan trọng — đó chính là kết quả của block".
So sánh với các ngôn ngữ khác:
- C / Java / Go / Python: bắt buộc
returnrõ ràng cho mọi return value. Không có khái niệm "expression cuối". - Ruby / Scala / Kotlin / Elixir: cũng có "last expression = return value" tương tự Rust.
- JavaScript: arrow function ngắn (
x => x * 2) tương tự. Nhưng function bình thường vẫn cầnreturn.
Trong Rust idiom, viết return x; ở dòng cuối là code-smell — clippy lint needless_return sẽ nhắc bạn bỏ semicolon và return.
Return Sớm Với return Keyword
return dùng để thoát sớm giữa chừng — không phải để trả giá trị ở cuối. Pattern điển hình: kiểm tra điều kiện đầu hàm (guard clause), nếu sai thì return Err / return early_value luôn.
fn validate(x: i32) -> Result<(), String> {
if x < 0 {
return Err("negative".into()); // early return - dùng `return`
}
if x > 1_000 {
return Err("too large".into()); // early return thứ 2
}
Ok(()) // happy path - expression cuối, không semicolon
}
fn classify(score: i32) -> &'static str {
// Guard clauses dùng return - khi false, thoát sớm
if score < 0 { return "invalid"; }
if score < 50 { return "fail"; }
if score < 80 { return "pass"; }
// Happy path cuối cùng - expression, không semicolon
"excellent"
}
fn main() {
println!("{:?}", validate(-5)); // Err("negative")
println!("{:?}", validate(2000)); // Err("too large")
println!("{:?}", validate(100)); // Ok(())
println!("{}", classify(95)); // excellent
println!("{}", classify(60)); // pass
println!("{}", classify(-1)); // invalid
}
Khi nào dùng return vs khi nào dùng expression cuối:
- Dùng
returnkhi thoát giữa chừng — guard clause, error early, success early. - Dùng expression cuối khi đến điểm cuối tự nhiên của hàm — happy path, branch cuối của
if/elsehaymatch. - Không dùng
return value;ở dòng cuối hàm — code-smell, clippy báoneedless_return.
Một idiom mạnh hơn nữa cho guard clause là let else (Bài 105) và operator ? cho propagate Result (Bài 143) — sẽ học sau khi đã quen với return truyền thống.
Bug Phổ Biến Semicolon
Đây là top bug của newcomer Rust — đặc biệt nếu trước đó quen với C/Java/Go vốn yêu cầu semicolon ở mọi dòng. Thừa 1 dấu ; ở dòng cuối hàm biến giá trị trả về từ T sang ():
fn add(a: i32, b: i32) -> i32 {
a + b; // sai: semicolon biến thành statement, trả ()
}
Compiler báo lỗi đầy đủ thông tin:
error[E0308]: mismatched types
--> src/main.rs:1:31
|
1 | fn add(a: i32, b: i32) -> i32 {
| --- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
2 | a + b;
| - help: remove this semicolon to return this value
Đọc kỹ message: compiler không chỉ báo lỗi mà còn chỉ chính xác vị trí semicolon cần xoá. rustc (và cargo check / rust-analyzer) cực thân thiện với newcomer — đừng bỏ qua phần help:.
Một số biến thể thường gặp của bug này:
// Biến thể 1: if-else trả giá trị, một nhánh có semicolon
fn abs_val(x: i32) -> i32 {
if x >= 0 {
x; // SAI - statement, trả ()
} else {
-x // OK - expression
}
// expected `i32`, found `()`
}
// Biến thể 2: match arm có semicolon
fn sign(x: i32) -> i32 {
match x {
0 => 0,
n if n > 0 => 1,
_ => { -1; } // SAI - block evaluate thành ()
}
}
// Biến thể 3: ngược lại - quên thêm semicolon ở statement giữa hàm
fn weird(x: i32) -> i32 {
let y = x + 1 // QUÊN semicolon - compiler nghĩ đây là expression cuối,
// nhưng còn dòng sau nên báo lỗi parse
y * 2
}
// SỬA tất cả các biến thể trên - thêm/bớt semicolon cho đúng
fn abs_val_fixed(x: i32) -> i32 {
if x >= 0 { x } else { -x } // cả 2 nhánh là expression, không semicolon
}
fn sign_fixed(x: i32) -> i32 {
match x {
0 => 0,
n if n > 0 => 1,
_ => -1, // arm trả i32 trực tiếp, không block
}
}
Mẹo debug: khi gặp mismatched types: expected T, found (), mở file ở dòng compiler chỉ và kiểm tra dòng ngay trước đóng ngoặc }. 90% là thừa semicolon.
Bài tiếp theo (Bài 46) sẽ đào sâu phân biệt expression vs statement ở mức nguyên lý — đó là chìa khoá để không bao giờ vướng bug này nữa.
Tổng Kết
- Parameter BẮT BUỘC type annotation (khác
let) — vì signature là API contract, cần explicit để compile từng module độc lập và phục vụ generic / recursive. - Nhiều parameter cùng type vẫn phải tách — không có shorthand kiểu Go/Pascal. Nếu thấy verbose là dấu hiệu nên refactor sang struct/slice/tuple.
- Rust không có default parameter; workaround:
Option<T>(ngắn), builder pattern (nhiều optional), hoặc 2-3 function tên khác (nhưString::newvsString::with_capacity). - Function thường không variadic — chỉ macro (
println!,vec!) làm được vì expand compile-time. Workaround: nhận&[T]hoặcVec<T>. - Return type qua
-> Tsau parameter list; bỏ thì ngầm là(). Return nhiều value qua tuple. - Idiom return: expression cuối KHÔNG semicolon cho happy path;
returnkeyword cho early-return / guard clause. - Bug top: thừa semicolon dòng cuối → trả
()thay vìT→ compile errormismatched types: expected T, found (). Đọchelp:củarustcđể fix nhanh.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Đoạn code
fn multiply(a, b: i32) -> i32 { a * b }không compile. Sửa và giải thích vì sao Rust không cho phép cú pháp này (so sánh với Go). - Viết function
greetnhận 1 tên (&str) bắt buộc và 1 lời chào (&str) có default là"Hello". Yêu cầu: dùngOption<&str>. Gọi 2 lần — 1 lần default, 1 lần override. - Tại sao
println!("{}", x)nhận số argument tuỳ ý nhưngfn my_print(args: ...)thì không? Mô tả ngắn bản chất khác biệt giữa macro và function thường về điểm này. - Đoạn code dưới không compile. Đọc error message giả định
"mismatched types: expected i32, found ()"và chỉ chính xác vị trí cần sửa, viết phiên bản đã sửa:fn cube(x: i32) -> i32 { let y = x * x; y * x; } - Viết function
fn safe_div(a: i32, b: i32) -> Result<i32, String>trảErr("divide by zero".into())nếub == 0, ngược lại trảOk(a / b). Yêu cầu: dùngreturncho guard clause + expression cuối cho happy path. Nêu rõ chỗ nào dùngreturnchỗ nào không.
Đáp án
- Sửa:
fn multiply(a: i32, b: i32) -> i32 { a * b }. Rust không có shorthand "shared type" như Go (func(a, b int)) vì design tối giản parser và để mỗi parameter có annotation tường minh, hỗ trợ tốt hơn cho generic / lifetime / pattern parameter sau này. Trade-off: phải gõ dài hơn — chấp nhận được vì khi param thực sự nhiều cùng type, thường nên gom thành struct hoặc slice. fn greet(name: &str, greeting: Option<&str>) { let g = greeting.unwrap_or("Hello"); println!("{g}, {name}!"); } fn main() { greet("Canh", None); // Hello, Canh! greet("Canh", Some("Chào")); // Chào, Canh! }println!là macro (có dấu!), được expand thành code Rust tại compile-time — compiler thấy code đã expand, không thấy "variadic", nên bao nhiêu argument cũng OK. Function thường thì compiler tạo 1 ABI cố định với số parameter cố định — không cách nào "biến thiên" số argument tại runtime (trừextern "C"để gọi C-API). Đây là lý do Rust gom mọi "variadic-like" feature vào macro.- Lỗi nằm ở dòng
y * x;— semicolon thừa biến expression thành statement, hàm trả()thay vìi32. Xoá semicolon cuối cùng:fn cube(x: i32) -> i32 { let y = x * x; y * x // expression cuối, KHÔNG semicolon }
Giải thích:fn safe_div(a: i32, b: i32) -> Result<i32, String> { if b == 0 { return Err("divide by zero".into()); // DÙNG return - thoát sớm } Ok(a / b) // KHÔNG return - expression cuối, happy path }return Err(...)ở guard clause vì cần thoát giữa chừng.Ok(a / b)ở cuối là expression — đúng idiom Rust, không cầnreturn(clippy báoneedless_returnnếu thêm).
Bài Tiếp Theo
Bài 46: Expression vs Statement Trong Rust — đào sâu khác biệt nguyên lý giữa expression (trả value, không kết bằng semicolon) và statement (không trả value, kết bằng semicolon). Sau bài đó bạn sẽ tự tin viết let y = { let x = 3; x + 1 };, không bao giờ vướng bug semicolon nữa, và hiểu vì sao gần như mọi cấu trúc trong Rust (kể cả if, match, loop) đều là expression.
