คุณเคยรู้สึกไหมเวลาที่คุณเริ่มเรียนภาษาใหม่แล้วคาดหวังว่าจะวิ่งฉิว แต่กลับสะดุดตั้งแต่ก้าวแรก? นั่นคือสิ่งที่ผมเจอตอนเริ่มเรียน Rust ผมมียูทิลิตี้ JavaScript ที่ใช้งานได้ดีอยู่ตัวหนึ่ง — เป็นอะไรที่ทำหน้าที่ parse และเปลี่ยนค่า (mutate) อาร์เรย์ไม่กี่ตัว — และผมคิดว่าผมจะเขียนมันใหม่ในช่วงพักดื่มกาแฟ ผมเปิดเอดิเตอร์ขึ้นมา พิมพ์โค้ดที่รู้สึกว่าดูเป็นธรรมชาติ แล้วก็รันคอมไพเลอร์ ผลคือมันระเบิดออกมา ไม่ใช่ด้วย syntax errors ไม่ใช่เพราะลืมใส่เซมิโคลอน แต่มันบอกผมว่าผมพยายามจะใช้ค่าที่ถูกย้าย (moved) ไปแล้ว ผมจ้องหน้าจอ... ย้ายเหรอ? ผมไม่ได้ย้ายอะไรเลย ผมแค่กำหนดค่ามันให้กับตัวแปรอื่นเท่านั้นเอง

พื้นที่ปลอดภัยของ JavaScript

JavaScript ฝึกให้คุณปฏิบัติกับข้อมูลเหมือนเป็นไวท์บอร์ดส่วนกลาง คุณประกาศออบเจกต์หรืออาร์เรย์ ส่งมันเข้าไปในฟังก์ชัน ให้ฟังก์ชันนั้นเพิ่ม property เข้าไป แล้วก็ส่งมันต่อไปที่อื่น ตัว garbage collector จะนั่งทำงานอยู่เบื้องหลังเหมือนพนักงานทำความสะอาดที่มองไม่เห็น คอยรอเก็บกวาดทุกอย่างที่คุณทิ้งไว้ คุณไม่เคยต้องถามว่า "ใครเป็นเจ้าของสตริงนี้?" คุณแค่ถามว่า "ฉันอ่านมันได้ไหม?" ถ้าคำตอบคือใช่ คุณก็จัดการมันได้เลย ถ้าคุณต้องการการอ้างอิง (reference) เพิ่ม คุณก็แค่พิมพ์ let b = a แล้วไปต่อ ทั้งสองตัวแปรจะชี้ไปยังหน่วยความจำตำแหน่งเดียวกัน และถ้า a เปลี่ยนแปลงค่า b ก็จะเห็นการเปลี่ยนแปลงนั้นทันที มันสะดวกดี แต่มันก็วุ่นวายด้วยเช่นกัน เพียงแต่ความวุ่นวายนั้นไม่ค่อยทำอันตรายคุณ เพราะ runtime จะเป็นคนจัดการเรื่องการทำความสะอาดให้เอง

ความตกใจครั้งแรกจากคอมไพเลอร์

Rust ไม่ไว้ใจคุณ ฟังดูอาจจะแรงไปหน่อย แต่นี่คือสิ่งแรกที่คุณจะได้เรียนรู้ ภาษาถูกสร้างขึ้นบนระบบความเป็นเจ้าของ (ownership system) ที่มีกฎเหล็กสามข้อ ข้อแรก ทุกค่าต้องมีเจ้าของเพียงคนเดียวเท่านั้น ข้อที่สอง จะมีเจ้าของได้เพียงคนเดียวในเวลาหนึ่งๆ และข้อที่สาม เมื่อเจ้าของนั้นหลุดออกนอกขอบเขต (out of scope) Rust จะลบค่านั้นทิ้งโดยอัตโนมัติ ไม่มีการใช้ garbage collector ไม่มีการนับการอ้างอิง (reference counting) อยู่เบื้องหลัง เว้นแต่คุณจะเลือกใช้มันเอง มีเพียงกฎสามข้อนี้ที่ถูกบังคับใช้โดยคอมไพเลอร์ก่อนที่โค้ดของคุณจะเริ่มทำงานเสียอีก

ตอนที่ผมเขียนยูทิลิตี้ JavaScript ตัวเดิมในเวอร์ชัน Rust ผมทำในสิ่งที่รู้สึกว่าเป็นธรรมชาติ ผมสร้าง vector ซึ่งเป็นคำตอบของ Rust สำหรับอาร์เรย์ และกำหนดค่าให้ตัวแปรที่สอง จากนั้นผมก็พยายามจะใช้ตัวแปรแรก คอมไพเลอร์ปฏิเสธ ใน JavaScript การเขียน let b = a คือการคัดลอกการอ้างอิง (reference) แต่ใน Rust มันคือการย้ายความเป็นเจ้าของ (moves ownership) ตัวแปรเดิมจะใช้งานไม่ได้อีกต่อไป คอมไพเลอร์ทำแบบนี้เพื่อรับประกันว่าคุณจะไม่มีทางมีสองเส้นทางที่เผลอไปเหยียบย่ำหน่วยความจำตำแหน่งเดียวกันโดยไม่ตั้งใจ ซึ่งเป็นสาเหตุที่ทำให้เกิด data races และบั๊ก use-after-free ในระบบอื่นๆ

ทำไม Rust ถึงชอบแย่งของเล่นคุณไป

ลองดูรูปแบบนี้ใน JavaScript:

let a = [1, 2, 3];
let b = a;
a.push(4);
// Both a and b see 4.

มันเป็นเรื่องปกติจนเป็นสัญชาตญาณ คุณจะไม่หยุดคิดเลยด้วยซ้ำ แต่ใน Rust สัญชาตญาณเดิมๆ นั้นจะทำให้คุณถูกคอมไพเลอร์ปฏิเสธก่อนที่คุณจะพิมพ์เสร็จเสียอีก เมื่อ b เป็นเจ้าของ vector แล้ว a ก็จะเป็นเพียงชื่อที่ว่างเปล่า คุณไม่สามารถ push ข้อมูลลงไปได้ คุณไม่สามารถแม้แต่จะมองมันด้วยซ้ำ เพราะตอนนี้หน่วยความจำนั้นเป็นของ b ไปแล้ว

ความรู้สึกนี้เหมือนเป็นการลงโทษ จนกว่าคุณจะตระหนักได้ว่ามันช่วยป้องกันอะไรไว้ หากตัวแปรสองตัวสามารถเปลี่ยนค่าข้อมูลใน heap ได้พร้อมกันโดยไม่มีการประสานงานกัน คุณจะเสี่ยงต่อการเกิดหน่วยความจำเสียหาย (memory corruption) Rust กำจัดบั๊กประเภทนี้ออกไปทั้งหมดโดยการทำให้การส่งต่อข้อมูลนั้นชัดเจน คอมไพเลอร์ไม่ได้จู้จี้จุกจิก แต่มันกำลังทำหน้าที่เป็นผู้เฝ้าประตู มันบังคับให้คุณต้องตัดสินใจในทุกขั้นตอนว่า โค้ดส่วนไหนของคุณที่มีหน้าที่รับผิดชอบข้อมูลชุดไหน

Borrowing: ทางออกที่จริงๆ แล้วคือฟีเจอร์

แน่นอนว่าถ้าทุกการกำหนดค่าต้องโอนความเป็นเจ้าของไปตลอดกาล การเขียนโปรแกรมคงจะเหนื่อยน่าดู คุณคงต้อง clone ทุกอย่าง ซึ่งเป็นการสิ้นเปลืองทั้งหน่วยความจำและความเร็ว Rust แก้ปัญหานี้ด้วยการ "ยืม" (borrowing)

การยืมช่วยให้คุณใช้ค่าได้โดยไม่ต้องเป็นเจ้าของ มันมีสองประเภท และความแตกต่างนั้นสำคัญมาก

  • Immutable borrows เขียนในรูปแบบ &T: สิ่งนี้ช่วยให้คุณอ่านข้อมูลได้ คุณสามารถมีการยืมแบบนี้กี่ครั้งก็ได้ในเวลาเดียวกั