CodeGym /Kurslar /C# SELF /Dəyişənlərin ələ keçirilməsi (

Dəyişənlərin ələ keçirilməsi ( Closures)

C# SELF
Səviyyə , Dərs
Mövcuddur

1. Closure nədir?

Proqramlaşdırmada closure — xarici kontekstdən dəyişənləri ələ keçirən funksiyadır. Sadə desək, əgər lambda-expression və ya anonim metod öz bədənindən kənarda elan olunmuş dəyişənlərdən istifadə edirsə, bu funksiya closure olur. O, həmin dəyişənlərin yaradıldığı anda hansı dəyərlərə malik olduğunu “yadda saxlayır”.

Həyatdan analoqiya:
Tutaq ki, siz sirli resepti vərəqə yazıb üzərinə qoyub zərfə salmısınız. Özünüz vərəqi itirsəniz belə (dəyişən birbaşa əlçatan olmaz), zərfdə olan kəs (lambda) hələ də həmin reseptə çıxışa malikdir.

Sadə nümunə:

int x = 42;
Func<int> getX = () => x;
Console.WriteLine(getX()); // 42

Burada getX closure-dir, çünki o, özündən kənarda elan olunmuş x dəyişənindən istifadə edir.

2. Dəyişənlərin ələ keçirilməsi niyə vacibdir?

C#-da closure-lar demək olar hər yerdə istifadə olunur:

  • Kolleksiyalarda və LINQ-sorgularında
  • Eventlərə və ya asynchronous metodlara parametr ötürmək üçün
  • Dövr daxilində event handlerlər yaratarkən
  • Müxtəlif çağırışlar arasında “kontekst” saxlamaq üçün

Closure olmasa, bir çox standart C# praktikaları mümkün olmazdı və ya çox əlverişsiz görünərdi.

Həqiqi həyat nümunəsi

Tutaq ki, siz xatırlatma tətbiqi hazırlayırsınız: istifadəçi bir sıra xatırlatmalar təyin edir və müəyyən vaxtda (dəqiqə, saat, həftə sonra...) uyğun mesaj göstərilməlidir. Handler-ə lambda ötürmək və onun nəyi xatırlayacağını “yadda saxlaması” asandır. Budur dəyişikliyin ələ keçirilməsi — klassik nümunə.

3. C# dəyişənləri necə ələ keçirir

Arxa planda C# maraqlı bir trik edir: əgər sizin lambda-expression xarici dəyişənlərdən istifadə edirsə, kompilyator avtomatik olaraq köməkçi sinif yaradır — display class. Bütün “ələ keçirilmiş” dəyişənlər bu sinifin sahələri olurlar.

Şematik qaydada belə görünür:


Xarici dəyişən ──► DisplayClass
                         ▲
                         │
                    Closure (lambda)

Kod üzrə illüstrasiya

“Qapağın altında” nə baş verir:

int x = 5;
Func<int> f = () => x;
// Burada kompilyator təxminən belə edir:
class DisplayClass
{
    public int x;
    public int Lambda() => x;
}
DisplayClass display = new DisplayClass();
display.x = 5;
Func<int> f = display.Lambda;

Bu izah edir ki, niyə closure dəyişənin elan olunduğu blokdan çıxdıqdan sonra belə onun aktuallığını görməyə davam edir.

4. Dəyişənin dəyəri “donur” yoxsa dəyişir?

C#-da dəyişənlər referansla ələ keçirilir, dəyər üzrə yox. Bu o deməkdir ki, əgər lambda-expression bir dəyişəndən istifadə edir və bu dəyişən başqa yerdə dəyişdirilərsə — lambda yeni dəyəri görəcək.

Nümunə:

int x = 10;
Func<int> getX = () => x;

x = 20;
Console.WriteLine(getX()); // 20, 10 deyil!

Tələbələr çox vaxt gözləyirlər ki, getX() həmişə 10 qaytaracaq, çünki dəyişən “ələ keçirilib”. Amma əslində lambda hələ də mövcud olan və dəyişilə bilən dəyişəni oxuyur.

Stabil dəyər nə vaxt alınır?

Əgər dəyişən dövrdə yeni scope ilə elan olunursa, məsələn foreach ilə və hər iterasiyada yeni dəyişən yaradılırsa — lambda cari dəyəri “yadda saxlayacaq”.

5. Nümunələr: dövrdə closure — tipik tələlər

Tez-tez rast gəlinən səhv

Hər birinin öz nömrəsini çapa verən delegat massiv yaratmaq istəyirik:

Action[] actions = new Action[5];
for (int i = 0; i < 5; i++)
{
    actions[i] = () => Console.WriteLine(i);
}
foreach (var action in actions)
    action();

Proqram nə çap edəcək?

5
5
5
5
5

Vay! Niyə 0,1,2,3,4 deyil?

Səbəb:
Lambda eyni i dəyişənini ələ keçirir və dövr keçdikcə o dəyişməyə davam edir. Siz delegatları sonra çağıranda i artıq 5-dir.

Necə düzgün etmək olar?

Hər iterasiya üçün dövr daxilində yeni dəyişən yaratmaq lazımdır:

Action[] actions = new Action[5];
for (int i = 0; i < 5; i++)
{
    int index = i; // Hər iterasiya üçün yeni dəyişən!
    actions[i] = () => Console.WriteLine(index);
}
foreach (var action in actions)
    action();

İndi proqram çap edəcək:

0
1
2
3
4

Bu display class ilə necə bağlıdır?

Birinci variantda bütün delegatlar eyni sahəyə “bağlanır” — ona görə nəticə eynidir. İkinci halda hər iterasiya üçün yeni lokal dəyişən yaradılır və hər delegat üçün unikal dəyərə malik ayrıca DisplayClass formalaşır.

6. Dəyişənlərin ələ keçirilməsinin praktik istifadə ssenariləri

Nümunə 1: “Kontekst” ilə event işlənməsi

Tutaq ki, kiçik tətbiqimizdə tapşırıqlar siyahısı var və hər birinə “icra et” düyməsi üçün handler bağlıdır. Lambda handler-in içində hansı tapşırığı icra edəcəyini “yadda saxlamalıdır”:

foreach (var task in tasks)
{
    button.Click += (sender, e) => CompleteTask(task);
}

Burada task dəyişəni hər iterasiyada ələ keçirilir. Dövrdə yuxarıda göstərilən tələyə düşməmək üçün onun düzgün şəkildə dövr daxilində elan olunduğuna əmin olmaq vacibdir.

Nümunə 2: Asinxron əməliyyatlar

Tez-tez closure asinxron məntiqə parametr ötürmək üçün istifadə olunur — məsələn, asinxron tapşırıq başladarkən dəyişəni lokal “slota” saxlamaq:

for (int i = 0; i < 3; i++)
{
    int index = i; // Mütləq!
    Task.Run(() => Console.WriteLine($"Task #{index}"));
}

Lokal dəyişən olmasa, bütün tapşırıqlar eyni nömrəni çap edəcək, adətən istənilən nəticə bu deyil.

Nümunə 3: LINQ-sorgular

LINQ kolleksiyalara tez-tez closure istifadə edir ki, xarici sahələrdən asılı olaraq elementləri filter və ya transformasiya etsin. Məsələn:

string prefix = "Task";
var filtered = tasks.Where(t => t.Name.StartsWith(prefix));

Burada Where-dəki lambda prefix dəyərini yadda saxlayıb StartsWith-i çağırır.

7. Closure-larla işləyərkən xüsusiyyətlər, məhdudiyyətlər və tipik səhvlər

Səhv №1: dövrdə bütün delegatlar üçün bir dəyişəndən istifadə.
Əgər dövrdə bütün delegatlar eyni dəyişənə referans verirsə, nəticə gözlənilməz olacaq. Hər delegat üçün dövr daxilində yeni lokal dəyişən yaratmaq və ümumi referansdan qaçmaq vacibdir.

Səhv №2: metoddan kənar dəyişənlərə closure.
Əgər closure class sahəsini və ya cari metoddan kənarda elan olunmuş dəyişəni ələ keçirirsə, o dəyişənə referans saxlanılacaq. Bu, heap-də obyektlərin GC tərəfindən azad edilməməsinə gətirib çıxara bilər.

Səhv №3: uzunömürlü delegatlar və closure-lar.
Əgər closure ilə delegat uzun müddət saxlanılır (məsələn, static sahədə), ona referans verən dəyişənlər də gözləniləndən daha uzun müddət yaddaşda qalacaq. Bu çox vaxt gizli yaddaş sızmasına və performans problemlərinə səbəb olur.

1
Sorğu/viktorina
, səviyyə, dərs
Əlçatan deyil
Lambda-ifadələr
Lambda-ifadələrin sintaksisi
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION