1. Giriş
Yəqin ki, artıq hiss etmisən, C#-da müxtəlif kolleksiyaların oxşar cəhətləri var. Məsələn, demək olar ki, istənilən kolleksiyanı foreach dövrü ilə gəzə bilərik:
var names = new List<string> { "Anya", "Boris", "Vika" };
// Diqqət — kod həm List, həm massiv, hətta HashSet ilə işləyir!
foreach (var name in names)
{
Console.WriteLine(name);
}
Bunun sirri nədir? Bütün müasir kolleksiyalar IEnumerable<T> interfeysini reallaşdırır — yəni bu bir "kontrakt"dır, hansı ki, kolleksiya elementləri bir-bir, ardıcıl qaytarmağı bacarır.
Təsəvvür elə bir şirkət var, orda istənilən işçi kolleksiyası üçün (istər şöbə, istər layihə komandası, istər korporativ tədbir iştirakçıları siyahısı) belə bir qayda var: əgər obyekt "IEnumerable" interfeysini reallaşdırırsa, deməli, sən həmişə bütün işçiləri müəyyən ardıcıllıqla gəzə bilərsən, fərqi yoxdur "onlar necə saxlanılır", əsas odur ki, onları bir-bir əldə etmək olur.
2. IEnumerable<T> interfeysi — içində nə var?
Gəlin baxaq, bu interfeys .NET-də necə görünür:
public interface IEnumerable<out T>
{
IEnumerator<T> GetEnumerator();
}
Burda cəmi bir metod var — GetEnumerator, hansı ki, IEnumerator<T> tipində obyekt qaytarır. Bu obyekt "sayma" prosesinə cavabdehdir: o bilir, indi hansı elementdəyik, növbətiyə necə keçmək olar, nə vaxt bitir.
Qısa desək: əgər bir class (və ya kolleksiya) IEnumerable<T> reallaşdırırsa, deməli, onu foreach dövründə gəzə bilərsən, elementləri bir-bir əldə edə bilərsən — və fərqi yoxdur, içəridə necə qurulub: istər siyahı, istər hash-table, istərsə də hard diskdə saxlanılsın.
Mini-sxem
Kolleksiya (məsələn, List<T>)
|
v
IEnumerable<T>
|
v
IEnumerator<T> (əsl "sayan" obyekt)
3. Praktika və kodun universallığı
Belə kontraktın böyük üstünlüyü — universallıqdır. Əgər funksiya və ya metod input kimi IEnumerable<T> qəbul edirsə, deməli, o istənilən kolleksiya tipi ilə işləyə bilər — massivlərdən və siyahılardan tutmuş hash-set, queue və hətta öz kolleksiyan da daxil!
Məsələn, elementlərin cəmini hesablamaq üçün funksiya:
// İstənilən IEnumerable<int> dəstəkləyən int toplularının cəmini hesablamaq olar
int Sum(IEnumerable<int> numbers)
{
int result = 0;
foreach (var n in numbers)
result += n;
return result;
}
// List<int> ilə işləyir
var list = new List<int> { 1, 2, 3 };
Console.WriteLine(Sum(list));
// Massivlə də işləyir!
int[] array = { 4, 5, 6 };
Console.WriteLine(Sum(array));
// Hətta filtrasiya edən metodun nəticəsi ilə də işləyir
Console.WriteLine(Sum(list.Where(x => x % 2 == 0))); // LINQ istifadə edirik
Həyatdan nümunə: universal metodlar
Təsəvvür elə, sənə istənilən string kolleksiyasında ən uzun sözü tapmaq üçün utilit yazmaq lazımdır. IEnumerable<string> sayəsində bunu istənilən mənbə üçün edə bilərsən — massiv, siyahı, filtrasiya metodlarının nəticəsi və s.:
string FindLongest(IEnumerable<string> words)
{
string longest = "";
foreach (var word in words)
if (word.Length > longest.Length)
longest = word;
return longest;
}
İstənilən uyğun toplu ilə istifadə etmək olar.
4. Niyə foreach dövrü IEnumerable<T> ilə işləyir?
Tez-tez verilən sual: ümumiyyətlə, niyə foreach dövrü istənilən kolleksiyanı "başa düşür"? Cavab sadədir: C# kompilyatoru class-da GetEnumerator metodunu axtarır və gözləyir ki, elə bir obyekt qaytaracaq ki, orda MoveNext() metodu və Current property-si var. Bu da standart interfeysdir — IEnumerator<T>.
Belə yanaşma sayəsində, hətta öz class-ını yaza bilərsən ki, elementləri ardıcıl "verir" (məsələn, Fibonacci ardıcıllığı generatoru), və əgər o IEnumerable<int> reallaşdırırsa, onunla foreach dövründə adi siyahı kimi işləmək olar.
5. Enumerator nədir və necə işləyir?
İstənilən IEnumerable<T> reallaşdıran kolleksiyanın içində xüsusi bir “sayacı” gizlənir — Enumerator (və ya elmi dildə: IEnumerator<T> interfeysini reallaşdıran obyekt). Məhz o, foreach dövründə kolleksiyanın elementlərini bir-bir "daşıyır".
IEnumerator<T> interfeysi
Standart Enumerator bunları bacarır:
public interface IEnumerator<T> : IDisposable
{
T Current { get; } // Cari element
bool MoveNext(); // Növbəti elementə keç
void Reset(); // Əvvələ qayıt (çox az istifadə olunur)
}
İçəridə necə işləyir?
- MoveNext() — "göstəricini" növbəti elementə aparır və true qaytarır, əgər element varsa. Əgər element qalmayıbsa — false qaytarır.
- Current — cari elementi verir (indi hansı elementdədirsə).
- Reset() — Enumerator-u əvvələ qaytarır (praktikada demək olar ki, istifadə olunmur).
- Dispose() — resursları azad edir (fayllarla və ya şəbəkə ilə işləyən kolleksiyalar üçün lazımdır).
Nümunə: foreach "arxa planda" necə işləyir
Sən belə yazanda:
var numbers = new List<int> { 1, 2, 3 };
foreach (var n in numbers)
Console.WriteLine(n);
Əslində kompilyator bunu təxminən belə kod halına salır:
var numbers = new List<int> { 1, 2, 3 };
// "Sayacı" alırıq
var enumerator = numbers.GetEnumerator();
while (enumerator.MoveNext())
{
var n = enumerator.Current;
Console.WriteLine(n);
}
// Kompilyator avtomatik Dispose() çağırır using blokunda (əgər enumerator IDisposable reallaşdırırsa)
Vacibdir: əgər kolleksiya xarici resurslarla işləyirsə (məsələn, fayllar, verilənlər bazası), Enumerator sayma bitəndə onları avtomatik azad edə bilər.
Vizual sxem
Başlanğıc -> GetEnumerator() -> Enumerator
|
v
MoveNext() -> Current
|
v
MoveNext() -> Current
|
...
|
v
MoveNext() == false -> son
6. IEnumerable və massivlər, siyahılar, set-lər: kim kimə qohumdur?
Gəlin baxaq, .NET-in hansı standart konteynerləri bu interfeysi reallaşdırır:
| Kolleksiya tipi | IEnumerable<T> reallaşdırır? | foreach dövrü ilə gəzilə bilər? |
|---|---|---|
Massiv () |
✅ | ✅ |
|
✅ | ✅ |
|
✅ (cütlüklər, açarlar, dəyərlər üzrə) | ✅ |
|
✅ | ✅ |
|
✅ | ✅ |
|
✅ | ✅ |
Hətta string (string) də adi IEnumerable reallaşdırır, yəni hər bir simvol üzrə də gəzə bilərsən.
7. Öz Enumerable-ını reallaşdırmaq
Maraqlı tapşırıq: gəlin minimal kolleksiya düzəldək, hansı ki, 0-dan N-ə qədər cüt ədədləri saxlayır və IEnumerable<int> reallaşdırır. Onda onu dövr ilə gəzə və LINQ ilə istifadə edə bilərik.
// "Gəzilə bilən" kolleksiya class-ı
class EvenNumbers : IEnumerable<int>
{
private int max;
public EvenNumbers(int max)
{
this.max = max;
}
public IEnumerator<int> GetEnumerator()
{
for (int i = 0; i <= max; i += 2)
yield return i; // Enumerator üçün xüsusi magiya
}
// Geri uyğunluq üçün qeyri-generik IEnumerable interfeysinin açıq reallaşdırılması
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
// İstifadə:
var evens = new EvenNumbers(10);
foreach(var e in evens)
Console.Write($"{e} "); // 0 2 4 6 8 10
Əsas məqam: əgər class-ın IEnumerable<T> reallaşdırırsa — avtomatik olaraq onu .NET-in bir çox aləti ilə uyğun edirsən: LINQ, foreach, enumerable qəbul edən metodlar və s.
8. Tipik səhvlər və nüanslar
Bəzən təcrübəsiz proqramçılar düşünür ki, IEnumerable<T> — ayrıca bir kolleksiyadır, hansı ki, elementləri var. Əslində — bu sadəcə "söz"dür ki, əgər biz saymağa başlasaq, bizə elementləri bir-bir "verəcəklər".
Əgər sənə təsadüfi indekslə müraciət lazımdırsa (myList[5]), Add və ya Remove kimi metodlardan istifadə etmək istəyirsənsə — IEnumerable<T> interfeysi burada kömək etməyəcək. Bu yalnız ardıcıl keçid üçündür!
Səhv: kolleksiyanı sayarkən dəyişməyə çalışmaq. Məsələn:
foreach (var item in myList)
{
if (item < 0)
myList.Remove(item); // TƏHLÜKƏLİ! InvalidOperationException
}
Daha yaxşısı, əvvəlcə silmək üçün ayrıca siyahı düzəltmək, ya da yeni kolleksiya yaradan metodlardan istifadə etməkdir:
// Təhlükəsiz yol — yeni kolleksiyanı əl ilə düzəldirik
var newList = new List<int>();
foreach (var item in myList)
{
if (item >= 0)
newList.Add(item);
}
myList = newList;
// Və ya indekslərlə tərsinə silirik
for (int i = myList.Count - 1; i >= 0; i--)
{
if (myList[i] < 0)
myList.RemoveAt(i);
}
GO TO FULL VERSION