Mengapa kata kunci 'keluar' digunakan dalam dua konteks yang tampaknya berbeda?

11

Dalam C #, outkata kunci dapat digunakan dalam dua cara berbeda.

  1. Sebagai pengubah parameter di mana argumen dilewatkan oleh referensi

    class OutExample
    {
        static void Method(out int i)
        {
            i = 44;
        }
        static void Main()
        {
            int value;
            Method(out value);
            // value is now 44
        }
    }
  2. Sebagai pengubah parameter tipe untuk menentukan kovarians .

    // Covariant interface. 
    interface ICovariant<out R> { }
    
    // Extending covariant interface. 
    interface IExtCovariant<out R> : ICovariant<R> { }
    
    // Implementing covariant interface. 
    class Sample<R> : ICovariant<R> { }
    
    class Program
    {
        static void Test()
        {
            ICovariant<Object> iobj = new Sample<Object>();
            ICovariant<String> istr = new Sample<String>();
    
            // You can assign istr to iobj because 
            // the ICovariant interface is covariant.
            iobj = istr;
        }
    }

Pertanyaan saya adalah: mengapa?

Bagi seorang pemula, hubungan antara keduanya tampaknya tidak intuitif . Penggunaan dengan obat generik tampaknya tidak ada hubungannya dengan melewati referensi.

Saya pertama kali belajar apa outyang terkait dengan melewati argumen dengan referensi, dan ini menghambat pemahaman saya tentang penggunaan mendefinisikan kovarians dengan obat generik.

Apakah ada koneksi antara penggunaan ini yang saya lewatkan?

Rowan Freeman
sumber
5
Koneksi ini sedikit lebih dimengerti jika Anda melihat penggunaan kovarians dan contravariance dalam System.Func<in T, out TResult>delegasi .
rwong
4
Juga, sebagian besar perancang bahasa mencoba meminimalkan jumlah kata kunci, dan menambahkan kata kunci baru dalam beberapa bahasa yang ada dengan basis kode besar menyakitkan (kemungkinan konflik dengan beberapa kode yang ada menggunakan kata itu sebagai nama)
Basile Starynkevitch

Jawaban:

20

Ada koneksi, namun agak longgar. Dalam C # kata kunci ´in´ dan ´out´ seperti namanya menyarankan untuk input dan output. Ini sangat jelas dalam hal parameter output, tetapi kurang bersih apa yang harus dilakukan dengan parameter template.

Mari kita lihat prinsip substitusi Liskov :

...

Prinsip Liskov memaksakan beberapa persyaratan standar pada tanda tangan yang telah diadopsi dalam bahasa pemrograman berorientasi objek yang lebih baru (biasanya di tingkat kelas daripada tipe; lihat subtipe nominal vs struktural untuk perbedaan):

  • Contravariance dari argumen metode dalam subtipe.
  • Kovarian jenis pengembalian dalam subtipe.

...

Lihat bagaimana contravariance dikaitkan dengan input dan covariance dikaitkan dengan output? Dalam C # jika Anda menandai variabel templat dengan outuntuk membuatnya kovarian, tetapi harap dicatat Anda hanya dapat melakukan ini jika parameter tipe yang disebutkan hanya muncul sebagai output (tipe pengembalian fungsi). Jadi yang berikut ini tidak valid:

interface I<out T>
{
  void func(T t); //Invalid variance: The type parameter 'T' must be
                  //contravariantly valid on 'I<T>.func(T)'.
                  //'T' is covariant.

}

Sama jika Anda menandai parameter tipe dengan in, itu berarti Anda hanya dapat menggunakannya sebagai input (parameter fungsi). Jadi yang berikut ini tidak valid:

interface I<in T>
{
  T func(); //Invalid variance: The type parameter 'T' must
            //be covariantly valid on 'I<T>.func()'. 
            //'T' is contravariant.

}

Jadi untuk meringkas, hubungan dengan outkata kunci adalah bahwa dengan parameter fungsi itu berarti bahwa itu adalah parameter output , dan untuk tipe parameter itu berarti bahwa tipe tersebut hanya digunakan dalam konteks output .

System.Funcjuga merupakan contoh yang baik apa rwong disebutkan dalam komentarnya. Dalam System.Funcsemua parameter input dengan in, dan parameter output dengan out. Alasannya persis seperti yang saya jelaskan.

Gábor Angyal
sumber
2
Jawaban bagus! Menyelamatkan saya beberapa ... tunggu saja ... mengetik! Ngomong-ngomong: bagian dari LSP yang Anda kutip sebenarnya sudah dikenal jauh sebelum Liskov. Itu hanya aturan subtyping standar untuk tipe fungsi. (Tipe parameter bersifat contravarian, tipe return bersifat kovarian). Kebaruan dari pendekatan Liskov adalah a) menyusun peraturan tidak dalam hal co-/ contravariance tetapi dalam hal pergantian substitusi (sebagaimana didefinisikan oleh pra / postkondisi) dan b) aturan sejarah , yang memungkinkan untuk menerapkan semua alasan ini untuk tipe data yang bisa berubah, yang sebelumnya tidak mungkin.
Jörg W Mittag
10

@ Gábor telah menjelaskan koneksi (contravariance untuk semua yang masuk "in", covariance untuk semua yang keluar "out"), tetapi mengapa menggunakan kembali kata kunci sama sekali?

Yah, kata kunci sangat mahal. Anda tidak dapat menggunakannya sebagai pengidentifikasi dalam program Anda. Tetapi hanya ada begitu banyak kata dalam bahasa Inggris. Jadi, terkadang Anda mengalami konflik, dan Anda harus dengan canggung mengganti nama variabel, metode, bidang, properti, kelas, antarmuka, atau struct untuk menghindari bentrok dengan kata kunci. Misalnya, jika Anda membuat model sebuah sekolah, apa yang Anda sebut kelas? Anda tidak dapat menyebutnya kelas, karena classkata kunci!

Menambahkan kata kunci ke bahasa bahkan lebih mahal. Itu pada dasarnya membuat semua kode yang menggunakan kata kunci ini sebagai pengidentifikasi ilegal, melanggar kompatibilitas mundur di semua tempat.

Kata kunci indan outsudah ada, sehingga hanya bisa digunakan kembali.

Mereka dapat menambahkan kata kunci kontekstual yang hanya kata kunci dalam konteks daftar parameter tipe, tetapi kata kunci apa yang akan mereka pilih? covariantdan contravariant? +dan -(seperti yang dilakukan Scala, misalnya)? superdan extendsseperti Jawa? Bisakah Anda mengingat dari atas kepala Anda parameter mana yang kovarian dan contravarian?

Dengan solusi saat ini, ada mnemonik yang bagus: parameter tipe output mendapatkan outkata kunci, parameter tipe input mendapatkan inkata kunci. Perhatikan simetri yang bagus dengan parameter metode: parameter output mendapatkan outkata kunci, parameter input mendapatkan inkata kunci (well, sebenarnya, tidak ada kata kunci sama sekali, karena input adalah default, tetapi Anda mendapatkan ide).

[Catatan: jika Anda melihat histori edit, Anda akan melihat bahwa saya awalnya mengubah keduanya dalam kalimat pengantar saya. Dan saya bahkan mendapat upvote selama waktu itu! Ini hanya untuk menunjukkan betapa pentingnya mnemonik itu.]

Jörg W Mittag
sumber
Cara untuk mengingat co-versus contra-variance adalah dengan mempertimbangkan apa yang terjadi jika suatu fungsi dalam antarmuka mengambil parameter dari jenis antarmuka generik. Jika seseorang memiliki interface Accepter<in T> { void Accept(T it);};, suatu Accepter<Foo<T>>akan menerima Tsebagai parameter input jika Foo<T>menerimanya sebagai parameter output, dan sebaliknya. Jadi, kontra- variasi. Sebaliknya, interface ISupplier<out T> { T get();};suatu Supplier<Foo<T>>varian Foomemiliki jenis varian apa pun - dengan demikian co- varians.
supercat