เหตุใด 'ref' และ 'out' จึงไม่สนับสนุนความหลากหลาย


124

ดำเนินการดังต่อไปนี้:

class A {}

class B : A {}

class C
{
    C()
    {
        var b = new B();
        Foo(b);
        Foo2(ref b); // <= compile-time error: 
                     // "The 'ref' argument doesn't match the parameter type"
    }

    void Foo(A a) {}

    void Foo2(ref A a) {}  
}

เหตุใดข้อผิดพลาดเวลาคอมไพล์ข้างต้นจึงเกิดขึ้น สิ่งนี้เกิดขึ้นกับทั้งสองrefและoutข้อโต้แย้ง

คำตอบ:


169

=============

UPDATE: ฉันใช้คำตอบนี้เป็นพื้นฐานสำหรับรายการบล็อกนี้:

เหตุใดพารามิเตอร์ ref and out จึงไม่อนุญาตให้มีการเปลี่ยนแปลงประเภท

ดูหน้าบล็อกสำหรับความคิดเห็นเพิ่มเติมเกี่ยวกับปัญหานี้ ขอบคุณสำหรับคำถามที่ดี

=============

สมมติว่าคุณได้เรียนAnimal, Mammal, Reptile, Giraffe, TurtleและTigerมีความสัมพันธ์ที่ subclassing ที่เห็นได้ชัด

void M(ref Mammal m)ตอนนี้สมมติว่าคุณมีวิธีการ Mทั้งอ่านและเขียนmได้


คุณสามารถส่งตัวแปรประเภทAnimalไปยังM?

ไม่ตัวแปรนั้นอาจมี a Turtleแต่Mจะถือว่ามี แต่สัตว์เลี้ยงลูกด้วยนม A Turtleไม่ใช่Mammal.

ข้อสรุปที่ 1 : refไม่สามารถสร้างพารามิเตอร์ให้ "ใหญ่กว่า" ได้ (มีสัตว์มากกว่าสัตว์เลี้ยงลูกด้วยนมดังนั้นตัวแปรจึง "ใหญ่ขึ้น" เพราะสามารถบรรจุสิ่งของได้มากกว่า)


คุณสามารถส่งตัวแปรประเภทGiraffeไปยังM?

ไม่MสามารถเขียนถึงmและMอาจต้องการเขียนTigerลงในmไฟล์. ตอนนี้คุณได้ใส่Tigerตัวแปรซึ่งเป็นประเภทGiraffeจริงๆ

ข้อสรุปที่ 2 : refพารามิเตอร์ไม่สามารถทำให้ "เล็กลง" ได้


N(out Mammal n)ตอนนี้พิจารณา

คุณสามารถส่งตัวแปรประเภทGiraffeไปยังN?

ไม่NสามารถเขียนถึงnและNอาจต้องการเขียนไฟล์Tiger.

สรุป 3 : outพารามิเตอร์ไม่สามารถทำให้ "เล็กลง" ได้


คุณสามารถส่งตัวแปรประเภทAnimalไปยังN?

อืมมม

ทำไมไม่? Nอ่านไม่ออกnก็เขียนได้แค่นั้นใช่ไหม คุณเขียนTigerถึงตัวแปรประเภทAnimalและคุณพร้อมแล้วใช่ไหม?

ไม่ถูกต้อง. กฎไม่ใช่ " Nเขียนถึงได้เท่านั้นn"

กฎคือสั้น ๆ :

1) Nต้องเขียนถึงnก่อนNส่งคืนตามปกติ (หากNโยนการเดิมพันทั้งหมดจะปิด)

2) Nมีการเขียนบางสิ่งบางอย่างก่อนที่จะอ่านอะไรบางอย่างจากnn

ที่อนุญาตลำดับของเหตุการณ์นี้:

  • ประกาศเขตข้อมูลประเภทxAnimal
  • ผ่านการxเป็นพารามิเตอร์outN
  • NเขียนTigerลงในซึ่งเป็นนามแฝงสำหรับnx
  • ในอีกกระทู้มีคนเขียนTurtleลงในxไฟล์.
  • Nความพยายามที่จะอ่านเนื้อหาของnและค้นพบในสิ่งที่คิดว่าเป็นตัวแปรประเภทTurtleMammal

เห็นได้ชัดว่าเราต้องการทำให้สิ่งนั้นผิดกฎหมาย

ข้อสรุปที่ 4 : outไม่สามารถสร้างพารามิเตอร์ให้ "ใหญ่ขึ้น" ได้


ข้อสรุปสุดท้าย : ทั้งrefมิได้outพารามิเตอร์อาจแตกต่างกันประเภทของพวกเขา การทำอย่างอื่นคือการทำลายประเภทความปลอดภัยที่ตรวจสอบได้

หากปัญหาเหล่านี้อยู่ในความสนใจประเภททฤษฎีพื้นฐานที่คุณพิจารณาการอ่านชุดของฉันเกี่ยวกับวิธีการแปรปรวนและ contravariance ทำงานใน C # 4.0


6
+1 คำอธิบายที่ยอดเยี่ยมโดยใช้ตัวอย่างระดับโลกแห่งความเป็นจริงที่แสดงให้เห็นถึงประเด็นปัญหาอย่างชัดเจน (เช่น - การอธิบายด้วย A, B และ C ทำให้ยากที่จะแสดงให้เห็นว่าเหตุใดจึงไม่ได้ผล)
Grant Wagner

4
ฉันรู้สึกถ่อมตัวเมื่ออ่านกระบวนการคิดนี้ ฉันคิดว่าฉันกลับไปที่หนังสือดีกว่า!
Scott McKenzie

ในกรณีนี้เราไม่สามารถใช้ตัวแปรคลาส Abstract เป็นอาร์กิวเมนต์และส่งต่อวัตถุคลาสที่ได้รับมาได้ !!
Prashant Cholachagudda

แต่เหตุใดจึงoutไม่สามารถสร้างพารามิเตอร์ให้ "ใหญ่ขึ้น" ได้ ลำดับที่คุณอธิบายสามารถนำไปใช้กับตัวแปรใดก็ได้ไม่ใช่เฉพาะoutตัวแปรพารามิเตอร์ และผู้อ่านต้องร่ายค่าอาร์กิวเมนต์Mammalก่อนที่เขาจะพยายามเข้าถึงเนื่องจากMammalและแน่นอนว่ามันอาจล้มเหลวได้หากเขาไม่เกรงใจ
astef

29

เนื่องจากในทั้งสองกรณีคุณต้องสามารถกำหนดค่าให้กับพารามิเตอร์ ref / out ได้

หากคุณพยายามส่ง b ไปยังเมธอด Foo2 เป็นข้อมูลอ้างอิงและใน Foo2 คุณพยายามระบุ a = new A () สิ่งนี้จะไม่ถูกต้อง
เหตุผลเดียวกับที่คุณเขียนไม่ได้:

B b = new A();

+1 ตรงประเด็นและอธิบายเหตุผลได้อย่างดีเยี่ยม
Rui Craveiro

10

คุณกำลังดิ้นรนกับปัญหา OOP แบบคลาสสิกของความแปรปรวนร่วม (และความขัดแย้ง) โปรดดูวิกิพีเดีย : ข้อเท็จจริงนี้อาจขัดต่อความคาดหวังโดยสัญชาตญาณ แต่เป็นไปไม่ได้ทางคณิตศาสตร์ที่จะอนุญาตให้มีการแทนที่คลาสที่ได้รับแทนคลาสพื้นฐานสำหรับอาร์กิวเมนต์ที่ไม่แน่นอน (กำหนดได้) (และ ยังภาชนะที่มีรายการที่จะมอบหมาย, ด้วยเหตุผลเพียงแค่เดียวกัน) ขณะที่ยังคงเคารพในหลักการของ Liskov เหตุใดจึงร่างไว้ในคำตอบที่มีอยู่และสำรวจเพิ่มเติมในบทความวิกิพีเดียและลิงก์จากที่นั่น

ภาษา OOP ที่ดูเหมือนจะทำได้ในขณะที่ยังคงเป็น typesafe แบบคงเดิมคือ "การโกง" (การแทรกการตรวจสอบประเภทไดนามิกที่ซ่อนอยู่หรือต้องตรวจสอบเวลาคอมไพล์ของแหล่งที่มาทั้งหมดเพื่อตรวจสอบ) ตัวเลือกพื้นฐานคือ: ยอมแพ้กับความแปรปรวนร่วมนี้และยอมรับการไขปริศนาของผู้ปฏิบัติงาน (ตามที่ C # ทำที่นี่) หรือย้ายไปใช้วิธีการพิมพ์แบบไดนามิก (เช่นเดียวกับภาษา OOP แรก Smalltalk ทำ) หรือย้ายไปที่ไม่เปลี่ยนรูป (single- การกำหนด) ข้อมูลเช่นเดียวกับภาษาที่ใช้งานได้ (ภายใต้ความไม่เปลี่ยนแปลงคุณสามารถรองรับความแปรปรวนร่วมและหลีกเลี่ยงปริศนาอื่น ๆ ที่เกี่ยวข้องเช่นการที่คุณไม่สามารถมี Square subclass Rectangle ในโลกข้อมูลที่เปลี่ยนแปลงไม่ได้)


4

พิจารณา:

class C : A {}
class B : A {}

void Foo2(ref A a) { a = new C(); } 

B b = null;
Foo2(ref b);

มันจะละเมิดประเภทความปลอดภัย


ยิ่งเป็นประเภท "b" ที่อนุมานไม่ชัดเจนเนื่องจากตัวแปรเป็นปัญหาที่นั่น

ฉันเดาว่าในบรรทัดที่ 6 คุณหมายถึง => B b = null;
Alejandro Miralles

@amiralles - ใช่นั่นvarผิดทั้งหมด แก้ไขแล้ว.
Henk Holterman

4

ในขณะที่คำตอบอื่น ๆ ได้อธิบายเหตุผลเบื้องหลังพฤติกรรมนี้อย่างรวบรัด แต่ฉันคิดว่าควรค่าแก่การกล่าวถึงว่าหากคุณจำเป็นต้องทำบางสิ่งในลักษณะนี้จริง ๆ คุณสามารถใช้ฟังก์ชันที่คล้ายกันได้โดยการทำให้ Foo2 เป็นวิธีการทั่วไปเช่น:

class A {}

class B : A {}

class C
{
    C()
    {
        var b = new B();
        Foo(b);
        Foo2(ref b); // <= no compile error!
    }

    void Foo(A a) {}

    void Foo2<AType> (ref AType a) where AType: A {}  
}

2

เพราะให้จะส่งผลให้วัตถุที่ไม่ถูกต้องเพราะเท่านั้นที่รู้วิธีการกรอกข้อมูลส่วนหนึ่งของFoo2ref BFoo2AB


0

ไม่ใช่ว่าคอมไพเลอร์บอกคุณว่าต้องการให้คุณโยนวัตถุอย่างชัดเจนเพื่อให้แน่ใจว่าคุณรู้ว่าเจตนาของคุณคืออะไร?

Foo2(ref (A)b)

ไม่สามารถทำได้ "อาร์กิวเมนต์ ref หรือ out ต้องเป็นตัวแปรที่กำหนดได้"

0

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

class Derp : interfaceX
{
   int somevalue=0; //specified that this class contains somevalue by interfaceX
   public Derp(int val)
    {
    somevalue = val;
    }

}


void Foo(ref object obj){
    int result = (interfaceX)obj.somevalue;
    //do stuff to result variable... in my case data access
    obj = Activator.CreateInstance(obj.GetType(), result);
}

main()
{
   Derp x = new Derp();
   Foo(ref Derp);
}

สิ่งนี้จะไม่รวบรวม แต่จะได้ผลหรือไม่?


0

หากคุณใช้ตัวอย่างที่ใช้ได้จริงสำหรับประเภทของคุณคุณจะเห็น:

SqlConnection connection = new SqlConnection();
Foo(ref connection);

และตอนนี้คุณมีฟังก์ชั่นของคุณที่รับบรรพบุรุษ ( เช่น Object ):

void Foo2(ref Object connection) { }

อาจมีอะไรผิดปกติกับสิ่งนั้น?

void Foo2(ref Object connection)
{
   connection = new Bitmap();
}

คุณเพียงแค่การจัดการเพื่อกำหนดที่คุณBitmapSqlConnection

นั่นไม่ดี


ลองอีกครั้งกับคนอื่น ๆ :

SqlConnection conn = new SqlConnection();
Foo2(ref conn);

void Foo2(ref DbConnection connection)
{
    conn = new OracleConnection();
}

คุณยัดส่วนOracleConnectionบนของSqlConnectionไฟล์.


0

ในกรณีของฉันฟังก์ชันของฉันยอมรับวัตถุและฉันไม่สามารถส่งอะไรได้ดังนั้นฉันจึงทำ

object bla = myVar;
Foo(ref bla);

และได้ผล

Foo ของฉันอยู่ใน VB.NET และตรวจสอบประเภทภายในและใช้ตรรกะมากมาย

ฉันขออภัยหากคำตอบของฉันซ้ำกัน แต่คำตอบอื่นยาวเกินไป

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