14 คะแนน โดย xguru 2024-11-05 | 3 ความคิดเห็น | แชร์ทาง WhatsApp

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 ความคิดเห็น

 
sunrabbit 2024-11-05

จริงๆ แล้วก็ไม่ใช่ปัญหาใหญ่อะไร เพราะสภาพแวดล้อมการพัฒนาส่วนใหญ่ก็มี language server อยู่แล้ว
มันจะอนุมานทุกอย่างแล้วแสดงให้ดูจากตรงนั้น

rust-analyzer ซึ่งเป็นรากฐานของ language server ใน RustRover ก็เป็นเครื่องมือที่ทรงพลังพอตัวเลย

 
sunrabbit 2024-11-05

ก็แค่รวบรวมพวก dark pattern ที่มีอยู่ในภาษาต่าง ๆ ทั่วไป
อันนี้อาจทำให้เกิดความสับสนได้!

ก็เป็นบทความประมาณความรู้สึกแบบนี้แหละ

 
kayws426 2024-11-05

โห... ชวนอึ้งเหมือนกันนะ แล้ว Rust วางแผนจะจัดการเรื่องนี้ยังไงกันล่ะ?