let-else Pattern
let-else là một tính năng được giới thiệu trong Rust 1.65 (2022), cho phép pattern matching với early return một cách elegant và concise.
Syntax cơ bản
#![allow(unused)] fn main() { let PATTERN = EXPRESSION else { DIVERGING_CODE }; }
Nếu pattern matching thất bại, code trong else block sẽ chạy. Block này phải diverge (return, break, continue, panic, etc.)
Ví dụ đơn giản
Với Option
fn process_value(maybe_value: Option<i32>) { let Some(value) = maybe_value else { println!("No value provided"); return; }; // value có type i32, không phải Option<i32> println!("Processing: {}", value); } fn main() { process_value(Some(42)); // Processing: 42 process_value(None); // No value provided }
Với Result
fn parse_config(input: &str) -> Result<(), String> { let Ok(port) = input.parse::<u16>() else { return Err("Invalid port number".to_string()); }; println!("Port: {}", port); Ok(()) } fn main() { parse_config("8080").ok(); // Port: 8080 parse_config("invalid").ok(); // Returns Err }
So sánh với các approaches khác
❌ Before let-else (verbose)
#![allow(unused)] fn main() { fn get_first_word(text: &str) -> Result<String, &'static str> { let words: Vec<&str> = text.split_whitespace().collect(); // Cách 1: if-let với nested logic if let Some(first) = words.first() { Ok(first.to_string()) } else { Err("No words found") } } }
#![allow(unused)] fn main() { fn get_first_word(text: &str) -> Result<String, &'static str> { let words: Vec<&str> = text.split_whitespace().collect(); // Cách 2: match match words.first() { Some(first) => Ok(first.to_string()), None => Err("No words found"), } } }
✅ With let-else (concise)
#![allow(unused)] fn main() { fn get_first_word(text: &str) -> Result<String, &'static str> { let words: Vec<&str> = text.split_whitespace().collect(); let Some(first) = words.first() else { return Err("No words found"); }; Ok(first.to_string()) } }
Use Cases
1. Function argument validation
fn create_user(name: Option<String>, age: Option<u32>) -> Result<User, String> { let Some(name) = name else { return Err("Name is required".to_string()); }; let Some(age) = age else { return Err("Age is required".to_string()); }; if age < 18 { return Err("Must be 18 or older".to_string()); } Ok(User { name, age }) } struct User { name: String, age: u32, } fn main() { match create_user(Some("Alice".to_string()), Some(25)) { Ok(user) => println!("Created user: {}", user.name), Err(e) => println!("Error: {}", e), } }
2. Parsing và validation
fn parse_port(s: &str) -> Result<u16, String> { let Ok(port) = s.parse::<u16>() else { return Err(format!("'{}' is not a valid port number", s)); }; if port < 1024 { return Err(format!("Port {} is reserved", port)); } Ok(port) } fn main() { println!("{:?}", parse_port("8080")); // Ok(8080) println!("{:?}", parse_port("invalid")); // Err(...) println!("{:?}", parse_port("80")); // Err("Port 80 is reserved") }
3. Destructuring structs/tuples
struct Point { x: i32, y: i32, } fn process_point(maybe_point: Option<Point>) { let Some(Point { x, y }) = maybe_point else { println!("No point provided"); return; }; println!("Point: ({}, {})", x, y); } fn main() { process_point(Some(Point { x: 10, y: 20 })); process_point(None); }
4. Working với enums
enum Message { Text(String), Number(i32), } fn process_text(msg: Message) -> Result<(), String> { let Message::Text(content) = msg else { return Err("Expected text message".to_string()); }; println!("Text: {}", content); Ok(()) } fn main() { process_text(Message::Text("Hello".to_string())).ok(); process_text(Message::Number(42)).ok(); }
5. Guard clauses
fn calculate_discount(price: f64, coupon: Option<String>) -> f64 { let Some(code) = coupon else { return price; // No discount }; let discount = match code.as_str() { "SAVE10" => 0.10, "SAVE20" => 0.20, _ => return price, // Invalid code }; price * (1.0 - discount) } fn main() { println!("{}", calculate_discount(100.0, Some("SAVE10".to_string()))); // 90 println!("{}", calculate_discount(100.0, None)); // 100 }
Multiple let-else trong sequence
fn process_config( host: Option<String>, port: Option<String>, ) -> Result<(String, u16), String> { let Some(host) = host else { return Err("Host is required".to_string()); }; let Some(port_str) = port else { return Err("Port is required".to_string()); }; let Ok(port) = port_str.parse::<u16>() else { return Err(format!("Invalid port: {}", port_str)); }; Ok((host, port)) } fn main() { let result = process_config( Some("localhost".to_string()), Some("8080".to_string()), ); println!("{:?}", result); // Ok(("localhost", 8080)) }
Pattern với slices
fn get_first_two(numbers: &[i32]) -> Result<(i32, i32), &'static str> { let [first, second, ..] = numbers else { return Err("Need at least 2 elements"); }; Ok((*first, *second)) } fn main() { println!("{:?}", get_first_two(&[1, 2, 3])); // Ok((1, 2)) println!("{:?}", get_first_two(&[1])); // Err(...) }
Combining với other patterns
let-else + if-let
#![allow(unused)] fn main() { fn process_nested(value: Option<Option<i32>>) { let Some(inner) = value else { println!("Outer None"); return; }; if let Some(number) = inner { println!("Number: {}", number); } else { println!("Inner None"); } } }
let-else + while-let
#![allow(unused)] fn main() { fn process_iterator(mut iter: impl Iterator<Item = Result<i32, String>>) { while let Some(result) = iter.next() { let Ok(value) = result else { eprintln!("Skipping error"); continue; }; println!("Value: {}", value); } } }
Best practices
✅ Use khi có early return
#![allow(unused)] fn main() { // ✅ Good: early return fn process(value: Option<i32>) -> Result<i32, String> { let Some(v) = value else { return Err("No value".to_string()); }; Ok(v * 2) } }
❌ Avoid cho simple cases
#![allow(unused)] fn main() { // ❌ Overkill for simple case let Some(value) = option else { panic!("No value"); }; // ✅ Better let value = option.expect("No value"); }
✅ Use cho validation chains
#![allow(unused)] fn main() { fn validate_user( name: Option<String>, email: Option<String>, age: Option<u32>, ) -> Result<User, String> { let Some(name) = name else { return Err("Name required".to_string()); }; let Some(email) = email else { return Err("Email required".to_string()); }; let Some(age) = age else { return Err("Age required".to_string()); }; // All validations passed Ok(User { name, age }) } }
Error messages
#![allow(unused)] fn main() { fn parse_number(s: &str) -> Result<i32, String> { let Ok(num) = s.parse::<i32>() else { // Có thể customize error message return Err(format!( "Failed to parse '{}' as number", s )); }; Ok(num) } }
let-else vs unwrap/expect
#![allow(unused)] fn main() { // ❌ Using unwrap (panics) let value = option.unwrap(); // ❌ Using expect (panics with message) let value = option.expect("Value required"); // ✅ Using let-else (controlled error handling) let Some(value) = option else { return Err("Value required".to_string()); }; }
Real-world example: Request handling
#![allow(unused)] fn main() { #[derive(Debug)] struct Request { path: String, method: String, body: Option<String>, } fn handle_create_user(req: Request) -> Result<String, String> { // Validate method if req.method != "POST" { return Err(format!("Expected POST, got {}", req.method)); } // Validate body exists let Some(body) = req.body else { return Err("Request body is required".to_string()); }; // Parse body as JSON (simplified) let Ok(user_data) = serde_json::from_str::<serde_json::Value>(&body) else { return Err("Invalid JSON".to_string()); }; // Extract name let Some(name) = user_data.get("name").and_then(|v| v.as_str()) else { return Err("Missing 'name' field".to_string()); }; Ok(format!("Created user: {}", name)) } }
Limitations
- else block must diverge: Phải return, break, continue, hoặc panic
- Cannot use với if-let chains: Chỉ work với
letstatements - Pattern must be refutable: Không work với irrefutable patterns