6-Bob: Tur va A'zo Asoslari

Tur a'zolarining turlari, tur ko'rinuvchanligi, a'zo foydalanuvchanligi, statik va partial klasslar, komponentlar, polimorfizm va versiyalash

Ushbu bobda men turlar va ularning a'zolari haqida asosiy tushunchalarni muhokama qilaman. Xususan, turda mavjud bo'lishi mumkin bo'lgan a'zolarning har xil turlari, tur ko'rinuvchanligi (type visibility) va a'zo foydalanuvchanligi (member accessibility) haqida gaplashamiz. Shuningdek, statik klasslar, partial (qisman) turlar, komponentlar, polimorfizm va versiyalash mavzulari ham ko'rib chiqiladi. Bob oxirida CLR virtual metodlarni qanday chaqirishi va turlarni versiyalashda virtual metodlar bilan qanday ishlash kerakligi haqida batafsil misollar keltiriladi.

Turlarning Turli Xil A'zolari

Tur da quyidagi a'zo turlari mavjud bo'lishi mumkin:

  • Konstanta (Constant) — kompilyatsiya vaqtida hech qachon o'zgarmaydigan qiymat sifatida identifikatsiyalangan belgi. Konstantalar har doim turning statik a'zosi hisoblanadi.
  • Maydon (Field) — value yoki reference turning instansiyasini saqlaydigan ma'lumot a'zosi. Maydon instansiyaga tegishli yoki turga tegishli (statik) bo'lishi mumkin.
  • Instansiya konstruktori (Instance constructor) — turning yangi instansiyasi yaratilganda instansiya maydonlarini to'g'ri holatga keltirish uchun ishlatiladigan metod.
  • Tur konstruktori (Type constructor) — turning statik maydonlarini to'g'ri holatga keltirish uchun ishlatiladigan metod.
  • Metod (Method) — turning holatini o'zgartiradigan va/yoki so'rovchi (query) sifatida ishlaydigan funksiya. Metodlar instansiyaga yoki turga (statik) tegishli bo'lishi mumkin.
  • Operator overloading (Operator overload) — biror operator turning instansiyasiga qo'llanilganda nima sodir bo'lishini belgilaydigan metod.
  • Konversiya operatori (Conversion operator) — bir turdan boshqasiga qanday yashirin yoki aniq konvertatsiya qilishni belgilaydigan metod.
  • Xususiyat (Property) — a'zolar foydalanuvchilarga turning holatini o'qish yoki o'zgartirish imkonini beruvchi mexanizm. Xususiyatlar instansiyaga yoki turga (statik) tegishli bo'lishi mumkin.
  • Hodisa (Event) — tur va/yoki tur instansiyasi tomonidan bildirishnoma (notification) yuborishga imkon beruvchi mexanizm. Hodisalar instansiyaga yoki turga (statik) tegishli bo'lishi mumkin.
  • Ichki tur (Nested type) — boshqa tur ichida aniqlangan tur.

Quyidagi C# kodi barcha mumkin bo'lgan a'zolarning namunasini o'z ichiga olgan tur ta'rifini ko'rsatadi. Bu yerda ko'rsatilgan kod (ogohlantirishlar bilan) kompilyatsiya qilinadi, lekin bu siz odatda yarataydigan turga xos emas. Hozircha men faqat kompilyator bu turni va uning a'zolarini metadata ga qanday aylantirishi haqida tushuncha bermoqchiman. Keyingi boblarda men alohida a'zolarni muhokama qilaman.

using System;

public sealed class SomeType {

    // Ichki klass
    private class SomeNestedType { }                        // 2

    // Konstanta, faqat o'qish va statik o'qish/yozish maydoni
    private const    Int32  c_SomeConstant = 1;             // 3
    private readonly String m_SomeReadOnlyField = "2";      // 4
    private static   Int32  s_SomeReadWriteField = 3;       // 5

    // Tur konstruktori
    static SomeType() { }                                   // 6

    // Instansiya konstruktorlari
    public SomeType(Int32 x) { }                            // 7
    public SomeType() { }                                   // 8

    // Instansiya va statik metodlar
    private String InstanceMethod() { return null; }        // 9
    public static void Main() {}                            // 10

    // Instansiya xususiyati
    public Int32 SomeProp {                                 // 11
        get { return 0; }                                   // 12
        set { }                                             // 13
    }

    // Instansiya parametrli xususiyat (indekser)
    public Int32 this[String s] {                           // 14
        get { return 0; }                                   // 15
        set { }                                             // 16
    }

    // Instansiya hodisasi
    public event EventHandler SomeEvent;                    // 17
}

Agar siz yuqoridagi turni kompilyatsiya qilib, ILDasm.exe da metadata ni tekshirsangiz, manba kodida aniqlangan barcha a'zolar kompilyator tomonidan ba'zi metadata yaratishiga sabab bo'lganini ko'rasiz. Aslida, ba'zi a'zolar kompilyatorga qo'shimcha a'zolar va metadata yaratishga sabab bo'ladi. Masalan, 17-raqamli hodisa a'zosi kompilyatorga maydon, ikki metod va ba'zi qo'shimcha metadata yaratishga sabab bo'ladi. Hozir siz ko'rayotgan narsalarni to'liq tushunishingizni kutmayman, lekin keyingi boblarda har bir a'zo turini o'rganganingizda ushbu misolga qaytib, kompilyator tomonidan yaratilgan metadata ga qanday ta'sir qilishini ko'rishingizni tavsiya qilaman.

Tur Ko'rinuvchanligi (Type Visibility)

Fayl darajasida turni aniqlashda (boshqa tur ichiga joylashtirilgan turni emas), siz turning ko'rinuvchanligini public yoki internal sifatida belgilashingiz mumkin. public tur e'lon qilingan assembly ichidagi barcha kodga, shuningdek boshqa assemblylardagi barcha kodga ko'rinadi. internal tur faqat e'lon qilingan assembly ichidagi kodga ko'rinadi va boshqa assemblylarda yozilgan kodga ko'rinmaydi. Agar siz ko'rinuvchanlikni aniq belgilamasangiz, C# kompilyatori turni internal (ikkitadan ko'proq cheklovchi) sifatida belgilaydi. Quyida bir nechta misol keltirilgan.

using System;

// Quyidagi tur public ko'rinuvchanlikka ega va uni shu assemblydagi
// kod hamda boshqa assemblylardagi kod foydalanishi mumkin.
public class ThisIsAPublicType { ... }

// Quyidagi tur internal ko'rinuvchanlikka ega va uni faqat
// shu assemblydagi kod foydalanishi mumkin.
internal class ThisIsAnInternalType { ... }

// Quyidagi tur internal, chunki public/internal aniq ko'rsatilmagan.
class ThisIsAlsoAnInternalType { ... }
Maslahat

Yangi tur yaratishda internal ko'rinuvchanlikdan boshlash yaxshi amaliyot. Agar tur boshqa assemblylar tomonidan ishlatilishi kerak bo'lsa, aniq public qilib belgilang. Standart holda C# kompilyatori internal ni tanlaydi, bu esa xavfsizroq variantdir.

Do'st Assemblylar (Friend Assemblies)

Quyidagi senariyni tasavvur qiling: bir kompaniyada TeamA jamosi utility turlarini o'z ichiga olgan assembly yaratadi va bu turlarni boshqa jamoa — TeamB ishlatishini kutadi. Vaqt jadvallari, joylashuv yoki boshqa sabablarga ko'ra ikkala jamoa barcha turlarini bitta assemblyga yig'a olmaydi; buning o'rniga har bir jamoa o'z assemblysini yaratadi.

TeamB assemblysining TeamA turlaridan foydalanishi uchun TeamA barcha utility turlarini public qilib belgilashi kerak. Biroq, bu turlar har qanday assemblyda ommaviy ravishda ko'rinishini anglatadi; boshqa kompaniyadagi dasturchilar ham ommaviy utility turlardan foydalanadigan kod yozishi mumkin, bu esa istalmagan holat. Yaxshisi TeamA turlarini internal deb aniqlash, lekin TeamB ga ham foydalanish imkoniyatini berish. CLR va C# buni do'st assemblylar (friend assemblies) orqali qo'llab-quvvatlaydi. Bu do'st assembly xususiyati bir assemblydagi internal turlarga qarshi unit testlarni o'z ichiga olgan assembly yaratish uchun ham juda foydali.

Assembly qurilganda, u boshqa assemblylarni "do'st" deb ko'rsatishi mumkin. Buning uchun System.Runtime.CompilerServices nomlar fazosida aniqlangan InternalsVisibleTo atributi ishlatiladi. Bu atribut do'st assemblyning nomini va public kalitini belgilaydigan string parametrga ega (atributga uzatiladigan string versiya, madaniyat yoki protsessor arxitekturasini o'z ichiga olmasligi kerak). E'tibor bering, do'st assemblylar assemblyning internal turlariga, shuningdek bu turlarning internal a'zolariga murojaat qilishlari mumkin.

Quyida bitta assembly ikkita kuchli nomlangan assemblyni — "Wintellect" va "Microsoft" — do'st assemblylar sifatida belgilash misoli keltirilgan.

using System;
using System.Runtime.CompilerServices; // InternalsVisibleTo atributi uchun

// Shu assemblyning internal turlariga quyidagi ikki assemblydagi
// har qanday kod murojaat qilishi mumkin (versiya yoki madaniyatdan qat'i nazar):
[assembly:InternalsVisibleTo("Wintellect, PublicKey=12345678...90abcdef")]
[assembly:InternalsVisibleTo("Microsoft, PublicKey=b77a5c56...1934e089")]

internal sealed class SomeInternalType { ... }
internal sealed class AnotherInternalType { ... }

Yuqoridagi assemblyning internal turlariga do'st assemblydan murojaat qilish oddiy. Masalan, "12345678...90abcdef" public kalitli "Wintellect" do'st assemblysining oldingi assemblydagi SomeInternalType internal turiga qanday murojaat qilishi:

using System;

internal sealed class Foo {
    private static Object SomeMethod() {
        // Bu "Wintellect" assemblisi boshqa assemblyning
        // internal turini xuddi public turdek ishlatadi
        SomeInternalType sit = new SomeInternalType();
        return sit;
    }
}

Assemblydagi turlarning internal a'zolari do'st assemblylarga ochiq bo'lganligi sababli, tur a'zolaringiz va ularni qaysi assemblylarda do'stlaringiz deb e'lon qilganingiz haqida yaxshilab o'ylashingiz kerak. E'tibor bering, C# kompilyatori InternalsVisibleTo atributini o'z ichiga olmagan do'st assemblysini kompilyatsiya qilishda /out:<file> kompilyator kalitidan foydalanishni talab qiladi. Buning sababi kompilyator natija fayl nomini kompilyatsiya tugaguniga qadar aniqlamaydi. /out:<file> kompilyator kalitini talab qilish kompilyatsiya ishlashini sezilarli darajada yaxshilaydi.

Bundan tashqari, agar siz modulni (assemblyga qarama-qarshi) C# ning /t:module kaliti yordamida kompilyatsiya qilsangiz va bu modul do'st assemblyning bir qismi bo'lishi kerak bo'lsa, modulni C# kompilyatorining /moduleassemblyname:<string> kaliti yordamida kompilyatsiya qilishingiz kerak.

A'zo Foydalanuvchanligi (Member Accessibility)

Tur a'zosini (ichki turlar ham kiradi) aniqlashda siz a'zoning foydalanuvchanligini (accessibility) belgilashingiz mumkin. A'zoning foydalanuvchanligi qaysi a'zolarga referent koddan qonuniy ravishda murojaat qilish mumkinligini bildiradi. CLR mumkin bo'lgan foydalanuvchanlik modifikatorlari to'plamini aniqlaydi, lekin har bir dasturlash tili sintaksis va terminologiyani tanlaydi. Masalan, CLR "Assembly" terminidan foydalanib, a'zo bir xil assembly ichidagi har qanday kodga foydalanilishi mumkinligini bildirsa, C# da bu uchun internal kalit so'zi ishlatiladi.

Quyidagi jadvalda a'zolarga qo'llanishi mumkin bo'lgan oltita foydalanuvchanlik modifikatori ko'rsatilgan. Jadval satrlari eng cheklovchidan (Private) eng kam cheklovchigacha (Public) tartiblangan.

CLR TerminC# Kalit So'zTavsif
Private private A'zoga faqat e'lon qilingan turdagi metodlar yoki har qanday ichki turdagi metodlar murojaat qilishi mumkin.
Family protected A'zoga e'lon qilingan turdagi metodlar, har qanday ichki turdagi metodlar yoki assemblydan qat'i nazar uning hosila turlaridagi metodlar murojaat qilishi mumkin.
Family and Assembly (qo'llab-quvvatlanmaydi) A'zoga e'lon qilingan turdagi metodlar, har qanday ichki turdagi metodlar yoki bir xil assemblydagi har qanday hosila turlardagi metodlar murojaat qilishi mumkin.
Assembly internal A'zoga faqat e'lon qilingan assemblydagi metodlar murojaat qilishi mumkin.
Family or Assembly protected internal A'zoga har qanday ichki tur, har qanday hosila tur (assemblydan qat'i nazar) yoki e'lon qilingan assemblydagi har qanday metodlar murojaat qilishi mumkin.
Public public A'zoga har qanday assemblydagi barcha metodlar murojaat qilishi mumkin.

Albatta, har qanday a'zo foydalanuvchi bo'lishi uchun u ko'rinadigan turda aniqlangan bo'lishi kerak. Masalan, agar AssemblyA internal turni public metod bilan aniqlasa, AssemblyB dagi kod public metodni chaqira olmaydi, chunki internal tur AssemblyB ga ko'rinmaydi.

Kodni kompilyatsiya qilishda til kompilyatori kodning tur va a'zolarga to'g'ri murojaat qilishini tekshirish uchun javobgar. Agar kod noto'g'ri tur yoki a'zoga murojaat qilsa, kompilyator tegishli xato xabarini chiqarish uchun javobgardir. Bundan tashqari, just-in-time (JIT) kompilyatori IL kodini mashina ko'rsatmalariga kompilyatsiya qilishda maydon va metodlarga havolalar qonuniy ekanligini tekshiradi. Masalan, agar JIT kompilyatori private maydon yoki metodga noto'g'ri murojaat qilishga urinayotgan kodni aniqlasa, FieldAccessException yoki MethodAccessException ni tashlaydi.

IL kodini tekshirish murojaat qilingan a'zoning foydalanuvchanligi ish vaqtida to'g'ri hurmat qilinishini ta'minlaydi, hatto til kompilyatori foydalanuvchanlikni tekshirishni e'tiborsiz qoldirgan bo'lsa ham. Ehtimolroq holat shuki, til kompilyatori boshqa assemblydagi public a'zoga murojaat qiladigan kod kompilyatsiya qilgan; lekin ish vaqtida assemblyning yangi versiyasi yuklanadi va bu yangi versiyada public a'zo protected yoki private ga o'zgartirilgan.

C# da, agar siz a'zoning foydalanuvchanligini aniq e'lon qilmasangiz, kompilyator odatda (lekin har doim emas) private ni standart sifatida tanlaydi (eng cheklovchisi). CLR interfeys turining barcha a'zolari public bo'lishini talab qiladi. C# kompilyatori buni biladi va dasturchilarni interfeys a'zolari ustida aniq foydalanuvchanlikni ko'rsatishdan taqiqlaydi; kompilyator faqat barcha a'zolarni public qiladi.

Qo'shimcha ma'lumot

C# Til Spetsifikatsiyasidagi "Declared Accessibility" (E'lon qilingan foydalanuvchanlik) bo'limiga murojaat qiling. U turlar va a'zolarga qanday foydalanuvchanliklar qo'llanishi mumkinligi va e'lon qilingan kontekstga qarab C# qanday standart foydalanuvchanlikni tanlashi haqida to'liq qoidalar to'plamini o'z ichiga oladi.

Bundan tashqari, CLR Family and Assembly deb nomlangan foydalanuvchanlikni taklif qilishini payqagan bo'lsangiz kerak. C# bu foydalanuvchanlikni tilga kiritmagani sababi C# jamoasi bu foydalanuvchanlik asosan foydasiz deb qaror qilgan.

Hosila tur asosiy turda aniqlangan a'zoni qayta yozayotganda (override), C# kompilyatori asl a'zo va qayta yozuvchi a'zo bir xil foydalanuvchanlikka ega bo'lishini talab qiladi. Ya'ni, agar asosiy klassdagi a'zo protected bo'lsa, hosila klassdagi qayta yozuvchi a'zo ham protected bo'lishi kerak. Biroq, bu C# cheklovi, CLR cheklovi emas. CLR asosiy klassdan hosila bo'lganda a'zoning foydalanuvchanligini kamroq cheklovchi qilishga ruxsat beradi, lekin ko'proq cheklovchi qilishga ruxsat bermaydi. Masalan, klass asosiy klassida aniqlangan protected metodni qayta yozishi va qayta yozilgan metodni public (ko'proq foydalanuvchi) qilishi mumkin. Lekin klass asosiy klassida aniqlangan protected metodni qayta yozib, qayta yozilgan metodni private (kamroq foydalanuvchi) qilishi mumkin emas. Buning sababi, hosila klass foydalanuvchisi doim asosiy turga cast qilib, asosiy klass metodiga murojaat qilishi mumkin. Agar CLR hosila turning metodini kamroq foydalanuvchi qilishga ruxsat berganida, bajarib bo'lmaydigan da'vo qilgan bo'lar edi.

Statik Klasslar (Static Classes)

Ba'zi klasslar hech qachon instansiyalanishi mo'ljallanmagan, masalan, Console, Math, Environment va ThreadPool. Bu klasslar faqat static a'zolarga ega va aslida bu klasslar bir-biriga bog'liq a'zolarni guruhlash usuli sifatida mavjud. Masalan, Math klassi matematikaga oid amallarni bajaradigan bir to'plam metodlarni aniqlaydi. C# da static kalit so'zi yordamida instansiyalab bo'lmaydigan klasslarni aniqlash mumkin. Bu kalit so'z faqat klasslarga qo'llanishi mumkin, strukturalarga emas, chunki CLR doimo value turlarning instansiyalanishiga ruxsat beradi va buni to'xtatish yoki oldini olishning iloji yo'q.

Kompilyator static klassga ko'plab cheklovlarni qo'yadi:

  • Klass bevosita System.Object dan hosila bo'lishi kerak, chunki boshqa har qanday asosiy klassdan hosila bo'lish mantiqsiz, sababi meros faqat obyektlarga tegishli va static klassning instansiyasini yaratib bo'lmaydi.
  • Klass hech qanday interfeysni amalga oshirmasligi kerak, chunki interfeys metodlari faqat klassning instansiyasi orqali chaqiriladi.
  • Klass faqat static a'zolarni (maydonlar, metodlar, xususiyatlar va hodisalar) aniqlashi kerak. Har qanday instansiya a'zolari kompilyatorga xatolik beradi.
  • Klass maydon, metod parametri yoki lokal o'zgaruvchi sifatida ishlatilishi mumkin emas, chunki bularning barchasi instansiyaga ishora qiluvchi o'zgaruvchini ko'rsatadi va bu ruxsat etilmaydi. Agar kompilyator bunday foydalanishlarning biron birini aniqlasa, xatolik beradi.

Quyida ba'zi static a'zolarni aniqlagan static klass misoli keltirilgan; bu kod kompilyatsiya qilinadi (ogohlantirish bilan) lekin hech qanday qiziqarli ish qilmaydi.

using System;

public static class AStaticClass {
    public static void AStaticMethod() { }

    public static String AStaticProperty {
        get { return s_AStaticField; }
        set { s_AStaticField = value; }
    }

    private static String s_AStaticField;

    public static event EventHandler AStaticEvent;
}

Agar siz yuqoridagi kodni kutubxona (DLL) assemblyga kompilyatsiya qilib, natijani ILDasm.exe da ko'rsangiz, static kalit so'zi kompilyatorga klassni ham abstract va ham sealed qilishiga olib kelishini ko'rasiz. Bundan tashqari, kompilyator turga instansiya konstruktori (.ctor) metodini chiqarmaydi.

Partial (Qisman) Klasslar, Strukturalar va Interfeyslar

Ushbu bo'limda men partial (qisman) klasslar, strukturalar va interfeyslarni muhokama qilaman. partial kalit so'zi C# kompilyatoriga bitta klass, struktura yoki interfeys ta'rifi uchun manba kodi bir yoki bir nechta manba kod fayllarini qamrab olishi mumkinligini bildiradi. E'tibor bering, kompilyator turning barcha partial (qismlarini) kompilyatsiya vaqtida birlashtiradi; CLR doimo to'liq tur ta'riflari bilan ishlaydi. Tur uchun manba kodini bir nechta fayllarga bo'lishning uchta asosiy sababi bor:

  • Manba kodni boshqarish (Source control) — Turning ta'rifi ko'p manba koddan iborat deylik va dasturchi uni manba kodni boshqarish tizimidan o'zgartirish uchun oladi. Boshqa hech bir dasturchi turni bir vaqtda o'zgartira olmaydi va keyinchalik birlashtirishsiz ham. partial kalit so'zi turni bir nechta manba kod fayllariga bo'lish imkonini beradi, ularning har birini alohida tekshirish mumkin, shunda bir nechta dasturchilar turni bir vaqtda tahrirlashi mumkin.
  • Klass, struktura yoki interfeysni bitta fayl ichida alohida mantiqiy birliklarga bo'lish — Men ba'zan bitta tur bir nechta xususiyatlarni taqdim etadi. Amalga oshirishimni soddalashtirish uchun men ba'zan bitta manba kod faylidagi partial turni bir necha marta e'lon qilaman. Keyin, har bir partial turda men bitta xususiyatni barcha maydonlari, metodlari, xususiyatlari, hodisalari va boshqalar bilan amalga oshiraman.
  • Kod generatorlari (Code spitters) — Microsoft Visual Studio da yangi loyiha yaratganingizda, ba'zi manba kod fayllari loyihaning bir qismi sifatida avtomatik yaratiladi. Visual Studio dizaynerlaridan foydalanganingizda, Visual Studio avtomatik ravishda manba kod fayllariga kod yozadi. partial kalit so'zi sizning kodingiz va dizayner tomonidan yaratilgan kodning alohida fayllarda saqlanishini ta'minlaydi, shunda siz tasodifan yaratilgan kodni tahrirlab, dizaynerlarning noto'g'ri ishlashiga sabab bo'lmaysiz.

partial kalit so'zi barcha fayllardagi turlarga qo'llaniladi. Fayllar birgalikda kompilyatsiya qilinganda, kompilyator kodni birlashtirib, natijadagi .exe yoki .dll assembly faylidagi bitta turni hosil qiladi. Ushbu bo'limning boshida aytilganidek, partial turlar xususiyati to'liq C# kompilyatori tomonidan amalga oshiriladi; CLR partial turlar haqida umuman hech narsa bilmaydi. Shu sababli, tur uchun barcha manba kod fayllari bir xil dasturlash tilidan foydalanishi kerak va ularning barchasi bitta kompilyatsiya birligi sifatida kompilyatsiya qilinishi kerak.

Komponentlar, Polimorfizm va Versiyalash

Obyektga yo'naltirilgan dasturlash (OOP) ko'p yillar davomida mavjud. 1970-yillarning oxiri/1980-yillarning boshlarida ilovalar ancha kichik bo'lgan va barcha kodni ilova ishlashi uchun bitta kompaniya yozgan. Albatta, operatsion tizimlar bor edi va ilovalar bu operatsion tizimlarning imkoniyatlaridan foydalangan, lekin bu operatsion tizimlar bugungi operatsion tizimlarga qaraganda juda kam xususiyatlarni taklif qilgan.

Bugungi kunda dasturiy ta'minot ancha murakkab va foydalanuvchilar ilovalardan GUIlar, menyu elementlari, sichqoncha kiritish, planshet kiritish, printer chiqishi, tarmoqlash va boshqa boy xususiyatlarni taklif qilishini talab qiladi. Shu sababli operatsion tizimlarimiz va ishlab chiqish platformalarimiz so'nggi yillarda sezilarli darajada o'sdi. Bundan tashqari, ilova dasturchilariga foydalanuvchilar kutgan tarzda ishlashi uchun zarur bo'lgan barcha kodni yozish endi mumkin emas yoki iqtisodiy jihatdan samarali emas. Bugungi kunda ilovalar turli kompaniyalar tomonidan ishlab chiqilgan koddan tashkil topgan. Bu kod obyektga yo'naltirilgan paradigmadan foydalanib birlashtiriladi.

Komponent dasturiy ta'minotni ishlab chiqish (Component Software Programming — CSP) OOP ni shu darajaga ko'tardi. Komponentning ba'zi xususiyatlari:

  • Komponent (.NET Framework da assembly) "nashr qilingan" tuyg'usiga ega.
  • Komponentning o'z identifikatsiyasi mavjud (nomi, versiyasi, madaniyati va public kaliti).
  • Komponent o'z identifikatsiyasini doimo saqlaydi (assemblydagi kod hech qachon boshqa assemblyga statik ravishda bog'lanmaydi; .NET doimo dinamik bog'lanishdan foydalanadi).
  • Komponent o'zi bog'liq bo'lgan komponentlarni aniq ko'rsatadi (reference metadata jadvallari).
  • Komponent o'z klasslarini va a'zolarini hujjatlashtirishi kerak. C# buni manba kodda Extensible Markup Language (XML) hujjatlashtirish va kompilyatorning /doc buyruq qatori kaliti orqali ta'minlaydi.
  • Komponent o'zi talab qiladigan xavfsizlik ruxsatlarini ko'rsatishi kerak. CLR ning kod kirish xavfsizligi (CAS) vositalari buni ta'minlaydi.
  • Komponent har qanday xizmatlar uchun o'zgarmaydigan interfeysni (obyekt modelini) nashr etadi. Xizmat — bu asl komponent bilan orqaga mos bo'lgan yangi versiya komponent. Odatda xizmat versiyasi xatolik tuzatishlarini, xavfsizlik yamoqlarini va ehtimol kichik xususiyat yaxshilanishlarini o'z ichiga oladi. Lekin xizmat yangi bog'liqliklar yoki qo'shimcha xavfsizlik ruxsatlarini talab qila olmaydi.

Oxirgi bandda aytilganidek, CSP ning katta qismi versiyalash bilan bog'liq. Komponentlar va komponentlar vaqt o'tishi bilan o'zgaradi va turli vaqt jadvallarida jo'natiladi. Versiyalash CSP uchun butunlay yangi murakkablik darajasini kiritadi.

.NET Framework da versiya raqami to'rt qismdan iborat: major qism, minor qism, build qism va revision qism. Masalan, versiya raqami 1.2.3.4 bo'lgan assemblining major qismi 1, minor qismi 2, build qismi 3 va revision qismi 4. Major/minor qismlar odatda assemblyning barqaror xususiyat to'plamini ifodalash uchun, build/revision qismlar esa assemblining xususiyat to'plamining xizmatini ifodalash uchun ishlatiladi.

Masalan, kompaniya 2.7.0.0 versiyali assemblyni jo'natadi deylik. Agar kompaniya keyinchalik bu komponentdagi xatolikni tuzatmoqchi bo'lsa, faqat build/revision qismlari o'zgargan yangi assembly yaratadi, masalan 2.7.1.34 versiyasi. Bu assembly asl komponent (2.7.0.0 versiyasi) bilan orqaga mos xizmat ekanligini ko'rsatadi.

Boshqa tomondan, agar kompaniya sezilarli o'zgarishlar kiritilgan yangi versiyani yaratmoqchi bo'lsa va bu asl assembly bilan orqaga mos bo'lishi mo'ljallanmagan bo'lsa, kompaniya aslida yangi komponent yaratyapti va yangi assembly asl komponentdan farqli major/minor qismli versiya raqamiga ega bo'lishi kerak (masalan, 3.0.0.0 versiyasi).

Muhim

Men hozirgina versiya raqamlarini qanday qo'llash kerakligi haqida gapirdim. Afsuski, CLR versiya raqamlarini shu tarzda boshqarmaydi. Bugungi kunda CLR versiya raqamini shaffof qiymat sifatida ko'radi va agar assembly boshqa assemblyning 1.2.3.4 versiyasiga bog'liq bo'lsa, CLR faqat 1.2.3.4 versiyasini yuklashga harakat qiladi (agar bog'lash yo'naltirishlar mavjud bo'lmasa).

Komponent Versiyalashga Ta'sir Qiluvchi C# Kalit So'zlari

C# turlar va/yoki tur a'zolariga qo'llanishi mumkin bo'lgan komponent versiyalashga ta'sir qiluvchi beshta kalit so'zni taklif qiladi. Bu kalit so'zlar CLR tomonidan komponent versiyalashni qo'llab-quvvatlash uchun taqdim etilgan xususiyatlarga to'g'ridan-to'g'ri mos keladi.

C# Kalit So'zTurMetod/Xususiyat/HodisaKonstanta/Maydon
abstract Turning instansiyasini yaratib bo'lmasligini bildiradi Hosila tur bu a'zoni qayta yozishi va amalga oshirishi kerak, shundan keyin hosila turning instansiyasi yaratilishi mumkin (ruxsat etilmagan)
virtual (ruxsat etilmagan) Bu a'zoni hosila tur qayta yozishi (override) mumkinligini bildiradi (ruxsat etilmagan)
override (ruxsat etilmagan) Hosila tur asosiy turning a'zosini qayta yozayotganini bildiradi (ruxsat etilmagan)
sealed Tur asosiy tur sifatida ishlatilishi mumkin emasligini bildiradi A'zoni hosila tur qayta yoza olmasligini bildiradi. Bu kalit so'z faqat virtual metodni qayta yozayotgan metodga qo'llanishi mumkin (ruxsat etilmagan)
new Ichki turga qo'llanilganda, a'zo asosiy klassda mavjud bo'lishi mumkin bo'lgan o'xshash a'zo bilan hech qanday aloqasi yo'qligini bildiradi Metod, xususiyat, hodisa, konstanta yoki maydonga qo'llanilganda, a'zo asosiy klassda mavjud bo'lishi mumkin bo'lgan o'xshash a'zo bilan hech qanday aloqasi yo'qligini bildiradi Maydon yoki konstantaga qo'llanilganda, a'zo asosiy klassda mavjud bo'lishi mumkin bo'lgan o'xshash a'zo bilan hech qanday aloqasi yo'qligini bildiradi

Men ushbu kalit so'zlarning qiymatini va ishlatilishini oldinda keltirilgan "Virtual Metodlar Bilan Turlarni Versiyalashda Ishlash" bo'limida batafsil ko'rsataman. Lekin versiyalash senariylariga o'tishdan oldin, keling CLR virtual metodlarni qanday chaqirishiga e'tibor qarataylik.

CLR Virtual Metodlarni, Xususiyatlarni va Hodisalarni Qanday Chaqiradi

Ushbu bo'limda men metodlarga e'tibor qarataman, lekin bu muhokama virtual xususiyatlar va hodisalarga ham tegishli. Xususiyatlar va hodisalar aslida metodlar sifatida amalga oshiriladi; bu tegishli boblarda ko'rsatiladi.

Metodlar turning biror operatsiyasini bajaradigan kodni ifodalaydi (statik metodlar) yoki turning instansiyasi ustida amal bajaradigan kodni ifodalaydi (instansiya metodlari). Barcha metodlar nomga, imzoga va qaytarish turiga ega (bu void bo'lishi mumkin). CLR turga bir xil nomli, lekin parametrlar to'plami yoki qaytarish turi boshqacha bo'lgan bir nechta metodlarni aniqlashga ruxsat beradi.

Quyida Employee klassi turli xil metodlarni ko'rsatadi.

internal class Employee {
    // Novirtual instansiya metod
    public          Int32    GetYearsEmployed()  { ... }

    // Virtual metod (virtual instansiyani nazarda tutadi)
    public virtual  String   GetProgressReport() { ... }

    // Statik metod
    public static   Employee Lookup(String name) { ... }
}

Kompilyator ushbu kodni kompilyatsiya qilganda, natijadagi assemblyning metod ta'rifi jadvalida uchta yozuv hosil qiladi. Har bir yozuvda metod instansiya, virtual yoki statik ekanligini ko'rsatadigan bayroqlar to'plami mavjud.

call va callvirt IL Ko'rsatmalari

Ushbu metodlarning birortasini chaqirish uchun kod yozilganda, chaqiruvchi kodni chiqarayotgan kompilyator metod ta'rifining bayroqlarini tekshirib, to'g'ri IL kodini yaratish uchun qanday chiqarish kerakligini aniqlaydi. CLR metodni chaqirish uchun ikkita IL ko'rsatmasini taklif qiladi:

  • call IL ko'rsatmasi statik, instansiya va virtual metodlarni chaqirish uchun ishlatilishi mumkin. call ko'rsatmasi statik metodni chaqirish uchun ishlatilganda, CLR chaqirishi kerak bo'lgan metodni belgilaydigan turni ko'rsatishingiz kerak. call ko'rsatmasi instansiya yoki virtual metodni chaqirish uchun ishlatilganda, obyektga ishora qiluvchi o'zgaruvchini ko'rsatishingiz kerak. call ko'rsatmasi bu o'zgaruvchi null emasligini faraz qiladi. Boshqacha aytganda, o'zgaruvchining o'zi turi CLR qaysi metodni chaqirishi kerakligini ko'rsatadigan turni bildiradi. Agar o'zgaruvchining turi metodni aniqlamasa, asosiy turlar tekshiriladi. call ko'rsatmasi tez-tez virtual metodni novirtual ravishda chaqirish uchun ishlatiladi.
  • callvirt IL ko'rsatmasi instansiya va virtual metodlarni chaqirish uchun ishlatilishi mumkin, statik metodlar uchun emas. callvirt ko'rsatmasi instansiya yoki virtual metodni chaqirish uchun ishlatilganda, obyektga ishora qiluvchi o'zgaruvchini ko'rsatishingiz kerak. callvirt IL ko'rsatmasi novirtual instansiya metodni chaqirish uchun ishlatilganda, o'zgaruvchining turi CLR chaqirishi kerak bo'lgan metodni belgilaydigan turni bildiradi. callvirt IL ko'rsatmasi virtual instansiya metodni chaqirish uchun ishlatilganda, CLR chaqiruv qilish uchun ishlatiladigan obyektning haqiqiy turini aniqlaydi va keyin metodni polimorf ravishda chaqiradi. Turni aniqlash uchun chaqiruv uchun ishlatiladigan o'zgaruvchi null bo'lmasligi kerak. Boshqacha aytganda, JIT kompilyatori o'zgaruvchi qiymati null emasligini tekshiradigan kodni yaratadi. Agar u null bo'lsa, callvirt ko'rsatmasi CLR ga NullReferenceException ni tashlashga sabab bo'ladi. Bu qo'shimcha tekshiruv callvirt IL ko'rsatmasi call ko'rsatmasiga qaraganda biroz sekinroq bajarilishini anglatadi. E'tibor bering, bu null tekshiruvi callvirt ko'rsatmasi novirtual instansiya metodni chaqirish uchun ishlatilganda ham bajariladi.

Keling, bularning barchasini birlashtirb, C# ushbu turli IL ko'rsatmalarni qanday ishlatishini ko'raylik.

using System;

public sealed class Program {
    public static void Main() {
        Console.WriteLine(); // Statik metodni chaqirish

        Object o = new Object();
        o.GetHashCode();     // Virtual instansiya metodni chaqirish
        o.GetType();         // Novirtual instansiya metodni chaqirish
    }
}

Agar siz yuqoridagi kodni kompilyatsiya qilib, natijadagi IL ni ko'rsangiz, quyidagini ko'rasiz.

.method public hidebysig static void Main() cil managed {
  .entrypoint
  // Code size 26 (0x1a)
  .maxstack 1
  .locals init (object o)
  IL_0000: call       void System.Console::WriteLine()
  IL_0005: newobj     instance void System.Object::.ctor()
  IL_000a: stloc.0
  IL_000b: ldloc.0
  IL_000c: callvirt   instance int32 System.Object::GetHashCode()
  IL_0011: pop
  IL_0012: ldloc.0
  IL_0013: callvirt   instance class System.Type System.Object::GetType()
  IL_0018: pop
  IL_0019: ret
} // end of method Program::Main

E'tibor bering, C# kompilyatori Console ning WriteLine metodini chaqirish uchun call IL ko'rsatmasidan foydalanadi. Bu kutilgan, chunki WriteLine statik metod. Keyin, GetHashCode ni chaqirish uchun callvirt IL ko'rsatmasi ishlatilishiga e'tibor bering. Bu ham kutilgan, chunki GetHashCode virtual metod. Nihoyat, C# kompilyatori GetType metodini chaqirish uchun ham callvirt IL ko'rsatmasidan foydalanishiga e'tibor bering. Bu kutilmagan, chunki GetType virtual metod emas. Biroq, bu ishlaydi, chunki CLR JIT-kompilyatsiya qilayotganda GetType virtual metod emasligini biladi va JIT-kompilyatsiya qilingan kod shunchaki GetType ni novirtual ravishda chaqiradi.

Albatta, savol tug'iladi: nima uchun C# kompilyatori oddiyroq call ko'rsatmasini chiqarmadi? Javob shuki, C# jamoasi JIT kompilyatori chaqiruv qilish uchun ishlatiladigan obyekt null emasligini tekshiradigan kodni yaratishi kerak deb qaror qildi. Bu novirtual instansiya metodlarini chaqirish kerakligidan biroz sekinroq bo'lishini anglatadi. Bu shuningdek quyidagi C# kodi NullReferenceException ni hosil qilishini bildiradi. Ba'zi boshqa dasturlash tillarida esa bu kod muammosiz ishlagan bo'lar edi.

using System;

public sealed class Program {
    public Int32 GetFive() { return 5; }
    public static void Main() {
        Program p = null;
        Int32 x = p.GetFive();  // C# da NullReferenceException tashlanadi
    }
}

Nazariy jihatdan, yuqoridagi kod muammosiz ishlashi kerak. Ha, p o'zgaruvchisi null, lekin novirtual metod (GetFive) chaqirilganda CLR faqat p ning ma'lumot turini bilishi kerak, ya'ni Program. Agar GetFive haqiqatan ham chaqirilsa, this argumentining qiymati null bo'lar edi. Argument metod ichida ishlatilmaganligi sababli, hech qanday NullReferenceException tashlanmasdi. Lekin C# kompilyatori call o'rniga callvirt ko'rsatmasini chiqarganligi sababli, yuqoridagi kod NullReferenceException bilan tugaydi.

Diqqat

Agar siz metodni novirtual deb aniqlagan bo'lsangiz, uni kelajakda hech qachon virtual ga o'zgartirmasligingiz kerak. Buning sababi, ba'zi kompilyatorlar novirtual metodni callvirt o'rniga call ko'rsatmasi yordamida chaqiradi. Agar metod novirtualdan virtualga o'zgarsa va mos yozuvchi kod qayta kompilyatsiya qilinmasa, virtual metod novirtual ravishda chaqiriladi va ilova oldindan aytib bo'lmaydigan natijalar beradi. Agar mos yozuvchi kod C# da yozilgan bo'lsa, bu muammo emas, chunki C# barcha instansiya metodlarini callvirt yordamida chaqiradi. Lekin bu mos yozuvchi kod boshqa dasturlash tilida yozilgan bo'lsa, muammo bo'lishi mumkin.

Ba'zan kompilyator virtual metodni chaqirish uchun callvirt o'rniga call ko'rsatmasini ishlatadi. Bu dastlab ajablanarli tuyulishi mumkin, lekin quyidagi kod nima uchun bunday bo'lishi kerakligini ko'rsatadi.

internal class SomeClass {
    // ToString Object asosiy klassida aniqlangan virtual metod.
    public override String ToString() {

        // Kompilyator 'call' IL ko'rsatmasini ishlatib
        // Object ning ToString ni novirtual ravishda chaqiradi.

        // Agar kompilyator 'call' o'rniga 'callvirt' ishlatganida,
        // bu metod o'zini rekursiv chaqirgan bo'lar edi
        // va stek to'lib ketguncha davom etardi.
        return base.ToString();
    }
}

base.ToString() chaqirilganda (virtual metod), C# kompilyatori asosiy turdagi ToString metodi novirtual ravishda chaqirilishini ta'minlash uchun call ko'rsatmasini chiqaradi. Agar ToString virtual ravishda chaqirilsa, chaqiruv oqimning steki to'lib ketguncha rekursiv ravishda davom etadi, bu esa albatta istalmagan holat.

Kompilyatorlar value tur tomonidan aniqlangan metodlarni chaqirishda call ko'rsatmasini ishlatishga moyil, chunki value turlar sealed (muhrlangan). Bu ularning virtual metodlarida ham polimorfizm bo'lmasligini anglatadi, bu esa chaqiruv ishlashini tezlashtiradi. Bundan tashqari, value tur instansiyasining tabiati uning hech qachon null bo'la olmasligini kafolatlaydi, shuning uchun NullReferenceException hech qachon tashlanmaydi. Nihoyat, agar value turning virtual metodini virtual ravishda chaqirmoqchi bo'lsangiz, CLR metod jadvaliga murojaat qilish uchun value tur instansiyasiga havola olishi kerak. Bu value turni boxing qilishni talab qiladi. Boxing heapga ko'proq yuk qo'yadi, garbage collection ni tez-tez ishga tushiradi va ishlashni yomonlashtiradi.

call yoki callvirt instansiya yoki virtual metodni chaqirish uchun ishlatilishidan qat'i nazar, bu metodlar doimo yashirin this argumentini metodning birinchi parametri sifatida qabul qiladi. this argumenti amal bajarilayotgan obyektga ishora qiladi.

Turni loyihalashda siz aniqlagan virtual metodlar sonini minimallashtirishga harakat qilishingiz kerak. Birinchidan, virtual metodni chaqirish novirtual metodni chaqirishga qaraganda sekinroq. Ikkinchidan, virtual metodlar JIT kompilyatori tomonidan inline qilinishi mumkin emas, bu esa ishlashni yanada yomonlashtiradi. Uchinchidan, virtual metodlar komponentlarni versiyalashni qiyinlashtiradi (keyingi bo'limda tasvirlanganidek). To'rtinchidan, asosiy turni aniqlashda qulay overloaded metodlar to'plamini taklif qilish odatiy holat. Agar siz bu metodlar polimorf bo'lishini istasangiz, eng yaxshi ish — eng murakkab metodni virtual qilib, barcha qulay overloaded metodlarni novirtual qoldirish. Bu ko'rsatmaga amal qilish komponentni versiyalash qobiliyatini hosila turlarga salbiy ta'sir qilmasdan yaxshilaydi. Quyida misol keltirilgan.

public class Set {
    private Int32 m_length = 0;

    // Bu qulay overload virtual emas
    public Int32 Find(Object value) {
        return Find(value, 0, m_length);
    }

    // Bu qulay overload virtual emas
    public Int32 Find(Object value, Int32 startIndex) {
        return Find(value, startIndex, m_length - startIndex);
    }

    // Eng boy funksionallikli metod virtual va qayta yozilishi mumkin
    public virtual Int32 Find(Object value, Int32 startIndex, Int32 endIndex) {
        // Haqiqiy amalga oshirish bu yerda...
    }

    // Boshqa metodlar bu yerda
}

Tur Ko'rinuvchanligi va A'zo Foydalanuvchanligidan Oqilona Foydalanish

.NET Framework da ilovalar turli kompaniyalar tomonidan ishlab chiqilgan bir nechta assemblylarda aniqlangan turlardan tashkil topgan. Bu shuni anglatadiki, dasturchi u foydalanayotgan komponentlar va bu komponentlar ichidagi turlarni kam nazorat qiladi. Dasturchi odatda manba kodiga kirish huquqiga ega emas (va ehtimol komponentni yaratish uchun qaysi dasturlash tili ishlatilganini ham bilmaydi) va komponentlar turli vaqt jadvallarida versiyalanadi. Bundan tashqari, polimorfizm va himoyalangan a'zolar tufayli asosiy klass dasturchisi hosila klass dasturchisi yozgan kodga ishonishi kerak. Va, albatta, hosila klass dasturchisi ham o'zi meros olinayotgan asosiy klassdagi kodga ishonishi kerak. Bular komponentlar va turlarni loyihalashda chindan ham o'ylab ko'rish kerak bo'lgan masalalarning faqat bir qismi.

Ushbu bo'limda men tur ko'rinuvchanligi va a'zo foydalanuvchanligini to'g'ri belgilash haqida bir nechta so'z aytmoqchiman.

Birinchidan, yangi turni aniqlashda kompilyatorlar standart holatda klassni sealed qilishi kerak, shunda klass asosiy klass sifatida ishlatilishi mumkin emas. Buning o'rniga ko'plab kompilyatorlar, jumladan C#, standart holatda muhrlanmagan (unsealed) klasslarga yo'l qo'yadi va dasturchi klassni aniq sealed kalit so'zi yordamida muhrlab belgilashi kerak. Men bugungi kompilyatorlar noto'g'ri standartni tanlaganini o'ylayman. Muhrlanmagan klassga qaraganda muhrlanganining uchta ustunligi bor:

  • Versiyalash — Klass dastlab muhrlanganida, u kelajakda muhrlanmaganiga mos kelishni buzmasdan o'zgartirilishi mumkin. Biroq, klass muhrlangandan keyin, uni kelajakda muhrlangan ga o'zgartirib bo'lmaydi, chunki bu barcha hosila klasslarni buzadi. Bundan tashqari, agar muhrlanmagan klass har qanday muhrlanmagan virtual metodlarni aniqlasa, virtual metod chaqiruvlarining tartibi yangi versiyalarda saqlanishi kerak yoki kelajakda hosila turlarni buzish xavfi mavjud.
  • Ishlash — Avvalgi bo'limda muhokama qilinganidek, virtual metodni chaqirish novirtual metodni chaqirishdek sezilmaydigan tezlikda bajarilmaydi, chunki CLR chaqirish uchun qaysi turni aniqlash kerakligini ish vaqtida obyektning turini tekshirib aniqlashi kerak. Biroq, agar JIT kompilyatori sealed turga chaqiruvni ko'rsa, u metodni novirtual ravishda chaqirish orqali samaraliroq kod yaratishi mumkin. Buni qila olish sababi u klass sealed ekanligini bilgani uchun hosila klass bo'lishi mumkin emasligini biladi. Masalan, quyidagi kodda JIT kompilyatori virtual ToString metodini novirtual ravishda chaqirishi mumkin.
using System;
public sealed class Point {
    private Int32 m_x, m_y;

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

    public override String ToString() {
        return String.Format("({0}, {1})", m_x, m_y);
    }

    public static void Main() {
        Point p = new Point(3, 4);

        // C# kompilyatori bu yerda callvirt ko'rsatmasini chiqaradi, lekin
        // JIT kompilyatori bu chaqiruvni optimizatsiya qiladi va
        // p ning turi Point (sealed klass) bo'lganligi sababli
        // ToString ni novirtual ravishda chaqiradigan kod yaratadi.
        Console.WriteLine(p.ToString());
    }
}
  • Xavfsizlik va oldindan aytilishi — Klass o'z holatini himoya qilishi va hech qachon o'zining buzilishiga yo'l qo'ymasligi kerak. Klass muhrlanmagan bo'lsa, hosila klass asosiy klass maydonlari ochiq yoki himoyalangan bo'lsa, ularga murojaat qilib, asosiy klass holatini manipulyatsiya qilishi mumkin. Bundan tashqari, virtual metod hosila klass tomonidan qayta yozilishi mumkin va hosila klass asosiy klass amalga oshirishini chaqirish yoki chaqirmaslikni hal qilishi mumkin. Metod, xususiyat yoki hodisani virtual qilish orqali asosiy klass o'z xatti-harakati va holati ustidan ba'zi nazoratni berib qo'yadi. Diqqat bilan o'ylanmasa, bu obyektning oldindan aytib bo'lmaydigan tarzda ishlashiga va potensial xavfsizlik teshiklarini ochishiga olib kelishi mumkin.

Muhrlangan klass bilan muammo shundaki, ba'zan dasturchilar mavjud turdan hosila klass yaratib, qo'shimcha maydonlar yoki holat ma'lumotlarini biriktirmoqchi bo'lishadi. CLR allaqachon qurilgan turni yordamchi metodlar yoki maydonlar bilan kengaytirish mexanizmini taklif qilmasa-da, siz C# ning kengaytma metodlarini (8-Bobda muhokama qilinadi) yordamida yordamchi metodlarni simulyatsiya qilishingiz mumkin va ConditionalWeakTable klassi (21-Bobda muhokama qilinadi) yordamida qo'shimcha holat ma'lumotlarini obyektga simulyatsiya qilib biriktrishingiz mumkin.

Men o'z klasslarimni aniqlashda amal qiladigan ko'rsatmalar:

  • Klassni aniqlashda men uni doimo aniq sealed deb belgilayman, agar men chindan ham hosila klasslar tomonidan ixtisoslashtirishga ruxsat beruvchi asosiy klass bo'lishini mo'ljallamagan bo'lsam. Yuqorida aytilganidek, bu C# va boshqa ko'plab kompilyatorlar bugungi standart holatiga qarama-qarshi. Men shuningdek standart holatda klassni internal qilaman, agar men klassni assemblyimdan tashqariga ommaviy ravishda ko'rsatmoqchi bo'lmasam. Yaxshiyamki, agar siz turning ko'rinuvchanligini aniq ko'rsatmasangiz, C# kompilyatori standart holatda internal ni tanlaydi. Agar men chindan ham boshqalar hosila qila oladigan klass aniqlash kerak deb hisoblasam, lekin ixtisoslashtirish istamasam, men yuqoridagi sealed virtual metodlar texnikasini qo'llab, klassni yopiq simulyatsiya qilaman.
  • Klass ichida men doimo ma'lumot maydonlarini private deb belgilayman va bu haqda hech qachon ikkilanmayman. Yaxshiyamki, C# standart holatda maydonlarni private qiladi. Men C# barcha maydonlar private bo'lishini va protected, internal, public kabi boshqa modifikatorlarni qo'llamaslikni talab qilganini afzal ko'rar edim. Holatni ochish eng oson yo'l bo'lib, obyektingizning oldindan aytib bo'lmaydigan tarzda ishlashiga, potensial xavfsizlik teshiklarini ochishga olib keladi. Hatto ba'zi maydonlarni internal deb e'lon qilganingizda ham, bitta assemblydagi barcha kodni kuzatish juda qiyin, ayniqsa bir nechta dasturchilar bir xil assemblyga kompilyatsiya qilinadigan kod yozayotgan bo'lsa.
  • Klass ichida men doimo metodlarim, xususiyatlarim va hodisalarimni private va novirtual deb aniqlashga harakat qilaman. Yaxshiyamki, C# ham standart holatda shunday qiladi. Albatta, men turdan funksionallikni ko'rsatish uchun metod, xususiyat yoki hodisani public qilaman. Men bu a'zolarning biron birini protected yoki internal qilishdan saqlanishga harakat qilaman, chunki bu mening turimni potensial zaiflikka duchor qiladi. Biroq, men a'zoni protected yoki internal qilishni virtual qilishga qaraganda afzal ko'raman, chunki virtual a'zo ko'p nazoratni berib qo'yadi va hosila klassning to'g'ri xatti-harakatiga katta bog'liqlik yaratadi.
  • Qadimgi OOP maqoli bor: ishlar juda murakkablashganida, ko'proq tur yarating. Biror algoritm amalga oshirishi murakkablashganda, men alohida funksionallik qismlarini inkapsulyatsiya qiluvchi yordamchi turlarni aniqlashga harakat qilaman. Agar men bu yordamchi turlarni faqat bitta uber-tur tomonidan ishlatilishi uchun aniqlayotgan bo'lsam, yordamchi turlarni uber-tur ichida ichki (nested) turlar sifatida aniqlashni afzal ko'raman. Bu scoping uchun va ichki turda kodni tashqi turda aniqlangan private a'zolarga murojaat qilish imkoniyatini beradi. Biroq, ommaviy ko'rinadigan ichki turlarni hech qachon aniqlamang (Code Analysis vositasi, FxCopCmd.exe, buni tavsiya qiladi).

Virtual Metodlar Bilan Turlarni Versiyalashda Ishlash

Avval aytilganidek, Komponent Dasturiy Ta'minot Dasturlash muhitida versiyalash juda muhim masala. Men 3-Bobda, "Umumiy va Kuchli Nomlangan Assemblylar" bo'limida bu versiyalash muammolarining ba'zilari haqida gapirgan edim va administrator ilovani u qurilgan va sinovdan o'tkazilgan assemblylar bilan bog'lashini ta'minlash uchun qanday ishlatishini muhokama qilgan edim. Biroq, boshqa versiyalash muammolari manba kodni moslik muammolarini keltirib chiqaradi. Masalan, agar tur asosiy tur sifatida ishlatiladigan bo'lsa, uning a'zolarini qo'shish yoki o'zgartirishda juda ehtiyot bo'lishingiz kerak. Keling, bir nechta misolni ko'rib chiqaylik.

CompanyA quyidagi turni loyihalagan — Phone.

namespace CompanyA {
    public class Phone {
        public void Dial() {
            Console.WriteLine("Phone.Dial");
            // Telefon raqamini terish ishlari bu yerda.
        }
    }
}

Endi CompanyB boshqa turni aniqlaydi — BetterPhone, bu CompanyA ning Phone turini asosiy tur sifatida ishlatadi.

namespace CompanyB {
    public class BetterPhone : CompanyA.Phone {
        public void Dial() {
            Console.WriteLine("BetterPhone.Dial");
            EstablishConnection();
            base.Dial();
        }

        protected virtual void EstablishConnection() {
            Console.WriteLine("BetterPhone.EstablishConnection");
            // Ulanishni o'rnatish ishlari bu yerda.
        }
    }
}

CompanyB kodni kompilyatsiya qilishga uringanida, C# kompilyatori quyidagi ogohlantirishni beradi.

warning CS0108: 'CompanyB.BetterPhone.Dial()' hides inherited member
'CompanyA.Phone.Dial()'.
Use the new keyword if hiding was intended.

Bu ogohlantirish dasturchiga BetterPhone da Phone da aniqlangan Dial metodini yashiradigan Dial metodi aniqlanayotganini bildiradi. Bu yangi metod Dial ning semantik ma'nosini o'zgartirishi mumkin (aslida CompanyA uni yarataganida belgilagan semantik ma'nodan).

Bu kompilyatorning potensial semantik nomutanosiblik haqida ogohlantirishining juda yaxshi xususiyati. Kompilyator shuningdek ogohlantirishni olib tashlash uchun BetterPhone klassidagi Dial ta'rifiga new kalit so'zini qo'shishni aytadi. Tuzatilgan BetterPhone klassi:

namespace CompanyB {
    public class BetterPhone : CompanyA.Phone {

        // Bu Dial metodi Phone ning Dial metodi bilan aloqasi yo'q.
        public new void Dial() {
            Console.WriteLine("BetterPhone.Dial");
            EstablishConnection();
            base.Dial();
        }

        protected virtual void EstablishConnection() {
            Console.WriteLine("BetterPhone.EstablishConnection");
            // Ulanishni o'rnatish ishlari bu yerda.
        }
    }
}

Bu vaqtda CompanyB o'z ilovasida BetterPhone.Dial ni ishlatishi mumkin. CompanyB yozishi mumkin bo'lgan namuna kodi:

public sealed class Program {
    public static void Main() {
        CompanyB.BetterPhone phone = new CompanyB.BetterPhone();
        phone.Dial();
    }
}

Bu kod ishga tushganda quyidagi natija ko'rsatiladi.

BetterPhone.Dial
BetterPhone.EstablishConnection
Phone.Dial

Bu natija CompanyB xohlagan xatti-harakatni olayotganini ko'rsatadi. Dial ga chaqiruv BetterPhone da aniqlangan yangi Dial metodini chaqiradi, bu esa virtual EstablishConnection metodini va keyin Phone asosiy turning Dial metodini chaqiradi.

1-Senariy: CompanyA Phone ga Yangi Virtual Metod Qo'shadi

Endi tasavvur qiling, bir nechta kompaniyalar CompanyA ning Phone turini ishlatishga qaror qilishdi. Ular shuningdek Dial metodida ulanishni o'rnatish qobiliyati juda foydali xususiyat ekanligini qaror qilishdi. Bu fikr CompanyA ga berildi va CompanyA endi o'z Phone klassini qayta ko'rib chiqadi.

namespace CompanyA {
    public class Phone {
        public void Dial() {
            Console.WriteLine("Phone.Dial");
            EstablishConnection();
            // Telefon raqamini terish ishlari bu yerda.
        }

        protected virtual void EstablishConnection() {
            Console.WriteLine("Phone.EstablishConnection");
            // Ulanishni o'rnatish ishlari bu yerda.
        }
    }
}

Endi CompanyB BetterPhone turini CompanyA ning Phone turining yangi versiyasidan kompilyatsiya qilganda, kompilyator quyidagi xabarni beradi.

warning CS0114: 'CompanyB.BetterPhone.EstablishConnection()' hides inherited
member 'CompanyA.Phone.EstablishConnection()'. To make the current member override
that implementation, add the override keyword. Otherwise, add the new keyword.

Kompilyator sizni Phone va BetterPhone ning ikkalasida ham EstablishConnection metodi borligini va ularning semantikasi bir xil bo'lishi mumkin yoki bo'lmasligi mumkinligini ogohlantiradi; BetterPhone ni qayta kompilyatsiya qilish Phone turining birinchi versiyasidan foydalanganidek bir xil xatti-harakatni bermay qolishi mumkin.

Agar CompanyB EstablishConnection metodlari ikkala turda ham semantik jihatdan bir xil emas deb qaror qilsa, CompanyB kompilyatorga BetterPhone da aniqlangan Dial va EstablishConnection metodi to'g'ri foydalanish uchun va uning Phone asosiy turidagi EstablishConnection metodi bilan hech qanday aloqasi yo'qligini ayta oladi. CompanyB buni EstablishConnection metodiga new kalit so'zini qo'shish orqali amalga oshiradi.

namespace CompanyB {
    public class BetterPhone : CompanyA.Phone {

        // 'new' bilan bu metodni asosiy turning
        // Dial metodi bilan aloqasiz deb belgilash.
        public new void Dial() {
            Console.WriteLine("BetterPhone.Dial");
            EstablishConnection();
            base.Dial();
        }

        // 'new' bilan bu metodni asosiy turning
        // EstablishConnection metodi bilan aloqasiz deb belgilash.
        protected new virtual void EstablishConnection() {
            Console.WriteLine("BetterPhone.EstablishConnection");
            // Ulanishni o'rnatish ishlari bu yerda.
        }
    }
}

Ushbu kodda new kalit so'zi kompilyatorga metadata chiqarishni buyuradi va CLR ga BetterPhone ning EstablishConnection metodi BetterPhone turi tomonidan kiritilgan yangi funksiya sifatida qaralishi kerakligini bildiradi. CLR Phone va BetterPhone metodlari o'rtasida hech qanday aloqa yo'qligini biladi.

Bir xil ilova kodi (Main metodi) ishga tushganda, natija quyidagicha bo'ladi:

BetterPhone.Dial
BetterPhone.EstablishConnection
Phone.Dial
Phone.EstablishConnection

Bu natija ko'rsatadiki, Main ning Dial ga chaqiruvi BetterPhone.Dial da aniqlangan yangi Dial metodini chaqiradi, bu esa BetterPhone tomonidan aniqlangan virtual EstablishConnection metodini chaqiradi. BetterPhone ning EstablishConnection metodi qaytgandan keyin, Phone ning Dial metodi chaqiriladi va u EstablishConnection ni chaqiradi. Lekin BetterPhone ning EstablishConnection metodi new bilan belgilanganligi sababli, u Phone ning virtual EstablishConnection metodining qayta yozishi (override) hisoblanmaydi. Natijada, Phone ning Dial metodi Phone ning EstablishConnection metodini chaqiradi — bu kutilgan xatti-harakatdir.

Muhim

Agar kompilyator metodlarni standart holda qayta yozish (override) sifatida ko'rganida (xuddi native C++ kompilyatori qilganidek), BetterPhone dasturchisi Dial va EstablishConnection metod nomlarini ishlata olmas edi. Bu butun manba kod bazasida o'zgarishlarning to'lqinli ta'sirini keltirib chiqarar edi, manba va ikkilik moslikni buzar edi. Bu turdagi keng tarqalgan o'zgarish istalmagan, ayniqsa o'rta va katta loyihalarda. Biroq, agar metod nomi o'zgartilishi faqat manba kodidagi o'rtacha yangilanishlarga olib kelsa, siz Dial va EstablishConnection ning ikki turli ma'nolarini boshqa dasturchilarni chalkashtirib yubormaslik uchun metodlarning nomini o'zgartirish kerak.

2-Senariy: CompanyB override ni Ishlatadi

Aksincha, CompanyB CompanyA ning Phone turining yangi versiyasini olib, Phone ning Dial va EstablishConnection semantikasi aynan ular izlayotgan narsa ekanligini qaror qilishi mumkin. Bu holda CompanyB o'z BetterPhone turini Dial metodini butunlay olib tashlash orqali o'zgartiradi. Bundan tashqari, CompanyB endi kompilyatorga BetterPhone ning EstablishConnection metodi Phone ning EstablishConnection metodi bilan bog'liq ekanligini aytmoqchi bo'lganligi sababli, new kalit so'zi olib tashlanishi kerak. Oddiy new ni olib tashlash yetarli emas, chunki kompilyator BetterPhone ning EstablishConnection metodining niyatini aniq ayta olmaydi. O'z niyatini aniq ifodalash uchun CompanyB dasturchisi BetterPhone ning EstablishConnection metodini virtual dan override ga o'zgartirishi kerak. Yangi BetterPhone versiyasi quyidagi ko'rinishda bo'ladi.

namespace CompanyB {
    public class BetterPhone : CompanyA.Phone {

        // Dial metodi o'chirildi (asosiy turdan meros qilinadi).

        // 'new' ni olib tashlash va 'virtual' ni 'override' ga o'zgartirish
        // orqali bu metodni asosiy turning
        // EstablishConnection metodi bilan bog'liq deb belgilash.
        protected override void EstablishConnection() {
            Console.WriteLine("BetterPhone.EstablishConnection");
            // Ulanishni o'rnatish ishlari bu yerda.
        }
    }
}

Endi bir xil ilova kodi (Main metodi) ishga tushganda, natija quyidagicha bo'ladi.

Phone.Dial
BetterPhone.EstablishConnection

Bu natija ko'rsatadiki, Main ning Dial ga chaqiruvi Phone da aniqlangan va BetterPhone tomonidan meros qilingan Dial metodini chaqiradi. Keyin Phone ning Dial metodi virtual EstablishConnection metodini chaqirganda, BetterPhone ning EstablishConnection metodi chaqiriladi, chunki u Phone tomonidan aniqlangan virtual EstablishConnection ni qayta yozadi (override).

Xulosa

Ushbu bobda biz turlar va ularning a'zolari haqidagi asosiy tushunchalarni ko'rib chiqdik:

  • Tur a'zolari — konstanta, maydon, konstruktor, metod, operator, xususiyat, hodisa va ichki turlarni o'z ichiga oladi.
  • Tur ko'rinuvchanligipublic va internal modifikatorlari bilan boshqariladi. Do'st assemblylar InternalsVisibleTo atributi orqali internal turlarga kirish huquqini olishi mumkin.
  • A'zo foydalanuvchanligiprivate, protected, internal, protected internal va public modifikatorlari mavjud.
  • Statik klasslarstatic kalit so'zi bilan belgilangan klasslar faqat statik a'zolarni o'z ichiga olishi mumkin va hech qachon instansiyalanishi mumkin emas.
  • Partial turlarpartial kalit so'zi tur ta'rifini bir nechta manba kod fayllariga bo'lish imkonini beradi.
  • Komponentlar va versiyalashabstract, virtual, override, sealed va new kalit so'zlari komponent versiyalashni boshqaradi.
  • CLR metod chaqiruvlaricall va callvirt IL ko'rsatmalari turli usullarda metodlarni chaqiradi. C# barcha instansiya metodlarini callvirt orqali chaqiradi.
  • Versiyalash amaliyoti — turlarni sealed deb aniqlash, maydonlarni private qilish va virtual metodlarni minimallash orqali mustahkam va versiyalash mumkin bo'lgan komponentlar yaratish mumkin.
Tavsiya

Keyingi bobda biz konstantalar va maydonlarni (Chapter 7) batafsil ko'rib chiqamiz. Bu erda o'rganilgan tur ko'rinuvchanligi va a'zo foydalanuvchanligi tushunchalarini doimiy ravishda qo'llash, sifatli va xavfsiz klass iyerarxiyalarini yaratishda juda muhim.