ใช้ชนิดที่ถูกยืมสำหรับอาร์กิวเมนต์
คำอธิบาย
การกำหนดชนิดข้อมูลของพารามิเตอร์ฟังก์ชันให้เป็นชนิดข้อมูลเป้าหมายของการทำ deref coercion จะช่วยเพิ่มความยืดหยุ่นให้แก่โค้ดของคุณได้อย่างมากเมื่อต้องตัดสินใจเลือกชนิดของอาร์กิวเมนต์ ด้วยวิธีนี้ ฟังก์ชันของคุณจะสามารถเปิดรับข้อมูลนำเข้าได้หลากหลายชนิดมากยิ่งขึ้น
หลักการนี้ไม่ได้จำกัดอยู่เพียงแค่ชนิดข้อมูลที่แปลงเป็น slice ได้ หรือชนิดข้อมูลแบบ fat pointer (พอยน์เตอร์พร้อม metadata) เท่านั้น แต่อันที่จริงแล้ว คุณควรเลือกใช้ไทป์แบบยืม (borrowed type) แทนการยืมไทป์ที่เป็นเจ้าของข้อมูล (borrowing the owned type) อยู่เสมอ เช่น เลือกใช้ &str แทน &String, เลือกใช้ &[T] แทน &Vec<T> หรือเลือกใช้ &T แทน &Box<T>
การใช้ไทป์แบบยืมจะช่วยให้คุณหลีกเลี่ยงการอ้างอิงทางอ้อม (indirection) ซ้ำซ้อนหลายชั้น ในกรณีที่ชนิดข้อมูลที่เป็นเจ้าของนั้นมีชั้นของการอ้างอิงทางอ้อมอยู่แล้ว ตัวอย่างเช่น String มีการอ้างอิงทางอ้อมอยู่หนึ่งชั้นอยู่แล้ว ดังนั้นการใช้ &String จึงกลายเป็นการอ้างอิงทางอ้อมซ้อนกันถึงสองชั้น เราสามารถหลีกเลี่ยงภาระนี้ได้โดยการเปลี่ยนมาใช้ &str แทน แล้วปล่อยให้คอมไพเลอร์แปลงค่าจาก &String ไปเป็น &str ผ่านกลไก deref coercion โดยอัตโนมัติเมื่อใดก็ตามที่มีการเรียกใช้งานฟังก์ชัน
ตัวอย่าง
ในตัวอย่างนี้ เราจะแสดงให้เห็นถึงความแตกต่างระหว่างการใช้ &String กับการใช้ &str เป็นอาร์กิวเมนต์ของฟังก์ชัน แต่แนวคิดเดียวกันนี้ก็สามารถนำไปปรับใช้กับการเลือก &[T] แทน &Vec<T> หรือการเลือก &T แทน &Box<T> ได้เช่นเดียวกัน
ลองพิจารณาตัวอย่างที่เราต้องการตรวจสอบว่าคำคำหนึ่งมีสระเรียงติดกันตั้งแต่สามตัวขึ้นไปหรือไม่ ในกรณีนี้ เราไม่จำเป็นต้องเป็นเจ้าของสตริงเพื่อตรวจสอบเงื่อนไขดังกล่าว เราจึงส่งข้อมูลเข้ามาในรูปของการอ้างอิง (Reference) แทน
โค้ดจะมีหน้าตาประมาณนี้:
fn three_vowels(word: &String) -> bool {
let mut vowel_count = 0;
for c in word.chars() {
match c {
'a' | 'e' | 'i' | 'o' | 'u' => {
vowel_count += 1;
if vowel_count >= 3 {
return true;
}
}
_ => vowel_count = 0,
}
}
false
}
fn main() {
let ferris = "Ferris".to_string();
let curious = "Curious".to_string();
println!("{}: {}", ferris, three_vowels(&ferris));
println!("{}: {}", curious, three_vowels(&curious));
// This works fine, but the following two lines would fail:
// println!("Ferris: {}", three_vowels("Ferris"));
// println!("Curious: {}", three_vowels("Curious"));
}
โค้ดด้านบนนี้ทำงานได้ตามปกติเนื่องจากเราส่งค่าที่มีชนิดเป็น &String เข้าไปเป็นพารามิเตอร์ แต่หากเรานำคอมเมนต์ในสองบรรทัดสุดท้ายออก โค้ดตัวอย่างจะคอมไพล์ไม่ผ่านทันที นั่นเป็นเพราะว่าชนิดข้อมูลแบบ &str ไม่สามารถถูกแปลงย้อนกลับเป็น &String ได้โดยอัตโนมัติ เราสามารถแก้ไขปัญหานี้ได้อย่างง่ายดายด้วยการเปลี่ยนชนิดข้อมูลของพารามิเตอร์ในฟังก์ชันของเรา
ตัวอย่างเช่น หากเราเปลี่ยนการประกาศฟังก์ชันเป็น:
fn three_vowels(word: &str) -> bool {
ทั้งสองเวอร์ชันก็จะสามารถคอมไพล์ผ่านได้อย่างราบรื่นและให้ผลลัพธ์เหมือนกัน:
Ferris: false
Curious: true
แต่ช้าก่อน! ประโยชน์ของเรื่องนี้ยังไม่หมดเพียงเท่านี้ คุณอาจจะแย้งในใจว่า: นั่นไม่ใช่เรื่องใหญ่ เพราะอย่างไรเสียเราก็แทบไม่เคยส่งค่า &'static str เข้ามาตรงๆ อยู่แล้ว (เหมือนในตัวอย่างที่เราส่ง "Ferris") แต่ถึงแม้จะมองข้ามกรณีนี้ไป คุณก็จะพบว่าการใช้ &str ยังคงมอบความยืดหยุ่นให้มากกว่าการใช้ &String อยู่ดี
ทีนี้ลองมาดูตัวอย่างที่มีคนส่งประโยคข้อความมาให้เราหนึ่งประโยค และเราต้องการตรวจสอบว่ามีคำใดในประโยคนั้นที่มีสระติดกันสามตัวหรือไม่ เราควรนำฟังก์ชันเดิมที่เขียนไว้แล้วกลับมาใช้ใหม่ โดยส่งคำแต่ละคำในประโยคเข้าไปตรวจสอบทีละคำ
ตัวอย่างโค้ดจะมีลักษณะดังนี้:
fn three_vowels(word: &str) -> bool {
let mut vowel_count = 0;
for c in word.chars() {
match c {
'a' | 'e' | 'i' | 'o' | 'u' => {
vowel_count += 1;
if vowel_count >= 3 {
return true;
}
}
_ => vowel_count = 0,
}
}
false
}
fn main() {
let sentence_string =
"Once upon a time, there was a friendly curious crab named Ferris".to_string();
for word in sentence_string.split(' ') {
if three_vowels(word) {
println!("{word} has three consecutive vowels!");
}
}
}
เมื่อรันตัวอย่างนี้โดยที่ฟังก์ชันประกาศรับอาร์กิวเมนต์ชนิด &str จะให้ผลลัพธ์ดังนี้:
curious has three consecutive vowels!
อย่างไรก็ตาม ตัวอย่างนี้จะไม่สามารถทำงานได้เลยหากฟังก์ชันของเราประกาศรับอาร์กิวเมนต์เป็นชนิด &String นั่นเป็นเพราะว่า String Slice ที่ได้จาก split มีชนิดข้อมูลเป็น &str ไม่ใช่ &String ซึ่งการจะแปลงเป็น &String จำเป็นต้องมีการจัดสรรหน่วยความจำบน heap (Allocation) ใหม่ และคอมไพเลอร์ไม่สามารถแปลงให้ได้โดยอัตโนมัติ ในขณะที่การแปลงจาก String ไปเป็น &str นั้นแทบไม่มี overhead และคอมไพเลอร์ทำการแปลงให้โดยอัตโนมัติ (implicit)
ดูเพิ่มเติม
- Rust Language Reference ว่าด้วย Type Coercions
- สำหรับบทความอภิปรายเพิ่มเติมเกี่ยวกับวิธีจัดการ
Stringและ&strสามารถอ่านได้ที่ บทความชุดนี้ (2015) โดย Herman J. Radtke III - บล็อกของ Steve Klabnik ว่าด้วย ‘When should I use String vs &str?’