ไวยากรณ์ที่แนบเนียนที่สุดของ Rust
(zkrising.com)let และ const ของ Rust
letใช้สำหรับประกาศตัวแปรใหม่- อยู่ในรูป
let PAT = EXPR;ซึ่งทรงพลังกว่าที่เห็น - เมื่อใช้ร่วมกับ pattern matching ก็ให้ความสามารถที่สะดวกมาก
let (a, b) = (5, 10);let maybe_string: Option<String> = ..;let Some(value) = maybe_string else { panic!("die horribly")};
- อยู่ในรูป
constคือค่าคงที่ที่คำนวณตอนคอมไพล์และถูกฝังเข้าไปในโค้ดที่คอมไพล์แล้วโดยตรงconst MY_VAR: &str = "heyyyyyyyy man";const SECRET: i32 = 0x1234;- อยู่ในรูป
const IDENT: TYPE = EXPR;ต้องระบุชนิด และใช้ pattern ไม่ได้
จุดที่ทำให้งง
constสามารถถูกใช้งานได้โดยไม่ต้องสนใจลำดับการประกาศ (hoisting)
// คอมไพล์ได้แม้ X จะถูกประกาศหลัง Y
const Y: i32 = X + X;
const X: i32 = 5;
- ประกาศภายในฟังก์ชันได้ด้วย และยัง hoist ได้ในสถานะนั้นอีก
fn oh_boy() -> i32 {
return X;
const X: i32 = 5;
// ^ คอมไพล์ได้และทำงานได้ ไม่มี warning!
}
- ถ้าคุณทำงานร่วมกับโปรแกรมเมอร์ที่เพิ่งเริ่มเรียน Rust และมาจาก JavaScript ฟีเจอร์นี้เหมาะมากสำหรับทำให้พวกเขางง
- นี่เป็นผลลัพธ์ที่ไม่อันตรายของฟีเจอร์อันยอดเยี่ยม แต่ตอนนี้ลองเขียนผลลัพธ์ที่อันตรายกันดู
Match ของ Rust
// let PAT = EXPR;
let x = 5;
// ในกรณีนี้ `x` คือ pattern เรากำลังตรวจว่าเอา `5` ใส่เข้าไปใน `x` ได้ไหม
// pattern นี้ match เสมอ -- เพราะสามารถใส่ 5 ลงในตัวแปรชื่อ `x` ได้เสมอ
// ไม่ใช่ทุก pattern ที่จะ match ได้เสมอไป ตัวอย่างเช่น:
let (5, x) = (a, b);
// ที่นี่ expression จะ "match" กับ pattern ก็ต่อเมื่อ a == 5
//
// สิ่งนี้เรียกว่า pattern แบบ "refutable"
//
// ในการประกาศ `let` pattern แบบ refutable ต้องจัดการกรณีที่ถูก "ปฏิเสธ" ด้วย:
let (5, x) = (a, b) else { panic!() };
//
// ...ไม่เช่นนั้นคุณอาจได้ตัวแปรที่ "มีอยู่แบบมีเงื่อนไข" ซึ่งไม่ใช่เรื่องดี
- งั้นมาดู
matchกันmatchคืออะไร?
// match คือรายการของ pattern พร้อมสิ่งที่จะทำเมื่อ match กัน
//
// match EXPR {
// PAT => EXPR
// PAT => EXPR
// ..
// }
match (a, b) {
(5, x) => {
// ถ้า (a,b) match กับ (5,x) ก็จะรันบล็อกนี้
},
(x, 5) => {
// แบบเดียวกัน: ถ้า (a,b) match กับ (x, 5) ...
},
(x, y) => {
// และนี่คือ pattern แบบ "จับทุกอย่าง" ซึ่งทำงานเหมือน let (x,y) = (a,b)
}
}
มาสร้างความเจ็บปวดกัน
- การทำให้คนอื่นสับสนก็สนุกดี แต่จะเป็นยังไงถ้ามันนำไปสู่ความทุกข์เต็มรูปแบบและบั๊กจริง ๆ?
- สำหรับผม นี่คือไวยากรณ์ที่ แนบเนียนที่สุด ของ Rust:
- ประโยคที่น่าสนใจที่สุดของบทความนี้: ไวยากรณ์ที่แนบเนียนที่สุดของ Rust คือการที่ค่าคงที่นั้นเป็น pattern ได้ด้วย
- ไวยากรณ์นี้เพิ่ม ergonomics ที่ดีบางอย่างให้กับการ match:
let input: i32 = ..;
const GOOD: i32 = 1;
const BAD: i32 = 2;
match input {
// อันนี้เช็กว่า input == GOOD เพราะ GOOD เป็นค่าคงที่
GOOD => println!("input was 1"),
// อันนี้เช็กว่า input == BAD เพราะ BAD เป็นค่าคงที่
BAD => println!("input was 2"),
// อันนี้คือประกาศ otherwise = input และ match เสมอ...
otherwise => println!("input was {otherwise}"),
}
แต่การเขียนค่าคงที่ด้วยตัวพิมพ์ใหญ่นั้นเป็นเพียงธรรมเนียมเท่านั้น คอมไพเลอร์แค่เตือนว่าไม่ควรทำแบบอื่น
const good: i32 = 1;
const bad: i32 = 2;
match input {
// เอ่อ...
good => {},
bad => {},
otherwise => {},
}
ตอนนี้เรามีสามแขนงที่หน้าตาเหมือนกัน แต่สิ่งที่มันทำขึ้นอยู่กับว่ามีค่าคงที่ชื่อนั้นอยู่หรือไม่!
ให้มันแย่ลงไปอีก ด้านล่างนี้จะเกิดอะไรขึ้น?
const GOOD: i32 = 1;
match input {
// พิมพ์ผิด...
GOD => println!("input was 1"),
otherwise => println!("input was not 1")
}
ตรงนี้คอมไพเลอร์จะเตือน แต่โค้ดนี้จะพิมพ์ input was 1 ออกมาเสมอ
หรือถ้าเอาแบบใกล้เคียงความจริงมากขึ้น:
// โอ๊ะ เผลอคอมเมนต์หรือเผลอลบ import นี้ไป
// use crate::{SOME_GL_CONSTANT, OTHER_THING}
// แย่แล้ว!
match value {
SOME_GL_CONSTANT => ..,
OTHER_THING => ..,
_ => ..,
}
สิ่งนี้ทำให้คนสับสน โดยเฉพาะตอนที่พวกเขาพยายามทำอะไรเท่ ๆ กับ enum
enum MyEnum {
A, B, C
}
// ปกติจะเขียนแบบนี้
match value {
MyEnum::A => ..,
MyEnum::B => ..,
MyEnum::C => ..,
}
// แต่คุณจะเขียนแบบนี้ก็ได้
use MyEnum::*;
match value {
A => {},
B => {},
C => {}
}
// แล้วถ้าคุณแก้ MyEnum ทีหลัง...
enum MyEnum { A, B, D, E };
use MyEnum::*;
// อันนี้ยังคอมไพล์ผ่าน!
match value {
A => {},
B => {},
C => {},
}
// ตอนนี้ `C` กลายเป็น pattern แบบ "จับทุกอย่าง" แล้ว เพราะไม่มีอะไรชื่อ `C` อยู่ใน scope
// คุณกำลังทำ let C = value อยู่ ซึ่งมัน match เสมอ!!!
Clippy มีกฎเตือนมากมายว่าอย่าทำแบบนี้ เพราะมันทำให้คนงงตลอดเวลา
แต่เรื่องนี้ยังทำให้งงได้อีก:
// bind x เข้ากับ 5 แบบ irrefutable...
let x = 5;
// ...เดี๋ยวก่อน...
const x: i32 = 4;
โค้ดนี้คอมไพล์ไม่ผ่าน เพราะ const x คือ pattern, ค่าคงที่ถูก hoist, และตอนนี้โค้ดนี้จึงถูกตีความเหมือนกับ:
let 4 = 5;
// error[E0005]: refutable pattern in local binding
// --> src/main.rs:3:5
// |
// 3 | let x = 5;
// | ^
// | |
// | pattern `i32::MIN..=3_i32` และ `5_i32..=i32::MAX` ไม่ถูกครอบคลุม
// | pattern ที่หายไปไม่ถูกครอบคลุม เพราะ `x` ถูกตีความเป็น constant pattern ไม่ใช่ตัวแปรใหม่
// | help: ให้ประกาศตัวแปรแทน: `x_var`
// |
// = note: `let` binding ต้องการ "irrefutable pattern" เช่น `struct` หรือ `enum` ที่มีเพียง variant เดียว
การ match ว่า "expr เท่ากับ 4" ไม่ใช่การ match แบบหักล้างไม่ได้ และมันไม่ได้จัดการกรณีอื่นไว้
ทำให้คนรอบตัวรำคาญกันให้หมด
// สมมติว่า `maybe` เป็น Option<&str> อาจเป็นข้อความบางอย่าง หรืออาจเป็น None ก็ได้
let maybe_username: Option<&str> = ..;
// นี่คือแพตเทิร์นปกติของ Rust สำหรับ match แบบบรรทัดเดียว ถ้ามัน match กับ Some(..) เราก็ทำอะไรบางอย่างกับสตริงนั้นได้
if let Some(username) = maybe_username {
// ดังนั้นโค้ดนี้จะรันเมื่อ username มีอยู่...
return username.to_uppercase();
}
// แต่เดี๋ยวก่อน... ตอนนี้โค้ดนั้นจะรันก็ต่อเมื่อ 'username' match กับ Some("hey") เท่านั้น
const username: &str = "hey";
การรวมกันของ hoisting ของค่าคงที่กับความจริงที่ว่าค่าคงที่เป็น pattern ได้ ทำให้คุณสามารถเขียน Rust ที่ชวนปวดหัวได้
นี่ไม่ใช่ปัญหาจริง ๆ
- ในทางปฏิบัติ เหตุผลเดียวที่สิ่งนี้จะทำให้สับสนได้ก็คือคุณสามารถเขียน
let UPPERCASEและconst lowercaseได้ - ถ้าการสร้างตัวแปรที่ขึ้นต้นด้วยตัวพิมพ์ใหญ่เป็น lint error ความสับสนนี้ก็คงไม่เกิดขึ้น
- เพราะคุณจะไม่เผลอ bind อะไรบางอย่าง ทั้งที่จริงตั้งใจจะ match กับ enum variant หรือค่าคงที่
- แต่ขอให้ชัดเจนว่านี่เป็นเพียงความประหลาดที่น่าสนุกของภาษาเท่านั้น
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 ความคิดเห็น
จริงๆ แล้วก็ไม่ใช่ปัญหาใหญ่อะไร เพราะสภาพแวดล้อมการพัฒนาส่วนใหญ่ก็มี language server อยู่แล้ว
มันจะอนุมานทุกอย่างแล้วแสดงให้ดูจากตรงนั้น
rust-analyzerซึ่งเป็นรากฐานของ language server ใน RustRover ก็เป็นเครื่องมือที่ทรงพลังพอตัวเลยก็แค่รวบรวมพวก dark pattern ที่มีอยู่ในภาษาต่าง ๆ ทั่วไป
อันนี้อาจทำให้เกิดความสับสนได้!
ก็เป็นบทความประมาณความรู้สึกแบบนี้แหละ
โห... ชวนอึ้งเหมือนกันนะ แล้ว Rust วางแผนจะจัดการเรื่องนี้ยังไงกันล่ะ?