ตัวแปรและการแก้ไขค่าได้
ดังที่ได้กล่าวถึงในหัวข้อ “การเก็บค่าด้วยตัวแปร” โดยค่าเริ่มต้นแล้ว ตัวแปรจะไม่สามารถแก้ไขค่าได้ (immutable) นี่เป็นหนึ่งในหลายๆ แนวทางที่ภาษา Rust ผลักดันให้คุณเขียนโค้ดเพื่อใช้ประโยชน์จากความปลอดภัยและความสะดวกในเรื่องการทำงานแบบขนาน (concurrency) ที่ Rust นำเสนอ อย่างไรก็ตาม คุณก็ยังคงมีทางเลือกที่จะทำตัวแปรของคุณให้แก้ไขค่าได้ (mutable) อยู่ดี มาลองสำรวจดูว่ามีวิธีอย่างไร และเพราะเหตุใด Rust จึงผลักดันให้คุณนิยมใช้ความไม่สามารถแก้ไขค่าได้ และทำไมในบางครั้งคุณถึงอยากจะข้ามตัวเลือกนี้ไป
เมื่อตัวแปรไม่สามารถแก้ไขค่าได้ เมื่อมีการผูก (bind) ค่าเข้ากับชื่อหนึ่งๆ ไปแล้ว คุณจะไม่สามารถเปลี่ยนค่านั้นได้อีก เพื่อสาธิตให้เห็นภาพนี้ ให้ลองสร้างโปรเจกต์ใหม่ชื่อว่า variables ในไดเรกทอรี projects ของคุณ โดยใช้คำสั่ง cargo new variables
จากนั้น ในไดเรกทอรี variables ใหม่ของคุณ ให้เปิดไฟล์ src/main.rs และแทนที่โค้ดในนั้นด้วยโค้ดดังต่อไปนี้ ซึ่งจะยังไม่สามารถคอมไพล์ผ่านได้ในตอนนี้:
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 คุณควรจะได้รับข้อความแสดงข้อผิดพลาดเกี่ยวกับการแก้ไขค่าไม่ได้ ดังแสดงในผลลัพธ์ต่อไปนี้:
$ 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 errors) อาจเป็นเรื่องที่น่าหงุดหงิดใจ แต่แท้จริงแล้วพวกมันมีความหมายเพียงว่าโปรแกรมของคุณยังไม่ได้ทำงานตามที่คุณต้องการได้อย่างปลอดภัยเท่านั้น พวกมันไม่ได้หมายความว่าคุณไม่ใช่โปรแกรมเมอร์ที่ดี! ชาว Rustacean ที่มีประสบการณ์ก็ยังคงพบข้อผิดพลาดจากคอมไพเลอร์อยู่ดี
คุณได้รับข้อความแสดงข้อผิดพลาด cannot assign twice to immutable variable `x` (ไม่สามารถกำหนดค่าสองครั้งให้กับตัวแปร x ที่แก้ไขค่าไม่ได้) เนื่องจากคุณพยายามกำหนดค่าเป็นครั้งที่สองให้กับตัวแปร x ซึ่งไม่สามารถแก้ไขค่าได้
การที่เราได้รับข้อผิดพลาดในขั้นตอนการคอมไพล์ (compile-time errors) เมื่อพยายามเปลี่ยนค่าที่ถูกระบุว่าแก้ไขค่าไม่ได้นั้นถือเป็นสิ่งสำคัญ เพราะเหตุการณ์ในลักษณะนี้สามารถนำไปสู่บั๊กได้ หากส่วนหนึ่งของโค้ดทำงานภายใต้สมมติฐานว่าค่านั้นจะไม่มีวันเปลี่ยนแปลง แต่อีกส่วนหนึ่งของโค้ดกลับไปเปลี่ยนแปลงค่านั้น มันก็เป็นไปได้ว่าโค้ดส่วนแรกจะไม่ทำงานตามที่มันถูกออกแบบมา สาเหตุของบั๊กประเภทนี้อาจเป็นเรื่องยากที่จะสืบหาในภายหลัง โดยเฉพาะอย่างยิ่งเมื่อโค้ดส่วนที่สองนั้นเปลี่ยนแปลงค่าเป็นครั้งคราวเท่านั้น ตัวคอมไพเลอร์ของ Rust รับประกันว่าเมื่อคุณระบุว่าค่านั้นจะไม่มีการเปลี่ยนแปลง มันก็จะไม่มีการเปลี่ยนแปลงจริงๆ ดังนั้นคุณจึงไม่จำเป็นต้องคอยตรวจสอบสิ่งนี้ด้วยตัวเอง ส่งผลให้โค้ดของคุณเข้าใจได้ง่ายขึ้น
อย่างไรก็ตาม ความสามารถในการแก้ไขค่าได้ (mutability) นั้นมีประโยชน์อย่างมากและช่วยให้เขียนโค้ดได้สะดวกยิ่งขึ้น แม้ว่าโดยค่าเริ่มต้นแล้วตัวแปรจะไม่สามารถแก้ไขค่าได้ แต่คุณสามารถทำให้พวกมันสามารถแก้ไขค่าได้โดยการเติม 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
เราจะได้รับอนุญาตให้เปลี่ยนค่าที่ผูกกับ x จาก 5 เป็น 6 ได้เมื่อใช้ mut ท้ายที่สุดแล้ว การตัดสินใจว่าจะใช้ความสามารถในการแก้ไขค่าได้หรือไม่นั้นขึ้นอยู่กับคุณ และขึ้นอยู่กับว่าคุณคิดว่าแบบใดจะชัดเจนที่สุดในสถานการณ์นั้นๆ
การประกาศค่าคงที่ (Constants)
เช่นเดียวกับตัวแปรที่แก้ไขค่าไม่ได้ ค่าคงที่ (constants) คือค่าที่ผูกกับชื่อหนึ่งๆ และไม่ได้รับอนุญาตให้เปลี่ยนแปลง แต่มีข้อแตกต่างบางประการระหว่างค่าคงที่และตัวแปร
ข้อแรก คุณไม่ได้รับอนุญาตให้ใช้ mut กับค่าคงที่ ค่าคงที่ไม่ใช่แค่ไม่สามารถแก้ไขค่าได้เป็นค่าเริ่มต้น แต่พวกมันไม่สามารถแก้ไขค่าได้ตลอดเวลา คุณประกาศค่าคงที่โดยใช้คำสำคัญ const แทนคำสำคัญ let และชนิดข้อมูล (type) ของค่าคงที่จะต้องระบุกำกับไว้เสมอ เราจะครอบคลุมเรื่องชนิดข้อมูลและการระบุชนิดข้อมูลในหัวข้อถัดไป “ชนิดข้อมูล” ดังนั้นจึงไม่ต้องกังวลกับรายละเอียดในตอนนี้ เพียงแค่รู้ว่าคุณต้องระบุชนิดข้อมูลเสมอ
ค่าคงที่สามารถถูกประกาศในขอบเขต (scope) ใดก็ได้ รวมถึงขอบเขตระดับโกลบอล (global scope) ซึ่งทำให้พวกมันมีประโยชน์สำหรับค่าที่โค้ดหลายๆ ส่วนจำเป็นต้องใช้งาน
ข้อแตกต่างประการสุดท้ายคือ ค่าคงที่อาจถูกตั้งค่าได้เฉพาะด้วยนิพจน์ค่าคงที่ (constant expression) เท่านั้น ไม่สามารถตั้งค่าด้วยผลลัพธ์ที่สามารถคำนวณได้เฉพาะในตอนที่โปรแกรมรัน (runtime) เท่านั้น
นี่คือตัวอย่างการประกาศค่าคงที่:
#![allow(unused)]
fn main() {
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
}
ชื่อของค่าคงที่คือ THREE_HOURS_IN_SECONDS และค่าของมันถูกตั้งเป็นผลลัพธ์ของการคูณ 60 (จำนวนวินาทีในหนึ่งนาที) ด้วย 60 (จำนวนนาทีในหนึ่งชั่วโมง) ด้วย 3 (จำนวนชั่วโมงที่เราต้องการนับในโปรแกรมนี้) รูปแบบการตั้งชื่อตามหลักปฏิบัติของภาษา Rust สำหรับค่าคงที่คือการใช้ตัวพิมพ์ใหญ่ทั้งหมดและใช้เครื่องหมายขีดล่างคั่นระหว่างคำ ตัวคอมไพเลอร์สามารถประเมินผลกลุ่มการทำงานที่มีจำกัดได้ตั้งแต่ขั้นตอนคอมไพล์ ทำให้เราเลือกที่จะเขียนค่านี้ออกมาในแบบที่เข้าใจและตรวจสอบได้ง่ายขึ้น แทนที่จะต้องตั้งค่าคงที่นี้ให้เป็นเลข 10,800 ตรงๆ สามารถดูข้อมูลเพิ่มเติมเกี่ยวกับการดำเนินการที่สามารถนำมาใช้ในการประกาศค่าคงที่ได้ใน หัวข้อเรื่องการประเมินค่าคงที่ของเอกสารอ้างอิง Rust
ค่าคงที่จะใช้งานได้ตลอดระยะเวลาที่โปรแกรมทำงาน ภายใต้ขอบเขตที่พวกมันถูกประกาศ คุณสมบัตินี้ทำให้ค่าคงที่มีประโยชน์สำหรับค่าต่างๆ ในโดเมนแอปพลิเคชันของคุณที่ส่วนต่างๆ ของโปรแกรมจำเป็นต้องรับทราบร่วมกัน เช่น คะแนนสูงสุดที่ผู้เล่นเกมจะสามารถได้รับ หรือความเร็วของแสง
การตั้งชื่อให้กับค่าที่ฝังตัวเลขลงไปในโปรแกรม (hardcoded values) ที่ใช้งานกระจัดกระจายอยู่ในโปรแกรมด้วยการประกาศเป็นค่าคงที่จะช่วยสื่อสารความหมายของค่านั้นๆ ให้แก่ผู้ที่จะมาคอยดูแลโค้ดนี้ในอนาคตได้อย่างดี นอกจากนี้ยังช่วยให้มีเพียงจุดเดียวในโค้ดที่คุณต้องเข้ามาแก้ไขหากจำเป็นต้องปรับปรุงค่าคงที่นี้ในอนาคต
การบังเงา (Shadowing)
ดังที่คุณได้เห็นในบทเรียนการสร้างเกมทายตัวเลขใน บทที่ 2 คุณสามารถประกาศตัวแปรใหม่ที่มีชื่อเดียวกันกับตัวแปรที่มีอยู่ก่อนหน้านี้ได้ ชาว Rustacean เรียกสิ่งนี้ว่า ตัวแปรแรกถูก บังเงา (shadowed) โดยตัวแปรตัวที่สอง ซึ่งหมายความว่าตัวแปรตัวที่สองคือสิ่งที่จะปรากฏให้คอมไพเลอร์มองเห็นเมื่อคุณเรียกใช้ชื่อตัวแปรนั้น ในทางปฏิบัติ ตัวแปรตัวที่สองจะมาบดบังตัวแรก โดยสิทธิ์การใช้ชื่อตัวแปรทั้งหมดจะตกเป็นของตัวมันเองจนกว่าตัวมันจะโดนบังเงาซ้ำอีกรอบหรือสิ้นสุดขอบเขต (scope) ลง เราสามารถทำบังเงาตัวแปรได้โดยใช้ชื่อตัวแปรเดิมและประกาศใช้งานคำสำคัญ 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}");
}
โปรแกรมนี้จะผูกตัวแปร x เข้ากับค่า 5 เป็นอันดับแรก จากนั้นมันจะสร้างตัวแปร x ตัวใหม่ด้วยการประกาศคำสำคัญซ้ำว่า let x = โดยดึงค่าเริ่มต้นเดิมมาบวกเพิ่ม 1 ส่งผลให้มูลค่าของ x กลายเป็น 6 หลังจากนั้นภายใต้ขอบเขตการทำงานด้านในที่สร้างด้วยปีกกา คำสั่ง let แถวที่สามก็จะทำบังเงาตัวแปร x และสร้างตัวแปรใหม่ขึ้นมา โดยจะนำค่าก่อนหน้านี้มาคูณกับ 2 ส่งผลให้ x มีค่ากลายเป็น 12 และเมื่อหมดรอบขอบเขตการทำงานชั้นใน การบังเงาด้านในจะยุติลง และ x จะกลับมามีค่าเป็น 6 ตามเดิม เมื่อเราสั่งรันโปรแกรมนี้ มันจะแสดงผลลัพธ์ดังนี้:
$ 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) นั้นแตกต่างจากการระบุตัวแปรเป็นแบบ mut เนื่องจากเราจะได้รับข้อผิดพลาดในการคอมไพล์ทันทีหากเราเผลอสั่งกำหนดค่าใหม่ให้แก่ตัวแปรนี้โดยไม่ผ่านการระบุสิทธิ์ด้วยคำสำคัญ let การเลือกใช้งานคำสั่ง let จะช่วยให้เราปรับเปลี่ยนค่าข้อมูลได้ตามสมควร และมีผลลัพธ์ให้ตัวแปรยังคงไม่สามารถแก้ไขค่าได้อยู่ดีภายหลังจากขั้นตอนปรับเปลี่ยนข้อมูลเหล่านั้นเสร็จสมบูรณ์
ความแตกต่างอีกประการหนึ่งระหว่างการใช้งาน mut และการบังเงาคือ เนื่องจากในกระบวนการบังเงาเรากำลังทำสร้างตัวแปรตัวใหม่ขึ้นมาจริงๆ เมื่อเราเรียกใช้งานคำสำคัญ let อีกครั้ง เราจึงสามารถเปลี่ยนแปลงประเภทชนิดข้อมูลของตัวแปรไปพร้อมกับนำชื่อเดิมกลับมาใช้ใหม่ได้ ตัวอย่างเช่น สมมติว่าโปรแกรมของเราต้องการขอข้อมูลจากผู้ใช้ว่าต้องการเว้นวรรคระหว่างข้อความมากน้อยเพียงใดโดยการป้อนตัวอักษรเว้นวรรค (spaces) แล้วเราต้องการนำข้อมูลที่พิมพ์เข้ามานั้นมาจัดเก็บเป็นข้อมูลตัวเลขจำนวนแทน:
fn main() {
let spaces = " ";
let spaces = spaces.len();
}
ตัวแปร spaces แรกสุดจะเป็นข้อมูลชนิดข้อความ (string type) และตัวแปร spaces แถวที่สองจะเป็นข้อมูลชนิดตัวเลขจำนวน (number type) การบังเงาจึงช่วยป้องกันไม่ให้เราต้องมาคอยคิดหาชื่อตัวแปรที่แตกต่างกัน เช่น spaces_str และ spaces_num ทำให้เราสามารถนำชื่อที่สั้นกระชับอย่าง spaces กลับมาใช้ใหม่ได้ อย่างไรก็ตาม หากเราเลือกประยุกต์ใช้งาน mut จัดการเรื่องนี้แทนดังที่แสดงตัวอย่างด้านล่างนี้ เราจะได้รับข้อผิดพลาดในขั้นตอนคอมไพล์ทันที:
fn main() {
let mut spaces = " ";
spaces = spaces.len();
}
ข้อความแจ้งเตือนข้อผิดพลาดระบุว่าเราไม่สามารถปรับเปลี่ยนชนิดข้อมูลประเภทตัวแปรได้:
$ 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
เมื่อเราได้เรียนรู้วิธีการทำงานของตัวแปรแล้ว ถัดไปมาลองทำความเข้าใจกับชนิดข้อมูลต่างๆ ที่ตัวแปรสามารถเก็บได้กัน