- เรียนภาษา Rust ผ่านเว็บ Tour of Rust
- Tour of Rust - Ch.1 - The Basics
- Tour of Rust - Ch.2 - Basic Control Flow
- Tour of Rust - Ch.3 - Basic Data Structure Types
- Tour of Rust - Ch.4 - Generic Types
- Tour of Rust - Ch.5 - Ownership & Borrowing Data
Tour of Rust - Ch.5 - Ownership & Borrowing Data
เขียนวันที่ : Jun 27, 2022
บันทึกการเรียน Tour Of Rust บทเรียนที่ 5 - Tour of Rust - Chapter 5 - Ownership & Borrowing Data
Borrowing กับ Ownership เป็นเรื่องเกี่ยวกับการจัดการหน่วยความจำของข้อมูล เอาจริงๆ เป็นเรื่องที่ผมก็ยังไม่แม่น และต้องอ่าน Dcos และค้นข้อมูลอ่านเพิ่มเติมอยู่ตลอดเวลา แต่ถ้าเอาแบบไม่ลงลึกมาก ถ้าเข้าใจ Pass by value กับ pass by reference ก็น่าจะพอเข้าใจได้
Chapter 5 - Ownership & Borrowing Data 🦀
วันนี้จดบันทึกเรื่อง Ownership และ Borrowing ถึงแม้ส่วนใหญ่ จะก็อปโค๊ดจาก Tour of Rust มาเป็นตัวอย่าง ก็เถอะ 🤣
Ownership
เรื่องของ Ownership หรือความเป็นเจ้าของ (เรียกแบบภาษาไทย) ส่วนตัวผมขอทับศัพท์เป็น Ownership ละกัน
เวลาเราสร้าง instance ขึ้นมาซัก type นึง และกำหนดค่า variable ให้มัน มันจะทำการสร้างพื้นที่หน่วยความจำ ที่ตัว Rust Compiler มันจะเช็คตลอดการทำงาน (Lifetime) ตัวแปรที่ถูก assign (binding) เราเรียกว่า owner
struct Foo {
x: i32,
}
fn main() {
// สร้าง instance structs และผูกกับตัวแปร `foo`
// เพื่อสร้างพื้นที่หน่วยความจำ
// `foo` คือ owner
let foo = Foo { x: 42 };
// foo is the owner
}
Scope-Based Resource Management
ในภาษา Rust ใช้ end of scope (หรือจุดสิ้นสุดขอบเขต) เป็นที่ไว้สำหรับทำลายโครงสร้าง(deconstruct) และ คืนหน่วยความจำ(deallocate) เราเรียกขั้นตอนนี้ว่า drop
รายละเอียดหน่วยความจำ:
- Rust ไม่มี garbage collection
- เราเรียกสิ่งนี้ว่า Resource Acquisition Is Initialization ( RAII ) ในภาษา C++
struct Foo {
x: i32,
}
fn main() {
let foo_a = Foo { x: 42 };
let foo_b = Foo { x: 13 };
println!("{}", foo_a.x);
println!("{}", foo_b.x);
// foo_b drop
// foo_a drop
}
Dropping is Hierarchical
เวลาตัว struct ถูก drop มันจะ drop จากตัวมันเองก่อน แล้วก็ค่อยๆไปที่ตัวลูกของมัน และต่อไปเป็นทอดๆเรื่อยๆ
รายละเอียดหน่วยความจำ:
- ตัว Rust จะคืนหน่วยความจำให้อัตโนมัติ ทำให้เรามั่นใจว่าจะเกิด memory leak น้อยลง
- การจัดการ memory ทำการ drop ได้เพียงแค่ครั้งเดียว
struct Bar {
x: i32,
}
struct Foo {
bar: Bar,
}
fn main() {
let foo = Foo { bar: Bar { x: 42 } };
println!("{}", foo.bar.x);
// `foo` drop ก่อน จากนั้นต่อด้วย `foo.bar`
}
Moving Ownership
เมื่อ Ownership ถูกส่งไปเป็น argument ของ function ตัว Ownership จะถูกย้ายไปเป็น paremeter ของ function แทน เมื่อย้ายไปแล้ว ตัวแปรเดิมจะไม่สามารถใช้งานได้อีกต่อไป
struct Foo {
x: i32,
}
fn do_something(f: Foo) {
println!("{}", f.x);
// 2. f drop ตรงนี้
}
fn main() {
let foo = Foo { x: 42 };
// 1. `foo` ถูกส่งไปใน do_something
do_something(foo);
// 3. ถูก drop ไปหลังจาก do_something แล้ว
}
Returning Ownership
Ownership สามารถได้คืนจาก function
Borrowing Ownership with References
การอ้างอิง (References) ช่วยให้เราเข้าถึง resource โดยใช้ตัวแปร &
ตัว reference ก็สามารถ drop ได้เหมือนกันกับ resources อื่นๆ
struct Foo {
x: i32,
}
fn main() {
let foo = Foo { x: 42 };
let f = &foo;
println!("{}", f.x);
// f is dropped here
// foo is dropped here
}
Borrowing Mutable Ownership with References
นอกจากนี้เรายังสามารถใช้เครื่องหมาย &mut
เพื่อทำการยืม (borrow) การเข้าถึง resource ที่เปลี่ยนค่าได้
โดย Ownership ไ่ม่สามารถย้ายหรือแก้ไขอะไรได้ในขณะที่ถูก borrow อยู่
Dereferencing
เราใช้ &mut
สำหรับการอ้างถึง (Reference) และใช้ *
เพื่อกำหนด owner ใหม่ให้มัน
ในทางกลับกัน เราก็สามารถที่จะ copy ค่านั้นออกมาด้วยการใช้ *
fn main() {
let mut foo = 42;
let f = &mut foo;
let bar = *f; // copy ค่า value ของ owner
*f = 13; // set the reference's owner's value
println!("{}", bar);
println!("{}", foo);
}
Passing Around Borrowed Data
สรุป กฎที่ใช้ในการอ้างอิง (Reference) ได้ดังนี้:
- Rust ยอมให้มีการอ้างอิงถึง mutable reference ได้ค่าเดียว หรือ หลายตัวที่เป็นแบบเปลี่ยนแปลงค่าไม่ได้ แต่ไม่สามารถทำทั้งสองอย่างพร้อมกันได้
- Reference จะต้องไม่มี lifetime นานกว่า owner
struct Foo {
x: i32,
}
fn do_something(f: &mut Foo) {
f.x += 1;
// mutable reference f drop ตรงนี้
}
fn main() {
let mut foo = Foo { x: 42 };
do_something(&mut foo);
// เพราะว่า mutable references ทั้งหมดถูก drop ใน function
// do_something แล้ว ทำให้เราสามารถสร้างตัวใหม่ได้
do_something(&mut foo);
// foo is dropped here
}
References Of References
Reference ก็สามารถใช้อ้างอิงถึง References อื่นได้เช่นกัน
Explicit Lifetimes
ถึงแม้ว่าเราจะไม่รู้ หรือไม่เห็น Lifetime ของตัวแปร แต่ว่า Compiler ของ Rust รู้ว่า lifetime เป็นยังไง และมีการตรวจสอบตลอด ทำให้มั่นใจได้ว่า จะไม่มี reference ใดที่มี lifetime นานกว่า owner ได้
การระบุ lifetime ให้เริ่มต้นด้วย เครื่อง '
เช่น 'a
, 'b
เป็นต้น
struct Foo {
x: i32,
}
// the parameter foo and return value share the same lifetime
fn do_something<'a>(foo: &'a Foo) -> &'a i32 {
return &foo.x;
}
fn main() {
let mut foo = Foo { x: 42 };
let x = &mut foo.x;
*x = 13;
// x is dropped here, allowing us to create a non-mutable reference
let y = do_something(&foo);
println!("{}", y);
// y is dropped here
// foo is dropped here
}
Multiple Lifetimes
การมี Lifetimes หลายตัว ทำได้ก็จริง แต่บางที Compiler ก็ยังไม่สามารถแยกแยะ lifetime ของทุกตัวได้ทั้งหมด
Static Lifetimes
ตัวแปรแบบ static เป็น memory resource ที่ถูกสร้าง ณ เวลา compile และจะคงอยู่ตั้งแต่โปรแกรมเริ่มทำงานจบจบโปรแกรม โดยต้องระบุ type ให้มันชัดเจน
static lifetime สามารถเกิดตอน runtime ก็ได้ ไม่เฉพาะแค่ตอน compile
ตัว static lifetime เราจะใช้ 'static
เป็นตัวกำหนด และตัว static lifetime จะไม่มีวันโดน drop
let msg: &'static str = "Hello World!";
Lifetimes in Data Types
เหมือนกับ function ตัว Data Types เราก็สามารถใส่ lifetime ให้กับสมาชิกแต่ละตัวได้ โดย Rust จะทำการตรวจอยู่เสมอว่า Reference มันจะไม่มี lifetime มากกว่า Owner
i:&'a i32
}
fn main() {
let x = 42;
let foo = Foo {
i: &x
};
println!("{}",foo.i);
}
🎉 จบแล้ว ยาวมากและมึนมากๆ 🤣
สรุปบทที่ 5
เอาจริงๆ ส่วนตัวผมพบว่า บทนี้เป็นเรื่องที่เข้าใจยากที่สุดสำหรับผมเลย ขนาดแปลมา บางทีผมก็ยังไม่เข้าใจเท่าไหร่ ทั้ง Eng ทั้งไทยนะ ก็ยังงๆ สุดท้าย ยังไงก็ต้องไป research อ่านเพิ่มเติมอยู่ดี