14 điểm bởi xguru 2024-11-05 | 3 bình luận | Chia sẻ qua WhatsApp

letconst trong Rust

  • let được dùng để khai báo biến mới
    • Có dạng let PAT = EXPR;, và mạnh hơn vẻ ngoài của nó
    • Khi kết hợp với pattern matching, nó cung cấp những tính năng tiện lợi
      • let (a, b) = (5, 10);
      • let maybe_string: Option<String> = ..;
      • let Some(value) = maybe_string else { panic!("die horribly")};
  • const là hằng số được tính tại thời điểm biên dịch và được nhúng trực tiếp vào mã đã biên dịch
    • const MY_VAR: &str = "heyyyyyyyy man"; const SECRET: i32 = 0x1234;
    • Có dạng const IDENT: TYPE = EXPR;, bắt buộc phải ghi rõ kiểu và không thể dùng pattern

Điểm gây bối rối

  • const có thể được dùng bất kể thứ tự khai báo (hoisting)
// Vẫn biên dịch dù X được định nghĩa sau Y  
const Y: i32 = X + X;  
const X: i32 = 5;  
  • Cũng có thể được khai báo bên trong hàm, và khi đó vẫn được hoisting
fn oh_boy() -> i32 {  
	return X;  
	const X: i32 = 5;  
	// ^ Biên dịch và chạy được. Không có warning!  
}  
  • Nếu bạn làm việc với một lập trình viên mới học Rust, xuất thân từ JavaScript, đây là một tính năng rất dễ khiến họ hoang mang
  • Đó là một hệ quả vô hại của một tính năng hay, nhưng giờ ta hãy viết ra hệ quả có hại

Match trong Rust

// let PAT = EXPR;  
let x = 5;  
  
// Ở đây, `x` là pattern. Nó kiểm tra xem có thể đưa `5` vào `x` hay không  
// Pattern này luôn khớp -- vì luôn có thể đưa 5 vào biến tên là `x`  
  
// Không phải mọi pattern đều nhất thiết phải khớp. Ví dụ:  
let (5, x) = (a, b);  
// Ở đây biểu thức chỉ "khớp" với pattern nếu a == 5  
//  
// Đây được gọi là pattern "có thể bị bác bỏ" (refutable)  
//  
// Trong khai báo `let`, pattern refutable phải xử lý trường hợp bị "từ chối":  
let (5, x) = (a, b) else { panic!() };  
//  
// ...nếu không bạn có thể có một biến chỉ "tồn tại có điều kiện", và điều đó không tốt  
  • Vậy hãy nói về match. match là gì?
// match là một danh sách các pattern và việc cần làm nếu khớp với chúng  
//  
// match EXPR {  
//    PAT => EXPR  
//    PAT => EXPR  
//    ..  
// }  
  
match (a, b) {  
	(5, x) => {  
		// Nếu (a,b) khớp với (5,x), khối này sẽ chạy  
	},  
	(x, 5) => {  
		// Tương tự: nếu (a,b) khớp với (x, 5) thì..  
	},  
	(x, y) => {  
		// Và đây là pattern "bắt mọi thứ", giống với cách let (x,y) = (a,b) hoạt động  
	}  
}  

Hãy tạo ra đau khổ

  • Làm người khác bối rối thì vui đấy, nhưng còn việc gây ra bất hạnh thực sự và bug thật thì sao?
  • Theo tôi, đây là cú pháp tinh vi nhất của Rust:
    • Câu thú vị nhất trong bài này: cú pháp tinh vi nhất của Rust là chính hằng số cũng là một pattern
  • Cú pháp này thêm một số ergonomic tốt quanh việc matching:
let input: i32 = ..;  
  
const GOOD: i32 = 1;  
const BAD: i32 = 2;  
  
match input {  
	// Dòng này kiểm tra input == GOOD, vì GOOD là một hằng số  
	GOOD => println!("input was 1"),  
	// Dòng này kiểm tra input == BAD, vì BAD là một hằng số.  
	BAD => println!("input was 2"),  
	// Dòng này định nghĩa otherwise = input, và luôn khớp...  
	otherwise => println!("input was {otherwise}"),  
}  

Tuy nhiên, việc viết hằng số bằng chữ in hoa chỉ là quy ước. Trình biên dịch chỉ cảnh báo bạn đừng làm khác đi.

const good: i32 = 1;  
const bad: i32 = 2;  
match input {  
	// Ừm...  
	good => {},  
	bad => {},  
	otherwise => {},  
}  

Giờ chúng ta có ba nhánh trông giống hệt nhau, nhưng việc chúng làm lại phụ thuộc vào việc có tồn tại hằng số mang tên đó hay không!
Hãy làm nó tệ hơn nữa. Chuyện gì xảy ra ở dưới đây?

const GOOD: i32 = 1;  
match input {  
	// Gõ nhầm...  
	GOD => println!("input was 1"),  
	otherwise => println!("input was not 1")  
}  

Ở đây trình biên dịch sẽ cảnh báo, nhưng đoạn mã này sẽ luôn in ra input was 1
Hoặc một ví dụ thực tế hơn:

// Oops, lỡ comment hoặc xóa import này rồi  
// use crate::{SOME_GL_CONSTANT, OTHER_THING}  
  
// Ôi không!  
match value {  
	SOME_GL_CONSTANT => ..,  
	OTHER_THING => ..,  
	_ => ..,  
}  

Điều này khiến mọi người bối rối. Đặc biệt là khi họ đang thử những thứ hay ho với enum.

enum MyEnum {  
	A, B, C  
}  
  
// Bình thường người ta viết thế này  
match value {  
	MyEnum::A => ..,  
	MyEnum::B => ..,  
	MyEnum::C => ..,  
}  
  
// Nhưng bạn cũng có thể viết thế này  
use MyEnum::*;  
match value {  
	A => {},  
	B => {},  
	C => {}  
}  
// Và rồi, nếu bạn thay đổi MyEnum...  
enum MyEnum { A, B, D, E };  
use MyEnum::*;  
  
// Nó vẫn biên dịch được!  
match value {  
	A => {},  
	B => {},  
	C => {},  
}  
  
// `C` giờ trở thành pattern "bắt mọi thứ", vì không còn thứ gì tên `C` trong phạm vi nữa.  
// Bạn đang làm let C = value, và nó luôn khớp!!!  

Clippy có rất nhiều rule để cảnh báo bạn đừng làm vậy, vì chuyện này luôn khiến người ta bối rối.
Nhưng còn có thể gây bối rối hơn nữa:

// bind x với 5 theo cách irrefutable...  
let x = 5;  
  
// ...khoan đã...  
const x: i32 = 4;  

Đoạn mã này sẽ không biên dịch. Bởi vì const x là một pattern, hằng số thì được hoisting, và giờ đoạn mã này được đánh giá như sau:

let 4 = 5;  
  
// error[E0005]: refutable pattern in local binding  
//  --> src/main.rs:3:5  
//   |  
// 3 | let x = 5;  
//   |     ^  
//   |     |  
//   |     các pattern `i32::MIN..=3_i32` và `5_i32..=i32::MAX` không được bao phủ  
//   |     các pattern bị thiếu vì `x` được hiểu là pattern hằng số chứ không phải biến mới  
//   |     trợ giúp: hãy giới thiệu một biến khác: `x_var`  
//   |  
//   = ghi chú: binding `let` yêu cầu một "irrefutable pattern", ví dụ như `struct` hoặc `enum` chỉ có một variant  

"expr bằng 4" không phải là một phép khớp không thể bị bác bỏ, và nó không xử lý trường hợp không phải như vậy

Làm phiền tất cả những người xung quanh

// Giả sử `maybe` là Option<&str>. Nó có thể là một đoạn văn bản, hoặc là None.  
let maybe_username: Option<&str> = ..;  
  
// Đây là pattern Rust phổ biến trong one-line match. Nếu nó khớp với Some(..) thì ta có thể làm gì đó với chuỗi đó.  
if let Some(username) = maybe_username {  
	// Vậy nên đoạn mã này sẽ chạy nếu username tồn tại...  
	return username.to_uppercase();  
}  
  
// Nhưng mà... bây giờ đoạn mã đó chỉ chạy khi 'username' khớp với Some("hey")  
const username: &str = "hey";  

Sự kết hợp giữa hoisting của hằng số và việc hằng số là pattern cho phép bạn viết những đoạn Rust như câu đố

Đây không phải vấn đề thực sự

  • Thực tế mà nói, lý do duy nhất khiến chuyện này có thể gây bối rối là vì bạn có thể viết let UPPERCASEconst lowercase
  • Nếu việc tạo biến bắt đầu bằng chữ in hoa là lỗi lint, thì sự bối rối đã không xảy ra
    • Bạn sẽ không thể vô tình bind thứ gì đó khi đang cố match với enum variant hay hằng số
  • Nhưng nói cho rõ, đây chỉ là một nét kỳ quặc thú vị của ngôn ngữ mà thôi
macro_rules! f {  
  ($cond: expr) => {  
    if let Some(x) = $cond {  
      println!("i am some == {x}!");  
    } else {  
      println!("i am none");  
    }  
  }  
}  
  
fn main() {  
    f!(Some(100));  
  
    {  
        f!(Some(100));  
        return;  
  
        const x: i32 = 5;  
    }  
}  

3 bình luận

 
sunrabbit 2024-11-05

Thực ra đây không phải vấn đề quá lớn, vì trong hầu hết môi trường phát triển đều có language server
và nó sẽ tự suy luận rồi hiển thị hết ở đó.

rust-analyzer, nền tảng của language server trong RustRover, là một công cụ khá mạnh

 
sunrabbit 2024-11-05

Chỉ là gom lại những kiểu dark pattern có ở bất kỳ ngôn ngữ nào
kiểu như: cái này có thể gây nhầm lẫn!

đại khái là một bài viết với cảm giác như vậy thôi

 
kayws426 2024-11-05

Ồ... đúng là kiểu vậy nhỉ. Không biết Rust định xử lý chuyện này thế nào?