file .rs เดิมทุก byte เปลี่ยนแค่บรรทัด edition แล้วห้าในหกโปรแกรมให้ผลคนละอย่าง
ความเชื่อที่ทำให้เรื่องนี้ผ่านการรีวิวไปได้คือ “edition เปลี่ยนแค่ไวยากรณ์”
หน้านี้เอาความเชื่อนั้นไปวัด ซอร์ส 6 file ถูกคอมไพล์สองครั้ง ด้วย package
สองใบที่ต่างกันแค่บรรทัด edition ไม่มีการคัดลอกซอร์ส ทั้งสองใบชี้มาที่ file เดียวกัน
ผลคือ 5 จาก 6 ให้ผลคนละอย่าง
เคสที่หักคำทำนาย closure ที่ move แล้วหยุดเป็น Send
หัวข้อที่มีชื่อว่า “เคสที่หักคำทำนาย closure ที่ move แล้วหยุดเป็น Send”คนที่เคยได้ยินว่า edition 2021 ทำให้ borrow checker ผ่อนคลายลง จะทายว่าทุกเคส เดินไปทางเดียวกันคือจากแดงเป็นเขียว
เคสนี้เดินย้อนทาง
use std::thread;
struct Ptr(*mut i32);unsafe impl Send for Ptr {}
fn main() { let mut v = 10; let px = Ptr(&mut v as *mut i32); let t = thread::spawn(move || unsafe { *(px.0) += 10; }); t.join().unwrap();}Ptr ประกาศตัวเองว่าเป็น Send ส่วนของที่อยู่ข้างในคือ *mut i32 ซึ่งไม่ใช่
บน edition 2018 closure จับ px ทั้งก้อน สิ่งที่ข้ามเธรดไปจึงเป็น Ptr และมันเป็น
Send โปรแกรมคอมไพล์ผ่าน
บน edition 2021 closure จับแค่ px.0 สิ่งที่ข้ามเธรดไปจึงเป็น raw pointer เปล่า ๆ
ซึ่งไม่ใช่ Send โปรแกรมแดงด้วย E0277
Send กับ Sync ของ closure ไม่ได้ประกาศไว้ที่ไหน มันถูกอนุมานจากสิ่งที่ closure
จับเข้าไป · เมื่อ edition 2021 เปลี่ยนว่าจับอะไร มันจึงเปลี่ยนว่า closure ตัวนั้น
ข้ามเธรดได้หรือไม่ได้ ทั้งที่ผู้เขียนไม่ได้แตะบรรทัดไหนเลย
E0277 ตัวนี้เป็นตัวเดียวกับที่คอร์ส
Rust ตั้งแต่ศูนย์ และ
async กับ Tokio สอนให้อ่านมาแล้ว
กฎเดียวที่อยู่ใต้ทั้งหกเคส
หัวข้อที่มีชื่อว่า “กฎเดียวที่อยู่ใต้ทั้งหกเคส”edition 2018 ให้ closure จับ ตัวแปร ส่วน edition 2021 ให้มันจับ place คือ เส้นทางที่ชี้ลงไปถึง field ที่ใช้จริง
a.y จึงเป็นสิ่งที่ถูกจับ ไม่ใช่ a ทั้งก้อน
ทุกอย่างที่เหลือในหน้านี้เป็นผลของกฎข้อเดียวนี้ ทั้งข้อที่ทำให้ code ที่เคยแดง กลายเป็นเขียว และข้อที่ทำให้ code ที่เคยเขียวกลายเป็นแดง
ตารางทั้งหกเคส
หัวข้อที่มีชื่อว่า “ตารางทั้งหกเคส”รันบน cargo 1.97.1 (c980f4866 2026-06-30)
| เคส | edition 2018 | edition 2021 | lint เตือนไหม |
|---|---|---|---|
partial_move | E0505 | ผ่าน | เงียบ |
field_mut | E0502 | ผ่าน | เงียบ |
move_partial | E0382 | ผ่าน | เงียบ |
send_regression | ผ่าน | E0277 | เตือน |
drop_order | ผ่าน | ผ่าน | เตือน |
unchanged | ผ่าน | ผ่าน | เงียบ |
สามเคสแรกคือทิศที่คนคาดไว้ แดงเป็นเขียว เพราะ closure เลิกจับของที่ไม่ได้ใช้
partial_move แดงเพราะ drop(a.x) ชนกับ closure ที่จับ a ทั้งก้อน
· field_mut แดงเพราะการยืม &r.rb ชนกับ closure ที่ยืม r ทั้งก้อนแบบ mut
· ทั้งสองเคส field ที่ชนกันเป็นคนละ field กันแท้ ๆ
move ไม่ได้แปลว่าเอาทั้ง struct อีกต่อไป
หัวข้อที่มีชื่อว่า “move ไม่ได้แปลว่าเอาทั้ง struct อีกต่อไป”เคส move_partial เป็นข้อที่หักความเข้าใจของคนมากที่สุด เพราะคำว่า move
อ่านแล้วเหมือนบอกว่า “เอาไปทั้งหมด”
struct S { a: String, b: String,}
fn main() { let s = S { a: String::from("a"), b: String::from("b"), }; let c = move || println!("{}", s.a); println!("{}", s.b); c();}บน edition 2018 บรรทัดที่พิมพ์ s.b แดงด้วย E0382 เพราะ
move ย้าย s ทั้งก้อนเข้า closure ไปแล้ว
บน edition 2021 move ย้ายเฉพาะ s.a ส่วน s.b ยังเป็นของ main อยู่ โปรแกรมจึงผ่าน
move บังคับให้สิ่งที่ถูกจับ ถูกจับด้วยการย้ายค่า แทนการยืม · ส่วนคำถามว่า
อะไร ถูกจับ เป็นคำถามที่ edition เป็นคนตอบ และคำตอบเปลี่ยนไปแล้ว
เคสที่ไม่มีอะไรแดงให้เห็น
หัวข้อที่มีชื่อว่า “เคสที่ไม่มีอะไรแดงให้เห็น”drop_order คอมไพล์ผ่านทั้งสอง edition ไม่มี error ไม่มี warning จากตัวคอมไพเลอร์
ในการบิลด์ปกติ
แต่มันพิมพ์คนละลำดับ
| edition | สิ่งที่พิมพ์ออกมา |
|---|---|
| 2018 | drop t.0 → drop t.1 → after call |
| 2021 | drop t.0 → after call → drop t.1 |
closure จับทูเพิลที่มีสมาชิกสองตัวซึ่งมี Drop เป็นของตัวเอง
บน 2018 closure จับทั้งทูเพิล สมาชิกตัวที่สองจึงตายพร้อมกับ closure ตอนถูกเรียก
· บน 2021 closure จับแค่สมาชิกตัวแรก สมาชิกตัวที่สองจึงยังเป็นของ main
และตายตอนจบฟังก์ชัน
ถ้าสมาชิกตัวนั้นถือ lock ถือ transaction ที่ยังไม่ commit หรือถือ file ที่ยังไม่ flush การเลื่อนจุดตายคือการเลื่อนจุดที่ผลข้างเคียงเกิด · เทสต์ที่ตรวจเฉพาะค่าที่คืนออกมา จะไม่เห็นความต่างนี้เลย
lint เตือนใคร และทำไมมันเงียบใส่อีกสามเคส
หัวข้อที่มีชื่อว่า “lint เตือนใคร และทำไมมันเงียบใส่อีกสามเคส”rustc มี lint สำหรับการย้าย edition อยู่ตัวหนึ่งชื่อ
rust-2021-incompatible-closure-captures
เปิดมันบนต้นไม้ edition 2018 ทั้งหกเคส แล้วนับ มันเตือน 2 เคส คือ send_regression กับ drop_order
สามเคสที่แดงเป็นเขียว มันเงียบสนิท
ตอนแรกผลนี้อ่านเหมือน lint ทำงานไม่ครบ แต่ไม่ใช่ · เคสที่มันเงียบใส่คือเคสที่ คอมไพล์ไม่ผ่านบน edition 2018 อยู่แล้ว และ code ที่ยังคอมไพล์ไม่ผ่าน ไม่มีอะไรให้ย้าย
เคสที่คอมไพล์ผ่านบน 2018 มีอยู่ 3 เคส · ในนั้น 2 เคสเปลี่ยนพฤติกรรม และ lint เตือนครบทั้ง 2 · ที่เหลือคือช่องควบคุม และ lint เงียบใส่มันอย่างที่ควรเป็น
สิ่งที่ lint นี้ทำ คือชี้จุดที่ code จะยัง คอมไพล์ผ่าน หลังย้าย แต่ทำงานคนละอย่าง · ส่วนจุดที่ย้ายแล้วแดง คอมไพเลอร์เป็นคนบอกเองอยู่แล้วตอนบิลด์ ไม่ต้องมี lint มาบอกซ้ำ
ผลที่ตามมาสำหรับคนย้าย edition จริง: การที่บิลด์ผ่านหลังย้าย ไม่ใช่หลักฐานว่าย้ายสำเร็จ ต้องอ่าน warning ของ lint ตัวนี้ด้วย
ข้อความที่ lint พ่นออกมามีคำแนะนำติดมาด้วย คือให้เติม let _ = &t; เข้าไปในตัว closure
เพื่อบังคับให้มันจับทั้งก้อนเหมือนเดิม
เราลองทำตามแล้ววัดซ้ำ ผลคือลำดับ drop กลับมาเหมือนกันทั้งสอง edition และ warning ก็หายไปพร้อมกัน สองอย่างนี้ผูกกันอยู่จริง ไม่ใช่คนละเรื่องที่บังเอิญมาด้วยกัน
สิ่งที่หน้านี้ไม่ได้วัด
หัวข้อที่มีชื่อว่า “สิ่งที่หน้านี้ไม่ได้วัด”- edition 2024 หน้านี้เทียบ 2018 กับ 2021 เท่านั้น
- closure ใน async block ซึ่งมีกฎการจับของตัวเองเพิ่มเข้ามาอีกชั้น
rustcรุ่นอื่น ทุกแถวในตารางมาจาก cargo 1.97.1 (c980f4866 2026-06-30) รุ่นเดียว- ข้อความของ error ตัววัดตรึงเฉพาะรหัส เพราะข้อความขยับได้ทุกรุ่น ส่วนรหัสไม่ขยับ
ตัววัดอยู่ที่ scripts/rust-closure-capture/ และรันด้วย npm run verify:closure-capture
· มันอยู่ในเกตของ CI จึงมีคนรันทุกครั้งที่มีคน push ไม่ใช่ตัวเลขที่วัดครั้งเดียวแล้ววางไว้