19-Bob: Nullable Value Turlar

Value turlarni null qilib belgilash, C# ning nullable sintaksisi, null-coalescing operatori va CLR ning maxsus qo'llab-quvvatlashi

Ma'lumki, value tur o'zgaruvchisi hech qachon null bo'la olmaydi; u doimo value turning o'ziga xos qiymatini o'z ichiga oladi. Aslida, shuning uchun ham ular value turlar deb ataladi. Afsuski, ba'zi holatlarda bu muammo tug'diradi. Masalan, ma'lumotlar bazasini loyihalashda ustun ma'lumot turini 32-bitlik butun son sifatida aniqlash mumkin, bu Framework Class Library (FCL) dagi Int32 ma'lumot turiga mos keladi. Lekin ma'lumotlar bazasidagi ustun qiymatning nullable ekanligini ko'rsatishi mumkin. Ya'ni, satrning ushbu ustunida qiymat bo'lmasligi ham mumkin. Ma'lumotlar bazasi ma'lumotlari bilan Microsoft .NET Framework yordamida ishlash ancha qiyin bo'lishi mumkin, chunki common language runtime (CLR) da Int32 qiymatini null sifatida ifodalashning imkoni yo'q.

Eslatma

Microsoft ADO.NET ning jadval adapterlari nullable turlarni qo'llab-quvvatlaydi. Lekin afsuski, System.Data.SqlTypes nomdoshidagi turlar nullable turlar bilan bittadan-bitta moslikka ega emas. Masalan, SqlDecimal turi maksimum 38 raqamga ega, oddiy Decimal turi esa faqat 29 raqamga yetadi. Bundan tashqari, SqlString turi o'zining locale va solishtirish parametrlarini qo'llab-quvvatlaydi, bu oddiy String turida mavjud emas.

Yana bir misol. Java da java.util.Date klassi reference turdir, shuning uchun bu turdagi o'zgaruvchini null ga o'rnatish mumkin. Lekin CLR da System.DateTime value turdir va DateTime o'zgaruvchisi hech qachon null bo'la olmaydi. Agar Java da yozilgan ilova veb-xizmat orqali CLR da ishlaydigan ilovaga sana/vaqt yubormoqchi bo'lsa va Java ilovasi null yuborsa, muammo yuzaga keladi, chunki CLR buni qabul qilib, qayta ishlash imkoniga ega emas.

Ushbu vaziyatni yaxshilash uchun Microsoft CLR ga nullable value turlar tushunchasini qo'shdi. Bu mexanizm qanday ishlashini tushunish uchun, avval FCL da aniqlangan System.Nullable<T> strukturasini ko'rib chiqishimiz kerak.

System.Nullable<T> Strukturasi

Quyida System.Nullable<T> turining mantiqiy ko'rinishi keltirilgan:

[Serializable, StructLayout(LayoutKind.Sequential)]
public struct Nullable<T> where T : struct {

    // Bu 2 maydon holatni ifodalaydi
    private Boolean hasValue = false; // null deb faraz qilinadi
    internal T value = default(T);    // Barcha bitlar nol

    public Nullable(T value) {
        this.value = value;
        this.hasValue = true;
    }

    public Boolean HasValue { get { return hasValue; } }

    public T Value {
        get {
            if (!hasValue) {
                throw new InvalidOperationException(
                    "Nullable object must have a value.");
            }
            return value;
        }
    }

    public T GetValueOrDefault() { return value; }

    public T GetValueOrDefault(T defaultValue) {
        if (!HasValue) return defaultValue;
        return value;
    }

    public override Boolean Equals(Object other) {
        if (!HasValue) return (other == null);
        if (other == null) return false;
        return value.Equals(other);
    }

    public override int GetHashCode() {
        if (!HasValue) return 0;
        return value.GetHashCode();
    }

    public override string ToString() {
        if (!HasValue) return "";
        return value.ToString();
    }

    public static implicit operator Nullable<T>(T value) {
        return new Nullable<T>(value);
    }

    public static explicit operator T(Nullable<T> value) {
        return value.Value;
    }
}

Ko'rib turganingizdek, bu klass null bo'lishi ham mumkin bo'lgan value tur tushunchasini inkapsulyatsiya qiladi. Nullable<T> o'zi value tur bo'lganligi sababli, uning instansiyalari ancha yengil bo'lib qoladi. Ya'ni, instansiyalar stekda bo'lishi mumkin va instansiya hajmi asl value turga qo'shimcha ravishda Boolean maydoni hajmicha kattaroqdir. E'tibor bering, Nullable ning tur parametri T struct ga cheklangan. Bu reference tur o'zgaruvchilari allaqachon null bo'la olishi sababli shunday qilingan.

Endi, agar kodingizda nullable Int32 ishlatmoqchi bo'lsangiz, quyidagidek yozishingiz mumkin:

Nullable<Int32> x = 5;
Nullable<Int32> y = null;
Console.WriteLine("x: HasValue={0}, Value={1}", x.HasValue, x.Value);
Console.WriteLine("y: HasValue={0}, Value={1}", y.HasValue, y.GetValueOrDefault());

Bu kodni kompilyatsiya qilib ishga tushirganimda, quyidagi natijani oldim:

x: HasValue=True, Value=5
y: HasValue=False, Value=0

C# ning Nullable Value Turlarni Qo'llab-quvvatlashi

Yuqoridagi kodda C# ning ikkita Nullable<Int32> o'zgaruvchisini (x va y) initsializatsiya qilish uchun ancha sodda sintaksisdan foydalanishga ruxsat berganiga e'tibor bering. Aslida, C# jamoasi nullable value turlarni C# tiliga birinchi darajali fuqarolar sifatida integratsiya qilmoqchi bo'lgan va ularni to'liq qo'llab-quvvatlamoqchi. Shu maqsadda C# nullable value turlar bilan ishlash uchun toza sintaksisni taklif qiladi. C# x va y o'zgaruvchilarini savol belgisi yordamida e'lon qilish va initsializatsiya qilish imkonini beradi:

Int32? x = 5;
Int32? y = null;

C# da Int32?Nullable<Int32> uchun sinonim. Lekin C# bundan ham ilgari ketadi. C# sizga nullable instansiyalarda konvertatsiya va castlar bajarishga, shuningdek nullable instansiyalarga operatorlar qo'llashga ruxsat beradi.

Konvertatsiyalar va Castlar

Quyidagi kod bularning namunalarini ko'rsatadi:

private static void ConversionsAndCasting() {
    // Non-nullable Int32 dan Nullable<Int32> ga yashirin konvertatsiya
    Int32? a = 5;

    // 'null' dan Nullable<Int32> ga yashirin konvertatsiya
    Int32? b = null;  // "Int32? b = new Int32?();" bilan bir xil
                      // bu HasValue ni false qiladi

    // Nullable<Int32> dan non-nullable Int32 ga aniq konvertatsiya
    Int32 c = (Int32) a;

    // Nullable primitiv turlar o'rtasida cast qilish
    Double? d = 5; // Int32 -> Double?  (d 5.0 double qiymatga ega)
    Double? e = b; // Int32? -> Double? (e null)
}

C# shuningdek nullable instansiyalarga operatorlar qo'llashga ham ruxsat beradi. Quyidagi kod buning namunalarini ko'rsatadi:

Nullable Instansiyalarga Operatorlar Qo'llash

private static void Operators() {
    Int32? a = 5;
    Int32? b = null;

    // Unar operatorlar (+ ++ - -- ! ~)
    a++;      // a = 6
    b = -b;   // b = null

    // Binar operatorlar (+ - * / % & | ^ << >>)
    a = a + 3;   // a = 9
    b = b * 3;   // b = null;

    // Tenglik operatorlari (== !=)
    if (a == null) { /* yo'q */ } else { /* ha  */ }
    if (b == null) { /* ha  */ } else { /* yo'q */ }
    if (a != b)    { /* ha  */ } else { /* yo'q */ }

    // Solishtirish operatorlari (<> <= >=)
    if (a < b)     { /* yo'q */ } else { /* ha  */ }
}

C# operatorlarni quyidagicha izohlaydi:

  • Unar operatorlar (+, ++, -, --, !, ~) — Agar operand null bo'lsa, natija ham null bo'ladi.
  • Binar operatorlar (+, -, *, /, %, &, |, ^, <<, >>) — Agar biror operand null bo'lsa, natija ham null bo'ladi. Lekin & va | operatorlari Boolean? operandlarda ishlayotganda istisno mavjud: bu ikki operatorning xatti-harakati SQL ning uch qiymatli mantiqiy tizimi bilan bir xil bo'lishi uchun maxsus qoidalar qo'llaniladi. Bu ikki operator uchun, agar ikkala operand ham null bo'lmasa, operator odatdagidek ishlaydi; va agar ikkala operand ham null bo'lsa, natija null bo'ladi. Maxsus xatti-harakat faqat bitta operand null bo'lgandagina namoyon bo'ladi.

Boolean? uchun & va | Operatorlar Jadvali

Quyidagi jadval true, false va null ning barcha kombinatsiyalari uchun bu ikki operator tomonidan hosil qilinadigan natijalarni ko'rsatadi:

Operand1 → Operand2 ↓truefalsenull
True & = true
| = true
& = false
| = true
& = null
| = true
False & = false
| = true
& = false
| = false
& = false
| = null
Null & = null
| = true
& = false
| = null
& = null
| = null
  • Tenglik operatorlari (==, !=) — Agar ikkala operand ham null bo'lsa, ular teng. Agar bitta operand null bo'lsa, ular teng emas. Agar ikkala operand ham null bo'lmasa, teng yoki teng emasligini aniqlash uchun qiymatlarni solishtiradi.
  • Munosabat operatorlari (<, >, <=, >=) — Agar biror operand null bo'lsa, natija false bo'ladi. Agar ikkala operand ham null bo'lmasa, qiymatlarni solishtiradi.

Nullable Instansiyalar bilan IL Kod Hajmi

Nullable instansiyalar bilan manipulyatsiya qilish juda ko'p kod yaratishini bilishingiz kerak. Masalan, quyidagi metodni ko'ring:

private static Int32? NullableCodeSize(Int32? a, Int32? b) {
    return a + b;
}

Men bu metodni kompilyatsiya qilganimda, oraliq til (IL) kodining ancha ko'p hosil bo'lganini ko'rdim, bu esa nullable turlarda amallar bajarish non-nullable turlardagi bir xil amallardan sekinroq ekanligini ham anglatadi. Quyida kompilyator yaratgan IL kodning C# ekvivalenti keltirilgan:

private static Nullable<Int32> NullableCodeSize(
    Nullable<Int32> a, Nullable<Int32> b) {

    Nullable<Int32> nullable1 = a;
    Nullable<Int32> nullable2 = b;
    if (!(nullable1.HasValue & nullable2.HasValue)) {
        return new Nullable<Int32>();
    }
    return new Nullable<Int32>(
        nullable1.GetValueOrDefault() + nullable2.GetValueOrDefault());
}

Qayta Yuklangan Operatorlar bilan Nullable Turlar

Nihoyat, siz turli operatorlarni qayta yuklaydigan o'z value turlaringizni aniqlashingiz mumkinligini ta'kidlamoqchiman. Men buni qanday qilishni 8-bob "Metodlar" dagi "Operator Qayta Yuklash Metodlari" bo'limida muhokama qilaman. Agar o'zingizning value turingizning nullable instansiyasini ishlatsangiz, kompilyator to'g'ri ishni bajaradi va qayta yuklangan operatoringizni chaqiradi. Masalan, siz == va != operatorlarini aniqlaydigan Point value turiga ega deb faraz qilaylik:

using System;

internal struct Point {
    private Int32 m_x, m_y;
    public Point(Int32 x, Int32 y) { m_x = x; m_y = y; }

    public static Boolean operator==(Point p1, Point p2) {
        return (p1.m_x == p2.m_x) && (p1.m_y == p2.m_y);
    }

    public static Boolean operator!=(Point p1, Point p2) {
        return !(p1 == p2);
    }
}

Bu nuqtada, siz Point turining nullable instansiyalarini ishlatishingiz mumkin va kompilyator qayta yuklangan operatorlaringizni chaqiradi:

internal static class Program {
    public static void Main() {
        Point? p1 = new Point(1, 1);
        Point? p2 = new Point(2, 2);

        Console.WriteLine("Are points equal? " + (p1 == p2).ToString());
        Console.WriteLine("Are points not equal? " + (p1 != p2).ToString());
    }
}

Yuqoridagi kodni kompilyatsiya qilib ishga tushirganda, quyidagi natijani oldim:

Are points equal? False
Are points not equal? True

C# ning Null-Coalescing Operatori (??)

C# da null-coalescing operator (??) deb ataladigan operator mavjud bo'lib, u ikkita operand qabul qiladi. Agar chapdagi operand null bo'lmasa, uning qiymati qaytariladi. Agar chapdagi operand null bo'lsa, o'ngdagi operandning qiymati qaytariladi. Null-coalescing operatori o'zgaruvchining standart qiymatini o'rnatishning juda qulay usulini taklif qiladi.

Null-coalescing operatorining ajoyib xususiyati shundaki, u reference turlar bilan ham, nullable value turlar bilan ham ishlatilishi mumkin. Quyida null-coalescing operatoridan foydalanishni ko'rsatadigan kod keltirilgan:

private static void NullCoalescingOperator() {
    Int32? b = null;

    // Quyidagi satr ekvivalent:
    // x = (b.HasValue) ? b.Value : 123
    Int32 x = b ?? 123;
    Console.WriteLine(x);   // "123"

    // Quyidagi satr ekvivalent:
    // String temp = GetFilename();
    // filename = (temp != null) ? temp : "Untitled";
    String filename = GetFilename() ?? "Untitled";
}

Ba'zi odamlar null-coalescing operatorining ?: operatori uchun shunchaki sintaktik shakar ekanligini va C# kompilyator jamoasi bu operatorni tilga qo'shmasligi kerak edi deyishadi. Biroq, null-coalescing operatori ikkita muhim sintaktik yaxshilanishni taqdim etadi.

Birinchisi shundan iboratki, ?? operatori kompozitsiya senariylarida yaxshiroq ishlaydi. Masalan, quyidagi bitta satr:

String s = SomeMethod1() ?? SomeMethod2() ?? "Untitled";

quyidagi kod bo'lagiga qaraganda o'qish va tushunish ancha oson:

String s;
var sm1 = SomeMethod1();
if (sm1 != null) s = sm1;
else {
    var sm2 = SomeMethod2();
    if (sm2 != null) s = sm2;
    else s = "Untitled";
}

Ikkinchi yaxshilanish shundan iboratki, ?? ifodalar bilan yaxshiroq ishlaydi:

Func<String> f = () => SomeMethod() ?? "Untitled";

Bu kod quyidagi satrga qaraganda o'qish va tushunish ancha oson, chunki u o'zgaruvchi tayinlash va bir nechta ifodalarni talab qiladi:

Func<String> f = () => { var temp = SomeMethod();
    return temp != null ? temp : "Untitled";};

CLR ning Nullable Value Turlarni Maxsus Qo'llab-quvvatlashi

CLR nullable value turlar uchun ichki qo'llab-quvvatlashga ega. Bu maxsus qo'llab-quvvatlash boxing, unboxing, GetType chaqirish, interfeys metodlarini chaqirish uchun mo'ljallangan va nullable turlarga CLR ga yanada uzviylashtirilgan holda integratsiyalash imkonini beradi. Bu ularning xatti-harakatini tabiiyroq va aksariyat dasturchilar kutganidek qiladi. Keling, CLR ning nullable turlar uchun maxsus qo'llab-quvvatlashini batafsil ko'rib chiqaylik.

Nullable Value Turlarni Boxing Qilish

Tasavvur qiling, mantiqan null ga o'rnatilgan Nullable<Int32> o'zgaruvchingiz bor. Agar bu o'zgaruvchi Object kutayotgan metodga uzatilsa, o'zgaruvchini boxing qilish kerak va boxlanmish Nullable<Int32> ga havola metodga uzatiladi. Bu ideal emas, chunki metod aslida null bo'lmagan qiymat qabul qilmoqda, garchi Nullable<Int32> o'zgaruvchisi mantiqan null qiymatini o'z ichiga olsa ham. Buni tuzatish uchun CLR nullable o'zgaruvchini boxing qilayotganda nullable turlar muhitda birinchi darajali fuqarolar ekanligini illyuziyasini saqlab qolish uchun maxsus kod bajaradi.

Xususan, CLR Nullable<T> instansiyasini boxing qilayotganda, uning null ekanligini tekshiradi va agar shunday bo'lsa, CLR hech narsani boxing qilmaydi va null qaytaradi. Agar nullable instansiya null bo'lmasa, CLR nullable instansiyadan qiymatni oladi va uni boxing qiladi. Boshqacha aytganda, qiymati 5 bo'lgan Nullable<Int32> qiymati 5 bo'lgan boxlanmish Int32 ga aylanadi. Quyidagi kod bu xatti-harakatni ko'rsatadi:

// Nullable<T> ni boxing qilish null yoki boxlanmish T hosil qiladi
Int32? n = null;
Object o = n;    // o null
Console.WriteLine("o is null={0}", o == null);   // "True"

n = 5;
o = n;           // o boxlanmish Int32 ga ishora qiladi
Console.WriteLine("o's type={0}", o.GetType());  // "System.Int32"

Nullable Value Turlarni Unboxing Qilish

CLR boxlanmish value tur T ni T ga yoki Nullable<T> ga unbox qilishga ruxsat beradi. Agar boxlanmish value turga havola null bo'lsa va siz uni Nullable<T> ga unbox qilayotgan bo'lsangiz, CLR Nullable<T> ning qiymatini null ga o'rnatadi. Quyidagi kod bu xatti-harakatni ko'rsatadi:

// Boxlanmish Int32 yaratish
Object o = 5;

// Uni Nullable<Int32> va Int32 ga unbox qilish
Int32? a = (Int32?) o;   // a = 5
Int32  b = (Int32)  o;   // b = 5

// null ga initsializatsiya qilingan havola yaratish
o = null;

// Uni Nullable<Int32> va Int32 ga "unbox" qilish
a = (Int32?) o;          // a = null
b = (Int32)  o;          // NullReferenceException

Nullable Value Tur Orqali GetType Chaqirish

Nullable<T> obyektida GetType chaqirilganda, CLR aslida yolg'on gapiradi va Nullable<T> turi o'rniga T turini qaytaradi. Quyidagi kod bu xatti-harakatni ko'rsatadi:

Int32? x = 5;

// Quyidagi satr "System.Int32" ni ko'rsatadi;
// "System.Nullable<Int32>" EMAS
Console.WriteLine(x.GetType());

Nullable Value Tur Orqali Interfeys Metodlarini Chaqirish

Quyidagi kodda men n ni, ya'ni Nullable<Int32> ni, interfeys turi bo'lgan IComparable<Int32> ga cast qilayapman. Biroq, Nullable<T> turi IComparable<Int32> interfeysini Int32 kabi amalga oshirmaydi. C# kompilyatori bu kodni kompilyatsiya qilishga ruxsat beradi va CLR ning tekshiruvchisi (verifier) bu kodni qonuniy deb hisoblaydi, bu sizga qulayroq sintaksisdan foydalanish imkonini beradi.

Int32? n = 5;
Int32 result = ((IComparable) n).CompareTo(5);   // Kompilyatsiya va ishlaydi
Console.WriteLine(result);                        // 0

Agar CLR bu maxsus qo'llab-quvvatlashni taqdim qilmasa, interfeys metodini nullable value turda chaqirish uchun kod yozish ancha noqulay bo'lar edi. Chaqiruvni amalga oshirishdan oldin unboxlanmish value turga interfeysga cast qilish kerak bo'lardi:

Int32 result = ((IComparable) (Int32) n).CompareTo(5);  // Noqulay
Muhim xulosa

Nullable value turlar CLR ning value turlar tizimiga juda muhim qo'shimchadir. Ular ma'lumotlar bazasi bilan ishlash, veb-xizmatlar bilan aloqa qilish va boshqa ko'plab senariylarda null holatni tabiiy ravishda ifodalash imkonini beradi. C# ning ? sintaksisi, ?? null-coalescing operatori va CLR ning maxsus boxing/unboxing qo'llab-quvvatlashi tufayli nullable turlar bilan ishlash qulay va intuitiv bo'ladi. O'z kodingizda nullable turlardan to'g'ri foydalaning — qiymat bo'lmasligi mumkin bo'lgan barcha holatlarda Nullable<T> ishlatishni ko'rib chiqing.