ตรงข้ามกับลักษณะการยืมสำหรับ Copy types?

Aug 18 2020

ผมเคยเห็นBorrowลักษณะที่ใช้ในการกำหนดฟังก์ชั่นที่ยอมรับทั้งเจ้าของพิมพ์หรือการอ้างอิงเช่นหรือT วิธีการที่เรียกว่าในการทำงานเพื่อให้ได้&Tborrow()&T

มีลักษณะบางอย่างที่อนุญาตให้ใช้สิ่งที่ตรงกันข้าม (เช่นฟังก์ชันที่ยอมรับTหรือ&Tและได้รับT) สำหรับCopyประเภทหรือไม่?

เช่นสำหรับตัวอย่างนี้:

use std::borrow::Borrow;

fn foo<T: Borrow<u32>>(value: T) -> u32 {
    *value.borrow()
}

fn main() {
    println!("{}", foo(&5));
    println!("{}", foo(5));
}

สิ่งนี้เรียกร้องborrow()ให้มีการอ้างอิงซึ่งจะถูกยกเลิกการอ้างอิงทันที

มีการใช้งานอื่นที่เพียงแค่คัดลอกค่าหากTถูกส่งผ่านและยกเลิกการอ้างอิงหาก&Tได้รับ หรือข้างต้นเป็นวิธีสำนวนในการเขียนสิ่งนี้?

คำตอบ

5 trentcl Aug 18 2020 at 14:09

ไม่มีลักษณะผกผันจริง ๆ สำหรับBorrowเพราะมันไม่ได้มีประโยชน์จริง ๆ เมื่อผูกไว้กับฟังก์ชันในลักษณะเดียวกันBorrowคือ เหตุผลเกี่ยวข้องกับความเป็นเจ้าของ

ทำไม "ผกผันBorrow" จึงมีประโยชน์น้อยกว่าBorrow?

ฟังก์ชั่นที่ต้องการการอ้างอิง

พิจารณาฟังก์ชันที่ต้องการอ้างอิงอาร์กิวเมนต์เท่านั้น:

fn puts(arg: &str) {
    println!("{}", arg);
}

การยอมรับStringอาจเป็นเรื่องไร้สาระที่นี่เพราะputsไม่จำเป็นต้องเป็นเจ้าของข้อมูล แต่การยอมรับ&strหมายความว่าบางครั้งเราอาจบังคับให้ผู้โทรเก็บข้อมูลไว้นานเกินความจำเป็น:

{
    let output = create_some_string();
    output.push_str(some_other_string);
    puts(&output);
    // do some other stuff but never use `output` again
} // `output` isn't dropped until here

ปัญหาที่outputไม่จำเป็นหลังจากส่งต่อไปputsแล้วและผู้โทรรู้เรื่องนี้ แต่putsต้องการข้อมูลอ้างอิงดังนั้นoutputต้องมีชีวิตอยู่จนกว่าจะสิ้นสุดการบล็อก เห็นได้ชัดว่าคุณสามารถแก้ไขปัญหานี้ในโทรโดยการเพิ่มบล็อกเพิ่มเติมและบางครั้งletแต่putsยังสามารถทำทั่วไปเพื่อให้โทรมอบหมายความรับผิดชอบของการทำความสะอาดoutput:

fn puts<T: Borrow<str>>(arg: T) {
    println!("{}", arg.borrow());
}

การยอมรับT: Borrowเพื่อputsให้ผู้โทรมีความยืดหยุ่นในการตัดสินใจว่าจะเก็บอาร์กิวเมนต์ไว้รอบ ๆ หรือจะย้ายไปไว้ในฟังก์ชัน

ฟังก์ชันที่ต้องการค่าที่เป็นเจ้าของ

ตอนนี้พิจารณากรณีของฟังก์ชันที่ต้องเป็นเจ้าของจริง:

struct Wrapper(String);
fn wrap(arg: String) -> Wrapper {
    Wrapper(arg)
}

ในกรณีนี้การยอมรับ&strจะเป็นเรื่องโง่เพราะwrapจะต้องเรียกto_owned()มัน หากผู้โทรมีข้อมูลStringที่ไม่ได้ใช้งานอีกต่อไปก็จะต้องคัดลอกข้อมูลที่เพิ่งย้ายไปยังฟังก์ชันโดยไม่จำเป็น ในกรณีนี้ยอมรับเป็นตัวเลือกที่มีความยืดหยุ่นมากขึ้นเพราะจะช่วยให้ผู้ที่โทรมาจะตัดสินใจว่าจะทำให้โคลนหรือผ่านการมีอยู่String Stringการมีลักษณะ "ผกผันBorrow" จะไม่เพิ่มความยืดหยุ่นใด ๆ ที่arg: Stringไม่มีให้

แต่Stringไม่เคยโต้แย้งเหมาะกับการทำงานมากที่สุดเพราะมีหลายชนิดที่แตกต่างกันของสตริง: &str, Cow<str>, Box<str>... เราสามารถทำให้wrapเล็ก ๆ น้อย ๆ เหมาะกับการทำงานมากขึ้นด้วยการพูดว่าจะยอมรับสิ่งที่สามารถแปลงintoString

fn wrap<T: Into<String>>(arg: T) -> Wrapper {
    Wrapper(arg.into())
}

ซึ่งหมายความว่าคุณสามารถเรียกมันว่าwrap("hello, world")โดยไม่ต้องเรียก.to_owned()ตามตัวอักษร ซึ่งไม่ใช่การชนะแบบยืดหยุ่น - ผู้โทรสามารถโทร.into()แทนได้ตลอดเวลาโดยไม่สูญเสียความทั่วไป - แต่เป็นการชนะตามหลักสรีรศาสตร์

สิ่งที่เกี่ยวกับCopyประเภท?

ตอนนี้คุณถามเกี่ยวกับCopyประเภท ส่วนใหญ่ข้อโต้แย้งข้างต้นยังคงมีผลบังคับใช้ หากคุณกำลังเขียนฟังก์ชันที่ต้องการใช้putsเพียง a การ&Aใช้งานT: Borrow<A>อาจยืดหยุ่นกว่าสำหรับผู้โทร สำหรับฟังก์ชั่นเหมือนwrapที่ต้องการทั้งหมดก็มีความยืดหยุ่นมากขึ้นในการยอมรับA Aแต่สำหรับCopyประเภทความได้เปรียบด้านการยศาสตร์ในการยอมรับT: Into<A>นั้นมีความชัดเจนน้อยกว่า

  • สำหรับประเภทจำนวนเต็มเนื่องจากข้อมูลทั่วไปยุ่งกับการอนุมานประเภทการใช้โดยปกติจะทำให้ใช้ตัวอักษรน้อยลงตามหลักสรีรศาสตร์ คุณอาจต้องใส่คำอธิบายประกอบประเภทอย่างชัดเจน
  • เนื่องจาก&u32ไม่ได้ใช้งานInto<u32>เคล็ดลับเฉพาะนั้นจะไม่ได้ผลที่นี่
  • เนื่องจากCopyประเภทต่างๆพร้อมใช้งานเป็นค่าที่เป็นเจ้าของจึงไม่ค่อยใช้โดยอ้างอิงในตอนแรก
  • สุดท้ายเปลี่ยน&Aเป็นAเมื่อA: Copyเป็นง่ายๆเป็นเพียงการเพิ่ม*; ความสามารถในการข้ามขั้นตอนนั้นอาจไม่ใช่การชนะที่น่าสนใจเพียงพอที่จะถ่วงดุลความซับซ้อนที่เพิ่มขึ้นของการใช้ยาชื่อสามัญในกรณีส่วนใหญ่

สรุปได้ว่าfooควรยอมรับvalue: u32และปล่อยให้ผู้โทรตัดสินใจว่าจะรับค่านั้นอย่างไร

ดูสิ่งนี้ด้วย

  • เป็นเรื่องธรรมดากว่าที่จะส่งต่อค่าหรือ pass-by-reference เมื่อเมธอดต้องการความเป็นเจ้าของค่า?
2 Sunreef Aug 18 2020 at 11:00

ด้วยฟังก์ชันที่คุณมีคุณสามารถใช้เฉพาะ a u32หรือประเภทที่สามารถยืมu32ได้

คุณสามารถทำให้ฟังก์ชันของคุณกว้างขึ้นได้โดยใช้อาร์กิวเมนต์แม่แบบที่สอง

fn foo<T: Copy, N: Borrow<T>>(value: N) -> T {
    *value.borrow()
}

อย่างไรก็ตามนี่เป็นเพียงวิธีแก้ปัญหาบางส่วนเนื่องจากจะต้องใช้คำอธิบายประกอบประเภทในบางกรณีเพื่อให้ทำงานได้อย่างถูกต้อง

ตัวอย่างเช่นมันทำงานนอกกรอบด้วยusize:

let v = 0usize;
println!("{}", foo(v));

ไม่มีปัญหาสำหรับคอมไพเลอร์ที่จะเดาว่าfoo(v)เป็นไฟล์usize.

อย่างไรก็ตามหากคุณลองfoo(&v)คอมไพเลอร์จะบ่นว่าไม่พบประเภทเอาต์พุตที่ถูกต้องTเนื่องจาก&Tสามารถใช้Borrowลักษณะหลายอย่างสำหรับประเภทต่างๆ คุณต้องระบุอย่างชัดเจนว่าคุณต้องการใช้เป็นเอาต์พุตใด

let output: usize = foo(&v);