12-Bob: Genericlar
Framework Class Library dagi genericlar, genericlar infratuzilmasi, generic interfeyslar, generic delegatlar, kontravariantlik/kovariantlik, generic metodlar va cheklovlar
Obyektga yo'naltirilgan dasturlash bilan tanish bo'lgan dasturchilar uning taklif qiladigan afzalliklarini bilishadi. Katta afzalliklardan biri — koddan qayta foydalanish (code reuse), ya'ni bazaviy (base) klassning barcha imkoniyatlarini meros qilib oluvchi klass hosil qilish qobiliyati. Hosila (derived) klass bazaviy klassning virtual metodlarini oddiygina qayta yozishi (override) yoki dasturchi ehtiyojlarini qondirish uchun bazaviy klass xatti-harakatini sozlash maqsadida yangi metodlar qo'shishi mumkin. Genericlar umumiy til ish vaqti muhiti (CLR) va dasturlash tillari tomonidan taqdim etiladigan yana bir kod qayta foydalanish shakli: algoritmdan qayta foydalanish.
Asosan, bitta dasturchi saralash, qidirish, almashtirish, solishtirish yoki konvertatsiya qilish kabi algoritmni aniqlaydi. Biroq, algoritmni aniqlovchi dasturchi algoritm qaysi ma'lumot tur(lar)i ustida ishlashini belgilamaydi; algoritm turli turdagi obyektlarga umumiy (generik) tarzda qo'llanilishi mumkin. Boshqa dasturchi keyinchalik ushbu mavjud algoritmdan foydalanishi mumkin, faqat algoritm ishlashi kerak bo'lgan aniq ma'lumot tur(lar)ini ko'rsatishi kifoya — masalan, Int32 lar, String lar, DateTime lar ustida ishlaydigan saralash algoritmi yoki solishtirish algoritmi.
Ko'pgina algoritmlar tur ichida kapsullanadi va CLR generic reference turlar, generic value turlar yaratishga imkon beradi (lekin generic enumerated turlar yaratishga ruxsat bermaydi). Bundan tashqari, CLR generic interfeyslar va generic delegatlar yaratishga ham imkon beradi. Ba'zan bitta metod foydali algoritmni kapsulalashi mumkin, shuning uchun CLR reference tur, value tur yoki interfeysda aniqlangan generic metodlar yaratishga ham imkon beradi.
Keling, tezkor misolni ko'rib chiqaylik. Framework Class Library (FCL) obyektlar to'plamini boshqarishni biladigan generic ro'yxat algoritmini aniqlaydi; ushbu obyektlarning ma'lumot turi generic algoritm tomonidan belgilanmagan. Generic ro'yxat algoritmidan foydalanmoqchi bo'lgan birov aniq ma'lumot turini keyinroq ko'rsatishi mumkin.
Ushbu generic ro'yxat algoritmini kapsullaydigan FCL klassi List<T> deb ataladi ("List of Tee" deb o'qiladi) va System.Collections.Generic nomlar fazosida aniqlangan. Ushbu klass ta'rifi quyidagicha ko'rinadi (kod juda qisqartirilgan):
[Serializable]
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>,
IList, ICollection, IEnumerable {
public List();
public void Add(T item);
public Int32 BinarySearch(T item);
public void Clear();
public Boolean Contains(T item);
public Int32 IndexOf(T item);
public Boolean Remove(T item);
public void Sort();
public void Sort(IComparer<T> comparer);
public void Sort(Comparison<T> comparison);
public T[] ToArray();
public Int32 Count { get; }
public T this[Int32 index] { get; set; }
}
Generic List klassini aniqlagan dasturchi klass nomi ortidan <T> qo'yib, uning ko'rsatilmagan ma'lumot turi bilan ishlashini bildiradi. Generic tur yoki metodni aniqlashda, turlar uchun ko'rsatiladigan har qanday o'zgaruvchilar (masalan, T) tur parametrlari (type parameters) deb ataladi. T — ma'lumot turi ishlatilishi mumkin bo'lgan manba kodidagi istalgan joyda ishlatiladigan o'zgaruvchi nomi. Masalan, List klass ta'rifida T metod parametrlari (Add metodi T turidagi parametrni qabul qiladi) va qaytarish turlari (ToArray metodi T turidagi bir o'lchovli massivni qaytaradi) uchun ishlatilayotganini ko'rasiz.
Eslatma
Microsoft dizayn ko'rsatmalari generic parametr o'zgaruvchilari T deb yoki kamida katta T harfi bilan boshlanishi kerakligini bildiradi (masalan, TKey va TValue). Katta T harfi type so'zini bildiradi, xuddi katta I harfi interface ni bildirgani kabi (IComparable da bo'lgani kabi).
Endi generic List<T> turi aniqlangach, boshqa dasturchilar ushbu generic algoritmdan foydalanishlari mumkin — ular algoritmning ishlashini xohlagan aniq ma'lumot turini ko'rsatadilar. Generic tur yoki metodni ishlatishda, ko'rsatilgan ma'lumot turlari tur argumentlari (type arguments) deb ataladi. Masalan, dasturchi List algoritmidan DateTime tur argumentini ko'rsatib foydalanishni xohlashi mumkin.
Mana buni ko'rsatadigan kod:
private static void SomeMethod() {
// DateTime obyektlari ustida ishlaydigan ro'yxat yaratish
List<DateTime> dtList = new List<DateTime>();
// Ro'yxatga DateTime obyektini qo'shish
dtList.Add(DateTime.Now); // Boxing yo'q
// Yana bir DateTime obyektini qo'shish
dtList.Add(DateTime.MinValue); // Boxing yo'q
// Ro'yxatga String obyektini qo'shishga urinish
dtList.Add("1/1/2004"); // Kompilyatsiya xatosi
// Ro'yxatdan DateTime obyektini olish
DateTime dt = dtList[0]; // Cast kerak emas
}
Genericlar dasturchilar uchun yuqoridagi kodda ko'rsatilganidek quyidagi katta afzalliklarni taqdim qiladi:
- Manba kodini himoyalash — Generic algoritmdan foydalanadigan dasturchi algoritm manba kodiga ega bo'lishi shart emas. C++ shablonlari (templates) bilan esa algoritmning manba kodi algoritmdan foydalanadigan dasturchi uchun mavjud bo'lishi kerak.
- Tur xavfsizligi — Generic algoritm aniq tur bilan ishlatilganda, kompilyator va CLR buni tushunadi va faqat ko'rsatilgan ma'lumot turiga mos obyektlar algoritm bilan ishlatilishini ta'minlaydi. Nomos turdagi obyektni ishlatishga urinish kompilyatsiya xatosi yoki ish vaqti istisnosiga olib keladi.
- Toza kod — Kompilyator tur xavfsizligini ta'minlagani uchun, manba kodingizda kamroq castlar kerak bo'ladi, ya'ni kodingizni yozish va saqlash osonroq.
- Yaxshiroq ishlash — Genericlardan oldin, umumlashtirilgan algoritmni aniqlashning yagona usuli barcha a'zolarini
Object ma'lumot turi bilan ishlash uchun aniqlash edi. Agar algoritmni value tur instansiyalari bilan ishlatmoqchi bo'lsangiz, CLR value tur instansiyasini algoritmning a'zolarini chaqirishdan oldin boxing qilishi kerak edi. Boxing managed heapda xotira ajratilishiga sabab bo'ladi, bu esa garbage collectionni ko'paytiradi va ilovaning ishlashiga salbiy ta'sir ko'rsatadi. Generic algoritm endi aniq value turi bilan ishlash uchun yaratilishi mumkinligi sababli, value tur instansiyalari qiymat bo'yicha uzatilishi mumkin va CLR hech qanday boxing qilishi shart emas.
Genericlarning ishlash afzalliklarini ko'rsatish uchun generic List algoritmining ishlashini FCL ning generic bo'lmagan ArrayList algoritmi bilan solishtiradigan dastur yozilgan. Aslida, ikkala algoritmning ishlashi value tur obyektlari va reference tur obyektlari yordamida sinovdan o'tkazilgan. Mana dastur:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
public static class Program {
public static void Main() {
ValueTypePerfTest();
ReferenceTypePerfTest();
}
private static void ValueTypePerfTest() {
const Int32 count = 100000000;
using (new OperationTimer("List<Int32>")) {
List<Int32> l = new List<Int32>();
for (Int32 n = 0; n < count; n++) {
l.Add(n); // Boxing yo'q
Int32 x = l[n]; // Unboxing yo'q
}
l = null; // Garbage collect qilinishini ta'minlash
}
using (new OperationTimer("ArrayList of Int32")) {
ArrayList a = new ArrayList();
for (Int32 n = 0; n < count; n++) {
a.Add(n); // Boxing
Int32 x = (Int32) a[n]; // Unboxing
}
a = null; // Garbage collect qilinishini ta'minlash
}
}
private static void ReferenceTypePerfTest() {
const Int32 count = 100000000;
using (new OperationTimer("List<String>")) {
List<String> l = new List<String>();
for (Int32 n = 0; n < count; n++) {
l.Add("X"); // Reference nusxa
String x = l[n]; // Reference nusxa
}
l = null; // Garbage collect qilinishini ta'minlash
}
using (new OperationTimer("ArrayList of String")) {
ArrayList a = new ArrayList();
for (Int32 n = 0; n < count; n++) {
a.Add("X"); // Reference nusxa
String x = (String) a[n]; // Cast tekshiruvi va reference nusxa
}
a = null; // Garbage collect qilinishini ta'minlash
}
}
}
// Bu klass operatsiya ishlash vaqtini o'lchash uchun foydali
internal sealed class OperationTimer : IDisposable {
private Stopwatch m_stopwatch;
private String m_text;
private Int32 m_collectionCount;
public OperationTimer(String text) {
PrepareForOperation();
m_text = text;
m_collectionCount = GC.CollectionCount(0);
// Bu oxirgi gap bo'lishi kerak
m_stopwatch = Stopwatch.StartNew();
}
public void Dispose() {
Console.WriteLine("{0} (GCs={1,3}) {2}",
(m_stopwatch.Elapsed),
GC.CollectionCount(0) - m_collectionCount, m_text);
}
private static void PrepareForOperation() {
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
}
Ushbu dasturni release build sifatida kompilyatsiya qilib ishga tushirganimda (optimizatsiyalar yoqilgan holda), quyidagi natijani oldim:
00:00:01.6246959 (GCs= 6) List<Int32>
00:00:10.8555008 (GCs=390) ArrayList of Int32
00:00:02.5427847 (GCs= 4) List<String>
00:00:02.7944831 (GCs= 7) ArrayList of String
Bu natija shuni ko'rsatadiki, Int32 value turi bilan generic List algoritmidan foydalanish generic bo'lmagan ArrayList dan foydalanishdan ancha tezroq. Aslida, farq hayratlanarli: 1.6 soniya va deyarli 11 soniyaga qarshi. Bu taxminan 7 baravar tezroq! Bundan tashqari, Int32 value turini ishlatish ArrayList bilan ko'p boxing operatsiyalariga sabab bo'lib, 390 ta garbage collection hosil qildi. Bu orada List algoritmi faqat 6 ta garbage collection talab qildi.
Reference turlar bilan sinov natijasi unchalik katta emas. Bu yerda vaqtlar va garbage collectionlar soni deyarli bir xil. Shunday qilib, generic List algoritmi bu yerda sezilarli foyda keltirmayotganga o'xshaydi. Biroq, shuni yodda tutingki, generic algoritmdan foydalanilganda siz toza kod va kompilyatsiya vaqtidagi tur xavfsizligini ham olasiz.
Eslatma
CLR har bir ma'lumot turi uchun metod birinchi marta chaqirilganda nativ kod yaratishini tushunishingiz kerak. Bu ilovaning ishchi to'plamini (working set) oshiradi va ishlashga salbiy ta'sir qilishi mumkin. Bu haqda bobning "Genericlar Infratuzilmasi" bo'limida batafsilroq gaplashaman.
Framework Class Library dagi Genericlar
Genericlarning eng aniq ishlatilishi albatta kolleksiya klasslari bilan bog'liq va FCL siz foydalanishingiz uchun bir nechta generic kolleksiya klasslarini aniqlaydi. Ushbu klasslarning aksariyatini System.Collections.Generic va System.Collections.ObjectModel nomlar fazolarida topish mumkin. Shuningdek, System.Collections.Concurrent nomlar fazosida thread-safe generic kolleksiya klasslari ham mavjud.
Microsoft dasturchilarga generic kolleksiya klasslaridan foydalanishni tavsiya qiladi va endi generic bo'lmagan kolleksiya klasslaridan foydalanishni taqiqlaydi. Buning bir necha sababi bor. Birinchidan, generic bo'lmagan kolleksiya klasslari generic emas va shu sababli tur xavfsizligi, toza kod va yaxshiroq ishlash ololmaysiz. Ikkinchidan, generic klasslar generic bo'lmagan klasslarga qaraganda yaxshiroq obyekt modeliga ega. Masalan, kamroq virtual metodlar ishlatiladi, bu esa yaxshiroq ishlashga olib keladi, va generic kolleksiyalarga yangi a'zolar qo'shilgan.
Kolleksiya klasslari ko'plab interfeyslarni amalga oshiradi va kolleksiyalarga joylashtiriladigan obyektlar saralash va qidirish kabi operatsiyalar uchun ishlatadigan interfeyslarni ham amalga oshirishi mumkin. FCL ko'plab generic interfeys ta'riflari bilan birga keladi, shu sababli genericlarning afzalliklari interfeyslar bilan ishlashda ham amal qiladi. Ko'p ishlatiladigan interfeyslar System.Collections.Generic nomlar fazosida joylashgan.
Yangi generic interfeyslar eski generic bo'lmagan interfeyslar o'rniga emas; ko'pgina senariylarda ikkalasini ham ishlatishingiz kerak bo'ladi. Sabab — orqaga qarab muvofiqlash (backward compatibility). Masalan, List<T> klassi faqat IList<T> interfeysini amalga oshirganida, hech qanday kod List<DateTime> obyektini IList sifatida ko'ra olmas edi.
Shuningdek, System.Array klassi — barcha massiv turlarining bazaviy klassi — ko'plab statik generic metodlarni taklif qiladi, masalan AsReadOnly, BinarySearch, ConvertAll, Exists, Find, FindAll, FindIndex, FindLast, FindLastIndex, ForEach, IndexOf, LastIndexOf, Resize, Sort va TrueForAll. Mana ushbu metodlarning ba'zilari qanday ko'rinishga ega:
public abstract class Array : ICloneable, IList, ICollection, IEnumerable,
IStructuralComparable, IStructuralEquatable {
public static void Sort<T>(T[] array);
public static void Sort<T>(T[] array, IComparer<T> comparer);
public static Int32 BinarySearch<T>(T[] array, T value);
public static Int32 BinarySearch<T>(T[] array, T value,
IComparer<T> comparer);
...
}
Mana ushbu metodlarning ba'zilaridan foydalanishni ko'rsatadigan kod:
public static void Main() {
// Byte massivini yaratish va initsializatsiya qilish
Byte[] byteArray = new Byte[] { 5, 1, 4, 2, 3 };
// Byte[] saralash algoritmini chaqirish
Array.Sort<Byte>(byteArray);
// Byte[] binary qidirish algoritmini chaqirish
Int32 i = Array.BinarySearch<Byte>(byteArray, 1);
Console.WriteLine(i); // "0" ni ko'rsatadi
}
Genericlar Infratuzilmasi
Genericlar CLR ning 2.0 versiyasida qo'shildi va bu ko'p odamlar uzoq vaqt ishlagan katta vazifa edi. Genericlarni ishlashi uchun Microsoft quyidagilarni qilishi kerak edi:
- Tur argumentlaridan xabardor bo'lgan yangi Intermediate Language (IL) ko'rsatmalarini yaratish.
- Mavjud metadata jadvallar formatini o'zgartirish, shunda generic parametrlarga ega tur nomlari va metodlarni ifodalash mumkin bo'lsin.
- Turli dasturlash tillarini (C#, Microsoft Visual Basic .NET va boshq.) yangi sintaksisni qo'llab-quvvatlash uchun o'zgartirish, dasturchilarga generic reference va value turlar va metodlarni aniqlash va ishlatish imkonini berish.
- Kompilyatorlarni yangi IL ko'rsatmalari va o'zgartirilgan metadata formatini yaratish uchun o'zgartirish.
- Just-in-time (JIT) kompilyatorini to'g'ri nativ kod yaratuvchi yangi turga xabardor IL ko'rsatmalarini qayta ishlash uchun o'zgartirish.
- Yangi reflection a'zolarini yaratish, shunda dasturchilar turlar va a'zolarda generic parametrlar bor yoki yo'qligini so'rashi va ish vaqtida generic tur va metod ta'riflarini yarata olishi mumkin bo'lsin.
- Debuggerni generic turlar, a'zolar, maydonlar va lokal o'zgaruvchilarni ko'rsatish va manipulyatsiya qilish uchun o'zgartirish.
- Microsoft Visual Studio IntelliSense xususiyatini generic tur yoki metodni aniq ma'lumot turi bilan ishlatishda aniq a'zo prototiplarini ko'rsatish uchun o'zgartirish.
Keling, CLR genericlarni ichki tarzda qanday boshqarishini muhokama qilaylik. Ushbu ma'lumot generic algoritmni qanday arxitektura va loyihalashtirishingizga ta'sir qilishi mumkin. Shuningdek, bu mavjud generic algoritmdan foydalanish yoki foydalanmaslik qaroringizga ham ta'sir qilishi mumkin.
Ochiq va Yopiq Turlar (Open and Closed Types)
Kitobning turli boblarida men CLR ilova tomonidan ishlatiladigan har bir tur uchun ichki ma'lumot strukturasini yaratishini muhokama qildim. Bu ma'lumot strukturalari tur obyektlari (type objects) deb ataladi. Generic tur parametrlariga ega tur hali ham tur sifatida hisoblanadi va CLR har biri uchun ichki tur obyektini yaratadi. Bu reference turlar (klasslar), value turlar (structlar), interfeys turlari va delegat turlariga taalluqli.
Biroq, generic tur parametrlariga ega tur ochiq tur (open type) deb ataladi va CLR ochiq turning hech qanday instansiyasini yaratishga ruxsat bermaydi (xuddi CLR interfeys turining instansiyasini yaratishga to'sqinlik qilganiga o'xshash).
Kod generic turga murojaat qilganda, u tur argumentlari to'plamini ko'rsatishi mumkin. Agar barcha tur argumentlari uchun haqiqiy ma'lumot turlari berilgan bo'lsa, tur yopiq tur (closed type) deb ataladi va CLR yopiq turning instansiyalarini yaratishga ruxsat beradi. Biroq, kod generic turga murojaat qilib, ba'zi generic tur argumentlarini ko'rsatmasdan qoldirishi mumkin. Bu CLR da yangi ochiq tur obyektini yaratadi va ushbu turning instansiyalari yaratilishi mumkin emas.
using System;
using System.Collections.Generic;
// Qisman ko'rsatilgan ochiq tur
internal sealed class DictionaryStringKey<TValue> :
Dictionary<String, TValue> {
}
public static class Program {
public static void Main() {
Object o = null;
// Dictionary<,> 2 ta tur parametriga ega ochiq tur
Type t = typeof(Dictionary<,>);
// Ushbu tur instansiyasini yaratishga urinish (muvaffaqiyatsiz)
o = CreateInstance(t);
Console.WriteLine();
// DictionaryStringKey<> 1 ta tur parametriga ega ochiq tur
t = typeof(DictionaryStringKey<>);
// Ushbu tur instansiyasini yaratishga urinish (muvaffaqiyatsiz)
o = CreateInstance(t);
Console.WriteLine();
// DictionaryStringKey<Guid> yopiq tur
t = typeof(DictionaryStringKey<Guid>);
// Ushbu tur instansiyasini yaratishga urinish (muvaffaqiyatli)
o = CreateInstance(t);
// Haqiqatan ishladi
Console.WriteLine("Object type=" + o.GetType());
}
private static Object CreateInstance(Type t) {
Object o = null;
try {
o = Activator.CreateInstance(t);
Console.Write("Created instance of {0}", t.ToString());
}
catch (ArgumentException e) {
Console.WriteLine(e.Message);
}
return o;
}
}
Ushbu kodni kompilyatsiya qilib ishga tushirsam, quyidagi natijani olaman:
Cannot create an instance of System.Collections.Generic.
Dictionary`2[TKey,TValue] because Type.ContainsGenericParameters is true.
Cannot create an instance of DictionaryStringKey`1[TValue] because
Type.ContainsGenericParameters is true.
Created instance of DictionaryStringKey`1[System.Guid]
Object type=DictionaryStringKey`1[System.Guid]
Ko'rib turganingizdek, Activator ning CreateInstance metodi ochiq turning instansiyasini yaratmoqchi bo'lganingizda ArgumentException tashlaydi. Natijada tur nomlari backtick (`) belgisi va raqam bilan tugashini sezasiz. Raqam turning arity ini bildiradi, ya'ni tur talab qiladigan tur parametrlarining sonini ko'rsatadi.
Shuningdek, CLR turning statik maydonlarini tur obyekti ichida joylashtiradi. Shuning uchun har bir yopiq tur o'zining statik maydonlariga ega. Boshqacha qilib aytganda, agar List<T> da statik maydonlar aniqlangan bo'lsa, bu maydonlar List<DateTime> va List<String> o'rtasida umumiy emas; har bir yopiq tur obyektining o'z statik maydonlari bor. Shuningdek, agar generic tur statik konstruktor aniqlagan bo'lsa (8-bobda "Metodlar" da muhokama qilingan), bu konstruktor har bir yopiq tur uchun bir marta bajariladi.
internal sealed class GenericTypeThatRequiresAnEnum<T> {
static GenericTypeThatRequiresAnEnum() {
if (!typeof(T).IsEnum) {
throw new ArgumentException("T must be an enumerated type");
}
}
}
Generic Turlar va Meros Olish (Inheritance)
Generic tur — bu tur, va shuning uchun boshqa har qanday turdan hosil bo'lishi mumkin. Generic turni ishlatib tur argumentlarini ko'rsatganingizda, CLR da yangi tur obyektini aniqlayapsiz va yangi tur obyekti generic tur hosila bo'lgan har qanday turdan hosila bo'ladi. Boshqacha aytganda, List<T> Object dan hosila bo'lganligi sababli, List<String> va List<Guid> ham Object dan hosila bo'ladi.
Tur argumentlarini ko'rsatish meros olish iyerarxiyalari bilan hech qanday aloqasi yo'qligini tushunish sizga qanday casting qilish mumkin va mumkin emasligini aniqlashga yordam beradi.
Masalan, agar bog'langan ro'yxat (linked list) tugunlari (node) klassi quyidagicha aniqlangan bo'lsa:
internal sealed class Node<T> {
public T m_data;
public Node<T> m_next;
public Node(T data) : this(data, null) {
}
public Node(T data, Node<T> next) {
m_data = data; m_next = next;
}
public override String ToString() {
return m_data.ToString() +
((m_next != null) ? m_next.ToString() : String.Empty);
}
}
keyin men quyidagiga o'xshash bog'langan ro'yxat quradigan kod yoza olaman:
private static void SameDataLinkedList() {
Node<Char> head = new Node<Char>('C');
head = new Node<Char>('B', head);
head = new Node<Char>('A', head);
Console.WriteLine(head.ToString()); // "ABC" ni ko'rsatadi
}
Node klassida m_next maydoni bir xil turdagi ma'lumotga ega bo'lgan boshqa tugunga havola qilishi kerak. Bu bog'langan ro'yxat barcha ma'lumot elementlari bir xil turda (yoki hosila turda) bo'lgan tugunlarni o'z ichiga olishi kerakligini bildiradi.
Yaxshiroq yondashuv — generic bo'lmagan Node bazaviy klassni, keyin esa generic TypedNode klassini (bazaviy klass sifatida Node klassidan foydalanib) aniqlash. Endi har bir tugun turli ma'lumot turiga ega bo'lishi mumkin bo'lgan bog'langan ro'yxatga ega bo'lish mumkin:
internal class Node {
protected Node m_next;
public Node(Node next) {
m_next = next;
}
}
internal sealed class TypedNode<T> : Node {
public T m_data;
public TypedNode(T data) : this(data, null) {
}
public TypedNode(T data, Node next) : base(next) {
m_data = data;
}
public override String ToString() {
return m_data.ToString() +
((m_next != null) ? m_next.ToString() : String.Empty);
}
}
private static void DifferentDataLinkedList() {
Node head = new TypedNode<Char>('.');
head = new TypedNode<DateTime>(DateTime.Now, head);
head = new TypedNode<String>("Today is ", head);
Console.WriteLine(head.ToString());
}
Generic Tur Identifikatsiyasi (Generic Type Identity)
Ba'zida generic sintaksis dasturchilarni chalkashtirib yuboradi. Axir, manba kodingiz bo'ylab ko'plab kichik (<) va katta (>) belgilar bo'lishi mumkin va bu o'qilishni qiyinlashtiradi. Sintaksisni yaxshilash uchun ba'zi dasturchilar generic turdan hosila bo'lgan va barcha tur argumentlarini ko'rsatadigan yangi generic bo'lmagan klass turini aniqlashadi. Masalan, quyidagi kodni soddalashtirish uchun:
List<DateTime> dtl = new List<DateTime>();
ba'zi dasturchilar quyidagi klassni aniqlashi mumkin:
internal sealed class DateTimeList : List<DateTime> {
// Bu yerga hech qanday kod qo'yish shart emas!
}
Endi ro'yxat yaratadigan kod quyidagicha yozilishi mumkin:
DateTimeList dtl = new DateTimeList();
Garchi bu qulaylik kabi ko'rinsa ham, ayniqsa yangi turni parametrlar, lokal o'zgaruvchilar va maydonlar uchun ishlatsangiz, manba kodingizni o'qilishi osonlashtirish maqsadida hech qachon yangi klassni aniq yaratmang. Sabab shuki, tur identifikatsiyasi va ekvivalentligi yo'qoladi:
Boolean sameType = (typeof(List<DateTime>) == typeof(DateTimeList));
Yuqoridagi kod bajarilganda, sameType false ga initsializatsiya qilinadi, chunki ikki xil tur obyektlarini solishtiryapsiz. Bu shuni ham anglatadiki, DateTimeList ni qabul qiladigan qilib prototiplanglangan metod unga List<DateTime> ni uzata olmaydi. Ammo List<DateTime> ni qabul qiladigan metod unga DateTimeList ni uzatishi mumkin, chunki DateTimeList List<DateTime> dan hosila bo'ladi.
Yaxshiyamki, C# tur identifikatsiyasi va ekvivalentligiga umuman ta'sir qilmasdan generic yopiq turga soddalashtirilgan sintaksis bilan murojaat qilish usulini taklif qiladi; eski-yaxshi using direktivasidan foydalanishingiz mumkin:
using DateTimeList = System.Collections.Generic.List<System.DateTime>;
Bu yerda using direktivasi DateTimeList deb nomlangan belgi aniqlamoqda. Kod kompilyatsiya qilinganda, kompilyator DateTimeList ning barcha ishlatilishlarini System.Collections.Generic.List<System.DateTime> bilan almashtiradi. Shu sababli endi quyidagi satr bajarilganda sameType true ga initsializatsiya qilinadi:
Boolean sameType = (typeof(List<DateTime>) == typeof(DateTimeList));
Yana bir qulaylik sifatida, C# ning yashirin tarzda turlangan lokal o'zgaruvchi xususiyatidan foydalanishingiz mumkin, bunda kompilyator metod lokal o'zgaruvchisining turini unga tayinlayotgan ifoda turidan aniqlaydi:
using System;
using System.Collections.Generic;
...
internal sealed class SomeType {
private static void SomeMethod () {
// Kompilyator dtl ning turini
// System.Collections.Generic.List<System.DateTime> deb aniqlaydi
var dtl = new List<DateTime>();
...
}
}
Kod Portlashi (Code Explosion)
Generic tur parametrlarini ishlatadigan metod JIT-kompilyatsiya qilinganda, CLR metodning IL sini oladi, ko'rsatilgan tur argumentlarini o'rniga qo'yadi va keyin ushbu ma'lumot turlari ustida ishlaydigan metodga xos nativ kod yaratadi. Bu aynan siz xohlagan narsa va genericlarning asosiy xususiyatlaridan biri. Biroq, buning salbiy tomoni ham bor: CLR har bir metod/tur kombinatsiyasi uchun nativ kod yaratishda davom etadi. Bu kod portlashi (code explosion) deb ataladi. Bu ilovaning ishchi to'plamini sezilarli darajada oshirishi va shu orqali ishlashiga salbiy ta'sir ko'rsatishi mumkin.
Yaxshiyamki, CLR kod portlashini kamaytirish uchun ba'zi optimizatsiyalar o'rnatgan. Birinchidan, agar metod muayyan tur argumenti uchun chaqirilsa va keyinroq xuddi shu tur argumentini ishlatib qayta chaqirilsa, CLR ushbu metod/tur kombinatsiyasi uchun kodni faqat bir marta kompilyatsiya qiladi. Shunday qilib, agar bitta assembly List<DateTime> ishlatsa va butunlay boshqa assembly (xuddi shu AppDomain ga yuklangan) ham List<DateTime> ishlatsa, CLR List<DateTime> metodlarini faqat bir marta kompilyatsiya qiladi. Bu kod portlashini sezilarli darajada kamaytiradi.
CLR ning yana bir optimizatsiyasi bor: CLR barcha reference tur argumentlarini bir xil deb hisoblaydi va shuning uchun kod ulashlash (share) mumkin. Masalan, CLR tomonidan List<String> metodlari uchun kompilyatsiya qilingan kod List<Stream> metodlari uchun ham ishlatilishi mumkin, chunki String va Stream ikkalasi ham reference turlar. Aslida, har qanday reference tur uchun xuddi shu kod ishlatiladi. CLR bu optimizatsiyani amalga oshira oladi, chunki barcha reference tur argumentlari yoki o'zgaruvchilar aslida faqat ko'rsatkichlardir (32-bit Windows tizimida 32 bit, 64-bit Windows tizimida 64 bit) va barcha ko'rsatkichlar bir xil tarzda manipulyatsiya qilinadi.
Lekin agar biror tur argumenti value tur bo'lsa, CLR ushbu value tur uchun maxsus nativ kod yaratishi kerak. Buning sababi shundaki, value turlar o'lcham jihatidan farq qilishi mumkin. Va hatto agar ikkita value tur bir xil hajmda bo'lsa ham (masalan, Int32 va UInt32, ikkalasi ham 32 bit), CLR hali ham kod ulasha olmaydi, chunki turli nativ CPU ko'rsatmalari ushbu qiymatlarni manipulyatsiya qilish uchun ishlatiladi.
Generic Interfeyslar
Shubhasiz, generic reference va value turlarini aniqlash qobiliyati genericlarning asosiy xususiyati edi. Biroq, CLR uchun generic interfeyslarni ham qo'llab-quvvatlash juda muhim edi. Generic interfeyslarsiz, har safar generic bo'lmagan interfeys (IComparable kabi) yordamida value turni manipulyatsiya qilishga harakat qilganingizda, boxing va kompilyatsiya vaqtidagi tur xavfsizligini yo'qotish qaytadan sodir bo'ladi. Bu generic turlarning foydasini jiddiy cheklash bo'lar edi.
Shunday qilib, CLR generic interfeyslarni qo'llab-quvvatlaydi. Reference yoki value tur tur argumentlarini ko'rsatib generic interfeysni amalga oshirishi mumkin yoki tur argumentlarini ko'rsatmasdan qoldirib generic interfeysni amalga oshirishi mumkin. Keling, ba'zi misollarni ko'rib chiqaylik.
Mana FCL ning bir qismi sifatida keltirilgan generic interfeysning ta'rifi (System.Collections.Generic nomlar fazosida):
public interface IEnumerator<T> : IDisposable, IEnumerator {
T Current { get; }
}
Mana ushbu generic interfeysni amalga oshiradigan va tur argumentlarini ko'rsatadigan turga misol. E'tibor bering, Triangle obyekti Point obyektlar to'plamini sanab o'tishi (enumerate) mumkin va Current xususiyati Point ma'lumot turidadir:
internal sealed class Triangle : IEnumerator<Point> {
private Point[] m_vertices;
// IEnumerator<Point> ning Current xususiyati Point turidadir
public Point Current { get { ... } }
...
}
Endi tur argumentlarini ko'rsatmasdan qoldirilgan xuddi shu generic interfeysni amalga oshiradigan turga misol:
internal sealed class ArrayEnumerator<T> : IEnumerator<T> {
private T[] m_array;
// IEnumerator<T> ning Current xususiyati T turidadir
public T Current { get { ... } }
...
}
E'tibor bering, ArrayEnumerator obyekti T obyektlar to'plamini sanab o'tishi mumkin (T ko'rsatilmagan, generic ArrayEnumerator turidan foydalanib T uchun turni keyinroq ko'rsatish mumkin). Shuningdek, Current xususiyati endi ko'rsatilmagan T ma'lumot turidadir. Generic interfeyslar haqida ko'proq ma'lumot 13-bobda "Interfeyslar" da keltirilgan.
Generic Delegatlar
CLR generic delegatlarni qo'llab-quvvatlaydi, shunda har qanday turdagi obyekt tur-xavfsiz tarzda callback metodga uzatilishi mumkin. Bundan tashqari, generic delegatlar value tur instansiyasining boxingsiz callback metodga uzatilishiga imkon beradi. 17-bobda, "Delegatlar" da muhokama qilinganidek, delegat aslida faqat to'rtta metod bilan klass ta'rifidir: konstruktor, Invoke metodi, BeginInvoke metodi va EndInvoke metodi. Tur parametrlarini ko'rsatadigan delegat turni aniqlaganingizda, kompilyator delegat klassining metodlarini aniqlaydi va tur parametrlari ko'rsatilgan tur parametrining parametrlari/qaytarish turlariga ega metodlarga qo'llaniladi.
Masalan, quyidagi generic delegatni aniqlasangiz:
public delegate TReturn CallMe<TReturn, TKey, TValue>(TKey key, TValue value);
kompilyator buni mantiqan quyidagi klassga aylantiradi:
public sealed class CallMe<TReturn, TKey, TValue> : MulticastDelegate {
public CallMe(Object object, IntPtr method);
public virtual TReturn Invoke(TKey key, TValue value);
public virtual IAsyncResult BeginInvoke(TKey key, TValue value,
AsyncCallback callback, Object object);
public virtual TReturn EndInvoke(IAsyncResult result);
}
Eslatma
Iloji boricha Framework Class Library (FCL) da oldindan aniqlangan generic Action va Func delegatlaridan foydalanish tavsiya etiladi. Men ushbu delegat turlarini 17-bobning "Delegat Ta'riflari Yetarli (Generic Delegatlar)" bo'limida tasvirlayman.
Delegat va Interfeys Kontravariant va Kovariant Generic Tur Argumentlari
Delegatning har bir generic tur parametri kontravariant yoki kovariant sifatida belgilanishi mumkin. Ushbu xususiyat sizga generic delegat turining o'zgaruvchisini generic tur parametrlari farq qiladigan bir xil delegat turiga cast qilish imkonini beradi. Generic tur parametri quyidagilardan biri bo'lishi mumkin:
- Invariant — Generic tur parametri o'zgartirilishi mumkin emas degan ma'noni bildiradi. Men hozirgacha ushbu bobda faqat invariant generic tur parametrlarini ko'rsatdim.
- Kontravariant — Generic tur parametri klassdan uning hosila (derived) klassiga o'zgarishi mumkinligini bildiradi. C# da kontravariant generic tur parametrlarini
in kalit so'zi bilan ko'rsatasiz. Kontravariant generic tur parametrlari faqat kirish pozitsiyalarida, masalan metod argumentlari sifatida paydo bo'lishi mumkin.
- Kovariant — Generic tur argumenti klassdan uning bazaviy klasslaridan biriga o'zgarishi mumkinligini bildiradi. C# da kovariant generic tur parametrlarini
out kalit so'zi bilan ko'rsatasiz. Kovariant generic tur parametrlari faqat chiqish pozitsiyalarida, masalan metod qaytarish turi sifatida paydo bo'lishi mumkin.
Masalan, quyidagi delegat turi ta'rifi mavjud deb faraz qilaylik (bu haqiqatan ham mavjud):
public delegate TResult Func<in T, out TResult>(T arg);
Bu yerda generic tur parametri T in kalit so'zi bilan belgilangan, uni kontravariant qilib; va generic tur parametri TResult out kalit so'zi bilan belgilangan, uni kovariant qilib.
Endi, agar menda quyidagi o'zgaruvchi e'lon qilingan bo'lsa:
Func<Object, ArgumentException> fn1 = null;
Men uni boshqa Func turiga cast qila olaman, bunda generic tur parametrlari farq qiladi:
Func<String, Exception> fn2 = fn1; // Aniq cast kerak emas
Exception e = fn2("");
Bu shuni anglatadiki, fn1 Object ni qabul qilib ArgumentException qaytaradigan funksiyaga ishora qiladi. fn2 o'zgaruvchisi String ni qabul qilib Exception qaytaradigan metodga murojaat qilmoqchi. String ni Object ni xohlagan metodga uzatishingiz mumkin (chunki String Object dan hosila bo'ladi) va ArgumentException qaytaradigan metod natijasini Exception sifatida ko'rishingiz mumkin (chunki Exception ArgumentException ning bazaviy klassi) bo'lganligi sababli, yuqoridagi kod kompilyatsiya bo'ladi va tur xavfsizligini saqlab qoladi.
Eslatma
Variansiya faqat kompilyator turlar o'rtasida reference konvertatsiyasi mavjudligini tekshira olgan taqdirdagina qo'llaniladi. Boshqacha aytganda, variansiya value turlar uchun mumkin emas, chunki boxing kerak bo'ladi. Mening fikrimcha, bu cheklov variansiya xususiyatlarini unchalik foydali qilmaydi.
Masalan, quyidagi metod mavjud bo'lsa:
void ProcessCollection(IEnumerable<Object> collection) { ... }
Men uni List<DateTime> obyektiga havolani uzatib chaqira olmayman, chunki DateTime value turi va Object o'rtasida reference konvertatsiyasi mavjud emas. Bu muammoni ProcessCollection ni quyidagicha e'lon qilish orqali hal qilasiz:
void ProcessCollection<T>(IEnumerable<T> collection) { ... }
Shuningdek, variansiya metod argumenti out yoki ref kalit so'zi bilan uzatilgan generic tur parametriga ruxsat berilmaydi.
Delegatlar kabi, generic tur parametrlariga ega interfeyslar ham tur parametrlarini kontravariant yoki kovariant qilishi mumkin. Mana kovariant generic tur parametriga ega interfeysga misol:
public interface IEnumerator<out T> : IEnumerator {
Boolean MoveNext();
T Current { get; }
}
T kovariant bo'lganligi sababli, quyidagi kod kompilyatsiya qilinadi va muvaffaqiyatli ishlaydi:
// Bu metod har qanday reference turning IEnumerable sini qabul qiladi
Int32 Count(IEnumerable<Object> collection) { ... }
...
// Quyidagi chaqiruv IEnumerable<String> ni Count ga uzatadi
Int32 c = Count(new[] { "Grant" });
Muhim
Ba'zida dasturchilar nima uchun generic tur parametrlariga in yoki out ni aniq qo'yishlari kerakligini so'rashadi. Ular kompilyator delegat yoki interfeys deklaratsiyasini tekshirib, generic tur parametrlari kontravariant yoki kovariant ekanligini avtomatik aniqlashi kerak deb o'ylashadi. Garchi kompilyator buni avtomatik aniqlashi mumkin bo'lsa-da, C# jamoasi siz shartnoma e'lon qilayotganingizga va nima ruxsat berilishini aniq ko'rsatishingiz kerak deb hisoblaydi. Masalan, kompilyator generic tur parametri kontravariant deb aniqlasa va keyin siz kelajakda tur parametri chiqish pozitsiyasida ishlatiladigan a'zo qo'shsangiz, keyingi kompilyatsiya vaqtida kompilyator tur parametri invariant bo'lishi kerakligini aniqlaydi, lekin avvalgi kontravariant xatti-harakatiga tayanib yozilgan barcha kod saytlari endi xatolik berishi mumkin.
Generic argumentlarni va qaytarish turlarini qabul qiladigan delegatlardan foydalanilganda, iloji boricha kontravariantlik va kovariantlik uchun in va out kalit so'zlarini ko'rsatish tavsiya etiladi, chunki buni qilish hech qanday salbiy ta'sir ko'rsatmaydi va delegatingizning ko'proq senariylarda ishlatilishiga imkon beradi.
Generic Metodlar
Generic klass, struct yoki interfeysni aniqlaganingizda, ushbu turlarda aniqlangan har qanday metod tur tomonidan ko'rsatilgan tur parametriga murojaat qilishi mumkin. Tur parametri metod parametri, metod qaytarish turi yoki metod ichida aniqlangan lokal o'zgaruvchi sifatida ishlatilishi mumkin. Biroq, CLR metodga o'z tur parametrlarini ko'rsatish qobiliyatini ham qo'llab-quvvatlaydi. Va ushbu tur parametrlari parametrlar, qaytarish turlari yoki lokal o'zgaruvchilar uchun ham ishlatilishi mumkin.
Mana tur parametri va o'z tur parametriga ega metodni aniqlaydigan biroz sun'iy misol:
internal sealed class GenericType<T> {
private T m_value;
public GenericType(T value) { m_value = value; }
public TOutput Converter<TOutput>() {
TOutput result = (TOutput) Convert.ChangeType(m_value, typeof(TOutput));
return result;
}
}
Bu misolda, GenericType klassi o'z tur parametrini (T) aniqlaydi va Converter metodi o'z tur parametrini (TOutput) aniqlaydi. Bu GenericType ni har qanday tur bilan yaratilishiga imkon beradi. Converter metodi m_value maydoni bilan havolalangan obyektni chaqirilganida unga qanday tur argumenti berilganiga qarab turli turlarga konvertatsiya qilishi mumkin. Tur parametrlari va metod parametrlariga ega bo'lish qobiliyati ajoyib moslashuvchanlikni ta'minlaydi.
Generic metodning yetarlicha yaxshi namunasi — Swap metodi:
private static void Swap<T>(ref T o1, ref T o2) {
T temp = o1;
o1 = o2;
o2 = temp;
}
Kodni endi Swap ni quyidagicha chaqirish mumkin:
private static void CallingSwap() {
Int32 n1 = 1, n2 = 2;
Console.WriteLine("n1={0}, n2={1}", n1, n2);
Swap<Int32>(ref n1, ref n2);
Console.WriteLine("n1={0}, n2={1}", n1, n2);
String s1 = "Aidan", s2 = "Grant";
Console.WriteLine("s1={0}, s2={1}", s1, s2);
Swap<String>(ref s1, ref s2);
Console.WriteLine("s1={0}, s2={1}", s1, s2);
}
out va ref parametrlarni qabul qiladigan metodlar bilan generic turlardan foydalanish alohida qiziqarli bo'lishi mumkin, chunki out/ref argument sifatida uzatadigan o'zgaruvchingiz metod parametri bilan bir xil turda bo'lishi kerak. Bu masala 9-bobning "Parametrlarni Reference bo'yicha Metod ga Uzatish" bo'limida muhokama qilingan. Aslida, Interlocked klassining Exchange va CompareExchange metodlari aynan shu sababli generic overloadlarni taklif qiladi.
public static class Interlocked {
public static T Exchange<T>(ref T location1, T value) where T: class;
public static T CompareExchange<T>(
ref T location1, T value, T comparand) where T: class;
}
Generic Metodlar va Tur Xulosasi (Type Inference)
Ko'pgina dasturchilar uchun C# generic sintaksisi barcha kichik (<) va katta (>) belgilar bilan chalkash bo'lishi mumkin. Kod yaratish, o'qilishi va saqlashni yaxshilash uchun C# kompilyatori generic metodni chaqirishda tur xulosasi (type inference) ni taklif qiladi. Tur xulosasi kompilyator avtomatik ravishda (aniqlab) generic metodni chaqirishda ishlatiladigan turni aniqlashga harakat qilishini anglatadi. Mana tur xulosasini namoyish qiladigan ba'zi kod:
private static void CallingSwapUsingInference() {
Int32 n1 = 1, n2 = 2;
Swap(ref n1, ref n2); // Swap<Int32> ni chaqiradi
String s1 = "Aidan";
Object s2 = "Grant";
Swap(ref s1, ref s2); // Xato, tur aniqlab bo'lmaydi
}
Ushbu kodda, Swap chaqiruvlari kichik/katta belgilar ichida tur argumentlarini ko'rsatmasligiga e'tibor bering. Birinchi Swap chaqiruvida C# kompilyatori n1 va n2 Int32 ekanligini aniqladi va shuning uchun Int32 tur argumenti bilan Swap ni chaqirish kerakligini xulosa qildi.
Tur xulosasini amalga oshirishda, C# o'zgaruvchi bilan havolalangan obyektning haqiqiy turini emas, o'zgaruvchining e'lon qilingan turini ishlatadi. Shunday qilib, ikkinchi Swap chaqiruvida C# s1 String va s2 Object ekanligini ko'radi (garchi aslida u String ga ishora qilsa ham). s1 va s2 turli turlardagi o'zgaruvchilar bo'lganligi sababli, kompilyator Swap ning tur argumenti uchun ishlatadigan turni aniq aniqlay olmaydi va xatolik beradi.
Tur bir nechta metodlarni aniqlashi mumkin — biri aniq ma'lumot turini, boshqasi generic tur parametrini qabul qiladi:
private static void Display(String s) {
Console.WriteLine(s);
}
private static void Display<T>(T o) {
Display(o.ToString()); // Display(String) ni chaqiradi
}
Mana Display metodini chaqirishning bir necha usuli:
Display("Jeff"); // Display(String) ni chaqiradi
Display(123); // Display<T>(T) ni chaqiradi
Display<String>("Aidan"); // Display<T>(T) ni chaqiradi
Birinchi chaqiruvda kompilyator String ni qabul qiladigan Display metodi yoki generic Display metodi (T ni String bilan almashtirib) ni chaqirishi mumkin. Biroq, C# kompilyatori doimo generic mos kelishdan ko'ra aniqroq mos kelishni afzal ko'radi va shuning uchun String ni qabul qiladigan generic bo'lmagan Display metodiga chaqiruv yaratadi.
Uchinchi Display chaqiruvi generic tur argumenti String ni aniq ko'rsatadi. Bu kompilyatorga tur argumentlarini xulosalashga urinmaslikni, balki men aniq ko'rsatgan tur argumentlaridan foydalanishni aytadi. Bu holda generic Display chaqiriladi.
Genericlar va Boshqa A'zolar
C# da xususiyatlar (properties), indeksatorlar, hodisalar (events), operator metodlari, konstruktorlar va finalizatorlar o'zlari tur parametrlariga ega bo'la olmaydi. Biroq, ular generic tur ichida aniqlangan bo'lishi mumkin va ushbu a'zolardagi kod turning tur parametrlarini ishlata oladi.
C# ushbu a'zolarga o'z generic tur parametrlarini ko'rsatishga ruxsat bermaydi, chunki Microsoft C# jamoasi dasturchilar kamdan-kam hollarda ushbu a'zolarni generic sifatida ishlatish ehtiyojiga ega bo'lishlariga ishonadi. Bundan tashqari, ushbu a'zolarga generic yordamini qo'shish narxi juda yuqori bo'lar edi. Masalan, kodda + operatorini ishlatganingizda, kompilyator operator overload metodini chaqirishi mumkin. Kodingizda + operatori bilan birga tur argumentlarini ko'rsatishning hech qanday usuli yo'q.
Tekshiruvchanlik va Cheklovlar (Verifiability and Constraints)
Generic kodni kompilyatsiya qilishda, C# kompilyatori uni tahlil qiladi va kod bugungi kunda mavjud yoki kelajakda aniqlangan har qanday tur uchun ishlashini ta'minlaydi. Keling, quyidagi metodga qaraylik:
private static Boolean MethodTakingAnyType<T>(T o) {
T temp = o;
Console.WriteLine(o.ToString());
Boolean b = temp.Equals(o);
return b;
}
Bu metod T turidagi vaqtinchalik o'zgaruvchi (temp) e'lon qiladi, keyin metod bir nechta o'zgaruvchi tayinlashlari va bir nechta metod chaqiruvlarini bajaradi. Ushbu metod har qanday tur uchun ishlaydi. Agar T reference tur bo'lsa — ishlaydi. Value yoki sanab o'tish turi bo'lsa — ishlaydi. Interfeys yoki delegat turi bo'lsa — ishlaydi. Bu metod bugungi kunda mavjud barcha turlar va kelajakda aniqlanadigan turlar uchun ishlaydi, chunki har bir tur tayinlashni va Object tomonidan aniqlangan metodlar (ToString va Equals kabi) chaqirilishini qo'llab-quvvatlaydi.
Endi quyidagi metodga qarang:
private static T Min<T>(T o1, T o2) {
if (o1.CompareTo(o2) < 0) return o1;
return o2;
}
Min metodi o1 o'zgaruvchisida CompareTo metodini chaqirishga urinadi. Lekin CompareTo metodini taklif qilmaydigan ko'plab turlar mavjud va shuning uchun C# kompilyatori ushbu kodni kompilyatsiya qila olmaydi va barcha turlar uchun ishlashini kafolatlashga qodir emas. Agar yuqoridagi kodni kompilyatsiya qilishga urinib ko'rsangiz, kompilyator quyidagi xabarni beradi: error CS1061: 'T' does not contain a definition for 'CompareTo'...
Shunday qilib, genericlardan foydalanishda siz generic turning o'zgaruvchilarini e'lon qilishingiz, ba'zi o'zgaruvchi tayinlashlarini bajarishingiz, Object tomonidan aniqlangan metodlarni chaqirishingiz mumkin va tamom! Bu genericlarni amalda foydasiz qiladi. Yaxshiyamki, kompilyatorlar va CLR genericlarni foydali qiladigan cheklovlar (constraints) deb nomlangan mexanizmni qo'llab-quvvatlaydi.
Cheklov — generic argument uchun ko'rsatilishi mumkin bo'lgan turlar sonini cheklash usuli. Turlar sonini cheklash ushbu turlar bilan ko'proq narsalarni qilish imkonini beradi. Mana cheklov bilan Min metodining yangi versiyasi:
public static T Min<T>(T o1, T o2) where T : IComparable<T> {
if (o1.CompareTo(o2) < 0) return o1;
return o2;
}
C# where tokeni kompilyatorga T uchun ko'rsatilgan har qanday tur bir xil turning (T) generic IComparable interfeysini amalga oshirishi kerakligini aytadi. Ushbu cheklov tufayli, kompilyator endi metodga CompareTo metodini chaqirishga ruxsat beradi, chunki bu metod IComparable<T> interfeysi tomonidan aniqlangan.
Kod generic turga yoki metodga murojaat qilganda, kompilyator tur argumenti cheklovlarga mos kelishini ta'minlash uchun javobgardir. Masalan, quyidagi kod kompilyator xatosini keltirib chiqaradi: error CS0311: The type 'object' cannot be used as type parameter 'T' in the generic type or method 'SomeType.Min<T>(T, T)'...
private static void CallMin() {
Object o1 = "Jeff", o2 = "Richter";
Object oMin = Min<Object>(o1, o2); // Error CS0311
}
Kompilyator xatolik beradi, chunki System.Object IComparable<Object> interfeysini amalga oshirmaydi. Aslida, System.Object umuman hech qanday interfeysni amalga oshirmaydi.
Endi cheklovlar nima ekanligini va qanday ishlashini bilganingizdan keyin, ularni biroz chuqurroq ko'rib chiqamiz. Cheklovlar generic turning tur parametrlariga, shuningdek generic metod tur parametrlariga (Min metodidagi kabi) qo'llanilishi mumkin. CLR tur parametrlari nomlari yoki cheklovlari asosida overloadlashga ruxsat bermaydi; turlar yoki metodlarni faqat arity asosida overloadlashingiz mumkin:
// Quyidagi turlarni aniqlash mumkin:
internal sealed class AType {}
internal sealed class AType<T> {}
internal sealed class AType<T1, T2> {}
// Xato: cheklovsiz AType<T> bilan to'qnashadi
internal sealed class AType<T> where T : IComparable<T> {}
// Xato: AType<T1, T2> bilan to'qnashadi
internal sealed class AType<T3, T4> {}
internal sealed class AnotherType {
// Quyidagi metodlarni aniqlash mumkin:
private static void M() {}
private static void M<T>() {}
private static void M<T1, T2>() {}
// Xato: cheklovsiz M<T>() bilan to'qnashadi
private static void M<T>() where T : IComparable<T> {}
// Xato: M<T1, T2> bilan to'qnashadi
private static void M<T3, T4>() {}
}
Virtual generic metodning override qilishda, override qiluvchi metod bir xil miqdordagi tur parametrlarini ko'rsatishi kerak va bu tur parametrlari bazaviy klass metodi tomonidan belgilangan cheklovlarni meros oladi. Aslida, override qiluvchi metodga tur parametrlariga hech qanday cheklov ko'rsatishga ruxsat berilmaydi. Biroq, tur parametrlari nomlarini o'zgartirishi mumkin.
internal class Base {
public virtual void M<T1, T2>()
where T1 : struct
where T2 : class {
}
}
internal sealed class Derived : Base {
public override void M<T3, T4>()
where T3 : EventArgs // Xato
where T4 : class // Xato
{ }
}
Yuqoridagi kodni kompilyatsiya qilishga urinish kompilyator xabarini keltirib chiqaradi: error CS0460: Constraints for override and explicit interface implementation methods are inherited from the base method, so they cannot be specified directly. Derived klassining M<T3, T4> metodidagi ikki where satrini olib tashlasak, kod muammosiz kompilyatsiya bo'ladi.
Endi kompilyator/CLR tur parametriga qo'llash imkonini beradigan turli xil cheklovlar haqida gaplashaylik. Tur parametri birlamchi cheklov, ikkilamchi cheklov va/yoki konstruktor cheklovi yordamida cheklanishi mumkin. Men ushbu uch xil cheklovlarni keyingi uch bo'limda muhokama qilaman.
Birlamchi Cheklovlar (Primary Constraints)
Tur parametri nol yoki bitta birlamchi cheklov ko'rsatishi mumkin. Birlamchi cheklov sealed (muhrlangan) bo'lmagan klassni aniqlovchi reference tur bo'lishi mumkin. Quyidagi maxsus reference turlardan birini ko'rsata olmaysiz: System.Object, System.Array, System.Delegate, System.MulticastDelegate, System.ValueType, System.Enum yoki System.Void.
Reference tur cheklovini ko'rsatganingizda, kompilyatorga ko'rsatilgan tur argumenti cheklov turi bilan bir xil tur yoki cheklov turidan hosila bo'lgan tur bo'lishini va'da qilasiz. Masalan, quyidagi generic klassga qarang:
internal sealed class PrimaryConstraintOfStream<T> where T : Stream {
public void M(T stream) {
stream.Close(); // OK
}
}
Ushbu klass ta'rifida tur parametri T ning Stream (System.IO nomlar fazosida aniqlangan) birlamchi cheklovi mavjud. Bu PrimaryConstraintOfStream dan foydalanadigan kodning Stream yoki Stream dan hosila bo'lgan tur (masalan, FileStream) ning tur argumentini ko'rsatishi kerakligini kompilyatorga bildiradi.
Ikkita maxsus birlamchi cheklov mavjud: class va struct. class cheklovi ko'rsatilgan tur argumenti reference tur bo'lishini va'da qiladi. Har qanday klass turi, interfeys turi, delegat turi yoki massiv turi ushbu cheklovni qondiradi:
internal sealed class PrimaryConstraintOfClass<T> where T : class {
public void M() {
T temp = null; // Ruxsat — T reference turi bo'lganligi ma'lum
}
}
Ushbu misolda, temp ni null ga o'rnatish qonuniy, chunki T reference tur ekanligi ma'lum va barcha reference tur o'zgaruvchilarini null ga o'rnatish mumkin. Agar T cheklanmagan bo'lganida, T value tur bo'lishi mumkinligi sababli oldingi kod kompilyatsiya bo'lmas edi va value tur o'zgaruvchilarini null ga o'rnatish mumkin emas.
struct cheklovi ko'rsatilgan tur argumenti value tur bo'lishini va'da qiladi. Har qanday value tur, jumladan sanab o'tish turlari (enumerations), ushbu cheklovni qondiradi. Biroq, kompilyator va CLR System.Nullable<T> value turini maxsus tur sifatida ko'radi va nullable turlar ushbu cheklovni qondirmaydi:
internal sealed class PrimaryConstraintOfStruct<T> where T : struct {
public static T Factory() {
// Ruxsat — barcha value turlar yashirin ravishda
// public, parametrsiz konstruktorga ega
return new T();
}
}
Ushbu misolda, T ni new qilish qonuniy, chunki T value tur ekanligi ma'lum va barcha value turlar yashirin ravishda public, parametrsiz konstruktorga ega. Agar T cheklanmagan bo'lganida, reference turga cheklangan bo'lganida yoki class ga cheklangan bo'lganida, yuqoridagi kod kompilyatsiya bo'lmas edi, chunki ba'zi reference turlar public, parametrsiz konstruktorlarga ega emas.
Ikkilamchi Cheklovlar (Secondary Constraints)
Tur parametri nol yoki undan ko'p ikkilamchi cheklovlarni ko'rsatishi mumkin, bunda ikkilamchi cheklov interfeys turini ifodalaydi. Interfeys tur cheklovini ko'rsatganingizda, siz kompilyatorga ko'rsatilgan tur argumenti interfeysni amalga oshiradigan tur bo'lishini va'da qilasiz. Bir nechta interfeys cheklovlarini ko'rsatishingiz mumkinligi sababli, tur argumenti barcha interfeys cheklovlarini (va agar ko'rsatilgan bo'lsa, barcha birlamchi cheklovlarni) amalga oshiradigan turni ko'rsatishi kerak.
Yana bir turdagi ikkilamchi cheklov mavjud — tur parametr cheklovi (ba'zida yalang'och tur cheklovi (naked type constraint) deb ataladi). Bu cheklov interfeys chekloviga qaraganda kamroq ishlatiladi. U generic tur yoki metodga ko'rsatilgan tur argumentlari o'rtasida munosabat bo'lishi kerakligini ko'rsatishga imkon beradi. Mana tur parametr cheklovidan foydalanishni namoyish qiladigan generic metod:
private static List<TBase> ConvertIList<T, TBase>(IList<T> list)
where T : TBase {
List<TBase> baseList = new List<TBase>(list.Count);
for (Int32 index = 0; index < list.Count; index++) {
baseList.Add(list[index]);
}
return baseList;
}
ConvertIList metodi ikkita tur parametrini ko'rsatadi, bunda T parametri TBase tur parametri bilan cheklangan. Bu shuni anglatadiki, T uchun qanday tur argumenti ko'rsatilgan bo'lsa, tur argumenti TBase uchun ko'rsatilgan har qanday narsa bilan mos bo'lishi kerak. Mana ba'zi qonuniy va noqonuniy chaqiruvlarni ko'rsatadigan metod:
private static void CallingConvertIList() {
IList<String> ls = new List<String>();
ls.Add("A String");
// IList<String> ni IList<Object> ga aylantirish
IList<Object> lo = ConvertIList<String, Object>(ls);
// IList<String> ni IList<IComparable> ga aylantirish
IList<IComparable> lc = ConvertIList<String, IComparable>(ls);
// IList<String> ni IList<IComparable<String>> ga aylantirish
IList<IComparable<String>> lcs =
ConvertIList<String, IComparable<String>>(ls);
// IList<String> ni IList<String> ga aylantirish
IList<String> ls2 = ConvertIList<String, String>(ls);
// IList<String> ni IList<Exception> ga aylantirish
IList<Exception> le = ConvertIList<String, Exception>(ls); // Xato
}
Beshinchi chaqiruv xatolik beradi, chunki String Exception bilan mos emas (String Exception dan hosila bo'lmagan).
Konstruktor Cheklovlari (Constructor Constraints)
Tur parametri nol yoki bitta konstruktor cheklovni ko'rsatishi mumkin. Konstruktor cheklovni ko'rsatganingizda, siz kompilyatorga ko'rsatilgan tur argumenti public, parametrsiz konstruktorni amalga oshiradigan abstrakt bo'lmagan tur bo'lishini va'da qilasiz. E'tibor bering, C# kompilyatori struct cheklovi bilan bir vaqtda konstruktor cheklovni ko'rsatishni xato deb hisoblaydi, chunki ortiqcha — barcha value turlar yashirin ravishda public, parametrsiz konstruktorni taklif qiladi.
internal sealed class ConstructorConstraint<T> where T : new() {
public static T Factory() {
// Ruxsat — barcha value turlar yashirin ravishda
// public, parametrsiz konstruktorga ega va
// cheklov har qanday ko'rsatilgan reference turda ham
// public, parametrsiz konstruktor bo'lishini talab qiladi
return new T();
}
}
Bu misolda, T ni new qilish qonuniy, chunki T public, parametrsiz konstruktorga ega tur ekanligi ma'lum. Bu, albatta, barcha value turlar uchun to'g'ri va konstruktor cheklovi tur argumenti sifatida ko'rsatilgan har qanday reference turda ham to'g'ri bo'lishini talab qiladi.
Ba'zida dasturchilar tur parametrini konstruktor turli parametrlarni qabul qiladigan konstruktor cheklovi bilan e'lon qilishni xohlashadi. Hozircha, CLR (va shuning uchun C# kompilyatori) faqat parametrsiz konstruktorlarni qo'llab-quvvatlaydi. Microsoft bu deyarli barcha senariylar uchun yetarli bo'ladi deb hisoblaydi va men ham rozi.
Boshqa Tekshiruvchanlik Masalalari
Ushbu bo'limning qolgan qismida men genericlar bilan tekshiruvchanlik (verifiability) muammolari tufayli kutilmagan xatti-harakatga ega bo'lgan bir nechta boshqa kod konstruktsiyalarini ta'kidlab o'tmoqchiman va cheklovlarni kodning qayta tekshirila oladigan qilish uchun qanday ishlatish mumkinligini ko'rsataman.
Generic Tur O'zgaruvchisini Casting Qilish
Generic tur o'zgaruvchisini boshqa turga casting qilish cheklov bilan mos turga cast qilmaguningizcha noqonuniy:
private static void CastingAGenericTypeVariable1<T>(T obj) {
Int32 x = (Int32) obj; // Xato
String s = (String) obj; // Xato
}
Kompilyator ikkala satrda ham xatolik beradi, chunki T har qanday tur bo'lishi mumkin va castlar muvaffaqiyatli bo'lishiga kafolat yo'q. Buni kompilyatsiya qilish uchun avval Object ga cast qiling:
private static void CastingAGenericTypeVariable2<T>(T obj) {
Int32 x = (Int32) (Object) obj; // Xatolik yo'q
String s = (String) (Object) obj; // Xatolik yo'q
}
Garchi bu kod endi kompilyatsiya bo'lsa-da, ish vaqtida CLR InvalidCastException tashlashi mumkin.
Agar reference turga cast qilishga harakat qilsangiz, C# ning as operatoridan ham foydalanishingiz mumkin:
private static void CastingAGenericTypeVariable3<T>(T obj) {
String s = obj as String; // Xatolik yo'q
}
Generic Tur O'zgaruvchisini Standart Qiymatga O'rnatish
Generic tur o'zgaruvchisini null ga o'rnatish, agar generic tur reference turga cheklanmagan bo'lsa, noqonuniy:
private static void SettingAGenericTypeVariableToNull<T>() {
T temp = null; // CS0403 — 'T' value tur bo'lishi mumkin
}
T cheklanmaganligi sababli, u value tur bo'lishi mumkin va value tur o'zgaruvchisini null ga o'rnatish mumkin emas. Microsoft C# jamoasi dasturchilarga standart qiymat o'rnatish imkoniyatini berish foydali deb hisobladi. Shuning uchun C# kompilyatori default kalit so'zidan foydalanishga ruxsat beradi:
private static void SettingAGenericTypeVariableToDefaultValue<T>() {
T temp = default(T); // OK
}
default kalit so'zidan foydalanish C# kompilyatori va CLR ning JIT kompilyatoriga T reference tur bo'lsa temp ni null ga va T value tur bo'lsa temp ni hammasini-nol-bitlar (all-bits-zero) ga o'rnatish uchun kod yaratishni aytadi.
Generic Tur O'zgaruvchisini null Bilan Solishtirish
Generic tur o'zgaruvchisini null bilan == yoki != operatori yordamida solishtirish, generic tur cheklangan yoki cheklanmaganligidan qat'i nazar, qonuniy:
private static void ComparingAGenericTypeVariableWithNull<T>(T obj) {
if (obj == null) { /* Value turlar uchun hech qachon bajarilmaydi */ }
}
T cheklanmaganligi sababli, u reference yoki value tur bo'lishi mumkin. Agar T value tur bo'lsa, obj hech qachon null bo'la olmaydi. Buni C# kompilyatori xatolik bermaydi, balki kodni muammosiz kompilyatsiya qiladi. Metod value tur argumenti bilan chaqirilganda, JIT kompilyatori if testi hech qachon true bo'lmasligini ko'radi va if tekshiruvi yoki qavslar ichidagi kod uchun nativ kod yaratmaydi.
Ikkita Generic Tur O'zgaruvchisini Bir-Biri Bilan Solishtirish
Bir xil generic tur parametrining ikkita o'zgaruvchisini solishtirish, agar generic tur parametri reference tur ekanligi ma'lum bo'lmasa, noqonuniy:
private static void ComparingTwoGenericTypeVariables<T>(T o1, T o2) {
if (o1 == o2) { } // Xato
}
Bu misolda, T cheklanmagan va ikkita reference tur o'zgaruvchisini bir-biri bilan solishtirish qonuniy bo'lsa-da, value tur == operatorini overload qilmagan bo'lsa, ikkita value tur o'zgaruvchisini solishtirish noqonuniy. Agar T class ga cheklangan bo'lsa, bu kod kompilyatsiya bo'lar edi va == operatori o'zgaruvchilar bir xil obyektga ishora qilishini tekshiradi (aniq identifikatsiya).
Generic Tur O'zgaruvchilarini Operandlar Sifatida Ishlatish
Nihoyat, operatorlarni generic tur o'zgaruvchilari bilan ishlatish bilan bog'liq ko'plab muammolar mavjudligini ta'kidlash kerak. 5-bobda men C# va uning primitiv turlari — Byte, Int16, Int32, Int64, Decimal va hokazo — haqida gapirgan edim. Xususan, C# primitiv turlarga qo'llanilganda +, -, * va / kabi operatorlarni qanday talqin qilishni biladi. Xo'sh, bu operatorlar generic turning o'zgaruvchilariga qo'llanilishi mumkin emas, chunki kompilyator kompilyatsiya vaqtida turni bilmaydi.
Bu shuni anglatadiki, ixtiyoriy raqamli ma'lumot turida ishlaydigan matematik algoritmni yozish mumkin emas. Mana men yozishni xohlagan generic metod namunasi:
private static T Sum<T>(T num) where T : struct {
T sum = default(T) ;
for (T n = default(T) ; n < num ; n++)
sum += n;
return sum;
}
Men bu metodning kompilyatsiya bo'lishi uchun hamma narsani qildim — T ni struct ga cheklab, sum va n ni 0 ga initsializatsiya qilish uchun default(T) ishlatdim. Lekin bu kodni kompilyatsiya qilganimda, uchta xatolik oldim:
error CS0019: Operator '<' cannot be applied to operands of type 'T' and 'T'
error CS0023: Operator '++' cannot be applied to operand of type 'T'
error CS0019: Operator '+=' cannot be applied to operands of type 'T' and 'T'
Bu CLR ning generic qo'llab-quvvatlashidagi jiddiy cheklov va ko'plab dasturchilar (ayniqsa ilmiy, moliyaviy va matematik olamda) bu cheklovdan juda norozi. Ko'pchilik bu cheklovni chetlab o'tish uchun reflection (23-bobga qarang, "Assembly Yuklash va Reflection"), dynamic primitiv tur (5-bobga qarang), operator overloading va boshqalar ishlatishga harakat qilishdi. Lekin bularning barchasi jiddiy ishlash jarimalari yoki kodning o'qilishini qiyinlashtirishga olib keladi. Umid qilamiz, Microsoft buni CLR va kompilyatorlarning kelajakdagi versiyasida hal qiladi.
Xulosa
Ushbu bobda siz genericlar nima ekanligi, ular qanday ishlashi va ularning afzalliklarini o'rgandingiz. Genericlar tur xavfsizligi, toza kod va yaxshiroq ishlashni ta'minlaydi — ayniqsa value turlar bilan ishlashda boxing/unboxingni yo'q qiladi. FCL dagi generic kolleksiyalardan, generic interfeyslardan va generic delegatlardan foydalaning. Cheklovlar (constraints) genericlarni foydali qilib, tur parametrlariga qo'shimcha operatsiyalar bajarishga imkon beradi. Har doim in va out kalit so'zlarini delegat va interfeys tur parametrlariga iloji boricha qo'ying va kodingizda generic bo'lmagan kolleksiyalardan foydalanishdan saqlaning.