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.
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)
}
}
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!
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:
| Xususiyat | const | static readonly |
|---|---|---|
| Qiymat qachon aniqlanadi | Kompilyatsiya vaqtida | Runtime da (turni yuklashda) |
| IL kodga joylashtiriladi | Ha (inline) | Yo'q (xotiradan o'qiladi) |
| Xotira ajratiladi | Yo'q | Ha |
| Ruxsat berilgan turlar | Faqat primitiv turlar va string, yoki null | Istalgan tur |
| Versiyalash | Muammo bor (inline sababli) | Muammo yo'q |
| Ishlash tezligi | Tezroq (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 atamasi | C# modifikatori | Tavsif |
|---|---|---|
| Static | static | Maydon turning bir qismi, aniq instansiyaning emas |
| Instance | (standart) | Maydon turning aniq instansiyasi bilan bog'langan |
| InitOnly | readonly | Maydon faqat konstruktor metodida yozilishi mumkin |
| Volatile | volatile | Maydonga 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 emasreadonly— maydon faqat konstruktorda tayinlanishi mumkinvolatile— 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
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;
}
}
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:
constfaqat primitiv turlar vastringuchun ishlatilishi mumkin;readonlyesa istalgan tur uchunconstversiyalash muammosiga olib keladi;readonlyesa 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
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:
volatilemaydondan 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.volatilemaydonga 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)
}
}
}
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;
}
}
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;
}
}
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 vastringuchun ishlatilishi mumkin. Versiyalash muammosiga olib kelishi mumkin. static readonlymaydonlar — runtime da hisoblanadigan, o'zgarmas qiymatlar. Konstantalarga alternativa bo'lib, versiyalash muammosini hal qiladi.- Instance maydonlar — har bir obyekt uchun alohida xotirada saqlanadi.
readonlymaydonlar — faqat konstruktorda tayinlanishi mumkin. Reference tur maydonlari uchunreadonlyhavolani himoyalaydi, lekin obyektning o'zini emas.volatilemaydonlar — ko'p oqimli muhitda xotiraga murojaat tartibini to'g'ri saqlash uchun ishlatiladi.