ข้าม​ไป​ยัง​เนื้อหา

file .rs เดิม​ทุก byte เปลี่ยน​แค่​บรรทัด edition แล้ว​ห้า​ใน​หก​โปรแกรม​ให้​ผล​คนละ​อย่าง

ความ​เชื่อ​ที่​ทำให้​เรื่อง​นี้​ผ่าน​การ​รีวิว​ไป​ได้​คือ “edition เปลี่ยน​แค่​ไวยากรณ์”

หน้า​นี้​เอา​ความ​เชื่อ​นั้น​ไป​วัด ซอร์ส 6 file ถูก​คอม​ไพล์​สอง​ครั้ง ด้วย package สอง​ใบ​ที่​ต่าง​กัน​แค่​บรรทัด edition ไม่มี​การ​คัด​ลอก​ซอร์ส ทั้ง​สอง​ใบ​ชี้​มา​ที่ file เดียวกัน

ผล​คือ 5 จาก 6 ให้​ผล​คนละ​อย่าง

คน​ที่​เคย​ได้ยิน​ว่า 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

การ​จับ​ที่​แคบ​ลง ทำให้ closure เสีย trait ที่​เคย​มี

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 2018edition 2021lint เตือน​ไหม
partial_moveE0505ผ่านเงียบ
field_mutE0502ผ่านเงียบ
move_partialE0382ผ่านเงียบ
send_regressionผ่านE0277เตือน
drop_orderผ่านผ่านเตือน
unchangedผ่านผ่านเงียบ

สาม​เคส​แรก​คือ​ทิศ​ที่​คน​คาด​ไว้ แดง​เป็น​เขียว เพราะ closure เลิก​จับ​ของ​ที่​ไม่​ได้​ใช้

partial_move แดง​เพราะ drop(a.x) ชน​กับ closure ที่​จับ a ทั้ง​ก้อน · field_mut แดง​เพราะ​การ​ยืม &r.rb ชน​กับ closure ที่​ยืม r ทั้ง​ก้อน​แบบ mut · ทั้ง​สอง​เคส field ที่​ชน​กัน​เป็น​คนละ field กัน​แท้ ๆ

เคส 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 ตอบ​คำถาม​ว่า​จับ​อย่างไร ไม่​ได้​ตอบ​ว่า​จับ​อะไร

move บังคับ​ให้​สิ่ง​ที่​ถูก​จับ ถูก​จับ​ด้วย​การ​ย้าย​ค่า แทน​การ​ยืม · ส่วน​คำถาม​ว่า อะไร ถูก​จับ เป็น​คำถาม​ที่ edition เป็น​คน​ตอบ และ​คำ​ตอบ​เปลี่ยน​ไป​แล้ว

drop_order คอม​ไพล์​ผ่าน​ทั้ง​สอง edition ไม่มี error ไม่มี warning จาก​ตัว​คอม​ไพ​เลอ​ร์ ใน​การ​บิ​ลด์​ปกติ

แต่​มัน​พิมพ์​คนละ​ลำดับ

editionสิ่ง​ที่​พิมพ์​ออก​มา
2018drop t.0 → drop t.1 → after call
2021drop t.0 → after call → drop t.1

closure จับ​ทู​เพิล​ที่​มี​สมาชิก​สอง​ตัว​ซึ่ง​มี Drop เป็น​ของ​ตัว​เอง

บน 2018 closure จับ​ทั้ง​ทู​เพิล สมาชิก​ตัว​ที่​สอง​จึง​ตาย​พร้อม​กับ closure ตอน​ถูก​เรียก · บน 2021 closure จับ​แค่​สมาชิก​ตัว​แรก สมาชิก​ตัว​ที่​สอง​จึง​ยัง​เป็น​ของ main และ​ตาย​ตอน​จบ​ฟังก์ชัน

ลำดับ drop คือ​ที่​ที่ side effect อาศัย​อยู่

ถ้า​สมาชิก​ตัว​นั้น​ถือ lock ถือ transaction ที่​ยัง​ไม่ commit หรือ​ถือ file ที่​ยัง​ไม่ flush การ​เลื่อน​จุด​ตาย​คือ​การ​เลื่อน​จุด​ที่​ผล​ข้าง​เคียง​เกิด · เทสต์​ที่​ตรวจ​เฉพาะ​ค่าที่​คืน​ออก​มา จะ​ไม่​เห็น​ความ​ต่าง​นี้​เลย

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 ตัว​นี้​ไม่​ได้​กัน​การ​ย้าย edition ทั้งหมด มัน​กัน​เฉพาะ​การ​ย้าย​ที่​พัง​เงียบ

สิ่ง​ที่ 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 ไม่ใช่​ตัวเลข​ที่​วัด​ครั้ง​เดียว​แล้ว​วาง​ไว้