7-Bob: Konstantalar va Maydonlar

Konstantalar (const), maydonlar (fields), readonly va volatile modifikatorlari, static va instance maydonlarning CLR dagi ishlashi

Ushbu bobda turlarning ikki muhim a'zosi haqida gaplashamiz: konstantalar va maydonlar (fields). Konstantalar hech qachon o'zgarmaydigan belgilangan qiymatlarni ifodalaydi. Maydonlar esa tur bilan bog'langan ma'lumotlarni saqlash uchun ishlatiladi. Ushbu bobda siz konstantalar qanday aniqlanishi va kompilyator tomonidan qanday qayta ishlanishini, shuningdek maydonlarning har xil turlari va ularning CLR tomonidan qanday boshqarilishini o'rganasiz.

Konstantalar

Konstanta — bu hech qachon o'zgarmaydigan qiymatga ega bo'lgan belgi (symbol). Konstantani aniqlashda uning qiymati kompilyatsiya vaqtida ma'lum bo'lishi kerak. Kompilyator konstanta qiymatini assembliyning metama'lumotlariga (metadata) saqlaydi. Bu shuni anglatadiki, konstantani faqat kompilyator primitiv turlar deb hisoblaydigan turlar uchun aniqlash mumkin. C# da quyidagi turlar konstanta sifatida ishlatilishi mumkin: Boolean, Char, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Single, Double, Decimal, va String. Shuningdek, C# reference turli konstantalarni ham aniqlash imkonini beradi, agar qiymat null bo'lsa.

Konstanta const kalit so'zi bilan aniqlanadi:

using System;

public sealed class SomeLibraryType {
    // ESLATMA: C# "new" operatoridan foydalanib konstanta aniqlashga
    // ruxsat bermaydi, chunki "new" runtime da hisoblashni talab qiladi.
    // Konstantalar faqat kompilyator primitiv turlarida ruxsat beriladi.
    public const Int32 MaxEntriesInList = 50;
}

Konstantalar Qanday Ishlaydi

Konstanta aniqlanganda kompilyator konstanta qiymatini maqsad assembliyaning metama'lumotlariga saqlaydi. Bu shuni anglatadiki, konstantalar faqat kompilyator primitiv turlaridan birida aniqlanishi mumkin. CLR da konstanta uchun xotirada joy ajratilmaydi, chunki konstanta qiymat to'g'ridan-to'g'ri IL kodiga joylashtiriladi (embed qilinadi).

Keling, buni batafsil tushuntiraylik. Siz kodda konstantaga murojaat qilganingizda, kompilyator metama'lumotlardan konstanta belgilanmalarini qidiradi, so'ngra konstanta qiymatni to'g'ridan-to'g'ri chiqariladigan IL kodiga joylashtiradi. Quyidagi misolga qarang:

using System;

public sealed class SomeLibraryType {
    public const Int32 MaxEntriesInList = 50;
}

public sealed class Program {
    public static void Main() {
        Console.WriteLine(SomeLibraryType.MaxEntriesInList);
    }
}

Kompilyator Main metodi uchun IL kod yaratganida, u MaxEntriesInList qiymati 50 ekanligini ko'radi va 50 raqamini to'g'ridan-to'g'ri IL kodiga joylashtiradi. Aslida, kompilyator tugatgandan so'ng, SomeLibraryType klassiga umuman murojaat qilinmasligi mumkin. Agar SomeLibraryType alohida DLL da bo'lsa, bu DLL runtime da yuklanmasligi ham mumkin, chunki konstanta qiymat allaqachon IL kodiga joylashtirilgan.

Muhim

Konstanta qiymatlari to'g'ridan-to'g'ri kodga joylashtirilganligi sababli, konstantalar static a'zolar deb qaraladi, instance a'zolar emas. Konstantani aniqlash hech qachon xotirada joy ajratmaydi.

Keling, maxsus kodda bu qanday ishlashini ko'raylik:

using System;

public sealed class Program {
    public static void Main() {
        // Quyidagi satr chiqarilgan IL kodda shunchaki
        // 50 raqamiga aylanadi
        Console.WriteLine(SomeLibraryType.MaxEntriesInList);

        // Natijada hosil bo'lgan IL kodi taxminan quyidagicha:
        // IL_0000: ldc.i4.s 50
        // IL_0002: call void System.Console::WriteLine(int32)
    }
}
Maslahat

Konstanta aslida hech qanday xotirani band qilmaydi. Kompilyator konstanta qiymatini topib, uni to'g'ridan-to'g'ri IL kodiga joylashtiradi (inline qiladi). Shu sababli runtime da konstanta uchun xotira ajratilmaydi va konstantaning manzilini olish yoki uni reference orqali uzatish mumkin emas.

Konstantalarning Versiyalash Muammosi

Konstantalar haqida tushunish kerak bo'lgan eng muhim narsa shundaki, konstanta qiymatlar versiyalash (versioning) muammosiga olib kelishi mumkin. Keling, buni misol bilan ko'rib chiqaylik.

Tasavvur qiling, sizda ikki alohida assembly bor:

// DLL Assembly (SomeLibrary.dll)
public sealed class SomeLibraryType {
    public const Int32 MaxEntriesInList = 50;
}
// EXE Assembly (Program.exe) - SomeLibrary.dll ga murojaat qiladi
public sealed class Program {
    public static void Main() {
        Console.WriteLine(SomeLibraryType.MaxEntriesInList);
    }
}

Endi Program.exe ni kompilyatsiya qilganingizda, kompilyator MaxEntriesInList ning konstanta 50 ekanligini ko'radi va 50 qiymatini Program.exe ning IL kodiga to'g'ridan-to'g'ri joylashtiradi. Endi ish vaqtida SomeLibrary.dll umuman yuklanishi shart emas — 50 raqami allaqachon Program.exe ichida.

Endi tasavvur qiling, SomeLibrary.dll ishlab chiquvchisi MaxEntriesInList ni 1000 ga o'zgartiradi va faqat DLL ni qayta kompilyatsiya qiladi. Program.exe qayta kompilyatsiya qilinmaydi. Bu holda Program.exe ishlaganda u hali ham 50 ni ko'rsatadi, 1000 emas!

Diqqat: Versiyalash muammosi

Agar siz konstanta qiymat kelajakda o'zgarishi mumkin deb hisoblasangiz, const o'rniga static readonly maydon ishlatishingiz kerak. const faqat hech qachon o'zgarmaydigan qiymatlar uchun to'g'ri keladi — masalan, Math.PI, Int32.MaxValue va shu kabi matematik yoki tizimiy konstantalar.

Keling, yuqoridagi muammoni static readonly bilan qanday hal qilish mumkinligini ko'raylik:

// DLL Assembly (SomeLibrary.dll)
public sealed class SomeLibraryType {
    // static readonly ishlatiladi — qiymat runtime da o'qiladi
    public static readonly Int32 MaxEntriesInList = 50;
}

Endi Program.exe kompilyatsiya qilinganida, kompilyator MaxEntriesInList static readonly maydon ekanligini ko'radi va qiymatni inline qilmaydi. Buning o'rniga, runtime da SomeLibrary.dll yuklanadi va maydon qiymatini xotiradan o'qiydi. Shu tarzda agar DLL dagi qiymat o'zgartirilsa va qayta kompilyatsiya qilinsa, Program.exe avtomatik ravishda yangi qiymatni ko'radi.

Quyida const va static readonly orasidagi farqlarni jadvalda ko'rishingiz mumkin:

Xususiyatconststatic readonly
Qiymat qachon aniqlanadiKompilyatsiya vaqtidaRuntime da (turni yuklashda)
IL kodga joylashtiriladiHa (inline)Yo'q (xotiradan o'qiladi)
Xotira ajratiladiYo'qHa
Ruxsat berilgan turlarFaqat primitiv turlar va string, yoki nullIstalgan tur
VersiyalashMuammo bor (inline sababli)Muammo yo'q
Ishlash tezligiTezroq (inline)Biroz sekinroq (xotiradan o'qish)

Xulosa qilib aytganda, const dan faqat qiymat hech qachon o'zgarmaydigan hollarda foydalaning. Boshqa barcha holatlarda static readonly maydonlardan foydalaning.

Maydonlar (Fields)

Maydon (field) — bu qiymat turining instansiyasini yoki reference tur obyektiga havolani saqlaydigan ma'lumot a'zosi. Quyidagi jadvalda maydonlarga qo'llanishi mumkin bo'lgan CLR atamalarining modifikatorlari ko'rsatilgan.

CLR atamasiC# modifikatoriTavsif
StaticstaticMaydon turning bir qismi, aniq instansiyaning emas
Instance(standart)Maydon turning aniq instansiyasi bilan bog'langan
InitOnlyreadonlyMaydon faqat konstruktor metodida yozilishi mumkin
VolatilevolatileMaydonga murojaat qiladigan kod kompilyator optimizatsiya qilinmasligi kerak

Maydon Modifikatorlari

CLR faqat static va instance maydonlarni qo'llab-quvvatlaydi. Maydon sifatida ishlatiladigan har bir modifikatorni batafsil ko'rib chiqamiz.

C# da maydon quyidagi modifikatorlar bilan aniqlanishi mumkin:

  • static — maydon turning o'ziga tegishli, instansiyaga emas
  • readonly — maydon faqat konstruktorda tayinlanishi mumkin
  • volatile — kompilyator va CLR bu maydonga murojaat tartibini optimizatsiya qilmasligi kerak

Quyidagi misol turli maydon turlarini ko'rsatadi:

public sealed class SomeType {
    // Static maydon: turga tegishli
    public static Int32 s_count = 0;

    // Instance maydon: har bir instansiyaga tegishli
    public Int32 m_value;

    // Static readonly maydon: faqat statik konstruktorda tayinlanadi
    public static readonly Int32 s_maxCount = 100;

    // Instance readonly maydon: faqat instansiya konstruktorida tayinlanadi
    public readonly String m_name;

    // Volatile maydon: ko'p oqimli muhitda ishlatiladi
    public volatile Int32 m_flag;
}

Static Maydonlar

CLR da turni birinchi marta ushbu tur uchun kodga murojaat qiladigan AppDomain ga yuklaganida, statik maydonlar uchun dynamic xotira ajratiladi. Bu xotira tur yuklanishi bilan bog'langan bo'lib, turning har bir instansiyasi bilan emas. Turga statik maydonlar soni qancha ko'p bo'lsa, tur yuklanishida shuncha ko'p xotira ajratiladi. Ammo har bir maydon uchun faqat bitta nusxa mavjud — ya'ni turning barcha instansiyalari statik maydonni baham ko'radi.

public sealed class SomeType {
    // Bu maydon SomeType yuklanganda bir marta yaratiladi
    public static Int32 s_instanceCount = 0;

    public SomeType() {
        // Har bir yangi instansiya yaratilganda hisoblagichni oshirish
        s_instanceCount++;
    }
}

// Foydalanish
SomeType a = new SomeType();
SomeType b = new SomeType();
Console.WriteLine(SomeType.s_instanceCount); // "2" ko'rsatadi
Nomlash kelishuvlari

Microsoft C# kodlash standartlari bo'yicha statik maydonlar s_ prefiksi bilan, instansiya maydonlar esa m_ prefiksi bilan nomlanishi tavsiya etiladi. Bu kodda maydon turini tezda aniqlash imkonini beradi.

Instance Maydonlar

Instance (nusxa) maydonlari turning har bir instansiyasi uchun alohida xotirada saqlanadi. Reference turlar uchun instance maydonlar managed heapda — obyektning xotira bloqi ichida joylashtiriladi. Value turlar uchun esa instance maydonlar stek da yoki obyekt ichida (agar value tur reference tur ichida joylashgan bo'lsa) saqlanadi.

public sealed class Employee {
    // Har bir Employee instansiyasi o'zining m_Name va m_Age ga ega
    private String m_Name;
    private Int32  m_Age;

    public Employee(String name, Int32 age) {
        m_Name = name;
        m_Age  = age;
    }
}

// Har bir instansiya mustaqil
Employee e1 = new Employee("Ali", 25);
Employee e2 = new Employee("Vali", 30);
// e1 va e2 bir-biridan mustaqil m_Name va m_Age ga ega

CLR turning har bir instansiyasi uchun barcha instance maydonlariga xotira ajratadi. Instance maydon turga emas, balki aniq bir instansiyaga bog'langan.

Readonly Maydonlar

readonly kalit so'zi bilan belgilangan maydon faqat konstruktor metodida tayinlanishi (yozilishi) mumkin. Kompilyator va CLR tekshirish (verification) vositasi konstruktordan tashqarida readonly maydonga yozishga uringan har qanday kodni aniqlaydi va xatolik chiqaradi.

readonly maydonlar static yoki instance bo'lishi mumkin:

public sealed class SomeType {
    // Static readonly maydon: statik konstruktorda tayinlanishi kerak
    public static readonly Random s_random = new Random();

    // Instance readonly maydon: instansiya konstruktorida tayinlanishi kerak
    public readonly String m_name;

    public SomeType(String name) {
        // readonly maydon konstruktorda tayinlanishi mumkin
        m_name = name;
    }

    public String TryToChange() {
        // Quyidagi satr KOMPILYATSIYA XATOSI beradi:
        // m_name = "boshqa"; // Xato! readonly maydonga yozish mumkin emas
        return m_name;
    }
}
Muhim: readonly va const orasidagi farq

const maydon kompilyatsiya vaqtida hisoblanadi va IL kodga to'g'ridan-to'g'ri joylashtiriladi. readonly maydon esa runtime da hisoblanadi va xotirada saqlanadi. Bu ikki muhim farqga olib keladi:

  • const faqat primitiv turlar va string uchun ishlatilishi mumkin; readonly esa istalgan tur uchun
  • const versiyalash muammosiga olib keladi; readonly esa bunday muammosi yo'q

Quyida const va readonly farqini ko'rsatuvchi aniqroq misol:

public sealed class SomeType {
    // const: kompilyatsiya vaqtida inline qilinadi
    public const Int32 ConstField = 100;

    // static readonly: runtime da qiymat xotiradan o'qiladi
    public static readonly Int32 StaticReadonlyField = 100;
}

Endi boshqa assembliyadan bu ikki maydonga murojaat qilsak:

// Boshqa assembliyada:
Console.WriteLine(SomeType.ConstField);
// IL kodga 100 soni to'g'ridan-to'g'ri joylashtiriladi

Console.WriteLine(SomeType.StaticReadonlyField);
// Runtime da SomeType yuklanadi va maydon qiymati xotiradan o'qiladi
Diqqat: readonly reference tur maydonlari

Agar maydon reference tur bo'lsa va readonly deb belgilangan bo'lsa, o'zgarmas narsa havoladir, maydon ko'rsatayotgan obyekt emas. Quyidagi kod buni ko'rsatadi.

public sealed class AType {
    // InvalidChars doimo bitta massiv obyektiga ishora qiladi
    public static readonly Char[] InvalidChars = new Char[] { 'A', 'B', 'C' };
}

public sealed class AnotherType {
    public static void M() {
        // Quyidagi satrlar QONUNIY — massiv elementlarini o'zgartirish mumkin
        AType.InvalidChars[0] = 'X';
        AType.InvalidChars[1] = 'Y';
        AType.InvalidChars[2] = 'Z';

        // Quyidagi satr KOMPILYATSIYA XATOSI — havolani o'zgartirish mumkin emas
        // AType.InvalidChars = new Char[] { 'X', 'Y', 'Z' };
    }
}

Bu misolda InvalidChars readonly bo'lgani uchun havola (qaysi massivga ishora qilishi) o'zgartirib bo'lmaydi. Lekin massiv ichidagi elementlar erkin o'zgartirilishi mumkin. Bu readonly ning muhim xususiyati bo'lib, ko'plab dasturchilarni chalg'itishi mumkin.

Volatile Maydonlar

CLR ning kompyuter xotira modelida (memory model) ko'ra, ba'zi hollarda kompilyator va protsessor xotiraga murojaat tartibini o'zgartirishi (reorder) mumkin. Bu bitta oqimli dasturlarda muammo emas, lekin ko'p oqimli (multithreaded) muhitda kutilmagan natijalarga olib kelishi mumkin.

volatile kalit so'zi bilan belgilangan maydonga murojaat qilganingizda, C# kompilyator va CLR ga ushbu maydonga murojaat tartibini optimizatsiya qilmaslik haqida ko'rsatma beradi. Aniqrog'i:

  • volatile maydondan o'qish — volatile read deb ataladi. Volatile o'qish "acquiring semantics" ga ega: maydonga murojaat undan keyingi xotira murojaat(lar)idan oldin bo'lishi kafolatlangan.
  • volatile maydonga yozish — volatile write deb ataladi. Volatile yozish "releasing semantics" ga ega: maydonga yozish undan oldingi xotira murojaat(lar)idan keyin bo'lishi kafolatlangan.
public sealed class ThreadSharedData {
    // Ko'p oqimlardan murojaat qilinadi
    private volatile Boolean m_flag = false;
    private Int32 m_value = 0;

    // Bir oqim tomonidan chaqiriladi
    public void Thread1() {
        m_value = 5;       // 1-qadam
        m_flag = true;     // 2-qadam (volatile yozish)
        // volatile yozish kafolatlaydi: 1-qadam 2-qadamdan OLDIN bajariladi
    }

    // Boshqa oqim tomonidan chaqiriladi
    public void Thread2() {
        if (m_flag) {      // 3-qadam (volatile o'qish)
            // volatile o'qish kafolatlaydi: m_value ni o'qish
            // m_flag o'qilganidan KEYIN bajariladi
            Console.WriteLine(m_value); // 5 ko'rsatadi (0 emas)
        }
    }
}
Maslahat

C# kompilyatori faqat quyidagi turli maydonlarga volatile qo'yish imkonini beradi: reference turlar, Single, Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Char, va enum turlar (Int32 asosidagi). volatile ni Int64, UInt64, Double yoki boshqa value turlarga qo'yish mumkin emas, chunki bu turlar 32-bitli platformalarda atomik ravishda o'qilishi/yozilishi kafolatlanmaydi.

volatile o'rniga System.Threading.Volatile klassining Read va Write statik metodlaridan ham foydalanish mumkin. Bu metodlar istalgan turdagi maydonga volatile semantikani qo'llash imkonini beradi:

// Int64 maydonga volatile semantika qo'llash
private Int64 m_amount;

// Volatile o'qish
Int64 currentAmount = System.Threading.Volatile.Read(ref m_amount);

// Volatile yozish
System.Threading.Volatile.Write(ref m_amount, 123);

Maydonlarni Inline Initsializatsiya Qilish

Siz allaqachon ko'rganingizdek, oldingi misollarda ko'plab maydonlar inline tarzda initsializatsiya qilingan, ya'ni maydon e'lon qilinganda darhol qiymat berilgan. C# sizga bu qulay inline initsializatsiya sintaksisidan foydalanib klassning konstantalari va read/write hamda readonly maydonlarini initsializatsiya qilish imkonini beradi.

public sealed class SomeType {
    public static readonly Random s_random = new Random();
    private static Int32 s_numberOfWrites = 0;
    public readonly String m_name = "Unnamed";
    private DateTime m_createdAt = DateTime.Now;
}

8-bob "Metodlar" da ko'rib chiqilganidek, C# maydonni inline initsializatsiya qilishni konstruktorda maydonga qiymat berish uchun qisqartma (shorthand) sifatida ko'radi. Boshqacha aytganda, yuqoridagi kod aslida quyidagiga teng:

public sealed class SomeType {
    public static readonly Random s_random;
    private static Int32 s_numberOfWrites;
    public readonly String m_name;
    private DateTime m_createdAt;

    // Statik konstruktor
    static SomeType() {
        s_random = new Random();
        s_numberOfWrites = 0;
    }

    // Instansiya konstruktori
    public SomeType() {
        m_name = "Unnamed";
        m_createdAt = DateTime.Now;
    }
}
Maslahat: Ishlash haqida eslatma

C# da inline initsializatsiya yoki konstruktordagi tayinlash sintaksisi o'rtasida ba'zi ishlash (performance) masalalari bor. Agar turda bir nechta konstruktor bo'lsa va har bir maydon inline tarzda initsializatsiya qilingan bo'lsa, kompilyator har bir konstruktor boshiga initsializatsiya kodini joylashtiradi. Bu kod hajmini oshirishi mumkin. Bunday hollarda inline initsializatsiyani olib tashlab, bitta "umumiy" konstruktor yaratib, boshqa konstruktorlarni this() orqali ushbu konstruktorni chaqirishga yo'naltirish samaraliroq bo'lishi mumkin. Bu masala 8-bobda batafsil muhokama qilinadi.

Quyida bir nechta konstruktor mavjud bo'lganda inline initsializatsiyaning potensial muammosini ko'rsatuvchi misol:

// Inline initsializatsiya — har bir konstruktor uchun kod takrorlanadi
internal sealed class SomeType {
    private Int32  m_x = 5;
    private String m_s = "Hi there";
    private Double m_d = 3.14159;
    private Byte   m_b;

    // Har bir konstruktor m_x, m_s, m_d initsializatsiya kodini o'z ichiga oladi
    public SomeType()            { ... }
    public SomeType(Int32 x)     { ... }
    public SomeType(String s)    { ...; m_d = 10; }
}
// Yaxshiroq yondashuv — umumiy konstruktor orqali
internal sealed class SomeType {
    // Inline initsializatsiya YO'Q
    private Int32  m_x;
    private String m_s;
    private Double m_d;
    private Byte   m_b;

    // Bu konstruktor barcha maydonlarga boshlang'ich qiymat beradi
    public SomeType() {
        m_x = 5;
        m_s = "Hi there";
        m_d = 3.14159;
        m_b = 0xff;
    }

    // Boshqa konstruktorlar umumiy konstruktorni this() orqali chaqiradi
    public SomeType(Int32 x) : this() {
        m_x = x;
    }

    public SomeType(String s) : this() {
        m_s = s;
    }

    public SomeType(Int32 x, String s) : this() {
        m_x = x;
        m_s = s;
    }
}
Xulosa

Ushbu bobda siz C# dagi konstantalar va maydonlar haqida o'rgandingiz. Asosiy xulosalar:

  • Konstantalar (const) kompilyatsiya vaqtida hisoblanadi va IL kodga to'g'ridan-to'g'ri joylashtiriladi. Ular faqat primitiv turlar va string uchun ishlatilishi mumkin. Versiyalash muammosiga olib kelishi mumkin.
  • static readonly maydonlar — runtime da hisoblanadigan, o'zgarmas qiymatlar. Konstantalarga alternativa bo'lib, versiyalash muammosini hal qiladi.
  • Instance maydonlar — har bir obyekt uchun alohida xotirada saqlanadi.
  • readonly maydonlar — faqat konstruktorda tayinlanishi mumkin. Reference tur maydonlari uchun readonly havolani himoyalaydi, lekin obyektning o'zini emas.
  • volatile maydonlar — ko'p oqimli muhitda xotiraga murojaat tartibini to'g'ri saqlash uchun ishlatiladi.