Mengapa 'ref' dan 'out' tidak mendukung polimorfisme?

124

Ambil yang berikut ini:

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) {}  
}

Mengapa kesalahan waktu kompilasi di atas terjadi? Ini terjadi dengan argumen refdan keduanya out.

Andreas Grech
sumber

Jawaban:

169

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

UPDATE: Saya menggunakan jawaban ini sebagai dasar untuk entri blog ini:

Mengapa parameter ref dan out tidak mengizinkan variasi jenis?

Lihat halaman blog untuk komentar lebih lanjut tentang masalah ini. Terima kasih atas pertanyaan bagusnya.

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

Mari kita misalkan Anda memiliki kelas Animal, Mammal, Reptile, Giraffe, Turtledan Tiger, dengan hubungan subclassing jelas.

Sekarang misalkan Anda memiliki metode void M(ref Mammal m). Mbisa membaca dan menulis m.


Bisakah Anda mengirimkan variabel tipe Animalke M?

Tidak. Variabel tersebut dapat berisi a Turtle, tetapi Makan menganggap bahwa variabel tersebut hanya berisi Mamalia. A Turtlebukan Mammal.

Kesimpulan 1 : refParameter tidak bisa dibuat "lebih besar". (Ada lebih banyak hewan daripada mamalia, jadi variabelnya menjadi "lebih besar" karena bisa berisi lebih banyak hal.)


Bisakah Anda mengirimkan variabel tipe Giraffeke M?

Tidak. MBisa menulis ke m, dan Mmungkin ingin menulis Tigerke m. Sekarang Anda telah memasukkan a Tigerke dalam variabel yang sebenarnya bertipe Giraffe.

Kesimpulan 2 : refParameter tidak bisa dibuat "lebih kecil".


Sekarang pertimbangkan N(out Mammal n).

Bisakah Anda mengirimkan variabel tipe Giraffeke N?

Tidak. NDapat menulis ke n, dan Nmungkin ingin menulis Tiger.

Kesimpulan 3 : outParameter tidak bisa dibuat "lebih kecil".


Bisakah Anda mengirimkan variabel tipe Animalke N?

Hmm.

Nah, kenapa tidak? Ntidak bisa membaca n, hanya bisa menulis padanya, bukan? Anda menulis Tigerke variabel tipe Animaldan Anda sudah siap, bukan?

Salah. Aturannya bukanlah " Nhanya dapat menulis ke n".

Aturannya adalah, secara singkat:

1) Nharus menulis nsebelum Nkembali normal. (Jika Nmelempar, semua taruhan dibatalkan.)

2) Nharus menulis sesuatu nsebelum membaca sesuatu dari n.

Itu memungkinkan urutan kejadian ini:

  • Deklarasikan bidang xtipe Animal.
  • Teruskan xsebagai outparameter ke N.
  • Nmenulis Tigerke n, yang merupakan alias untuk x.
  • Di utas lain, seseorang menulis Turtleke x.
  • Nmencoba membaca isi dari n, dan menemukan a Turtledalam apa yang dianggapnya sebagai variabel tipe Mammal.

Jelas kami ingin membuatnya ilegal.

Kesimpulan 4 : outParameter tidak bisa dibuat "lebih besar".


Kesimpulan akhir : Baik parameter refmaupun outparameter tidak dapat mengubah jenisnya. Melakukan sebaliknya adalah merusak jenis keamanan yang dapat diverifikasi.

Jika masalah-masalah dalam teori tipe dasar ini menarik minat Anda, pertimbangkan untuk membaca seri saya tentang bagaimana kovarians dan kontravarian bekerja di C # 4.0 .

Eric Lippert
sumber
6
+1. Penjelasan yang bagus menggunakan contoh kelas dunia nyata yang secara jelas mendemonstrasikan masalah (yaitu - menjelaskan dengan A, B dan C membuat lebih sulit untuk menunjukkan mengapa itu tidak berhasil).
Grant Wagner
4
Saya merasa rendah hati membaca proses berpikir ini. Saya pikir lebih baik saya kembali ke buku!
Scott McKenzie
Dalam hal ini, kita benar-benar tidak dapat menggunakan variabel kelas Abstrak sebagai argumen dan meneruskan objek kelas turunannya !!
Prashant Cholachagudda
Namun, mengapa outparameter tidak bisa dibuat "lebih besar"? Urutan yang Anda jelaskan dapat diterapkan ke variabel apa pun, tidak hanya outvariabel parameter. Dan juga pembaca perlu memberikan nilai argumen Mammalsebelum dia mencoba mengaksesnya karena Mammaldan tentu saja bisa gagal jika dia tidak perhatian
astef
29

Karena dalam kedua kasus Anda harus dapat menetapkan nilai ke parameter ref / out.

Jika Anda mencoba memasukkan b ke dalam metode Foo2 sebagai referensi, dan di Foo2 Anda mencoba memasukkan a = A baru (), ini tidak valid.
Alasan yang sama Anda tidak bisa menulis:

B b = new A();
maciejkow.dll
sumber
+1 Langsung ke intinya dan jelaskan alasannya dengan baik.
Rui Craveiro
10

Anda sedang berjuang dengan masalah OOP klasik dari kovarian (dan kontravarian), lihat wikipedia : karena fakta ini mungkin menentang ekspektasi intuitif, secara matematis tidak mungkin untuk mengizinkan substitusi kelas turunan sebagai pengganti dari kelas dasar untuk argumen yang dapat diubah (dapat dialihkan) (dan juga kontainer yang barangnya dapat dialihkan, dengan alasan yang sama) dengan tetap menghormati prinsip Liskov . Mengapa demikian dijelaskan dalam jawaban yang ada, dan dieksplorasi lebih dalam di artikel wiki ini dan tautannya.

Bahasa OOP yang tampaknya melakukannya sementara tetap tradisional secara statis aman jenis "curang" (memasukkan pemeriksaan jenis dinamis tersembunyi, atau memerlukan pemeriksaan waktu kompilasi dari SEMUA sumber untuk diperiksa); pilihan dasarnya adalah: menyerah pada kovarian ini dan menerima kebingungan praktisi (seperti yang dilakukan C # di sini), atau beralih ke pendekatan pengetikan dinamis (seperti bahasa OOP pertama, Smalltalk, lakukan), atau pindah ke tidak dapat diubah (tunggal- assignment) data, seperti bahasa fungsional (di bawah immutability, Anda dapat mendukung kovarians, dan juga menghindari teka-teki terkait lainnya seperti fakta bahwa Anda tidak dapat memiliki persegi subclass Rectangle di dunia data yang bisa berubah).

Alex Martelli
sumber
4

Mempertimbangkan:

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

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

B b = null;
Foo2(ref b);

Itu akan melanggar keamanan tipe

Henk Holterman
sumber
Ini lebih merupakan jenis kesimpulan yang tidak jelas dari "b" karena var itulah masalahnya di sana.
Saya kira pada baris 6 yang Anda maksud => B b = null;
Alejandro Miralles
@ amiralles - ya, varitu benar-benar salah. Tetap.
Henk Holterman
4

Sementara tanggapan lain secara ringkas menjelaskan alasan di balik perilaku ini, saya pikir perlu disebutkan bahwa jika Anda benar-benar perlu melakukan sesuatu seperti ini, Anda dapat mencapai fungsi serupa dengan membuat Foo2 menjadi metode umum, seperti:

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 {}  
}
BrendanLoBuglio
sumber
2

Karena memberikan Foo2sebuah ref Bakan menghasilkan sebuah objek cacat karena Foo2hanya tahu bagaimana untuk mengisi Abagian dari B.

CannibalSmith
sumber
0

Bukankah itu kompiler yang memberi tahu Anda bahwa ia ingin Anda mentransmisikan objek secara eksplisit sehingga dapat yakin Anda tahu apa maksud Anda?

Foo2(ref (A)b)
dlamblin.dll
sumber
Tidak bisa melakukan itu, "Argumen ref atau keluar harus menjadi variabel yang dapat dialihkan"
0

Masuk akal dari perspektif keamanan, tetapi saya lebih suka jika kompilator memberikan peringatan daripada kesalahan, karena ada penggunaan sah dari objek polimoprik yang diteruskan oleh referensi. misalnya

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);
}

Ini tidak akan bisa dikompilasi, tapi akankah berhasil?

Oofpez
sumber
0

Jika Anda menggunakan contoh praktis untuk tipe Anda, Anda akan melihatnya:

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

Dan sekarang Anda memiliki fungsi yang mengambil leluhur ( yaitu Object ):

void Foo2(ref Object connection) { }

Apa yang salah dengan itu?

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

Anda baru saja berhasil menetapkan Bitmapke Anda SqlConnection.

Itu tidak baik.


Coba lagi dengan orang lain:

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

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

Anda memasukkan OracleConnectionover-top Anda SqlConnection.

Ian Boyd
sumber
0

Dalam kasus saya, fungsi saya menerima suatu objek dan saya tidak dapat mengirim apa pun jadi saya melakukannya

object bla = myVar;
Foo(ref bla);

Dan itu berhasil

Foo saya ada di VB.NET dan memeriksa tipe di dalamnya dan melakukan banyak logika

Saya minta maaf jika jawaban saya duplikat tetapi yang lain terlalu panjang

Shereef Marzouk
sumber