C # 'ın bir foreach içinde değişkeni yeniden kullanmasının bir nedeni var mı?


1684

C # 'da lambda ifadeleri veya anonim yöntemler kullanırken , değiştirilmiş kapatma tuzağına erişim konusunda dikkatli olmalıyız . Örneğin:

foreach (var s in strings)
{
   query = query.Where(i => i.Prop == s); // access to modified closure
   ...
}

Değiştirilen kapatma nedeniyle, yukarıdaki kod Where, sorgudaki tüm yan tümcelerin son değerine dayanmasına neden olur s.

As açıkladı buradan çünkü bu durumda, sdeğişken beyan foreachyukarıdaki derleyici böyle çevrilmiştir döngü:

string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}

bunun yerine:

while (enumerator.MoveNext())
{
   string s;
   s = enumerator.Current;
   ...
}

Belirttiği gibi burada , döngü dışında bir değişkeni bildirmek için herhangi bir performans avantajları vardır ve döngü kapsamı dışında değişkeni kullanmayı planlıyorsanız normal şartlar altında tek nedeni budur yapmak için düşünebilirsiniz:

string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}
var finalString = s;

Ancak bir foreachdöngüde tanımlanan değişkenler döngü dışında kullanılamaz:

foreach(string s in strings)
{
}
var finalString = s; // won't work: you're outside the scope.

Böylece derleyici, değişkeni, algılanabilir fayda üretmezken, genellikle bulunması ve hata ayıklaması zor olan bir hataya son derece eğilimli olacak şekilde bildirir.

foreachBu şekilde döngülerle yapabileceğiniz bir şey var mı, iç kapsamlı bir değişkenle derlenmişlerse yapamazsınız mı, yoksa bu sadece anonim yöntemler ve lambda ifadeleri kullanılabilir veya yaygın olandan önce yapılan keyfi bir seçim mi? O zamandan beri revize edilmedi mi?


4
Sorun nedir String s; foreach (s in strings) { ... }?
Brad Christie

5
@BradChristie OP gerçekten hakkında foreachdeğil, OP tarafından gösterilene benzer bir kodla sonuçlanan lamda ifadeleri hakkında ...
Yahia

22
@BradChristie: Bu derleniyor mu? ( Hata: Tür ve tanımlayıcının her ikisi de benim için bir foreach ifadesinde gereklidir )
Austin Salonen

32
@ JakobBotschNielsen: Bir lambda'nın üstü kapalı bir yerel; neden yığının üzerinde olacağını sanıyorsun? Ömrü yığın çerçevesinden daha uzundur !
Eric Lippert

3
@EricLippert: Kafam karıştı. Lambda'nın foreach değişkenine (dahili olarak döngü dışında bildirildi ) bir referans yakaladığını anlıyorum ve bu nedenle son değerine kıyasla karşılaştırma yapıyorsunuz; anladım. Ne anlamıyorum döngü içinde değişken bildirmek nasıl herhangi bir fark olacaktır. Derleyici yazarı açısından, bildirimin döngü içinde veya dışında olmasına bakılmaksızın yığın üzerinde yalnızca bir dize başvurusu (var 's') ayırıyorum; Kesinlikle her yinelemeyi yığının üzerine yeni bir referans göndermek istemem!
Anthony

Yanıtlar:


1407

Derleyici, değişkeni, algılanabilecek hiçbir fayda üretmezken, genellikle bulunması ve hata ayıklaması zor olan bir hataya oldukça eğilimli olacak şekilde bildirir.

Eleştiriniz tamamen haklı.

Bu sorunu burada ayrıntılı olarak tartışıyorum:

Zararlı sayılan döngü değişkeninin kapatılması

Her şekilde foreach döngüleri ile yapabileceğiniz bir şey var mı? ya da bu sadece anonim yöntemler ve lambda ifadeleri kullanıma sunulmadan ya da yaygınlaştırılmadan yapılan ve o zamandan beri revize edilmemiş keyfi bir seçim mi?

İkincisi. C # 1.0 spesifikasyonu aslında loop değişkeninin loop gövdesinin içinde mi yoksa dışında mı olduğunu söylemedi, çünkü gözlemlenebilir bir fark yaratmadı. Kapatma semantiği C # 2.0'da tanıtıldığında, döngü değişkenini "for" döngüsüyle tutarlı olarak döngü dışına koymak için seçim yapıldı.

Herkesin bu karardan pişman olduğunu söylemenin adil olduğunu düşünüyorum. Bu C # en kötü "gotchas" biridir ve biz bunu düzeltmek için kırılma değişikliği alacak. C 5. yılında foreach döngüsü değişken mantıksal olacak döngü gövdesinde ve bu nedenle kapanışları taze kopyaya her zaman alacak.

forDöngü değiştirilmeyecek ve değişiklik olmayacak C # önceki sürümleri "geri taşıdık". Bu nedenle, bu deyimi kullanırken dikkatli olmaya devam etmelisiniz.


177
Aslında C # 3 ve C # 4'te bu değişikliği geri aldık. C # 3'ü tasarladığımızda, (zaten C # 2'de var olan) sorunun daha da kötüye gideceğini fark ettik çünkü çok fazla lambda (ve sorgu LINQ sayesinde foreach döngülerinde kılık değiştirmiş lambdaslar. Sorunun C # 3'te düzeltmek yerine, çok geç düzeltmeyi garanti etmek için yeterince kötü olmasını beklediğimize
üzüldüm

75
Ve şimdi hatırlamak zorundayız foreach'güvenli' ama fordeğil.
Lepie

22
@michielvoo: Değişiklik geriye dönük uyumlu olmadığı anlamında kırılıyor. Eski bir derleyici kullanılırken yeni kod düzgün çalışmaz.
leppie

41
@Benjol: Hayır, bu yüzden almaya hazırız. Jon Skeet bana önemli bir kırılma değişikliği senaryosuna dikkat çekti, bu da birisi C # 5'de kod yazıyor, test ediyor ve daha sonra hala doğru olduğuna inanan C # 4 kullanan insanlarla paylaşıyor. Umarım böyle bir senaryodan etkilenen insan sayısı azdır.
Eric Lippert

29
Bir yana, ReSharper bunu her zaman yakaladı ve "değiştirilmiş kapanışa erişim" olarak rapor etti. Ardından, Alt + Enter tuşlarına basarak kodunuzu sizin için otomatik olarak düzeltir mi? jetbrains.com/resharper
Mike Chamberlain

191

Ne soruyorsun iyice onun blog yazısı Eric Lippert kapsamındadır döngü değişkeni üzerinde Kapanış zararlı kabul ve devam filmi.

Benim için en ikna edici argüman, her yinelemede yeni değişkene sahip olmanın for(;;)stil döngüsüyle tutarsız olacağıdır. int iHer bir yinelemesinde yeni olmasını bekler misiniz for (int i = 0; i < 10; i++)?

Bu davranışla ilgili en yaygın sorun yineleme değişkeni üzerinde bir kapatma yapmaktır ve kolay bir geçici çözümü vardır:

foreach (var s in strings)
{
    var s_for_closure = s;
    query = query.Where(i => i.Prop == s_for_closure); // access to modified closure

Bu sorunla ilgili blog yazım: C # 'da foreach değişkeni üzerinden kapanış .


18
Sonuçta, insanların bunu yazarken gerçekten istedikleri şey, birden fazla değişkene sahip olmak değil, değerin üzerine kapanmaktır . Genel durumda bunun için kullanılabilir bir sözdizimi düşünmek zor.
Random832

1
Evet, değerle kapatmak mümkün değil, ancak cevabımı eklemek için düzenlediğim çok kolay bir çözüm var.
Krizz

6
Referanslar üzerinde C # yakın kapanışları çok kötü. Varsayılan olarak değerleri kapatırlarsa, yerine değişkenlerin kapatılmasını kolayca belirleyebiliriz ref.
Sean U

2
@ Kriz, zorlanmış tutarlılığın tutarsız olmaktan daha zararlı olduğu bir durumdur. İnsanların beklediği gibi "sadece çalışmalı" ve değiştirilmiş kapanış sorununa (kendim gibi) erişmeden önce sorunları vuran insanların sayısı göz önüne alındığında, for döngüsünün aksine foreach kullanırken farklı bir şey beklediklerini açıkça görmeli. .
Andy

2
Random832 C # hakkında ancak Ortak LISP'de bilmiyorum @ orada bunun için bir sözdizimi olduğunu ve değişken değişkenler ve kapanışları ile herhangi bir dil (hayır, ki rakamlar zorunluluk ) de buna sahip. Değişen yere referans olarak ya da belirli bir zamanda sahip olduğu bir değeri kapatırız (bir kapanışın yaratılması). Bu , Python ve Scheme'deki benzer şeyleri tartışır ( cutref / vars ve değerlendirilen değerleri kısmen değerlendirilen kapanışlarda cutetutmak için ).
Ness

103

Bu şekilde ısırıldıktan sonra, herhangi bir kapatmaya aktarmak için kullandığım en iç kapsamda yerel olarak tanımlanmış değişkenleri dahil etme alışkanlığım var. Örneğinizde:

foreach (var s in strings)
    query = query.Where(i => i.Prop == s); // access to modified closure

Yaparım:

foreach (var s in strings)
{
    string search = s;
    query = query.Where(i => i.Prop == search); // New definition ensures unique per iteration.
}        

Bu alışkanlığınız olduğunda, aslında dış kapsamlara bağlamayı amaçladığınız çok nadir durumda bunu önleyebilirsiniz . Dürüst olmak gerekirse, şimdiye kadar yaptığımı sanmıyorum.


24
Bu tipik bir çözüm Katkı için teşekkür ederiz. Yeniden şekillendirici bu modeli tanıyacak ve dikkatinize çekecek kadar akıllıdır, bu da güzeldir. Bir süredir bu örüntüye değinmedim, ama Eric Lippert'in sözleriyle, "aldığımız en yaygın tek hata raporu", neden bundan kaçınmanın nedeninden daha fazlasını bilmek istedim .
StriplingWarrior

62

C # 5.0, bu sorun giderilmiştir ve döngü değişkenlerini kapatabilir ve beklediğiniz sonuçları alabilirsiniz.

Dil belirtimi şunları söylüyor:

8.8.4 Öngörme Beyanı

(...)

Formun foreach ifadesi

foreach (V v in x) embedded-statement

daha sonra şu değere genişletilir:

{
  E e = ((C)(x)).GetEnumerator();
  try {
      while (e.MoveNext()) {
          V v = (V)(T)e.Current;
          embedded-statement
      }
  }
  finally {
       // Dispose e
  }
}

(...)

vWhile döngüsünün içine yerleştirme , gömülü deyimde meydana gelen anonim işlevler tarafından nasıl yakalandığı için önemlidir. Örneğin:

int[] values = { 7, 9, 13 };
Action f = null;
foreach (var value in values)
{
    if (f == null) f = () => Console.WriteLine("First value: " + value);
}
f();

Eğer vwhile döngüsünün dışında ilan edildi, tüm tekrarlamalar arasında paylaşılan olacağını ve döngü için son değer olacağını geçtikçe değerini, 13ait çağırma budur fbasacaktır. Bunun yerine, her yinelemenin kendi değişkeni volduğu için f, ilk yinelemede yakalanan değer 7yazdırılacak olan değeri tutmaya devam edecektir. ( Not: vwhile döngüsünün dışında bildirilen C # 'ın önceki sürümleri . )


1
Neden C # bu erken sürümü while döngüsü içinde v ilan etti? msdn.microsoft.com/en-GB/library/aa664754.aspx
colinfang

4
@colinfang Eric'in cevabını mutlaka okuyun : C # 1.0 spesifikasyonu ( bağlantınızda VS 2003 hakkında konuşuyoruz, yani C # 1.2 ) aslında loop değişkeninin loop gövdesinin içinde mi yoksa dışında mı olduğunu söylemedi , çünkü gözlemlenebilir bir fark yaratmadı . Kapatma semantiği C # 2.0'da tanıtıldığında, döngü değişkenini "for" döngüsüyle tutarlı olarak döngü dışına koymak için seçim yapıldı.
Paolo Moretti

1
Yani linkteki örneklerin o zamanlar kesin bir özellik olmadığını mı söylüyorsunuz?
colinfang

4
@colinfang Kesin özelliklerdi. Sorun, daha sonra (C # 2.0 ile) tanıtılan bir özellik (yani işlev kapanışları) hakkında konuşmamızdır. C # 2.0 geldiğinde loop değişkenini loop dışına koymaya karar verdiler. Ve C # 5.0 ile tekrar fikrini değiştirdiler :)
Paolo Moretti
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.