เหตุใดจึงไม่แนะนำให้ยอมรับการอ้างอิงถึง String (& String), Vec (& Vec) หรือ Box (& Box) เป็นอาร์กิวเมนต์ของฟังก์ชัน


128

ฉันเขียนรหัส Rust ที่ใช้&Stringเป็นอาร์กิวเมนต์:

fn awesome_greeting(name: &String) {
    println!("Wow, you are awesome, {}!", name);
}

ฉันยังเขียนโค้ดที่ใช้อ้างอิงถึงVecหรือBox:

fn total_price(prices: &Vec<i32>) -> i32 {
    prices.iter().sum()
}

fn is_even(value: &Box<i32>) -> bool {
    **value % 2 == 0
}

อย่างไรก็ตามฉันได้รับคำติชมว่าการทำเช่นนี้ไม่ใช่ความคิดที่ดี ทำไมจะไม่ล่ะ?

คำตอบ:


163

TL; DR: หนึ่งสามารถใช้แทน&str, &[T]หรือ&Tเพื่อให้รหัสทั่วไปมากขึ้น


  1. สาเหตุหลักประการหนึ่งที่ต้องใช้ a Stringหรือ a Vecเป็นเพราะอนุญาตให้เพิ่มหรือลดความจุได้ แต่เมื่อคุณยอมรับการอ้างอิงที่ไม่เปลี่ยนรูปคุณไม่สามารถใช้วิธีการใด ๆ ที่น่าสนใจผู้ที่อยู่ในหรือVecString

  2. ยอมรับ&String, &Vecหรือ&Boxยังต้องมีการโต้แย้งที่จะได้รับการจัดสรรในกองก่อนที่คุณจะสามารถเรียกใช้ฟังก์ชัน ยอมรับ a &strอนุญาตให้สตริงลิเทอรัล (บันทึกไว้ในข้อมูลโปรแกรม) และยอมรับ&[T]หรือ&Tอนุญาตอาร์เรย์หรือตัวแปรที่จัดสรรสแต็ก การจัดสรรที่ไม่จำเป็นคือการสูญเสียประสิทธิภาพ โดยปกติจะปรากฏทันทีเมื่อคุณพยายามเรียกใช้วิธีการเหล่านี้ในการทดสอบหรือmainวิธีการ:

    awesome_greeting(&String::from("Anna"));
    total_price(&vec![42, 13, 1337])
    is_even(&Box::new(42))
  3. พิจารณาผลการดำเนินงานก็คือว่า&String, &Vecและ&Boxแนะนำชั้นที่ไม่จำเป็นของร้ายที่คุณต้อง dereference &Stringที่จะได้รับStringและจากนั้นดำเนินการ dereference &strที่สองที่จะจบลงที่

คุณควรยอมรับstring slice ( &str), slice ( &[T]) หรือเพียงแค่ reference ( &T) แทน &String, &Vec<T>หรือ&Box<T>จะได้รับการข่มขู่โดยอัตโนมัติไปยัง&str, &[T]หรือ&Tตามลำดับ

fn awesome_greeting(name: &str) {
    println!("Wow, you are awesome, {}!", name);
}
fn total_price(prices: &[i32]) -> i32 {
    prices.iter().sum()
}
fn is_even(value: &i32) -> bool {
    *value % 2 == 0
}

ตอนนี้คุณสามารถเรียกใช้วิธีการเหล่านี้ด้วยชุดประเภทที่กว้างขึ้น ยกตัวอย่างเช่นawesome_greetingสามารถเรียกว่ามีตัวอักษรสตริง ( "Anna") หรือStringส่วนที่จัด total_priceสามารถเรียกว่ามีการอ้างอิงไปยังอาร์เรย์ ( &[1, 2, 3]) หรือVecส่วนที่จัด


หากคุณต้องการเพิ่มหรือลบรายการออกจากStringหรือVec<T>คุณสามารถใช้ข้อมูลอ้างอิงที่เปลี่ยนแปลงได้ ( &mut Stringหรือ&mut Vec<T>):

fn add_greeting_target(greeting: &mut String) {
    greeting.push_str("world!");
}
fn add_candy_prices(prices: &mut Vec<i32>) {
    prices.push(5);
    prices.push(25);
}

โดยเฉพาะสำหรับชิ้นนี้คุณยังสามารถยอมรับหรือ&mut [T] &mut strสิ่งนี้ช่วยให้คุณสามารถเปลี่ยนค่าเฉพาะภายในชิ้นได้ แต่คุณไม่สามารถเปลี่ยนจำนวนรายการภายในชิ้นงานได้ (ซึ่งหมายความว่ามันถูก จำกัด ไว้มากสำหรับสตริง):

fn reset_first_price(prices: &mut [i32]) {
    prices[0] = 0;
}
fn lowercase_first_ascii_character(s: &mut str) {
    if let Some(f) = s.get_mut(0..1) {
        f.make_ascii_lowercase();
    }
}

5
แล้วtl; drในตอนต้นล่ะ? คำตอบนี้ค่อนข้างยาวแล้ว บางอย่างเช่น " &strกว้างกว่า (เช่นเดียวกับ: กำหนดข้อ จำกัด น้อยลง) โดยไม่มีความสามารถลดลง"? นอกจากนี้: จุดที่ 3 มักจะไม่สำคัญเท่าที่ฉันคิด โดยปกติแล้วVecs และStrings จะอาศัยอยู่บนสแต็กและมักจะอยู่ใกล้กับสแต็กเฟรมปัจจุบัน สแต็กมักจะร้อนและการแยกส่วนจะถูกเสิร์ฟจากแคชของ CPU
Lukas Kalbertodt

3
@Shepmaster: เกี่ยวกับต้นทุนการปันส่วนอาจเป็นมูลค่าการกล่าวถึงปัญหาเฉพาะของสตริงย่อย / ชิ้นส่วนเมื่อพูดถึงการจัดสรรที่จำเป็น total_price(&prices[0..4])ไม่จำเป็นต้องจัดสรรเวกเตอร์ใหม่สำหรับชิ้นส่วน
Matthieu M.

4
นี่คือคำตอบที่ยอดเยี่ยม ฉันเพิ่งเริ่มต้นใน Rust และกำลังจะรู้ว่าฉันควรใช้ a &strและทำไม (มาจาก Python ดังนั้นฉันมักจะไม่จัดการกับประเภทอย่างชัดเจน) เคลียร์ทุกอย่างให้
เรียบร้อย

2
เคล็ดลับที่ยอดเยี่ยมเกี่ยวกับพารามิเตอร์ เพียงแค่มีข้อสงสัยประการหนึ่ง: "การยอมรับ & String, & Vec หรือ & Box ยังต้องมีการจัดสรรก่อนจึงจะเรียกใช้ method ได้" ... ทำไมถึงเป็นเช่นนั้น? คุณช่วยชี้ส่วนในเอกสารที่ฉันสามารถอ่านโดยละเอียดได้ไหม (ฉันเป็นคนขอทาน) นอกจากนี้เราขอคำแนะนำที่คล้ายกันเกี่ยวกับประเภทการคืนสินค้าได้หรือไม่?
Nawaz

2
ฉันขาดข้อมูลเกี่ยวกับสาเหตุที่ต้องมีการจัดสรรเพิ่มเติม สตริงถูกเก็บไว้ในฮีปเมื่อยอมรับ & String เป็นอาร์กิวเมนต์ทำไมไม่เพียงแค่ Rust ส่งตัวชี้ที่เก็บไว้ในสแต็กที่ชี้ไปยังพื้นที่ฮีปฉันไม่เข้าใจว่าทำไมการส่ง & String จึงต้องมีการจัดสรรเพิ่มเติมโดยส่งสตริง ชิ้นควรต้องมีการส่งตัวชี้ที่เก็บไว้ในสแต็กที่ชี้ไปยังพื้นที่ฮีป?
cjohansson

22

นอกจากนี้ในการตอบ Shepmaster ของอีกเหตุผลหนึ่งที่จะยอมรับ&str(และในทำนองเดียวกัน&[T]ฯลฯ ) เป็นเพราะทั้งหมดของประเภทอื่น ๆนอกเหนือจาก Stringและที่ยังตอบสนองความ&str Deref<Target = str>หนึ่งในตัวอย่างที่โดดเด่นที่สุดคือCow<str>ซึ่งช่วยให้คุณมีความยืดหยุ่นมากว่าคุณกำลังจัดการกับข้อมูลที่เป็นเจ้าของหรือยืมมา

ถ้าคุณมี:

fn awesome_greeting(name: &String) {
    println!("Wow, you are awesome, {}!", name);
}

แต่คุณต้องเรียกมันว่าCow<str>คุณจะต้องทำสิ่งนี้:

let c: Cow<str> = Cow::from("hello");
// Allocate an owned String from a str reference and then makes a reference to it anyway!
awesome_greeting(&c.to_string());

เมื่อคุณเปลี่ยนประเภทอาร์กิวเมนต์เป็น&strคุณสามารถใช้ได้Cowอย่างราบรื่นโดยไม่มีการจัดสรรที่ไม่จำเป็นเช่นเดียวกับString:

let c: Cow<str> = Cow::from("hello");
// Just pass the same reference along
awesome_greeting(&c);

let c: Cow<str> = Cow::from(String::from("hello"));
// Pass a reference to the owned string that you already have
awesome_greeting(&c);

การยอมรับ&strจะทำให้การเรียกใช้ฟังก์ชันของคุณมีความสม่ำเสมอและสะดวกสบายยิ่งขึ้นและวิธีที่ "ง่ายที่สุด" ในตอนนี้ก็มีประสิทธิภาพมากที่สุดเช่นกัน ตัวอย่างเหล่านี้จะใช้ได้กับCow<[T]>ฯลฯ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.