ตัวแปรและ Mutability
อย่างที่กล่าวไว้ในส่วน “เก็บค่าด้วยตัวแปร” โดย default ตัวแปรเป็น immutable นี่เป็นหนึ่งในหลาย ๆ สัญญาณที่ Rust ให้คุณ เพื่อเขียนโค้ดในแบบที่ใช้ประโยชน์จากความปลอดภัยและ concurrency ที่ง่ายของ Rust อย่างไรก็ตาม คุณยังมีตัวเลือกในการทำให้ตัวแปร mutable มาสำรวจกันว่า ทำไม Rust ถึงสนับสนุนให้คุณเอนเอียงไปทาง immutability และทำไมบางครั้งคุณ อาจอยาก opt out
เมื่อตัวแปรเป็น immutable ทันทีที่ค่าถูก bind กับชื่อ คุณเปลี่ยนค่านั้นไม่
ได้ เพื่อแสดงให้เห็นเรื่องนี้ generate โปรเจกต์ใหม่ชื่อ variables ใน
directory projects ของคุณ ด้วย cargo new variables
จากนั้นใน directory variables ใหม่ของคุณ เปิด src/main.rs แล้วแทนที่ โค้ดของมันด้วยโค้ดต่อไปนี้ ซึ่งจะยัง compile ไม่ผ่าน:
Filename: src/main.rs
fn main() {
let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
บันทึกแล้วรันโปรแกรมด้วย cargo run คุณควรได้ error message เกี่ยวกับ
immutability ตามที่แสดงใน output นี้:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
2 | let x = 5;
| - first assignment to `x`
3 | println!("The value of x is: {x}");
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
2 | let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` (bin "variables") due to 1 previous error
ตัวอย่างนี้แสดงวิธีที่ compiler ช่วยคุณหา error ในโปรแกรม Compiler error อาจทำให้หงุดหงิด แต่จริง ๆ มันแค่หมายความว่าโปรแกรมของคุณยังไม่ได้ทำสิ่ง ที่คุณต้องการอย่างปลอดภัย — มันไม่ได้หมายความว่าคุณเขียนโปรแกรมไม่เก่ง! Rustacean ที่มีประสบการณ์ก็ยังเจอ compiler error
คุณได้รับ error message cannot assign twice to immutable variable `x`
เพราะคุณพยายาม assign ค่าที่สองให้ตัวแปร immutable x
มันสำคัญที่เราจะได้ compile-time error เมื่อพยายามเปลี่ยนค่าที่ถูกกำหนดเป็น immutable เพราะสถานการณ์นี้นี่แหละที่นำไปสู่ bug ได้ ถ้าโค้ดของเราส่วน หนึ่งทำงานบนสมมติฐานว่าค่าจะไม่เปลี่ยน และอีกส่วนหนึ่งของโค้ดเปลี่ยนค่า นั้น เป็นไปได้ที่โค้ดส่วนแรกจะไม่ทำสิ่งที่ออกแบบให้ทำ สาเหตุของ bug แบบนี้ อาจตามหายากหลังเกิดเหตุ โดยเฉพาะเมื่อโค้ดส่วนที่สองเปลี่ยนค่า บางครั้ง เท่านั้น Rust compiler รับประกันว่าเมื่อคุณบอกว่าค่าจะไม่เปลี่ยน มันก็จะ ไม่เปลี่ยนจริง ๆ คุณจึงไม่ต้องตามติดมันเอง โค้ดของคุณจึงคิดตามได้ง่ายขึ้น
แต่ mutability ก็มีประโยชน์มาก และทำให้เขียนโค้ดสะดวกขึ้นได้ แม้ตัวแปรจะ
เป็น immutable โดย default คุณก็ทำให้มัน mutable ได้ โดยเพิ่ม mut ไว้
หน้าชื่อตัวแปรเหมือนที่คุณทำใน
บทที่ 2 การเพิ่ม mut
ยังสื่อเจตนาให้ผู้อ่านโค้ดในอนาคต โดยบ่งบอกว่าส่วนอื่น ๆ ของโค้ดจะ
เปลี่ยนค่าตัวแปรนี้
เช่น มาเปลี่ยน src/main.rs เป็นแบบนี้:
Filename: src/main.rs
fn main() {
let mut x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
เมื่อรันโปรแกรมตอนนี้ เราจะได้:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/variables`
The value of x is: 5
The value of x is: 6
เราได้รับอนุญาตให้เปลี่ยนค่าที่ bind กับ x จาก 5 เป็น 6 เมื่อใช้
mut สุดท้าย การตัดสินใจว่าจะใช้ mutability หรือไม่ ขึ้นกับคุณ และขึ้น
กับสิ่งที่คุณคิดว่าชัดเจนที่สุดในสถานการณ์นั้น ๆ
ประกาศ Constant
เช่นเดียวกับตัวแปร immutable constant คือค่าที่ถูก bind กับชื่อ และไม่ อนุญาตให้เปลี่ยน แต่มีความแตกต่างไม่กี่ข้อระหว่าง constant และตัวแปร
ประการแรก คุณไม่ได้รับอนุญาตให้ใช้ mut กับ constant Constant ไม่ใช่แค่
immutable โดย default — มัน immutable ตลอดเวลา คุณประกาศ constant โดยใช้
keyword const แทน keyword let และ ต้อง annotate type ของค่า เราจะ
ครอบคลุม type และ type annotation ในส่วนถัดไป
“ชนิดข้อมูล” ดังนั้นยังไม่ต้องห่วงรายละเอียด
ตอนนี้ แค่รู้ว่าคุณต้อง annotate type เสมอ
Constant ประกาศได้ใน scope ใด ๆ รวมถึง global scope ซึ่งทำให้มันมีประโยชน์ สำหรับค่าที่โค้ดหลายส่วนต้องรู้
ความแตกต่างสุดท้ายคือ constant ตั้งค่าได้แค่เป็น constant expression ไม่ ใช่ผลของค่าที่คำนวณได้แค่ตอน runtime
นี่คือตัวอย่างการประกาศ constant:
#![allow(unused)]
fn main() {
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
}
ชื่อของ constant คือ THREE_HOURS_IN_SECONDS และค่าของมันถูกตั้งเป็นผลของ
การคูณ 60 (จำนวนวินาทีในนาที) ด้วย 60 (จำนวนนาทีในชั่วโมง) ด้วย 3 (จำนวน
ชั่วโมงที่เราอยากนับในโปรแกรมนี้) Convention การตั้งชื่อ constant ของ Rust
คือใช้ตัวพิมพ์ใหญ่ทั้งหมด พร้อม underscore ระหว่างคำ Compiler ประเมิน
operation ชุดจำกัดได้ตอน compile time ซึ่งให้เราเลือกเขียนค่านี้ในรูปแบบ
ที่เข้าใจและตรวจสอบได้ง่ายกว่า แทนที่จะตั้ง constant นี้เป็นค่า 10,800 ดู
ส่วน constant evaluation ของ Rust Reference สำหรับข้อมูล
เพิ่มเติมว่า operation ไหนใช้ได้เมื่อประกาศ constant
Constant ใช้งานได้ตลอดเวลาที่โปรแกรมรัน ภายใน scope ที่พวกมันถูกประกาศ คุณสมบัตินี้ทำให้ constant มีประโยชน์สำหรับค่าใน application domain ของ คุณที่หลายส่วนของโปรแกรมอาจต้องรู้ เช่น จำนวนคะแนนสูงสุดที่ผู้เล่นเกม สามารถได้รับ หรือความเร็วแสง
การตั้งชื่อค่า hardcode ที่ใช้ตลอดโปรแกรมเป็น constant มีประโยชน์ในการสื่อ ความหมายของค่านั้นให้ผู้ดูแลโค้ดในอนาคต ยังช่วยให้มีเพียงที่เดียวในโค้ดที่ คุณต้องเปลี่ยน หากค่า hardcode นั้นต้อง update ในอนาคต
Shadowing
อย่างที่คุณเห็นใน tutorial เกมทายตัวเลขใน
บทที่ 2 คุณ
ประกาศตัวแปรใหม่ที่มีชื่อเดียวกับตัวแปรก่อนหน้าได้ Rustacean บอกว่าตัวแปร
แรกถูก shadow ด้วยตัวที่สอง ซึ่งหมายความว่าตัวแปรที่สองคือสิ่งที่
compiler จะเห็น เมื่อคุณใช้ชื่อของตัวแปร ในทางผล ตัวแปรที่สองบดบังตัวแรก
รับการใช้ชื่อตัวแปรมาเป็นของตัวเอง จนกว่าตัวมันเองจะถูก shadow หรือ scope
จบ เรา shadow ตัวแปรได้โดยใช้ชื่อตัวแปรเดียวกัน และเขียน keyword let
ซ้ำ ดังนี้:
Filename: src/main.rs
fn main() {
let x = 5;
let x = x + 1;
{
let x = x * 2;
println!("The value of x in the inner scope is: {x}");
}
println!("The value of x is: {x}");
}
โปรแกรมนี้ bind x กับค่า 5 ก่อน จากนั้นสร้างตัวแปร x ใหม่ด้วยการเขียน
let x = ซ้ำ เอาค่าเดิมมาบวก 1 ทำให้ค่าของ x เป็น 6 จากนั้นใน inner
scope ที่สร้างด้วย curly bracket statement let ที่สามก็ shadow x และ
สร้างตัวแปรใหม่ คูณค่าก่อนหน้าด้วย 2 ให้ x มีค่า 12 เมื่อ scope นั้น
จบ inner shadowing สิ้นสุด และ x กลับเป็น 6 เมื่อเรารันโปรแกรมนี้ มัน
จะ output:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/variables`
The value of x in the inner scope is: 12
The value of x is: 6
Shadowing ต่างจากการ mark ตัวแปรเป็น mut เพราะเราจะได้ compile-time
error ถ้าเผลอลอง reassign ตัวแปรนี้โดยไม่ใช้ keyword let ด้วยการใช้
let เราทำ transformation ไม่กี่อย่างบนค่าได้ แต่ตัวแปรยังคงเป็น
immutable หลัง transformation เหล่านั้นเสร็จ
ความแตกต่างอีกอย่างระหว่าง mut กับ shadowing คือ เพราะเราเป็นการสร้าง
ตัวแปรใหม่จริง ๆ เมื่อใช้ keyword let อีกครั้ง เราเปลี่ยน type ของค่าได้
แต่ใช้ชื่อเดิม เช่น สมมติว่าโปรแกรมของเราถามผู้ใช้ให้แสดงจำนวน space ที่
ต้องการระหว่างข้อความบางตัว โดยป้อนอักขระ space แล้วเราอยากเก็บ input นั้น
เป็นตัวเลข:
fn main() {
let spaces = " ";
let spaces = spaces.len();
}
ตัวแปร spaces ตัวแรกเป็น type string และตัวแปร spaces ตัวที่สองเป็น
type ตัวเลข Shadowing จึงช่วยให้เราไม่ต้องคิดชื่อต่างกัน เช่น spaces_str
และ spaces_num แต่ใช้ชื่อ spaces ที่ง่ายกว่าซ้ำได้ อย่างไรก็ตาม ถ้า
เราลองใช้ mut สำหรับเรื่องนี้ ดังที่แสดงที่นี่ เราจะได้ compile-time
error:
fn main() {
let mut spaces = " ";
spaces = spaces.len();
}
Error บอกว่าเราไม่ได้รับอนุญาตให้ mutate type ของตัวแปร:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
--> src/main.rs:3:14
|
2 | let mut spaces = " ";
| ----- expected due to this value
3 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected `&str`, found `usize`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` (bin "variables") due to 1 previous error
ตอนนี้เราสำรวจวิธีที่ตัวแปรทำงานแล้ว มาดูชนิดข้อมูลที่พวกมันมีได้มากกว่า