
Tipe dasar objek dan implementasi antarmuka. Tinju
Tampaknya kami datang melalui neraka dan air yang tinggi dan dapat melakukan wawancara, bahkan yang untuk tim .NET CLR. Namun, jangan terburu-buru ke microsoft.com dan mencari lowongan. Sekarang, kita perlu memahami bagaimana tipe nilai mewarisi objek jika mereka tidak mengandung referensi ke SyncBlockIndex, bukan penunjuk ke tabel metode virtual. Ini akan sepenuhnya menjelaskan sistem kami tentang jenis dan semua potongan puzzle akan menemukan tempat mereka. Namun, kita akan membutuhkan lebih dari satu kalimat.
Sekarang, mari kita ingat lagi bagaimana tipe nilai dialokasikan dalam memori. Mereka mendapatkan tempat di memori tepat di tempat mereka berada. Jenis referensi mendapatkan alokasi pada tumpukan benda kecil dan besar. Mereka selalu memberikan referensi ke tempat di tumpukan tempat objek itu berada. Setiap tipe nilai memiliki metode seperti ToString, Equals, dan GetHashCode. Mereka virtual dan dapat ditimpa, tetapi tidak memungkinkan untuk mewarisi tipe nilai dengan metode utama. Jika tipe nilai menggunakan metode yang dapat ditimpa, mereka akan membutuhkan tabel metode virtual untuk merutekan panggilan. Ini akan mengarah pada masalah meneruskan struktur ke dunia yang tidak dikelola: ladang tambahan akan pergi ke sana. Akibatnya, ada deskripsi metode tipe nilai di suatu tempat, tetapi Anda tidak dapat mengaksesnya secara langsung melalui tabel metode virtual.
Ini mungkin membawa gagasan bahwa kurangnya warisan adalah buatan
Bab ini diterjemahkan dari bahasa Rusia bersama oleh penulis dan penerjemah profesional . Anda dapat membantu kami dengan terjemahan dari bahasa Rusia atau Inggris ke bahasa lain, terutama ke bahasa Cina atau Jerman.
Juga, jika Anda ingin berterima kasih kepada kami, cara terbaik yang dapat Anda lakukan adalah memberi kami bintang di github atau untuk repositori garpu
github / sidristij / dotnetbook .
Ini mungkin membawa gagasan bahwa kurangnya warisan adalah buatan:
- ada warisan dari suatu objek, tetapi tidak langsung;
- ada ToString, Equals dan GetHashCode di dalam tipe dasar. Dalam tipe nilai metode ini memiliki perilaku mereka sendiri. Ini berarti, bahwa metode ditimpa dalam kaitannya dengan
object
; - selain itu, jika Anda melemparkan tipe ke
object
, Anda memiliki hak penuh untuk memanggil ToString, Equals, dan GetHashCode; - saat memanggil metode instance untuk tipe nilai, metode tersebut mendapatkan struktur lain yang merupakan salinan asli. Itu berarti memanggil metode contoh seperti memanggil metode statis:
Method(ref structInstance, newInternalFieldValue)
. Memang, panggilan ini melewati this
, dengan satu pengecualian, namun. JIT harus mengkompilasi tubuh metode, sehingga tidak perlu mengimbangi bidang struktur, melompati pointer ke tabel metode virtual, yang tidak ada dalam struktur. Itu ada untuk tipe nilai di tempat lain .
Tipe berbeda dalam perilaku, tetapi perbedaan ini tidak begitu besar pada tingkat implementasi dalam CLR. Kami akan membicarakannya nanti.
Mari kita tulis baris berikut di program kami:
var obj = (object)10;
Ini akan memungkinkan kita untuk berurusan dengan nomor 10 menggunakan kelas dasar. Ini disebut tinju. Itu artinya kita memiliki VMT untuk memanggil metode virtual seperti ToString (), Equals dan GetHashCode. Pada kenyataannya tinju menciptakan salinan tipe nilai, tetapi bukan penunjuk ke sumber asli. Ini karena kita dapat menyimpan nilai asli di mana-mana: di stack atau sebagai bidang kelas. Jika kita melemparkannya ke tipe objek, kita dapat menyimpan referensi ke nilai ini selama yang kita inginkan. Ketika tinju terjadi:
- CLR mengalokasikan ruang pada heap untuk struktur + SyncBlockIndex + VMT dari tipe nilai (untuk memanggil ToString, GetHashCode, Equals);
- itu menyalin contoh tipe nilai di sana.
Sekarang, kami memiliki varian referensi tipe nilai. Suatu struktur benar-benar memiliki kumpulan bidang sistem yang sama dengan jenis referensi ,
menjadi tipe referensi yang lengkap setelah tinju. Struktur menjadi kelas. Sebut saja jungkir .NET. Ini nama yang adil.
Lihat saja apa yang terjadi jika Anda menggunakan struktur yang mengimplementasikan antarmuka menggunakan antarmuka yang sama.
struct Foo : IBoo { int x; void Boo() { x = 666; } } IBoo boo = new Foo(); boo.Boo();
Ketika kita membuat instance Foo, nilainya masuk ke stack sebenarnya. Kemudian kita menempatkan variabel ini ke dalam variabel tipe antarmuka dan struktur menjadi variabel tipe referensi. Selanjutnya, ada tinju dan kami memiliki tipe objek sebagai output. Tetapi ini adalah variabel tipe antarmuka. Itu berarti kita perlu konversi jenis. Jadi, panggilan itu terjadi dengan cara seperti ini:
IBoo boo = (IBoo)(box_to_object)new Foo(); boo.Boo();
Menulis kode seperti itu tidak efektif. Anda harus mengubah salinan alih-alih yang asli:
void Main() { var foo = new Foo(); foo.a = 1; Console.WriteLite(foo.a); // -> 1 IBoo boo = foo; boo.Boo(); // looks like changing foo.a to 10 Console.WriteLite(foo.a); // -> 1 } struct Foo: IBoo { public int a; public void Boo() { a = 10; } } interface IBoo { void Boo(); }
Pertama kali kita melihat kode, kita tidak harus tahu apa yang kita hadapi dalam kode selain kode kita dan melihat pemain yang dilemparkan ke antarmuka IBoo. Ini membuat kami berpikir Foo adalah kelas dan bukan struktur. Maka tidak ada pembagian visual dalam struktur dan kelas, yang membuat kita berpikir
hasil modifikasi antarmuka harus masuk ke foo, yang tidak terjadi karena boo adalah salinan foo. Itu menyesatkan. Menurut pendapat saya, kode ini harus mendapatkan komentar, sehingga pengembang lain dapat menghadapinya.
Hal kedua berkaitan dengan pemikiran sebelumnya bahwa kita dapat melemparkan tipe dari objek ke IBoo. Ini adalah bukti lain bahwa tipe nilai kotak adalah varian referensi dari tipe nilai. Atau, semua tipe dalam sistem tipe adalah tipe referensi. Kami hanya bisa bekerja dengan struktur seperti dengan tipe nilai, melewati nilainya sepenuhnya. Mendereferensi pointer ke objek seperti yang Anda katakan di dunia C ++.
Anda dapat keberatan jika itu benar, akan terlihat seperti ini:
var referenceToInteger = (IInt32)10;
Kami tidak hanya akan mendapatkan objek, tetapi juga referensi yang diketik untuk tipe nilai kotak. Itu akan menghancurkan seluruh ide jenis nilai (yaitu integritas nilai mereka) yang memungkinkan untuk optimasi yang hebat, berdasarkan pada sifat mereka. Mari kita hilangkan ide ini!
public sealed class Boxed<T> { public T Value; [MethodImpl(MethodImplOptions.AggressiveInlining)] public override bool Equals(object obj) { return Value.Equals(obj); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override string ToString() { return Value.ToString(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override int GetHashCode() { return Value.GetHashCode(); } }
Kami memiliki analog tinju lengkap. Namun, kita dapat mengubah isinya dengan memanggil metode instan. Perubahan ini akan memengaruhi semua bagian dengan referensi ke struktur data ini.
var typedBoxing = new Boxed<int> { Value = 10 }; var pureBoxing = (object)10;
Varian pertama tidak terlalu menarik. Alih-alih casting tipe kita membuat omong kosong. Baris kedua jauh lebih baik, tetapi dua garis hampir identik. Satu-satunya perbedaan adalah bahwa tidak ada pembersihan memori dengan nol selama tinju biasa setelah mengalokasikan memori pada heap. Struktur yang diperlukan mengambil memori dengan segera sedangkan varian pertama perlu dibersihkan. Ini membuatnya bekerja lebih lama dari tinju biasa sebesar 10%.
Sebagai gantinya, kita dapat memanggil beberapa metode untuk nilai kotak kami.
struct Foo { public int x; public void ChangeTo(int newx) { x = newx; } } var boxed = new Boxed<Foo> { Value = new Foo { x = 5 } }; boxed.Value.ChangeTo(10); var unboxed = boxed.Value;
Kami punya instrumen baru. Mari kita pikirkan apa yang bisa kita lakukan dengannya.
- Jenis
Boxed<T>
kami melakukan hal yang sama dengan tipe yang biasa: mengalokasikan memori pada heap, meneruskan nilai di sana dan memungkinkan untuk mendapatkannya, dengan melakukan semacam unbox; - Jika Anda kehilangan referensi ke struktur kotak, GC akan mengumpulkannya;
- Namun, sekarang kita dapat bekerja dengan tipe kotak, yaitu memanggil metodenya;
- Juga, kita bisa mengganti instance dari tipe nilai dalam SOH / LOH untuk yang lain. Kami tidak dapat melakukannya sebelumnya, karena kami harus melakukan unboxing, mengubah struktur ke yang lain dan melakukan boxing kembali, memberikan referensi baru kepada pelanggan.
Masalah utama tinju adalah menciptakan lalu lintas dalam memori. Lalu lintas dari sejumlah objek yang tidak diketahui, yang bagiannya dapat bertahan hingga generasi satu, di mana kita mendapatkan masalah dengan pengumpulan sampah. Akan ada banyak sampah dan kita bisa menghindarinya. Tetapi ketika kita memiliki lalu lintas objek berumur pendek, solusi pertama adalah penyatuan. Ini adalah akhir yang ideal dari .NET somersault.
var pool = new Pool<Boxed<Foo>>(maxCount:1000); var boxed = pool.Box(10); boxed.Value=70; // use boxed value here pool.Free(boxed);
Sekarang tinju dapat bekerja menggunakan pool, yang menghilangkan lalu lintas memori saat tinju. Kita bahkan dapat membuat objek hidup kembali dalam metode finalisasi dan menempatkan diri kembali ke kolam. Ini mungkin berguna ketika struktur kotak pergi ke kode asinkron selain milik Anda dan Anda tidak bisa mengerti ketika itu menjadi tidak perlu. Dalam hal ini, ia akan kembali ke kolam selama GC.
Mari kita simpulkan:
- Jika tinju kebetulan dan tidak seharusnya terjadi, jangan mewujudkannya. Ini dapat menyebabkan masalah dengan kinerja.
- Jika tinju diperlukan untuk arsitektur sistem, mungkin ada varian. Jika lalu lintas struktur kotak kecil dan hampir tidak terlihat, Anda dapat menggunakan tinju. Jika lalu lintas terlihat, Anda mungkin ingin melakukan pengumpulan tinju, menggunakan salah satu solusi yang disebutkan di atas. Itu menghabiskan beberapa sumber daya, tetapi membuat GC bekerja tanpa kelebihan;
Pada akhirnya mari kita lihat kode yang sama sekali tidak praktis:
static unsafe void Main() { // here we create boxed int object boxed = 10; // here we get the address of a pointer to a VMT var address = (void**)EntityPtr.ToPointerWithOffset(boxed); unsafe { // here we get a Virtual Methods Table address var structVmt = typeof(SimpleIntHolder).TypeHandle.Value.ToPointer(); // change the VMT address of the integer passed to Heap into a VMT SimpleIntHolder, turning Int into a structure *address = structVmt; } var structure = (IGetterByInterface)boxed; Console.WriteLine(structure.GetByInterface()); } interface IGetterByInterface { int GetByInterface(); } struct SimpleIntHolder : IGetterByInterface { public int value; int IGetterByInterface.GetByInterface() { return value; } }
Kode menggunakan fungsi kecil, yang bisa mendapatkan pointer dari referensi ke suatu objek. Perpustakaan tersedia di alamat github . Contoh ini menunjukkan bahwa tinju biasa mengubah int menjadi tipe referensi yang diketik. Ayo pergi
lihat langkah-langkah dalam proses:
- Lakukan tinju untuk integer.
- Dapatkan alamat objek yang diperoleh (alamat Int32 VMT)
- Dapatkan VMT dari SimpleIntHolder
- Ganti VMT integer kotak ke VMT struktur.
- Buat unboxing menjadi tipe struktur
- Tampilkan nilai bidang pada layar, dapatkan Int32, itu
kotak.
Saya melakukannya melalui antarmuka dengan sengaja karena saya ingin menunjukkan bahwa itu akan berhasil
seperti itu.
Tidak dapat dibatalkan \ <T>
Perlu disebutkan tentang perilaku tinju dengan tipe nilai Nullable. Fitur tipe nilai Nullable ini sangat menarik karena tinju tipe nilai yang merupakan jenis null menghasilkan null.
int? x = 5; int? y = null; var boxedX = (object)x; // -> 5 var boxedY = (object)y; // -> null
Ini membawa kita ke kesimpulan yang aneh: karena null tidak memiliki tipe, the
satu-satunya cara untuk mendapatkan jenis, berbeda dari yang kotak adalah sebagai berikut:
int? x = null; var pseudoBoxed = (object)x; double? y = (double?)pseudoBoxed;
Kode berfungsi hanya karena Anda dapat melemparkan tipe ke apa pun yang Anda suka
dengan nol.
Pergi lebih dalam di tinju
Sebagai bit terakhir, saya ingin memberi tahu Anda tentang jenis System.Enum . Logikanya ini harus menjadi tipe nilai karena ini adalah penghitungan biasa: aliasing angka ke nama dalam bahasa pemrograman. Namun, System.Enum adalah tipe referensi. Semua tipe data enum, didefinisikan di bidang Anda dan juga dalam .NET Framework diwarisi dari System.Enum. Ini adalah tipe data kelas. Selain itu, ini adalah kelas abstrak, yang diwarisi dari System.ValueType
.
[Serializable] [System.Runtime.InteropServices.ComVisible(true)] public abstract class Enum : ValueType, IComparable, IFormattable, IConvertible { // ... }
Apakah itu berarti bahwa semua enumerasi dialokasikan pada SOH dan ketika kami menggunakannya, kami membebani tumpukan dan GC? Sebenarnya tidak, karena kami hanya menggunakannya. Kemudian, kami mengira bahwa ada kumpulan enumerasi di suatu tempat dan kami hanya mendapatkan contoh mereka. Tidak lagi Anda dapat menggunakan enumerasi dalam struktur saat menyusun. Enumerasi adalah angka biasa.
Yang benar adalah bahwa CLR meretas struktur tipe data saat membentuknya jika ada enum mengubah kelas menjadi tipe nilai :
// Check to see if the class is a valuetype; but we don't want to mark System.Enum // as a ValueType. To accomplish this, the check takes advantage of the fact // that System.ValueType and System.Enum are loaded one immediately after the // other in that order, and so if the parent MethodTable is System.ValueType and // the System.Enum MethodTable is unset, then we must be building System.Enum and // so we don't mark it as a ValueType. if(HasParent() && ((g_pEnumClass != NULL && GetParentMethodTable() == g_pValueTypeClass) || GetParentMethodTable() == g_pEnumClass)) { bmtProp->fIsValueClass = true; HRESULT hr = GetMDImport()->GetCustomAttributeByName(bmtInternal->pType->GetTypeDefToken(), g_CompilerServicesUnsafeValueTypeAttribute, NULL, NULL); IfFailThrow(hr); if (hr == S_OK) { SetUnsafeValueClass(); } }
Kenapa melakukan ini? Secara khusus, karena gagasan pewarisan - untuk melakukan enum yang disesuaikan, Anda, misalnya, perlu menentukan nama nilai yang mungkin. Namun, tidak mungkin mewarisi tipe nilai. Jadi, pengembang mendesainnya menjadi tipe referensi yang dapat mengubahnya menjadi tipe nilai saat dikompilasi.
Bagaimana jika Anda ingin melihat tinju secara pribadi?
Untungnya, Anda tidak harus menggunakan disassembler dan masuk ke hutan kode. Kami memiliki teks-teks dari seluruh platform inti .NET dan banyak di antaranya identik dalam hal .NET Framework CLR dan CoreCLR. Anda dapat mengklik tautan di bawah ini dan langsung melihat penerapan tinju:
Di sini, satu-satunya metode yang digunakan untuk unboxing:
JIT_Unbox (..) , yang merupakan pembungkus di sekitar JIT_Unbox_Helper (..) .
Juga, menarik bahwa ( https://stackoverflow.com/questions/3743762/unboxing-does-not-create-a-copy-of-the-value-is-this-right ), unboxing tidak berarti menyalin. data ke heap. Tinju berarti melewati pointer ke heap saat menguji kompatibilitas tipe. IL opcode yang mengikuti penghapusan kotak akan menentukan tindakan dengan alamat ini. Data mungkin disalin ke variabel lokal atau tumpukan untuk memanggil metode. Kalau tidak, kita akan memiliki penyalinan ganda; pertama kali menyalin dari tumpukan ke suatu tempat, dan kemudian menyalin ke tempat tujuan.
Pertanyaan
Mengapa .NET CLR tidak dapat melakukan pooling untuk tinju itu sendiri?
Jika kami berbicara dengan pengembang Java apa pun, kami akan mengetahui dua hal:
- Semua tipe nilai di Jawa adalah kotak, artinya pada dasarnya bukan tipe nilai. Bilangan bulat juga kotak.
- Untuk alasan optimasi, semua bilangan bulat dari -128 ke 127 diambil dari kumpulan objek.
Jadi, mengapa ini tidak terjadi di. NET CLR selama tinju? Sederhana saja. Karena kita dapat mengubah konten dari tipe nilai kotak, yaitu kita dapat melakukan hal berikut:
object x = 1; x.GetType().GetField("m_value", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(x, 138); Console.WriteLine(x); // -> 138
Atau seperti ini (C ++ / CLI):
void ChangeValue(Object^ obj) { Int32^ i = (Int32^)obj; *i = 138; }
Jika kita berurusan dengan penyatuan, maka kita akan mengubah semua yang ada dalam aplikasi menjadi 138, yang tidak baik.
Berikutnya adalah esensi tipe nilai dalam .NET. Mereka berurusan dengan nilai, yang berarti mereka bekerja lebih cepat. Boxing jarang terjadi dan penambahan nomor kotak milik dunia fantasi dan arsitektur yang buruk. Ini sama sekali tidak berguna.
Mengapa tidak mungkin melakukan tinju di tumpukan alih-alih tumpukan, ketika Anda memanggil metode yang mengambil tipe objek, yang sebenarnya merupakan tipe nilai?
Jika tipe nilai tinju dilakukan pada stack dan referensi akan menuju heap, referensi di dalam metode bisa pergi ke tempat lain, misalnya metode dapat menempatkan referensi di bidang kelas. Metode kemudian akan berhenti, dan metode yang membuat tinju juga akan berhenti. Sebagai hasilnya, referensi akan menunjuk ke ruang mati di stack.
Mengapa tidak mungkin menggunakan Tipe Nilai sebagai bidang?
Terkadang kita ingin menggunakan struktur sebagai bidang struktur lain yang menggunakan struktur pertama. Atau lebih sederhana: gunakan struktur sebagai bidang struktur. Jangan tanya kenapa ini bisa bermanfaat. Tidak bisa. Jika Anda menggunakan struktur sebagai bidangnya atau melalui ketergantungan dengan struktur lain, Anda membuat rekursi, yang berarti struktur ukuran tak terbatas. Namun, .NET Framework memiliki beberapa tempat di mana Anda dapat melakukannya. Contohnya adalah System.Char
, yang berisi dirinya sendiri :
public struct Char : IComparable, IConvertible { // Member Variables internal char m_value; //... }
Semua tipe primitif CLR dirancang dengan cara ini. Kita, manusia biasa, tidak dapat menerapkan perilaku ini. Selain itu, kita tidak memerlukan ini: ini dilakukan untuk memberikan tipe primitif semangat OOP di CLR.
Charper ini diterjemahkan dari bahasa Rusia sebagai bahasa pengarang oleh penerjemah profesional . Anda dapat membantu kami membuat versi terjemahan teks ini ke bahasa lain termasuk Cina atau Jerman menggunakan versi Rusia dan Inggris teks sebagai sumber.
Juga, jika Anda ingin mengucapkan "terima kasih", cara terbaik yang dapat Anda pilih adalah memberi kami bintang di github atau repositori forking
https://github.com/sidristij/dotnetbook