1. Giriş
Təsəvvür elə: uzun, gözəl bir LINQ-sorgusu yazmısan və düşünürsən — «Voila, indi hər şey işləyəcək!» Amma sonra görürsən ki, heç nə baş vermir, ta ki elementləri saymayana və ya nəticəni massivə çevirməyənə qədər. Bu bug deyil, ficiadır.
.NET-də LINQ gecikdirilmiş icra istifadə edir: sorğu o vaxta qədər işə düşmür ki, sən həqiqətən datanı çıxarmağa başlamırsan. Bu, tənbəl ofisiant kimidir — mətbəxə qaçmayacaq, ta ki eşitməsin: «Yemək gətir!», sifariş artıq verilsə də.
Bu davranış, lazy evaluation (Lazy Evaluation) kimi tanınır və LINQ-u böyük, potensial sonsuz və ya resurs tələb edən data mənbələri ilə işləyərkən xüsusilə effektiv edir.
Gecikdirilmiş icra o deməkdir ki, LINQ-sorgusu təyin olunan kimi icra olunmur. O, yalnız sən həqiqətən kolleksiyadakı datanı dövr etməyə və ya baxmağa başlayanda işləməyə başlayır.
Misal-illüstrasiya
var numbers = new List<int> { 1, 2, 3, 4, 5 };
// LINQ-sorgusu
var query = numbers.Where(n =>{
Console.WriteLine($"Yoxlanılır {n}");
return n % 2 == 0;
});
Console.WriteLine("Sorğu təyin olundu, amma ədədlər yoxlanılmadı!");
// Ancaq indi dövr başlayır!
foreach (var n in query)
{
Console.WriteLine($"Cüt tapıldı: {n}");
}
Nə baş verəcək?
Bu kodu işə salanda görəcəksən ki, foreach dövrünə qədər konsola heç nə çıxmır — hətta sənin Where-dəki şərtlər də işləmir. Sən elementləri dövr etməyə başlayanda (məsələn, foreach ilə), sorğu icra olunmağa başlayır.
Bax, bu deferred execution-dur — istəməsən, heç kim heç nə eləmir!
2. Gecikdirilmiş icra nəyə lazımdır?
Gecikdirilmiş icra kodu təkcə şık yox, həm də çox effektiv edir. Niyə hansısa işi qabaqcadan görəsən ki, bəlkə nəticə heç lazım olmayacaq? Bu xüsusilə vacibdir, əgər böyük kolleksiyalarla və ya hələ gələn data axınları ilə işləyirsənsə — təsəvvür elə, mənbə sonsuz ola bilər. Bunu hamısını bir anda yaddaşa yükləmək sadəcə məntiqsizdir.
Bundan başqa, gecikdirilmiş icra sorğuları sərbəst birləşdirməyə və artırmağa imkan verir. Sən mürəkkəb LINQ-əməliyyatları zənciri qura bilərsən, narahat olmadan ki, onlar dərhal işləyəcək. Hər şey yalnız sən həqiqətən datanı almağa başlayanda baş verəcək — və bir dəqiqə tez yox.
Bənzətmə
Gecikdirilmiş icra — telefonda alış-veriş siyahısı kimidir: istədiyin qədər məhsul əlavə edə və redaktə edə bilərsən, amma mağazaya yalnız nəsə almağa hazır olanda gedirsən (və yalnız onda bu siyahı ilə işləyirsən).
3. Bu necə işləyir?
IEnumerable<T> qaytaran LINQ-sorguları adətən iteratorlar (yield return) və ya xüsusi tənbəl konstruksiyalarla reallaşdırılır. Hər dəfə kolleksiyanı dövr etməyə başlayanda (məsələn, foreach və ya ToList() çağıranda), sorğu yenidən işə düşür.
Vacib müşahidə
Əgər sən sorğunu təyin etdikdən sonra ilkin kolleksiyanı dəyişsən, yeni və dəyişmiş datalar da nəticəyə düşəcək.
Misal:
var numbers = new List<int> { 1, 2, 3 };
var query = numbers.Where(n => n > 1);
numbers.Add(4); // yeni ədəd əlavə olundu
foreach (var n in query)
{
Console.WriteLine(n); // çıxacaq 2, 3, 4
}
Sxem: LINQ-sorgu nə vaxt icra olunur?
flowchart TD
A[LINQ-sorgunun təyini] --> B{Sorğu icra olunur?}
B -- Yox --> C[Gözləmə]
B -- Bəli (məsələn, foreach, ToList) --> D[Sorğunun icrası]
D --> E[Nəticə və ya əməliyyat]
4. LINQ-un "tənbəlliyi" rol oynayan nümunələr
Tənbəl filtrasiya
var bigNumbers = Enumerable.Range(1, 1_000_000_000)
.Where(n => n % 123_456 == 0);
foreach (var n in bigNumbers.Take(5))
{
Console.WriteLine(n);
}
Burda nə baş verir?
Sorğu potensial olaraq milyardlıq kolleksiya yaradır, amma realda yalnız 5 ədəd filtrlənib qaytarılır! Qalan heç nə hesablanmır və yaddaşı tutmur.
Daxili sorğular və tranzaksiyalar
Tutaq ki, bizdə sifarişlər və məhsullar siyahısı var və biz müəyyən məhsulu olan ilk 5 sifarişi almaq istəyirik.
var orders = GetBigOrderList(); // təsəvvür elə, minlərlə sifariş var
var filtered = orders
.Where(order => order.Products.Any(p => p.Name == "Kofe"))
.Take(5);
foreach(var o in filtered)
{
Console.WriteLine(o.Id);
}
LINQ qalan sifarişlərə baxmır, 5 uyğun tapanda dayanır!
5. Gözlənilməz hallar
Gecikdirilmiş icra bəzən belə gözlənilməz hallara səbəb ola bilər:
Sorğunun təyinindən sonra mənbə datanın dəyişməsi
Yuxarıda göstərildiyi kimi, əgər sorğunu təyin etdikdən sonra ilkin kolleksiya dəyişsə, sorğu yeni datanı da görəcək.
Bir sorğunu dəfələrlə dövr etmək
LINQ-sorgu hər dəfə dövr edəndə yenidən icra olunur.
var query = numbers.Where(n => {
Console.WriteLine($"Yoxlanılır {n}");
return n % 2 == 0;
});
foreach(var n in query) {} // birinci dövr
foreach(var n in query) {} // ikinci dövr — yenə hər şey hesablanacaq
Əgər sənə təkrarlanan nəticə lazımdırsa — materializasiya et (.ToList() və ya .ToArray() ilə).
6. Hansı LINQ-sorğular GECİKDİRİLMİŞ deyil?
Bütün LINQ əməliyyatları gecikdirilmiş deyil. Bəzi metodlar dəqiq ("acgöz") icra edir (Immediate Execution). Məsələn:
- .ToList()
- .ToArray()
- .Count()
- .Average()
- .Sum()
- .First(), .Last(), .Single()
Bunlar hamısı LINQ-u dərhal icra etdirir, çünki IEnumerable yox, hazır nəticə qaytarır.
Misal:
var query = numbers.Where(n => n > 2);
var result = query.ToList(); // Burda sorğu dərhal icra olunur!
7. "Lazy evaluation" anlayışı (Lazy Evaluation)
Gecikdirilmiş icra (deferred execution) — .NET-də lazy evaluation-un bir konkret nümunəsidir (Lazy Evaluation).
Lazy evaluation — nəticə yalnız həqiqətən lazım olanda hesablanır. LINQ-dan başqa, C#-da lazy evaluation üçün başqa mexanizmlər də var.
Lazy<T> sinfi
C# xüsusi Lazy<T> tipi verir ki, dəyərləri "tələbə görə" yaradasan.
Sadə misal:
// Tənbəl ədəd yaradırıq, yalnız istəyəndə hesablanacaq
var lazyValue = new Lazy<int>(() =>
{
Console.WriteLine("Dəyəri hesablayıram!");
return 42;
});
Console.WriteLine("Lazy-obyekt yaradıldı, amma dəyər hesablanmadı");
Console.WriteLine($"Dəyər: {lazyValue.Value}"); // bax, burda hesablanacaq
Bütün bunlar nəyə lazımdır?
Məsələn, nadir lazım olan bir parametr saxlayırsan və onu hesablamaq uzun və ya resurs tələb edir.
8. Cədvəl: LINQ metodları və icra rejimi
| Metod | Gecikdirilmiş icra | Dərhal icra |
|---|---|---|
|
✅ | ❌ |
|
✅ | ❌ |
|
✅ | ❌ |
|
✅ | ❌ |
|
✅ | ❌ |
|
❌ | ✅ |
|
❌ | ✅ |
|
❌ | ✅ |
|
❌ | ✅ |
9. Tipik səhvlər və reallaşdırma xüsusiyyətləri
Təkrar dövrlərdən "artıq iş"
Gecikdirilmiş icra hər dövrdə yeni hesablama aparır, bəzən artıq hesablama ala bilərsən.
var expensiveQuery = bigList.Where(x => SomeHeavyCalculation(x));
var result1 = expensiveQuery.ToList(); // hesablandı — bir
var result2 = expensiveQuery.ToList(); // hesablandı — iki (eyni nəticə, amma vaxt yenidən gedir)
Çarəsi datanı bir dəfə materializasiya etməkdir:
var cached = expensiveQuery.ToList();
"Səhvi tut, əgər bacarsan"
Əgər sorğudakı funksiya istisna atırsa, o yalnız kolleksiya dövr ediləndə baş verəcək, sorğu təyin olunanda yox.
Dövr zamanı kolleksiyanı dəyişmək
Dövr etdiyin kolleksiyanı dəyişmək InvalidOperationException istisnasına səbəb ola bilər.
GO TO FULL VERSION