
Baru-baru ini, jenis referensi nullable telah menjadi topik hangat. Namun, jenis nilai nullable lama yang baik belum hilang dan masih aktif digunakan. Apakah Anda ingat betul nuansa bekerja dengan mereka? Saya sarankan Anda menyegarkan atau menguji pengetahuan Anda dengan membaca artikel ini. Contoh C # dan kode IL, referensi ke spesifikasi CLI dan kode CoreCLR disertakan. Saya mengusulkan untuk memulai dengan masalah yang menarik.
Catatan . Jika Anda tertarik dengan jenis referensi nullable, Anda dapat melihat beberapa artikel rekan saya: " Jenis Referensi Nullable di C # 8.0 dan Analisis Statis ", " Referensi Nullable Jangan Lindungi, dan Ini Buktinya ."
Lihatlah contoh kode di bawah ini dan jawab apa yang akan menjadi output ke konsol. Dan, sama pentingnya, mengapa. Mari kita segera setuju bahwa Anda akan menjawab apa adanya: tanpa petunjuk kompiler, dokumentasi, membaca literatur, atau semacamnya. :)
static void NullableTest()
{
int? a = null;
object aObj = a;
int? b = new int?();
object bObj = b;
Console.WriteLine(Object.ReferenceEquals(aObj, bObj)); // True or False?
}

Baiklah, mari kita pikirkan sedikit. Mari kita ambil beberapa garis pemikiran utama yang, menurut saya, bisa muncul.
1. Melanjutkan dari fakta bahwa int? - tipe referensi.
Mari beralasan seperti ini, apa int? Adalah tipe referensi. Dalam hal ini, sebuah nilai ditulis ke nol , itu juga akan dicatat dan aObj setelah tugas. Referensi ke beberapa objek akan ditulis dalam b . Ini juga akan ditulis ke bObj setelah penugasan. Akibatnya, Object.ReferenceEquals akan menganggap null dan referensi objek bukan null sebagai argumen , jadi ...
Sudah jelas, jawabannya Salah!
2. Kami melanjutkan dari fakta bahwa int? - tipe signifikan.
Atau mungkin Anda meragukan int itu? - tipe referensi? Dan apakah Anda yakin akan hal ini terlepas dari ekspresi int? a = null ? Nah, mari kita pergi dari sisi lain dan mulai dari apa int? - tipe signifikan.
Dalam hal ini, ekspresi int? a = null terlihat sedikit aneh, tetapi anggaplah lagi di C # gula dituangkan di atasnya. Ternyata a menyimpan beberapa jenis benda. b juga menyimpan beberapa jenis objek. Saat menginisialisasi variabel aObj dan bObj , objek yang disimpan di a dan b akan dikemas, sebagai akibatnya referensi berbeda akan ditulis ke aObj dan bObj . Ternyata Object.ReferenceEquals mengambil referensi ke objek yang berbeda sebagai argumen, oleh karena itu ...
Semuanya jelas, jawabannya Salah!
3. Kami berasumsi bahwa Nullable <T> digunakan di sini .
Katakanlah Anda tidak menyukai opsi di atas. Karena Anda tahu betul bahwa tidak ada int? sebenarnya tidak, tetapi ada tipe nilai Nullable <T> , dan dalam hal ini Nullable <int> akan digunakan . Anda juga memahami bahwa sebenarnya dalam a dan bakan ada objek yang identik. Pada saat yang sama, Anda tidak lupa bahwa saat menulis nilai ke aObj dan bObj , pengepakan akan terjadi, dan sebagai hasilnya, referensi ke objek yang berbeda akan diperoleh. Karena Object.ReferenceEquals menerima referensi ke objek yang berbeda, maka ...
Sudah jelas, jawabannya Salah!
4 .;)
Bagi mereka yang memulai dari tipe nilai - jika Anda tiba-tiba ragu untuk membandingkan referensi, Anda dapat melihat dokumentasi di Object.ReferenceEquals di docs.microsoft.com... Secara khusus, ini juga menyentuh topik jenis nilai dan tinju / unboxing. Benar, ini menjelaskan kasus ketika contoh tipe signifikan diteruskan langsung ke metode, kami mengeluarkan kemasan secara terpisah, tetapi intinya sama.
Saat membandingkan tipe nilai. Jika objA dan objB adalah tipe nilai, mereka dikotakkan sebelum diteruskan ke metode ReferenceEquals. Ini berarti bahwa jika objA dan objB mewakili contoh yang sama dari tipe nilai , metode ReferenceEquals mengembalikan false , seperti yang ditunjukkan contoh berikut.
Tampaknya di sini artikel dapat diselesaikan, tetapi hanya ... jawaban yang benar adalah Benar .
Baiklah, mari kita cari tahu.
Pemahaman
Ada dua cara - sederhana dan menarik.
Cara yang mudah
int? Apakah Nullable <int> . Buka dokumentasi Nullable <T> , di mana kita melihat bagian "Boxing dan Unboxing". Pada prinsipnya, itu saja - perilaku dijelaskan di sana. Tetapi jika Anda ingin lebih detail, saya mengundang Anda ke jalur yang menarik. ;)
Cara yang menarik
Kami tidak akan memiliki cukup dokumentasi di jalur ini. Dia menggambarkan perilakunya tetapi tidak menjawab pertanyaan 'mengapa'?
Apa sebenarnya int itu? dan nol dalam konteks yang sesuai? Mengapa cara kerjanya seperti ini? Apakah kode IL menggunakan perintah yang berbeda atau tidak? Apakah perilakunya berbeda di tingkat CLR? Ada keajaiban lain?
Mari kita mulai dengan mem-parsing entitas int? untuk mengingat dasar-dasarnya, dan secara bertahap beralih ke analisis kasus aslinya. Karena C # adalah bahasa yang agak "menarik", kami secara berkala akan merujuk ke kode IL untuk melihat esensi dari suatu hal (ya, dokumentasi C # bukan cara kami hari ini).
int?, Nullable <T>
Di sini kita akan melihat dasar-dasar tipe nilai nullable pada prinsipnya (apa itu, apa yang dikompilasi di IL, dll.). Jawaban atas pertanyaan dari tugas tersebut dibahas di bagian selanjutnya.
Mari kita lihat sepotong kode.
int? aVal = null;
int? bVal = new int?();
Nullable<int> cVal = null;
Nullable<int> dVal = new Nullable<int>();
Meskipun inisialisasi variabel ini terlihat berbeda di C #, kode IL yang sama akan dibuat untuk semuanya.
.locals init (valuetype [System.Runtime]System.Nullable`1<int32> V_0,
valuetype [System.Runtime]System.Nullable`1<int32> V_1,
valuetype [System.Runtime]System.Nullable`1<int32> V_2,
valuetype [System.Runtime]System.Nullable`1<int32> V_3)
// aVal
ldloca.s V_0
initobj valuetype [System.Runtime]System.Nullable`1<int32>
// bVal
ldloca.s V_1
initobj valuetype [System.Runtime]System.Nullable`1<int32>
// cVal
ldloca.s V_2
initobj valuetype [System.Runtime]System.Nullable`1<int32>
// dVal
ldloca.s V_3
initobj valuetype [System.Runtime]System.Nullable`1<int32>
Seperti yang Anda lihat, di C # semuanya dibumbui dengan gula sintaksis dari hati sehingga Anda dan saya bisa hidup lebih baik, nyatanya:
- int? - tipe signifikan.
- int? - sama seperti Nullable <int>. Kode IL bekerja dengan Nullable <int32> .
- int? aVal = null sama dengan Nullable <int> aVal = new Nullable <int> () . Dalam IL, ini berkembang menjadi pernyataan initobj yang melakukan inisialisasi default pada alamat yang dimuat.
Pertimbangkan potongan kode berikut:
int? aVal = 62;
Kami menemukan inisialisasi default - kami melihat kode IL yang sesuai di atas. Apa yang terjadi di sini ketika kita ingin menginisialisasi aVal ke 62?
Mari kita lihat kode IL:
.locals init (valuetype [System.Runtime]System.Nullable`1<int32> V_0)
ldloca.s V_1
ldc.i4.s 62
call instance void valuetype
[System.Runtime]System.Nullable`1<int32>::.ctor(!0)
Sekali lagi, tidak ada yang rumit - alamat aVal dimuat ke tumpukan evaluasi , serta nilai 62, dan kemudian konstruktor dengan tanda tangan Nullable <T> (T) dipanggil. Artinya, dua ekspresi berikut akan sepenuhnya identik:
int? aVal = 62;
Nullable<int> bVal = new Nullable<int>(62);
Anda dapat melihat hal yang sama dengan melihat kode IL lagi:
// int? aVal;
// Nullable<int> bVal;
.locals init (valuetype [System.Runtime]System.Nullable`1<int32> V_0,
valuetype [System.Runtime]System.Nullable`1<int32> V_1)
// aVal = 62
ldloca.s V_0
ldc.i4.s 62
call instance void valuetype
[System.Runtime]System.Nullable`1<int32>::.ctor(!0)
// bVal = new Nullable<int>(62)
ldloca.s V_1
ldc.i4.s 62
call instance void valuetype
[System.Runtime]System.Nullable`1<int32>::.ctor(!0)
Bagaimana dengan inspeksi? Misalnya, seperti apa sebenarnya kode berikut ini?
bool IsDefault(int? value) => value == null;
Benar, untuk pemahaman, mari beralih ke kode IL yang sesuai lagi.
.method private hidebysig instance bool
IsDefault(valuetype [System.Runtime]System.Nullable`1<int32> 'value')
cil managed
{
.maxstack 8
ldarga.s 'value'
call instance bool valuetype
[System.Runtime]System.Nullable`1<int32>::get_HasValue()
ldc.i4.0
ceq
ret
}
Seperti yang sudah Anda duga, sebenarnya tidak ada null - yang terjadi hanyalah panggilan ke properti Nullable <T> .HasValue . Artinya, logika yang sama di C # dapat ditulis lebih eksplisit dalam istilah entitas yang digunakan sebagai berikut.
bool IsDefaultVerbose(Nullable<int> value) => !value.HasValue;
Kode IL:
.method private hidebysig instance bool
IsDefaultVerbose(valuetype [System.Runtime]System.Nullable`1<int32> 'value')
cil managed
{
.maxstack 8
ldarga.s 'value'
call instance bool valuetype
[System.Runtime]System.Nullable`1<int32>::get_HasValue()
ldc.i4.0
ceq
ret
}
Mari kita rangkum:
- Tipe nilai Nullable diimplementasikan dengan mengorbankan tipe Nullable <T> ;
- int? - sebenarnya tipe yang dibangun dari tipe nilai generik Nullable <T> ;
- int? a = null - inisialisasi objek berjenis Nullable <int> dengan nilai default, sebenarnya tidak ada null di sini;
- jika (a == null) - sekali lagi, tidak ada null , ada panggilan ke properti Nullable <T> .HasValue .
Kode sumber dari tipe Nullable <T> dapat dilihat, misalnya, di GitHub di repositori dotnet / runtime - tautan langsung ke file kode sumber . Tidak banyak kode di sana, jadi demi kepentingan saya menyarankan Anda untuk melihat-lihat. Dari situ, Anda bisa mempelajari (atau mengingat) fakta-fakta berikut.
Untuk kenyamanan, tipe Nullable <T> mendefinisikan:
- operator konversi implisit dari T ke Nullable <T> ;
- operator konversi eksplisit dari Nullable <T> untuk T .
Logika utama pekerjaan diimplementasikan melalui dua bidang (dan properti yang sesuai):
- Nilai T - nilai itu sendiri, dibungkus di mana Nullable <T> ;
- bool hasValue adalah bendera yang menunjukkan apakah pembungkusnya berisi nilai. Dalam tanda kutip, seperti yang dikatakan Nullable <T> selalu mengandung nilai tipe T .
Sekarang kita memiliki memori yang menyegarkan dari jenis nilai nullable, mari kita lihat ada apa dengan kemasannya.
Pengepakan <T> tidak dapat dibatalkan
Izinkan saya mengingatkan Anda bahwa saat mengemas objek berjenis nilai, objek baru akan dibuat di heap. Cuplikan kode berikut menggambarkan perilaku ini:
int aVal = 62;
object obj1 = aVal;
object obj2 = aVal;
Console.WriteLine(Object.ReferenceEquals(obj1, obj2));
Hasil perbandingan referensi diharapkan salah , karena 2 operasi tinju telah terjadi dan dua objek diciptakan, referensi yang ditulis di obj1 dan obj2 .
Sekarang ubah int menjadi Nullable <int> .
Nullable<int> aVal = 62;
object obj1 = aVal;
object obj2 = aVal;
Console.WriteLine(Object.ReferenceEquals(obj1, obj2));
Hasilnya masih diharapkan - salah .
Dan sekarang, alih-alih 62, kami menulis nilai default.
Nullable<int> aVal = new Nullable<int>();
object obj1 = aVal;
object obj2 = aVal;
Console.WriteLine(Object.ReferenceEquals(obj1, obj2));
Iii ... hasilnya tiba-tiba jadi kenyataan . Tampaknya kita memiliki 2 operasi pengepakan yang sama, membuat dua objek dan tautan ke dua objek yang berbeda, tetapi hasilnya benar !
Ya, mungkin gula lagi, dan ada sesuatu yang berubah di tingkat kode IL! Ayo lihat.
Contoh N1.
Kode C #:
int aVal = 62;
object aObj = aVal;
Kode IL:
.locals init (int32 V_0,
object V_1)
// aVal = 62
ldc.i4.s 62
stloc.0
// aVal
ldloc.0
box [System.Runtime]System.Int32
// aObj
stloc.1
Contoh N2.
C # kode:
Nullable<int> aVal = 62;
object aObj = aVal;
Kode IL:
.locals init (valuetype [System.Runtime]System.Nullable`1<int32> V_0,
object V_1)
// aVal = new Nullablt<int>(62)
ldloca.s V_0
ldc.i4.s 62
call instance void
valuetype [System.Runtime]System.Nullable`1<int32>::.ctor(!0)
// aVal
ldloc.0
box valuetype [System.Runtime]System.Nullable`1<int32>
// aObj
stloc.1
Contoh N3.
Kode C #:
Nullable<int> aVal = new Nullable<int>();
object aObj = aVal;
Kode IL:
.locals init (valuetype [System.Runtime]System.Nullable`1<int32> V_0,
object V_1)
// aVal = new Nullable<int>()
ldloca.s V_0
initobj valuetype [System.Runtime]System.Nullable`1<int32>
// aVal
ldloc.0
box valuetype [System.Runtime]System.Nullable`1<int32>
// aObj
stloc.1
Seperti yang dapat kita lihat, pengepakan dilakukan dengan cara yang sama di mana-mana - nilai variabel lokal dimuat ke tumpukan evaluasi (instruksi ldloc ), setelah itu pengemasan itu sendiri dilakukan dengan memanggil perintah kotak , yang menunjukkan jenis yang akan kita kemas.
Kami beralih ke spesifikasi Common Language Infrastructure , melihat deskripsi perintah kotak dan menemukan catatan menarik mengenai tipe nullable:
Jika typeTok adalah tipe nilai, instruksi kotak mengubah val ke bentuk kotaknya. ...Jika ini adalah tipe nullable, ini dilakukan dengan memeriksa properti HasValue val; jika salah, referensi null didorong ke stack; jika tidak, hasil dari properti Value boxing val didorong ke stack.
Dari sini ada beberapa kesimpulan yang menandai 'i':
- status objek Nullable <T> diperhitungkan (bendera HasValue yang kami anggap sebelumnya dicentang ). Jika Nullable <T> tidak mengandung nilai ( HasValue adalah palsu ), yang kotak akan menghasilkan nol ;
- jika Nullable <T> berisi nilai ( HasValue - true ), maka bukan objek Nullable <T> yang akan dikemas , tetapi turunan dari tipe T , yang disimpan di bidang nilai dari tipe Nullable <T> ;
- logika khusus untuk menangani pengemasan Nullable <T> tidak diterapkan di tingkat C # atau bahkan di tingkat IL - ini diterapkan di CLR.
Mari kembali ke contoh Nullable <T> yang dibahas di atas.
Pertama:
Nullable<int> aVal = 62;
object obj1 = aVal;
object obj2 = aVal;
Console.WriteLine(Object.ReferenceEquals(obj1, obj2));
Kondisi barang sebelum pengepakan:
- T -> int ;
- nilai -> 62 ;
- hasValue -> benar .
Nilai 62 dikemas dua kali (ingat bahwa dalam kasus ini, instance tipe int dikemas , dan bukan Nullable <int> ), 2 objek baru dibuat, 2 referensi ke objek berbeda diperoleh, yang hasilnya salah .
Kedua:
Nullable<int> aVal = new Nullable<int>();
object obj1 = aVal;
object obj2 = aVal;
Console.WriteLine(Object.ReferenceEquals(obj1, obj2));
Kondisi barang sebelum pengepakan:
- T -> int ;
- nilai -> default (dalam hal ini, 0 adalah nilai default untuk int );
- hasValue -> salah .
Sejak HasValue adalah palsu , tidak ada objek diciptakan di heap, dan kotak kembali operasi nol , yang ditulis ke variabel obj1 dan obj2 . Membandingkan nilai-nilai ini, seperti yang diharapkan, menghasilkan kebenaran .
Dalam contoh asli, yang ada di awal artikel, hal yang persis sama terjadi:
static void NullableTest()
{
int? a = null; // default value of Nullable<int>
object aObj = a; // null
int? b = new int?(); // default value of Nullable<int>
object bObj = b; // null
Console.WriteLine(Object.ReferenceEquals(aObj, bObj)); // null == null
}
Untuk bersenang-senang, mari kita lihat kode sumber CoreCLR dari repositori dotnet / runtime yang disebutkan sebelumnya . Kami tertarik pada file object.cpp , khususnya - metode Nullable :: Box , yang berisi logika yang kami butuhkan:
OBJECTREF Nullable::Box(void* srcPtr, MethodTable* nullableMT)
{
CONTRACTL
{
THROWS;
GC_TRIGGERS;
MODE_COOPERATIVE;
}
CONTRACTL_END;
FAULT_NOT_FATAL(); // FIX_NOW: why do we need this?
Nullable* src = (Nullable*) srcPtr;
_ASSERTE(IsNullableType(nullableMT));
// We better have a concrete instantiation,
// or our field offset asserts are not useful
_ASSERTE(!nullableMT->ContainsGenericVariables());
if (!*src->HasValueAddr(nullableMT))
return NULL;
OBJECTREF obj = 0;
GCPROTECT_BEGININTERIOR (src);
MethodTable* argMT = nullableMT->GetInstantiation()[0].AsMethodTable();
obj = argMT->Allocate();
CopyValueClass(obj->UnBox(), src->ValueAddr(nullableMT), argMT);
GCPROTECT_END ();
return obj;
}
Inilah semua yang kita bicarakan di atas. Jika kami tidak menyimpan nilainya, kami mengembalikan NULL :
if (!*src->HasValueAddr(nullableMT))
return NULL;
Jika tidak, kami memproduksi kemasan:
OBJECTREF obj = 0;
GCPROTECT_BEGININTERIOR (src);
MethodTable* argMT = nullableMT->GetInstantiation()[0].AsMethodTable();
obj = argMT->Allocate();
CopyValueClass(obj->UnBox(), src->ValueAddr(nullableMT), argMT);
Kesimpulan
Demi kepentingan, saya mengusulkan untuk menunjukkan contoh dari awal artikel kepada kolega dan teman saya. Akankah mereka dapat memberikan jawaban yang benar dan membuktikannya? Jika tidak, undang mereka untuk membaca artikel tersebut. Jika mereka bisa - ya, hormat saya!
Saya berharap itu adalah petualangan kecil tapi menyenangkan. :)
PS Seseorang mungkin memiliki pertanyaan: bagaimana pendalaman topik ini dimulai? Kami membuat aturan diagnostik baru di PVS-Studio tentang fakta bahwa Object.ReferenceEquals berfungsi dengan argumen, salah satunya diwakili oleh tipe yang signifikan. Tiba-tiba ternyata dengan Nullable <T> ada momen tak terduga dalam perilaku packing. Kami melihat kode IL - kotak sebagai kotak... Lihat spesifikasi CLI - ya, itu dia! Tampaknya ini adalah kasus yang cukup menarik, yang patut diceritakan - sekali! - dan artikelnya ada di depan Anda.

Jika Anda ingin berbagi artikel ini dengan audiens berbahasa Inggris, silakan gunakan tautan terjemahan: Sergey Vasiliev. Periksa bagaimana Anda mengingat jenis nilai nullable. Yuk intip di bawah tenda .
PPS Ngomong-ngomong, baru-baru ini saya sedikit lebih aktif di Twitter, di mana saya memposting beberapa cuplikan kode yang menarik, me-retweet beberapa berita menarik dari dunia .NET dan semacamnya. Saya mengusulkan untuk melihat-lihat, jika tertarik - berlangganan ( tautan ke profil ).