กฎการลงทะเบียนอัตโนมัติที่ถูกต้องของ Rust คืออะไร


183

ฉันเรียนรู้ / ทดลองกับสนิมและในความสง่างามทั้งหมดที่ฉันพบในภาษานี้มีลักษณะเฉพาะอย่างหนึ่งที่ทำให้ฉันงงงัน

ตัวชี้การเกิดสนิมโดยอัตโนมัติเมื่อทำการเรียกเมธอด ฉันทำแบบทดสอบเพื่อกำหนดพฤติกรรมที่แน่นอน:

struct X { val: i32 }
impl std::ops::Deref for X {
    type Target = i32;
    fn deref(&self) -> &i32 { &self.val }
}

trait M { fn m(self); }
impl M for i32   { fn m(self) { println!("i32::m()");  } }
impl M for X     { fn m(self) { println!("X::m()");    } }
impl M for &X    { fn m(self) { println!("&X::m()");   } }
impl M for &&X   { fn m(self) { println!("&&X::m()");  } }
impl M for &&&X  { fn m(self) { println!("&&&X::m()"); } }

trait RefM { fn refm(&self); }
impl RefM for i32  { fn refm(&self) { println!("i32::refm()");  } }
impl RefM for X    { fn refm(&self) { println!("X::refm()");    } }
impl RefM for &X   { fn refm(&self) { println!("&X::refm()");   } }
impl RefM for &&X  { fn refm(&self) { println!("&&X::refm()");  } }
impl RefM for &&&X { fn refm(&self) { println!("&&&X::refm()"); } }


struct Y { val: i32 }
impl std::ops::Deref for Y {
    type Target = i32;
    fn deref(&self) -> &i32 { &self.val }
}

struct Z { val: Y }
impl std::ops::Deref for Z {
    type Target = Y;
    fn deref(&self) -> &Y { &self.val }
}


#[derive(Clone, Copy)]
struct A;

impl M for    A { fn m(self) { println!("A::m()");    } }
impl M for &&&A { fn m(self) { println!("&&&A::m()"); } }

impl RefM for    A { fn refm(&self) { println!("A::refm()");    } }
impl RefM for &&&A { fn refm(&self) { println!("&&&A::refm()"); } }


fn main() {
    // I'll use @ to denote left side of the dot operator
    (*X{val:42}).m();        // i32::m()    , Self == @
    X{val:42}.m();           // X::m()      , Self == @
    (&X{val:42}).m();        // &X::m()     , Self == @
    (&&X{val:42}).m();       // &&X::m()    , Self == @
    (&&&X{val:42}).m();      // &&&X:m()    , Self == @
    (&&&&X{val:42}).m();     // &&&X::m()   , Self == *@
    (&&&&&X{val:42}).m();    // &&&X::m()   , Self == **@
    println!("-------------------------");

    (*X{val:42}).refm();     // i32::refm() , Self == @
    X{val:42}.refm();        // X::refm()   , Self == @
    (&X{val:42}).refm();     // X::refm()   , Self == *@
    (&&X{val:42}).refm();    // &X::refm()  , Self == *@
    (&&&X{val:42}).refm();   // &&X::refm() , Self == *@
    (&&&&X{val:42}).refm();  // &&&X::refm(), Self == *@
    (&&&&&X{val:42}).refm(); // &&&X::refm(), Self == **@
    println!("-------------------------");

    Y{val:42}.refm();        // i32::refm() , Self == *@
    Z{val:Y{val:42}}.refm(); // i32::refm() , Self == **@
    println!("-------------------------");

    A.m();                   // A::m()      , Self == @
    // without the Copy trait, (&A).m() would be a compilation error:
    // cannot move out of borrowed content
    (&A).m();                // A::m()      , Self == *@
    (&&A).m();               // &&&A::m()   , Self == &@
    (&&&A).m();              // &&&A::m()   , Self == @
    A.refm();                // A::refm()   , Self == @
    (&A).refm();             // A::refm()   , Self == *@
    (&&A).refm();            // A::refm()   , Self == **@
    (&&&A).refm();           // &&&A::refm(), Self == @
}

( สนามเด็กเล่น )

ดังนั้นดูเหมือนว่ามากหรือน้อย:

  • คอมไพเลอร์จะแทรกตัวดำเนินการอ้างอิงจำนวนมากเท่าที่จำเป็นเพื่อเรียกใช้เมธอด
  • คอมไพเลอร์เมื่อแก้ไขวิธีการประกาศโดยใช้&self(โทรโดยอ้างอิง):
    • ก่อนอื่นให้ลองเรียกหาข้ออ้างอิงเดียว self
    • จากนั้นลองโทรหาประเภทที่แน่นอนของ self
    • จากนั้นพยายามแทรกตัวดำเนินการอ้างอิงจำนวนมากเท่าที่จำเป็นสำหรับการแข่งขัน
  • วิธีการประกาศโดยใช้self(เรียกค่าตาม) สำหรับประเภทTพฤติกรรมราวกับว่าพวกเขาได้รับการประกาศใช้&self(โทรโดยอ้างอิง) สำหรับประเภท&Tและเรียกการอ้างอิงถึงสิ่งที่อยู่ทางด้านซ้ายของผู้ประกอบการจุด
  • กฎระเบียบดังกล่าวข้างต้นจะถูกทดสอบครั้งแรกกับดิบในตัว dereferencing และถ้าไม่มีการแข่งขันเกินที่มีDerefลักษณะที่ถูกนำมาใช้

กฎการลงทะเบียนอัตโนมัติที่แน่นอนคืออะไร ใครสามารถให้เหตุผลที่เป็นทางการสำหรับการตัดสินใจออกแบบได้หรือไม่?


1
ฉันเคยโพสต์สิ่งนี้ไปยังsubreddit ของ Rustเพื่อหวังคำตอบที่ดี!
Shepmaster

เพื่อความสนุกสนานเป็นพิเศษให้ลองทำการทดสอบซ้ำในชื่อสามัญและเปรียบเทียบผลลัพธ์
2665887

คำตอบ:


138

รหัสเทียมของคุณถูกต้องมาก สำหรับตัวอย่างนี้สมมติว่าเรามีวิธีการเรียกที่foo.bar() foo: Tฉันจะใช้ไวยากรณ์ที่มีคุณสมบัติครบถ้วน (FQS) ให้เป็นที่ชัดเจนเกี่ยวกับสิ่งที่พิมพ์วิธีการที่จะถูกเรียกว่ามีเช่นหรือA::bar(foo) A::bar(&***foo)ฉันแค่เขียนตัวอักษรพิมพ์ใหญ่แบบกอง ๆ ตัวอักษรแต่ละตัวเป็นเพียงตัวพิมพ์ / คุณลักษณะโดยพลการยกเว้นTเป็นประเภทของตัวแปรดั้งเดิมเสมอfooที่วิธีการนั้นเรียกใช้เสมอ

แกนของอัลกอริทึมคือ:

  • สำหรับแต่ละ"ขั้นตอนการพิจารณาซ้ำ" U (นั่นคือตั้งค่าU = Tและจากนั้นU = *T... )
    1. หากมีวิธีการbarที่ประเภทของผู้รับ (ประเภทของselfวิธีการ) ตรงกับที่Uแน่นอนให้ใช้มัน ( "โดยวิธีค่า" )
    2. มิฉะนั้นให้เพิ่มการอ้างอิงอัตโนมัติหนึ่งรายการ (รับ&หรือ&mutรับ) และถ้าผู้รับบางวิธีตรงกับ&Uให้ใช้ ( วิธี "autorefd" )

สะดุดตาทุกอย่างพิจารณา "ประเภทรับ" ของวิธีการที่ไม่Selfประเภทของลักษณะเช่นimpl ... for Foo { fn method(&self) {} }คิดเกี่ยวกับ&Fooเมื่อตรงกับวิธีการและfn method2(&mut self)จะคิดเกี่ยวกับ&mut Fooเมื่อการจับคู่

มันเป็นข้อผิดพลาดหากมีวิธีการหลายวิธีที่ถูกต้องในขั้นตอนด้านใน (นั่นคืออาจมีเพียงศูนย์หรือวิธีการหนึ่งลักษณะที่ถูกต้องในแต่ละ 1 หรือ 2 แต่อาจมีหนึ่งที่ถูกต้องสำหรับแต่ละ: หนึ่ง จาก 1 จะถูกนำมาก่อน) และวิธีการโดยธรรมชาติจะมีความสำคัญมากกว่าลักษณะ มันเป็นข้อผิดพลาดถ้าเราไปถึงจุดสิ้นสุดของลูปโดยไม่พบสิ่งใดที่ตรงกัน มันเป็นข้อผิดพลาดที่มีDerefการใช้งานแบบวนซ้ำซึ่งทำให้วนซ้ำไม่สิ้นสุด (พวกเขาจะกด "ขีด จำกัด การเรียกซ้ำ")

กฎเหล่านี้ดูเหมือนจะทำในสิ่งที่ฉันหมายถึงในสถานการณ์ส่วนใหญ่แม้ว่าการมีความสามารถในการเขียนแบบฟอร์ม FQS ที่ชัดเจนมีประโยชน์มากในบางกรณีขอบและสำหรับข้อผิดพลาดที่สมเหตุสมผลสำหรับรหัสที่สร้างแมโคร

เพิ่มการอ้างอิงอัตโนมัติเพียงรายการเดียวเนื่องจาก

  • หากไม่มีข้อผูกพันสิ่งต่าง ๆ จะแย่ / ช้าเนื่องจากทุกประเภทสามารถมีหมายเลขอ้างอิงโดยพลการ
  • การอ้างอิงหนึ่ง&fooยังคงมีการเชื่อมต่อที่แข็งแกร่งในการfoo(มันเป็นที่อยู่ของfooตัวเอง) แต่การเริ่มต้นมากขึ้นที่จะสูญเสียมันเป็นที่อยู่ของตัวแปรชั่วคราวบางในกองที่ร้านค้า&&foo&foo

ตัวอย่าง

สมมติว่าเรามีสายfoo.refm()ถ้าfooมีประเภท:

  • Xแล้วเราเริ่มต้นด้วยU = X, refmมีประเภทรับ&...ดังนั้นขั้นตอนที่ 1 ไม่ตรงกับการอัตโนมัติโทษช่วยให้เรา&Xและนี้จะจับคู่ (มีSelf = X) เพื่อโทรRefM::refm(&foo)
  • &Xเริ่มต้นด้วยU = &Xซึ่งตรงกับ &selfในขั้นตอนแรก (กับSelf = X) และดังนั้นการโทรคือRefM::refm(foo)
  • &&&&&Xสิ่งนี้ไม่ตรงกับขั้นตอนใด ๆ (ลักษณะที่ไม่ได้ถูกนำมาใช้สำหรับ&&&&Xหรือ&&&&&X) ดังนั้นเราจึงพิจารณาอีกครั้งเพื่อรับU = &&&&Xซึ่งตรงกับ 1 (กับSelf = &&&X) และการโทรคือRefM::refm(*foo)
  • Zไม่ตรงกับขั้นตอนใดขั้นตอนหนึ่งดังนั้นจึงถูกปฏิเสธอีกครั้งเพื่อให้ได้รับYซึ่งยังไม่ตรงกันดังนั้นจึงถูกปฏิเสธอีกครั้งเพื่อให้ได้รับXซึ่งไม่ตรงกับ 1 แต่จะจับคู่หลังจากทำการตอบอัตโนมัติดังนั้นการโทรจึงเป็นRefM::refm(&**foo)เช่นนั้น
  • &&A, 1. ไม่ตรงกันและไม่ 2. เนื่องจากไม่มีการใช้คุณลักษณะสำหรับ&A(สำหรับ 1) หรือ&&A(สำหรับ 2) ดังนั้นจึงถูกอ้างอิงถึง&Aซึ่งตรงกับ 1. ด้วยSelf = A

สมมติว่าเรามีfoo.m()และนั่นAไม่ใช่Copyถ้าfooมีประเภท:

  • Aจากนั้นจึงU = Aจับคู่selfโดยตรงกับการโทรM::m(foo)ด้วยSelf = A
  • &Aแล้ว 1. ไม่ตรงและไม่ไม่ 2. (ค่า&Aมิได้&&Aดำเนินการลักษณะ) จึง dereferenced ไปAซึ่งจะแข่งขัน แต่M::m(*foo)ต้องใช้Aโดยค่าและด้วยเหตุนี้ย้ายออกจากfooดังนั้นข้อผิดพลาด
  • &&A1. ไม่ตรง แต่ autorefing ให้&&&Aซึ่งจะแข่งขันเพื่อโทรด้วยM::m(&foo)Self = &&&A

(คำตอบนี้ขึ้นอยู่กับรหัสและอยู่ใกล้กับ README ที่ล้าสมัยเล็กน้อย Niko Matsakis ผู้เขียนหลักของคอมไพเลอร์ / ภาษาส่วนนี้มองคำตอบนี้ด้วย)


15
คำตอบนี้ดูละเอียดและละเอียด แต่ฉันคิดว่ามันขาดกฎของฤดูร้อนที่สั้นและเข้าถึงได้ หนึ่งฤดูร้อนดังกล่าวได้รับในความคิดเห็นนี้โดย Shepmaster : "มัน [อัลกอริทึม deref] จะ deref หลาย ๆ ครั้งที่เป็นไปได้ ( &&String-> &String-> String->> str) แล้วอ้างอิงที่สูงสุดครั้งเดียว ( str-> &str)"
Lii

(ฉันไม่รู้ว่าคำอธิบายนั้นถูกต้องและสมบูรณ์เพียงใด)
Lii

1
การลงทะเบียนอัตโนมัติเกิดอะไรขึ้นในกรณีใดบ้าง มันใช้เฉพาะสำหรับการแสดงออกของผู้รับสำหรับวิธีการโทร? สำหรับการเข้าถึงข้อมูลยัง? การมอบหมายด้านขวา? ด้านซ้ายมือ? พารามิเตอร์ฟังก์ชั่น? ส่งคืนนิพจน์ค่าหรือไม่
Lii

1
หมายเหตุ: ปัจจุบัน nomicon มีบันทึกสิ่งที่ต้องทำเพื่อขโมยข้อมูลจากคำตอบนี้และเขียนมันขึ้นมาในstatic.rust-lang.org/doc/master/nomicon/dot-operator.html
SAMB

1
การข่มขู่ (A) มีการลองก่อนหน้านี้หรือ (B) ลองหลังจากนี้หรือ (C) ลองในทุกขั้นตอนของอัลกอริทึมนี้หรือ (D) อย่างอื่นหรือไม่?
haslersn

8

อ้างอิงสนิมมีบทที่เกี่ยวกับการแสดงออกวิธีการโทร ฉันคัดลอกส่วนที่สำคัญที่สุดด้านล่าง คำเตือน: เรากำลังพูดถึงการแสดงออกrecv.m()ซึ่งrecvเรียกว่า "การแสดงออกของผู้รับ" ด้านล่าง

ขั้นตอนแรกคือการสร้างรายการประเภทตัวรับสัญญาณของผู้สมัคร รับสิ่งเหล่านี้โดยการลดความซ้ำประเภทของการแสดงออกของผู้รับเพิ่มแต่ละประเภทที่พบในรายการจากนั้นในที่สุดก็พยายามบีบบังคับที่ไม่ได้ระบุในตอนท้ายและเพิ่มประเภทผลลัพธ์หากประสบความสำเร็จ จากนั้นสำหรับผู้สมัครแต่ละคนTให้เพิ่ม&Tและ&mut TไปยังรายการทันทีหลังจากTไปยังรายการทันทีหลังจากที่

ตัวอย่างเช่นถ้าผู้รับมีประเภทBox<[i32;2]>นั้นประเภทผู้สมัครจะได้รับBox<[i32;2]>, &Box<[i32;2]>, &mut Box<[i32;2]>, [i32; 2](โดย dereferencing) &[i32; 2], &mut [i32; 2], [i32](โดยการบังคับ unsized) และในที่สุดก็&[i32]&mut [i32]

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

  1. Tวิธีการโดยเนื้อแท้ (วิธีการดำเนินการโดยตรงบนT[¹])
  2. วิธีการใด ๆ Tที่มีให้โดยลักษณะที่มองเห็นได้ดำเนินการโดย [ ... ]

( หมายเหตุเกี่ยวกับ [¹] : จริง ๆ แล้วฉันคิดว่าการใช้ถ้อยคำนี้ผิดฉันเปิดปัญหาเราแค่เพิกเฉยประโยคนั้นในวงเล็บ)


ลองดูตัวอย่างเล็ก ๆ น้อย ๆ จากรหัสของคุณโดยละเอียด! สำหรับตัวอย่างของคุณเราสามารถเพิกเฉยต่อส่วนที่เกี่ยวกับ

(*X{val:42}).m()i32ประเภทการแสดงออกของผู้รับคือ เราทำตามขั้นตอนเหล่านี้:

  • การสร้างรายการประเภทตัวรับสัญญาณผู้สมัคร:
    • i32 ไม่สามารถยกเลิกการลงทะเบียนได้ดังนั้นเราจึงดำเนินการตามขั้นตอนที่ 1 แล้ว: [i32]
    • ต่อไปเราจะเพิ่มและ&i32 &mut i32รายการ:[i32, &i32, &mut i32]
  • ค้นหาวิธีสำหรับตัวรับสัญญาณแต่ละประเภท:
    • เราพบซึ่งมีประเภทเครื่องรับ<i32 as M>::m i32ดังนั้นเราเสร็จแล้ว


ป่านฉะนี้ง่ายมาก (&&A).m()ตอนนี้ขอเลือกเป็นตัวอย่างที่ยากมากขึ้น: &&Aประเภทของการแสดงออกของผู้รับคือ เราทำตามขั้นตอนเหล่านี้:

  • การสร้างรายการประเภทตัวรับสัญญาณผู้สมัคร:
    • &&Aสามารถยกเลิกการลงทะเบียน&Aได้ดังนั้นเราจึงเพิ่มรายการนั้นเข้าไปในรายการ &Aสามารถยกเลิกการลงทะเบียนอีกครั้งดังนั้นเราจึงเพิ่มลงAในรายการ Aไม่สามารถถูกปฏิเสธได้ดังนั้นเราจึงหยุด รายการ:[&&A, &A, A]
    • ถัดไปสำหรับแต่ละประเภทTในรายการเราเพิ่ม&Tและทันทีหลังจากที่&mut T Tรายการ:[&&A, &&&A, &mut &&A, &A, &&A, &mut &A, A, &A, &mut A]
  • ค้นหาวิธีสำหรับตัวรับสัญญาณแต่ละประเภท:
    • ไม่มีวิธีที่มีประเภทผู้รับ&&Aดังนั้นเราจึงไปที่ประเภทถัดไปในรายการ
    • เราพบว่าวิธีการที่แน่นอนมีประเภทรับ<&&&A as M>::m &&&Aดังนั้นเราจะทำ

นี่คือรายการตัวรับสัญญาณผู้สมัครสำหรับตัวอย่างทั้งหมดของคุณ ประเภทที่อยู่ใน⟪x⟫นั้นคือ "ชนะ" คือประเภทแรกที่สามารถหาวิธีที่เหมาะสม โปรดจำไว้ว่าประเภทแรกในรายการมักเป็นประเภทของการแสดงออกของผู้รับ สุดท้ายฉันจัดรูปแบบรายการเป็นสามบรรทัด แต่นั่นเป็นเพียงการจัดรูปแบบ: รายการนี้เป็นรายการแบบเรียบ

  • (*X{val:42}).m()<i32 as M>::m
    [i32, &i32, &mut i32]
  • X{val:42}.m()<X as M>::m
    [⟪X⟫, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&X{val:42}).m()<&X as M>::m
    [&X⟫, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&X{val:42}).m()<&&X as M>::m
    [&&X⟫, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&&X{val:42}).m()<&&&X as M>::m
    [&&&X⟫, &&&&X, &mut &&&X, 
     &&X, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&&&X{val:42}).m()<&&&X as M>::m
    [&&&&X, &&&&&X, &mut &&&&X,&&&X⟫, &&&&X, &mut &&&X, 
     &&X, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&&&&X{val:42}).m()<&&&X as M>::m
    [&&&&&X, &&&&&&X, &mut &&&&&X, 
     &&&&X, &&&&&X, &mut &&&&X,&&&X⟫, &&&&X, &mut &&&X, 
     &&X, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]


  • (*X{val:42}).refm()<i32 as RefM>::refm
    [i32,&i32, &mut i32]
  • X{val:42}.refm()<X as RefM>::refm
    [X,&X⟫, &mut X, 
     i32, &i32, &mut i32]
  • (&X{val:42}).refm()<X as RefM>::refm
    [&X⟫, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&X{val:42}).refm()<&X as RefM>::refm
    [&&X⟫, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&&X{val:42}).refm()<&&X as RefM>::refm
    [&&&X⟫, &&&&X, &mut &&&X, 
     &&X, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&&&X{val:42}).refm()<&&&X as RefM>::refm
    [&&&&X⟫, &&&&&X, &mut &&&&X, 
     &&&X, &&&&X, &mut &&&X, 
     &&X, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&&&&X{val:42}).refm()<&&&X as RefM>::refm
    [&&&&&X, &&&&&&X, &mut &&&&&X,&&&&X⟫, &&&&&X, &mut &&&&X, 
     &&&X, &&&&X, &mut &&&X, 
     &&X, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]


  • Y{val:42}.refm()<i32 as RefM>::refm
    [Y, &Y, &mut Y,
     i32,&i32, &mut i32]
  • Z{val:Y{val:42}}.refm()<i32 as RefM>::refm
    [Z, &Z, &mut Z,
     Y, &Y, &mut Y,
     i32,&i32, &mut i32]


  • A.m()<A as M>::m
    [⟪A⟫, &A, &mut A]
  • (&A).m()<A as M>::m
    [&A, &&A, &mut &A,
     ⟪A⟫, &A, &mut A]
  • (&&A).m()<&&&A as M>::m
    [&&A,&&&A⟫, &mut &&A,
     &A, &&A, &mut &A,
     A, &A, &mut A]
  • (&&&A).m()<&&&A as M>::m
    [&&&A⟫, &&&&A, &mut &&&A,
     &&A, &&&A, &mut &&A,
     &A, &&A, &mut &A,
     A, &A, &mut A]
  • A.refm()<A as RefM>::refm
    [A,&A⟫, &mut A]
  • (&A).refm()<A as RefM>::refm
    [&A⟫, &&A, &mut &A,
     A, &A, &mut A]
  • (&&A).refm()<A as RefM>::refm
    [&&A, &&&A, &mut &&A,&A⟫, &&A, &mut &A,
     A, &A, &mut A]
  • (&&&A).refm()<&&&A as RefM>::refm
    [&&&A,&&&&A⟫, &mut &&&A,
     &&A, &&&A, &mut &&A,
     &A, &&A, &mut &A,
     A, &A, &mut A]
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.