CodeGym /Kurslar /C# SELF /Kolleksiyadan elementin f...

Kolleksiyadan elementin foreach dövründə silinməsi

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

1. Giriş

C#-da proqramlaşdırmağa başlayan hər kəs gec-tez eyni problemlə qarşılaşır: bir kolleksiya var (məsələn, obyektlər siyahısı) və oradan hansısa şərtə görə lazımsız elementləri silmək lazımdır. Sanki asandır, əl avtomatik olaraq tanış və rahat foreach dövrünə gedir, axı bu ən “təhlükəsiz” və “dost” iterasiya yoludur. Amma birdən, ən gözlənilməz anda, əvvəllər sadə nümunələrdə olmayan qəribə bir runtime xətası çıxır və proqramın işi yerindəcə dayanır.

Gəlin baxaq, niyə belə olur, kolleksiya və iteratorların “qapağının altında” nə baş verir və elementləri necə düzgün silmək lazımdır ki, sürprizlər və buglar olmasın.

Niyə foreach ilə element silmək alınmır?

Təsəvvür elə, bir növbə insan var (bu bizim kolleksiyamızdır). Sən növbə ilə gedib hər kəsə soruşursan: "Səni saxlamaq yoxsa silmək?" Əgər sən dövr zamanı kimisə silməyə başlasan, bütün növbə yerini dəyişir, insanlar yerini dəyişir və sənin “növbəti adam — siyahıdakı növbəti” planın dərhal pozulur. Ola bilər ki, kimisə soruşmayasan, ya da kimisə iki dəfə soruşasan.

C#-da nümunə:


List<string> names = new List<string> { "Anton", "Boris", "Vika", "Grisha" };

foreach (string name in names)
{
    if (name.StartsWith("V"))
        names.Remove(name); // Bum! InvalidOperationException
}

Proqram "Vika"-ya çatanda və onu silmək istəyəndə, daxili iterator “reallıqla əlaqəni” itirir — və sən belə bir mesaj alırsan:
InvalidOperationException: Collection was modified; enumeration operation may not execute.

Bu sadəcə kapriz deyil — C# səni çətin tapılan buglardan və məlumat strukturunun pozulmasından qoruyur.

2. Niyə bu qədər sadə kod işləmir?

Bütün bunlar içəridə necə işləyir?

Sən foreach dövrü yazanda, kompilyator xüsusi bir obyekt — iterator (IEnumerator) yaradır, hansı ki, kolleksiyada cari mövqeyi izləyir. Bu obyekt başlanğıcda neçə element olduğunu, indi hansı elementin “aktiv” olduğunu yadda saxlayır və kolleksiya dövr zamanı dəyişməsin deyə ciddi nəzarət edir.

foreach içində element silmək və ya əlavə etmək cəhdi bu müqaviləni pozur. Niyə? Əgər sən elementləri siləndən sonra indekslər dəyişsə, iterator artıq növbəti elementə düzgün keçə bilməyəcək. Kimisə ötürə bilərsən, kimisə iki dəfə hesablaya bilərsən — nəticədə tam xaos ola bilər. Ona görə də .NET ilk dəyişiklikdə dərhal xəta atır.

“Birbaşa silmək” nəyə gətirib çıxarır

Təsəvvür elə belə bir proqram yazmısan:


List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
foreach (int x in numbers)
{
    if (x % 2 == 0)
        numbers.Remove(x);
}

Sanki hər şey məntiqlidir: bütün ədədlərdən keç, cütləri sil. Amma ikinci dövrdə proqram xəta atır — “kolleksiya dövr zamanı dəyişdirildi”.

Bəzən bu xəbərdarlığı “risk edib” keçmək istəyirsən. Amma xəta olmasa belə, kolleksiyanın strukturundan asılı olaraq nəticə proqnozlaşdırılmaz olar. Məsələn, təsadüfən bəzi elementləri “ötürə” bilərsən və ya lazım olanların hamısını silməyə bilərsən.

3. Bəs necə düzgün etmək olar?

Texnika №1: Tərsinə for dövrü

Məsələ ondadır ki, siləndə element “sağdakı”ları sola çəkir və əgər əvvəldən getsən, indeksləri qarışdırıb elementləri ötürə bilərsən. Bunun qarşısını almaq üçün sondan başlamaq ağıllıdır.


List<string> names = new List<string> { "Anton", "Boris", "Vika", "Grisha" };

for (int i = names.Count - 1; i >= 0; i--)
{
    if (names[i].StartsWith("V"))
        names.RemoveAt(i);
}

Bu nümunədə hər silmədən sonra silinən elementdən sonrakılar yerini dəyişir, amma hələ baxmadığımız indekslərə toxunulmur. Nəticədə heç nə ötürülməyəcək.

Texnika №2: Filtrlə, yeni siyahı yarat

Bəzən daha asan (və çox vaxt daha sürətli) olur ki, kolleksiyadan yalnız qalmalı olanları toplayıb, əsas siyahını yeni siyahı ilə əvəz edəsən.


var names = new List<string> { "Anton", "Boris", "Vika", "Grisha" };
names = names.Where(name => !name.StartsWith("V")).ToList();
// Nəticədə "Anton" və "Grisha" qalacaq

Bu üsul kolleksiya çox böyük deyilsə və obyektin əsas referansını saxlamaq vacib deyilsə yaxşıdır.

Texnika №3: Kolleksiya metodlarından istifadə et

Əgər klassik List<T> ilə işləyirsənsə, şərtə görə silmək üçün rahat metod var:


names.RemoveAll(name => name.StartsWith("V"));

Bütün proses içəridə düzgün işləyəcək və sən qısa və aydın kod alacaqsan.

Texnika №4: Silinməyə namizədləri topla

Elə kolleksiyalar var ki, “uçuşda” dəyişmək olmur (məsələn, Dictionary, HashSet və ya öz yazdığın sinif). Belə hallarda “silinməyə namizəd” üsulu işləyir:

  1. Əvvəlcə kolleksiyadan silmək istədiklərini ayrıca siyahıya topla.
  2. Sonra bu yeni siyahıdan keçib əsas kolleksiyadan sil.

Dictionary<int, string> dict = new Dictionary<int, string> { [1] = "bir", [2] = "iki", [3] = "üç" };
var toDelete = new List<int>();
foreach (var kvp in dict)
{
    if (kvp.Key % 2 == 0)
        toDelete.Add(kvp.Key);
}
foreach (var key in toDelete)
    dict.Remove(key);

4. Faydalı nüanslar

Yeni başlayanların səhvləri və mifləri

Ən yayılmış səhvlərdən biri — kolleksiyadan element silməyin dövr zamanı “nə isə işləyəcəyini” gözləməkdir, axı bəzi başqa dillərdə (məsələn, Python-da) bu tez-tez mümkündür. Amma C#-da bu qəti qadağandır — məhz sənin təhlükəsizliyin üçün: açıq xəta almaq səssiz və hiyləgər bugdan yaxşıdır, hansı ki, sonra heç kim tapa bilməz.

Başqa bir tipik səhv — for dövründə indeksi artırmaq, azaldan yox. Bu o deməkdir ki, siləndən sonra bütün sonrakı elementlər “yerini dəyişir” və bəziləri ötürüləcək. Həmişə sondan başla, əgər indekslə silirsənsə.

Hekayənin morali

“Kolleksiyadan elementləri şərtə görə silmək” tapşırığı C#-da yazılan hər ikinci proqramda var, amma bunu birbaşa foreach dövründə etmək olmaz — bu, dilin arxitekturasıdır və məlumatlarının bütövlüyü və gözlənilməz xətaların olmaması üçün belədir.
Bu qaydanı yadda saxla, səni debuggerlə gecə oyaq qalmaqdan xilas edəcək.

Həmişə düzgün necə etmək olar

  • Heç vaxt foreach dövründə birbaşa kolleksiyadan element silmə. Bu runtime xətası verəcək.
  • Siyahılar (List<T>) və massivlər üçün ya sondan for dövrü, ya da RemoveAll və LINQ ilə filtrasiya istifadə et.
  • Sözlüklər, setlər və digər mürəkkəb kolleksiyalar üçün — əvvəlcə silinməli elementləri topla, sonra bu siyahıdan keçib əsas kolleksiyadan sil.
  • Əmin deyilsənsə — düşün: kolleksiya siləndə necə dəyişir? Iterator necə davranacaq? Ən kiçik şübhə varsa, deməli, üsul düzgün deyil.
1
Sorğu/viktorina
, səviyyə, dərs
Əlçatan deyil
Elementlərin filtrlenməsi
Kolleksiyalarla işləmək
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION