Apa itu reifikasi?

163

Saya tahu bahwa Java mengimplementasikan parametric polymorphism (Generics) dengan penghapusan. Saya mengerti apa itu penghapusan.

Saya tahu bahwa C # mengimplementasikan polimorfisme parametrik dengan reifikasi. Saya tahu itu bisa membuat Anda menulis

public void dosomething(List<String> input) {}
public void dosomething(List<Int> input) {}

atau bahwa Anda bisa tahu pada saat runtime apa jenis parameter dari beberapa jenis parameterised adalah, tapi saya tidak mengerti apa yang .

  • Apa yang dimaksud dengan tipe reified?
  • Apa yang dimaksud dengan nilai reified?
  • Apa yang terjadi ketika suatu jenis / nilai diverifikasi?
Martijn
sumber
Ini bukan jawaban, tetapi dapat membantu: beust.com/weblog/2011/07/29/erasure-vs-reification
heringer
@heringer yang tampaknya menjawab pertanyaan "what is erasure" dengan cukup baik, dan tampaknya pada dasarnya menjawab "apa itu reifikasi" dengan "not erasure" - sebuah tema umum yang saya temukan ketika awalnya mencari jawaban sebelum memposting di sini.
Martijn
5
... dan ada saya berpikir bahwa ifreication adalah proses mengubah switchkonstruk kembali ke if/ else, ketika sebelumnya telah dikonversi dari if/ elseke switch...
Digital Trauma
8
Res , reis adalah bahasa Latin untuk hal , jadi reifikasi secara harfiah adalah halifikasi . Saya tidak ada yang berguna untuk berkontribusi sejauh penggunaan C # istilah, tetapi fakta dalam dan dari dirinya sendiri bahwa mereka menggunakannya membuat saya tersenyum.
KRyan

Jawaban:

209

Reifikasi adalah proses mengambil sesuatu yang abstrak dan menciptakan sesuatu yang konkret.

Istilah reifikasi dalam C # generics mengacu pada proses dimana definisi tipe generik dan satu atau lebih argumen tipe generik (hal abstrak) digabungkan untuk membuat tipe generik baru (hal konkret).

Untuk mengatakannya secara berbeda, itu adalah proses mengambil definisi List<T>dan intdan menghasilkan List<int>jenis yang konkret .

Untuk memahaminya lebih lanjut, bandingkan pendekatan berikut:

  • Dalam Java generics, definisi tipe generik diubah menjadi dasarnya satu tipe generik konkret yang dibagikan di semua kombinasi argumen tipe yang diizinkan. Dengan demikian, beberapa tipe (level kode sumber) dipetakan ke satu tipe (level biner) - tetapi sebagai hasilnya, informasi tentang argumen tipe instance dihilangkan dalam instance tersebut (tipe erasure) .

    1. Sebagai efek samping dari teknik implementasi ini, satu-satunya argumen tipe generik yang diizinkan secara asli adalah tipe-tipe yang dapat membagikan kode biner dari tipe konkretnya; yang berarti tipe-tipe yang lokasi penyimpanannya memiliki representasi yang dapat dipertukarkan; yang berarti tipe referensi. Menggunakan tipe nilai sebagai argumen tipe umum memerlukan tinju mereka (menempatkan mereka dalam pembungkus tipe referensi sederhana).
    2. Tidak ada kode yang diduplikasi untuk mengimplementasikan obat generik dengan cara ini.
    3. Ketik informasi yang mungkin tersedia saat runtime (menggunakan refleksi) hilang. Ini, pada gilirannya, berarti spesialisasi jenis generik (kemampuan untuk menggunakan kode sumber khusus untuk setiap kombinasi argumen generik tertentu) sangat terbatas.
    4. Mekanisme ini tidak memerlukan dukungan dari lingkungan runtime.
    5. Ada beberapa solusi untuk mempertahankan informasi jenis yang dapat digunakan oleh program Java atau bahasa berbasis JVM.
  • Dalam C # generics, definisi tipe generik dipertahankan dalam memori saat runtime. Setiap kali tipe beton baru diperlukan, lingkungan runtime menggabungkan definisi tipe generik dan argumen tipe dan membuat tipe baru (reifikasi). Jadi kita mendapatkan tipe baru untuk setiap kombinasi argumen tipe, saat runtime .

    1. Teknik implementasi ini memungkinkan segala jenis kombinasi argumen jenis akan dipakai. Menggunakan tipe nilai sebagai argumen tipe umum tidak menyebabkan tinju, karena tipe ini mendapatkan implementasinya sendiri. ( Tinju masih ada di C # , tentu saja - tetapi itu terjadi dalam skenario lain, bukan yang ini.)
    2. Duplikasi kode bisa menjadi masalah - tetapi dalam praktiknya tidak, karena implementasi yang cukup pintar ( ini termasuk Microsoft .NET dan Mono ) dapat berbagi kode untuk beberapa contoh.
    3. Informasi tipe dipertahankan, yang memungkinkan spesialisasi sampai batas tertentu, dengan memeriksa argumen tipe menggunakan refleksi. Namun, tingkat spesialisasi terbatas, sebagai akibat dari kenyataan bahwa definisi tipe generik dikompilasi sebelum reifikasi terjadi (ini dilakukan dengan menyusun definisi terhadap kendala pada parameter tipe - dengan demikian, kompiler harus dapat "mengerti" definisi bahkan tanpa adanya tipe argumen tertentu ).
    4. Teknik implementasi ini sangat bergantung pada dukungan runtime dan kompilasi JIT (itulah sebabnya Anda sering mendengar bahwa generik C # memiliki beberapa batasan pada platform seperti iOS , di mana pembuatan kode dinamis dibatasi).
    5. Dalam konteks generik C #, reifikasi dilakukan untuk Anda oleh lingkungan runtime. Namun, jika Anda ingin lebih memahami perbedaan antara definisi tipe generik dan tipe generik konkret, Anda selalu dapat melakukan reifikasi sendiri, menggunakan System.Typekelas (bahkan jika kombinasi argumen tipe generik tertentu yang Anda instantiating tidak t muncul dalam kode sumber Anda secara langsung).
  • Dalam template C ++, definisi template dipertahankan dalam memori pada waktu kompilasi. Setiap kali instantiasi baru dari tipe templat diperlukan dalam kode sumber, kompiler menggabungkan definisi templat dan argumen templat dan membuat tipe baru. Jadi kami mendapatkan tipe unik untuk setiap kombinasi argumen template, pada waktu kompilasi .

    1. Teknik implementasi ini memungkinkan segala jenis kombinasi argumen jenis akan dipakai.
    2. Ini dikenal untuk menggandakan kode biner tetapi rantai-alat yang cukup pintar masih bisa mendeteksi ini dan membagikan kode untuk beberapa contoh.
    3. Definisi templat itu sendiri tidak "dikompilasi" - hanya instantiasinya yang konkret yang benar-benar dikompilasi . Ini menempatkan lebih sedikit kendala pada kompiler dan memungkinkan tingkat spesialisasi templat yang lebih besar .
    4. Karena contoh template dilakukan pada waktu kompilasi, tidak ada dukungan runtime yang diperlukan di sini juga.
    5. Proses ini belakangan disebut sebagai monomorphization , terutama di komunitas Rust. Kata ini digunakan berbeda dengan polimorfisme parametrik , yang merupakan nama konsep dari mana obat generik berasal.
Theodoros Chatzigiannakis
sumber
7
Perbandingan hebat dengan templat C ++ ... mereka tampaknya berada di antara C # 's dan generik Java. Anda memiliki kode dan struktur berbeda untuk menangani tipe generik spesifik yang berbeda seperti di C #, tetapi semuanya dilakukan dalam waktu kompilasi seperti di Jawa.
Luaan
3
Juga, di C ++ ini memungkinkan untuk memperkenalkan spesialisasi templat, di mana masing-masing (atau hanya beberapa) jenis beton dapat memiliki implementasi yang berbeda. Jelas tidak mungkin di Jawa, tetapi tidak di C #.
quetzalcoatl
@quetzalcoatl meskipun satu alasan untuk menggunakan itu adalah untuk mengurangi jumlah kode yang diproduksi dengan tipe pointer, dan C # melakukan sesuatu yang sebanding dengan tipe referensi di belakang layar. Namun, itu hanya satu alasan untuk menggunakannya, dan pasti ada saat-saat ketika spesialisasi templat akan menyenangkan.
Jon Hanna
Untuk Java, Anda mungkin ingin menambahkan bahwa ketika informasi jenis dihapus, gips ditambahkan oleh kompiler, membuat bytecode tidak dapat dibedakan dari bytecode pra-generik.
Rusty Core
27

Reifikasi berarti secara umum (di luar ilmu komputer) "untuk membuat sesuatu yang nyata".

Dalam pemrograman, sesuatu dibenarkan jika kita dapat mengakses informasi dalam bahasa itu sendiri.

Untuk dua contoh yang sepenuhnya tidak berhubungan dengan generik tentang sesuatu yang C # lakukan dan tidak miliki terverifikasi, mari kita ambil metode dan akses memori.

Bahasa OO umumnya memiliki metode , (dan banyak yang tidak memiliki fungsi yang serupa meskipun tidak terikat ke kelas). Karena itu, Anda dapat menentukan metode dalam bahasa tersebut, menyebutnya, mungkin menimpanya, dan sebagainya. Tidak semua bahasa tersebut memungkinkan Anda menangani metode itu sendiri sebagai data untuk suatu program. C # (dan benar-benar, .NET, bukan C #) memungkinkan Anda menggunakan MethodInfoobjek yang mewakili metode, jadi dalam metode C # diverifikasi. Metode dalam C # adalah "objek kelas satu".

Semua bahasa praktis memiliki beberapa cara untuk mengakses memori komputer. Dalam bahasa tingkat rendah seperti C, kita bisa berurusan langsung dengan pemetaan antara alamat numerik yang digunakan oleh komputer, jadi suka int* ptr = (int*) 0xA000000; *ptr = 42;itu masuk akal (selama kita punya alasan yang bagus untuk mencurigai bahwa mengakses alamat memori 0xA000000dengan cara ini tidak akan menang ' t meledakkan sesuatu). Dalam C # ini tidak masuk akal (kita bisa memaksakannya dalam. NET, tetapi dengan manajemen memori .NET memindahkan hal-hal di sekitar itu tidak mungkin berguna). C # tidak memiliki alamat memori terverifikasi.

Jadi, sebagaimana ditolak berarti "menjadi nyata", "jenis yang ditegaskan" adalah jenis yang dapat kita "bicarakan" dalam bahasa yang dimaksud.

Dalam generik ini berarti dua hal.

Salah satunya adalah bahwa List<string>adalah jenis seperti stringatau intyang. Kita dapat membandingkan jenis itu, mendapatkan namanya, dan menanyakannya:

Console.WriteLine(typeof(List<string>).FullName); // System.Collections.Generic.List`1[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
Console.WriteLine(typeof(List<string>) == (42).GetType()); // False
Console.WriteLine(typeof(List<string>) == Enumerable.Range(0, 1).Select(i => i.ToString()).ToList().GetType()); // True
Console.WriteLine(typeof(List<string>).GenericTypeArguments[0] == typeof(string)); // True

Konsekuensi dari hal ini adalah bahwa kita dapat "berbicara tentang" tipe parameter metode generik (atau metode kelas generik) dalam metode itu sendiri:

public static void DescribeType<T>(T element)
{
  Console.WriteLine(typeof(T).FullName);
}
public static void Main()
{
  DescribeType(42);               // System.Int32
  DescribeType(42L);              // System.Int64
  DescribeType(DateTime.UtcNow);  // System.DateTime
}

Sebagai aturan, melakukan ini terlalu "bau", tetapi memiliki banyak kasus yang bermanfaat. Sebagai contoh, lihat:

public static TSource Min<TSource>(this IEnumerable<TSource> source)
{
  if (source == null) throw Error.ArgumentNull("source");
  Comparer<TSource> comparer = Comparer<TSource>.Default;
  TSource value = default(TSource);
  if (value == null)
  {
    using (IEnumerator<TSource> e = source.GetEnumerator())
    {
      do
      {
        if (!e.MoveNext()) return value;
        value = e.Current;
      } while (value == null);
      while (e.MoveNext())
      {
        TSource x = e.Current;
        if (x != null && comparer.Compare(x, value) < 0) value = x;
      }
    }
  }
  else
  {
    using (IEnumerator<TSource> e = source.GetEnumerator())
    {
      if (!e.MoveNext()) throw Error.NoElements();
      value = e.Current;
      while (e.MoveNext())
      {
        TSource x = e.Current;
        if (comparer.Compare(x, value) < 0) value = x;
      }
    }
  }
  return value;
}

Ini tidak melakukan banyak perbandingan antara jenis TSourcedan berbagai jenis untuk perilaku yang berbeda (umumnya merupakan tanda bahwa Anda seharusnya tidak menggunakan obat generik sama sekali), tetapi memang memecah antara jalur kode untuk jenis yang dapat null(harus kembali nulljika tidak ada elemen yang ditemukan, dan tidak boleh membuat perbandingan untuk menemukan minimum jika salah satu elemen dibandingkan adalah null) dan jalur kode untuk tipe yang tidak dapat null(harus dibuang jika tidak ada elemen yang ditemukan, dan tidak perlu khawatir tentang kemungkinan nullelemen ).

Karena TSource"nyata" dalam metode ini, perbandingan ini dapat dibuat baik pada saat runtime atau waktu jitting (umumnya waktu jitting, tentu saja kasus di atas akan melakukannya pada waktu jitting dan tidak menghasilkan kode mesin untuk jalur yang tidak diambil) dan kami memiliki pisahkan versi "nyata" dari metode untuk setiap kasus. (Meskipun sebagai optimasi, kode mesin dibagikan untuk metode yang berbeda untuk parameter tipe tipe referensi yang berbeda, karena dapat tanpa mempengaruhi ini, dan karenanya kita dapat mengurangi jumlah kode mesin yang dipasangkan).

(Tidak umum untuk berbicara tentang reifikasi tipe generik dalam C # kecuali jika Anda juga berurusan dengan Java, karena dalam C # kami hanya menerima reifikasi ini; semua tipe diverifikasi. Di Jawa, tipe non-generik disebut sebagai reified karena itu adalah perbedaan antara mereka dan tipe generik).

Jon Hanna
sumber
Anda tidak berpikir bisa melakukan apa yang Minberguna di atas? Sangat sulit untuk memenuhi perilakunya yang terdokumentasi.
Jon Hanna
Saya menganggap bug sebagai perilaku (tidak) yang terdokumentasi dan implikasi bahwa perilaku itu berguna (sebagai tambahan, perilaku Enumerable.Min<TSource>berbeda karena tidak melempar untuk tipe non-referensi pada koleksi kosong, tetapi mengembalikan default (TSource), dan hanya didokumentasikan sebagai "Mengembalikan nilai minimum dalam urutan umum." Saya berpendapat keduanya harus membuang koleksi kosong, atau bahwa elemen "nol" harus dilewatkan sebagai baseline, dan pembanding / fungsi perbandingan harus selalu diteruskan)
Martijn
1
Itu akan jauh lebih tidak berguna daripada Min saat ini, yang cocok dengan perilaku db umum pada tipe nullable tanpa mencoba yang mustahil pada tipe non-nullable. (Gagasan dasar bukan tidak mungkin, tetapi tidak terlalu berguna kecuali ada nilai yang Anda tahu tidak akan pernah ada di sumber)
Jon Hanna
1
Penciptaan nama yang lebih baik untuk ini. :)
tchrist
@tchrist sesuatu bisa tidak nyata.
Jon Hanna
15

Seperti yang sudah dicatat oleh duffymo, "reifikasi" bukanlah perbedaan utama.

Di Jawa, obat generik pada dasarnya ada untuk meningkatkan dukungan waktu kompilasi - ini memungkinkan Anda untuk menggunakan koleksi mis. Yang diketik dalam kode Anda, dan mengatur keselamatan jenis untuk Anda. Namun, ini hanya ada pada waktu kompilasi - kode kode yang dikompilasi tidak lagi memiliki pengertian generik; semua tipe generik ditransformasikan menjadi tipe "konkret" (menggunakan objectjika tipe generik tidak terikat), menambahkan konversi tipe dan memeriksa jenis sesuai kebutuhan.

Dalam .NET, obat generik adalah fitur integral dari CLR. Saat Anda mengompilasi tipe generik, itu tetap generik di IL yang dihasilkan. Bukan hanya ditransformasikan menjadi kode non-generik seperti di Jawa.

Ini memiliki beberapa dampak pada bagaimana obat generik bekerja dalam praktiknya. Sebagai contoh:

  • Java harus SomeType<?>mengizinkan Anda untuk melewati implementasi konkret dari tipe generik yang diberikan. C # tidak dapat melakukan ini - setiap tipe generik spesifik ( reified ) adalah tipenya sendiri.
  • Tipe generik tidak terbatas di Jawa berarti nilainya disimpan sebagai object. Ini dapat memiliki dampak kinerja ketika menggunakan tipe nilai dalam obat generik tersebut. Di C #, ketika Anda menggunakan tipe nilai dalam tipe generik, itu tetap tipe nilai.

Untuk memberikan sampel, anggap Anda memiliki Listtipe generik dengan satu argumen generik. Di Jawa, List<String>dan List<Int>pada akhirnya akan menjadi tipe yang sama persis saat runtime - tipe generik hanya benar-benar ada untuk kode waktu kompilasi. Semua panggilan ke mis GetValueakan ditransformasikan ke (String)GetValuedan (Int)GetValuemasing - masing.

Dalam C #, List<string>dan List<int>ada dua jenis yang berbeda. Mereka tidak dapat dipertukarkan, dan keamanan jenisnya juga diberlakukan pada saat runtime. Tidak peduli apa yang Anda lakukan, new List<int>().Add("SomeString")akan tidak pernah bekerja - penyimpanan yang mendasari dalam List<int>adalah benar-benar beberapa array integer, sedangkan di Jawa, maka perlu sebuah objectarray yang. Di C #, tidak ada pemain yang terlibat, tidak ada tinju dll.

Ini juga harus menjelaskan mengapa C # tidak dapat melakukan hal yang sama dengan Java SomeType<?>. Di Jawa, semua tipe generik "berasal dari" SomeType<?>akhirnya menjadi tipe yang sama persis. Dalam C #, semua berbagai spesifik SomeType<T>adalah tipe mereka sendiri. Menghapus pemeriksaan waktu kompilasi, dimungkinkan untuk lulus SomeType<Int>alih-alih SomeType<String>(dan sebenarnya, semua itu SomeType<?>artinya "abaikan pemeriksaan waktu kompilasi untuk jenis generik yang diberikan"). Dalam C #, itu tidak mungkin, bahkan untuk tipe turunan (yaitu, Anda tidak dapat melakukannya List<object> list = (List<object>)new List<string>();meskipun stringberasal dari object).

Kedua implementasi memiliki pro dan kontra. Sudah ada beberapa kali ketika saya ingin bisa membiarkan hanya SomeType<?>sebagai argumen dalam C # - tetapi itu tidak masuk akal cara kerja C # generics.

Luaan
sumber
2
Nah, Anda dapat menggunakan jenis-jenisnya List<>, Dictionary<,>dan seterusnya dalam C #, tetapi kesenjangan antara itu dan daftar konkret atau kamus tertentu membutuhkan sedikit refleksi untuk menjembatani. Perbedaan pada antarmuka memang membantu dalam beberapa kasus di mana kita mungkin pernah ingin menjembatani kesenjangan itu dengan mudah, tetapi tidak semua.
Jon Hanna
2
@JonHanna Anda dapat menggunakan List<>instantiate tipe generik spesifik baru - tetapi masih berarti membuat tipe spesifik yang Anda inginkan. Tetapi Anda tidak dapat menggunakan List<>sebagai argumen, misalnya. Tapi ya, setidaknya ini memungkinkan Anda untuk menjembatani kesenjangan menggunakan refleksi.
Luaan
.NET Framework memiliki tiga batasan umum hard-coded yang bukan tipe lokasi penyimpanan; semua kendala lainnya harus tipe lokasi penyimpanan. Lebih lanjut, satu-satunya kali tipe generik Tdapat memenuhi batasan tipe lokasi penyimpanan Uadalah ketika Tdan Umerupakan tipe yang sama, atau Umerupakan tipe yang dapat menyimpan referensi ke instance T. Tidak mungkin memiliki lokasi penyimpanan tipe yang bermakna SomeType<?>tetapi secara teori dimungkinkan untuk memiliki batasan generik dari tipe tersebut.
supercat
1
Tidak benar bahwa bytecode Java yang dikompilasi tidak memiliki pengertian generik. Hanya saja instance kelas tidak memiliki gagasan tentang generik. Ini adalah perbedaan penting; Saya sebelumnya telah menulis tentang ini di programmers.stackexchange.com/questions/280169/… , jika Anda tertarik.
ruakh
2

Reifikasi adalah konsep pemodelan berorientasi objek.

Reify adalah kata kerja yang berarti "membuat sesuatu abstrak nyata" .

Ketika Anda melakukan pemrograman berorientasi objek, biasanya memodelkan objek dunia nyata sebagai komponen perangkat lunak (misalnya Window, Button, Person, Bank, Vehicle, dll.)

Juga umum untuk merevisi konsep abstrak menjadi komponen juga (misalnya WindowListener, Broker, dll.)

Duffymo
sumber
2
Reifikasi adalah konsep umum "membuat sesuatu yang nyata" yang walaupun berlaku untuk pemodelan berorientasi objek seperti yang Anda katakan, juga memiliki makna dalam konteks implementasi obat generik.
Jon Hanna
2
Jadi saya sudah dididik dengan membaca jawaban ini. Saya akan mengubah jawaban saya.
duffymo
2
Jawaban ini tidak melakukan apa pun untuk mengatasi minat OP pada generik dan polimorfisme parametrik.
Erick G. Hagstrom
Komentar ini tidak melakukan apa pun untuk menanggapi minat siapa pun atau mendorong perwakilan Anda. Saya melihat Anda tidak menawarkan apa pun. Milik saya adalah jawaban pertama, dan memang mendefinisikan reifikasi sebagai sesuatu yang lebih luas.
duffymo
1
Jawaban Anda mungkin yang pertama, tetapi Anda menjawab pertanyaan yang berbeda, bukan yang ditanyakan oleh OP, yang akan jelas dari isi pertanyaan dan tag-nya. Mungkin Anda tidak membaca pertanyaan secara menyeluruh sebelum Anda menulis jawaban Anda, atau mungkin Anda tidak tahu bahwa istilah "reifikasi" memiliki makna yang mapan dalam konteks obat generik. Bagaimanapun, jawaban Anda tidak berguna. Downvote.
jcsahnwaldt Reinstate Monica