หนึ่งในจุดเด่นของภาษา Rust คือ การบริหารจัดการ memory ซึ่งโดยทั่วไปแล้ว ภาษาอื่นๆ ก็มีการบริหารจัดการ memory ให้อยู่แล้ว บางภาษาอาจจะใช้สิ่งที่เรียกว่า garbage collection ที่ทำหน้าที่มองหาตัวแปรที่ไม่ได้มีการใช้งานเป็นเวลานานแล้ว และจัดการคืน memory กลับไป แต่ในภาษา Rust จะมีกฏหรือของการใช้งาน memory เพิ่มเข้ามา และเราสามารถรู้ได้ในทันทีว่ามีตัวแปรไหนบ้างที่ไม่ได้ใช้งาน
บทความนี้เรามาลงรายละเอียดเกี่ยวกับเรื่องนี้กัน แต่ก่อนจะไปทำความเข้าใจเกี่ยวกับ ownership นั้น เรามีเรื่องที่ต้องเข้าใจกันก่อน นั่นคือ Stack และ Heam Memory Allocation ซึ่ง ไปอ่านทำความเข้าใจกันก่อนเลย
Variable Scope
เริ่มต้นกันด้วย scope ของตัวแปรกันก่อนเลย ตัวแปรในภาษา rust นั้นจะสามารถใช้งานได้เมื่อมันถูกประกาศขึ้นมา และใช้งานได้เฉพาะภายใน scope { ... }
นั้น เช่น
เราอาจจะจำมันแบบง่ายๆ คือ มันจะใช้งานได้ ภายใต้ scope ที่มันถูกประกาศไว้นั่นเอง
ในกรณีที่เป็นตัวแปร data types ทั่วไปนั้น ค่าจะถูกเก็บลง stack ทำให้ง่ายต่อการค้นหาและจัดการกับมัน rust เลยอนุญาตให้เราสามารถ copy
ค่าไปยังตัวแปรใหม่ตัวไหนก็ได้ โดยที่ตัวแปรนั้นจะยังสามารถใช้งานได้อยู่
หากเราส่งค่าไปยัง function
อื่น ตัวแปร x
และ y
ก็ยังสามารถใช้งานได้เหมือนเดิม
Ownership Rules
ในทางกลับกันหากตัวแปรนั้นถูกเก็บไว้ใน heap (เช่น String) มันจะยุ่งยากกว่าในการหาว่าข้อมูลไหนควรที่จะ clean up ออกไปเมื่อไม่มีการใช้งานแล้ว
ดังนั้น rust เลยสร้างกฏของการเป็นเจ้าของ (ownership) ไว้ดังนี้
- ค่าแต่ละตัวภายใน Rust จะต้องมีเจ้าของ
- ในการทำงานแต่ละครั้งค่าเหล่านั้นจะต้องมีเจ้าของแค่ตัวเดียว
- เมื่อออกจาก scope แล้ว ค่าหรือตัวแปรที่อยู่ใน scope จะถูกลบทิ้ง
กฏ 3 ข้อนี้ จะถูกนำเอามาใช้ในการเขียนโค๊ดของเรา ทีนี้เรามาดูตัวอย่างของ กฏ 3 ทั้งข้อนี้ กัน
Move
เริ่มต้นที่ทำความเข้าใจเรื่องของ ค่าแต่ละค่าจะต้องมีเจ้าของ และ ต้องมีเข้าของเพียงตัวเดียวเท่านั้น
การ assign ค่าไปยังอีกตัวแปรหนึ่งที่ เก็บบน heap
นั่น เราจะเรียกมันว่า การ Move
ซึ่งต่างจากตัวแปรที่เก็บลงบน stack ที่ใช้ใช้การ copy
ข้อมูลแทน (แทนการเปลี่ยนเจ้าของ)
ดังนั้น เมื่อเราทำการ move
ค่าจากตัวแปร x
ไปยังตัวแปร y
แล้วนั้น เจ้าของของค่านั้นเปลี่ยนไปเป็น y
ในทันที ทำให้เราไม่สามารถเรียกใช้งาน x
ได้ ดังภาพ
ดังนั้นถ้าหากเราสั่ง cargo build
จะแสดงข้อความ error ขึ้นมา และแนะนำให้งาน clone
แทนการ move
ค่า
Move to Function
ในกรณีเดียวกัน หากเราส่งค่าไปยัง function
อื่นแล้ว เราจะไม่สามารถใช้งานตัวแปรนั้นได้อีก ในตัวอย่าง ถ้าหากเราเรียกใช้งาน msg
หลังจากที่ส่งค่าไปยัง take_ownership
แล้ว มันจะแสดง error
ขึ้นมาเหมือนกัน
ดังนั้น หากยังต้องการใช้งาน msg
ต่อให้ใช้วิธีการ clone
หรือ Reference and borrow
แทน
เช่นเดียวกันกับการส่งค่ากลับมาจาก function
อื่น ก็เป็นการเปลี่ยนเจ้าของ (ownership
) จากตัวแปรหนึ่งไปอีกตัวแปรหนึ่ง
Clone
หากเราต้องการที่จะใช้งานตัวแปร x
อยู่ ให้ใช้วิธีการ clone
ข้อมูลนั้นให้แก่ตัวแปร y
แทน
วิธีนี้เป็นการคัดลองข้อมูลอีกชุดนึงขึ้นมาแล้วให้ y
เก็บค่า pointer
ที่ชี้ไปที่ตัวใหม่ ดังภาพ
Reference and borrow
ถ้าหากเราต้องการส่งข้อมูลไปยังตัวแปรอื่น เพื่อทำงานบางอย่าง แต่ยังอยากใช้งานตัวแปรนั้นได้เหมือนเดิม ให้ใช้วิธีการอ้างอิง Reference ไปยังตัวแปรนั้นแทน ถ้าใครเคยเขียนภาษา C มันคือ pass by reference
นั่นเอง
ซึ่งการใช้งานจะใช้ &
ในการอ้างอิงค่า pointer ถ้าเป็นตัวแปรที่ส่งให้ใช้ &
หน้าตัวแปร (&s1
) และตัวแปรที่ใช้รับค่าจะใช้ &
หน้าประเภทแทน (s: &String
)
หาต้องการแก้ไขค่าในตัวแปรนั้นให้เพิ่ม mut
เข้าไปด้วย ทั้งในตัวแปรที่ส่ง ใส่ไว้หน้า &
แบบนี้ &mut s
และตัวแปรที่รับ ใส่ไว้หน้า type some_string: &mut String
และอย่าลืมใส่ mut
ตอนประกาศตัวแปรด้วยนะ
Slices
ส่วนอีกวิธีในการแก้ไขค่าในตัวแปร มีอีกวิธีหนึ่งที่นิยมใช้งานกัน คือ การ slices ข้อมูล อธิบายง่ายๆ วิธีนี้แทนที่เราจะ refer ไปยังข้อมูลทั้งหมด แต่เราจะเลือก refer ไปที่ข้อมูลบางส่วนแทน ซึ่งจะทำให้การเข้าถึงและการแก้ไขข้อมูลนั้นเร็วขึ้นไปอีก
โดยเราจะใช้ &s
เพื่ออ้างอิงตำแหน่ง address ของข้อมูลที่เก็บค่าไว้ พร้อมทั่งระบุ range ที่ต้องการ slices ด้วย [0..5]
เราสามารถระบุ range ได้หลายแบบ ดังนี้
- ระบุเริ่มต้นที่ 0 และ n:
[0..5]
หรือ[..2]
- จุดเริ่มต้นที่ n ถึงจุดที่สิ้นสุด (m) แบบ fixed:
[2..5]
- จุดเริ่มต้นที่ n ถึงจุดที่สิ้นสุด (m) แบบไม่ dynamic :
let len = s.len();
->[3..len]
หรือ[3..]
- ระบุทั้งหมด:
[0..len]
หรือ[..]