C # 'da bir döngüde yakalanan değişken


216

C # ile ilgili ilginç bir sorunla karşılaştım. Aşağıdaki gibi bir kod var.

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    actions.Add(() => variable * 2);
    ++ variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

Ben 0, 2, 4, 6, 8 çıktı bekliyoruz. Ancak, aslında beş 10s çıktı.

Yakalanan bir değişkene atıfta bulunan tüm eylemlerden kaynaklanıyor gibi görünüyor. Sonuç olarak, çağrıldıklarında hepsinin çıktıları aynıdır.

Her eylem örneğinin kendi yakalanan değişkeni olması için bu sınırı aşmanın bir yolu var mı?


15
Ayrıca bkz. Eric Lippert'in konuyla ilgili Blog dizisi: Döngü Değişkenine Kapanış
Brian

10
Ayrıca, bir foreach içinde beklediğiniz gibi çalışmak için C # 5 değiştiriyorlar. (kırılma değişikliği)
Neal Tibrewala


3
@Neal: Her ne kadar bu örnek hala C # 5'de düzgün çalışmıyorsa da, hala beş 10s çıktı
Ian Oakes

6
C # 6.0'da (VS 2015) bugüne kadar beş 10 saniye çıktığını doğruladı. Kapanma değişkenlerinin bu davranışının değişime aday olduğuna şüphe ediyorum. Captured variables are always evaluated when the delegate is actually invoked, not when the variables were captured.
RBT

Yanıtlar:


196

Evet - döngünün içindeki değişkenin bir kopyasını alın:

while (variable < 5)
{
    int copy = variable;
    actions.Add(() => copy * 2);
    ++ variable;
}

Bunu, C # derleyicisinin değişken bildirimine her vuruşunda "yeni" bir yerel değişken oluşturduğunu düşünebilirsiniz. Aslında, uygun yeni kapatma nesneleri oluşturur ve birden çok kapsamdaki değişkenlere başvurursanız (uygulama açısından) karmaşıklaşır, ancak işe yarar :)

Bu sorunun bir daha yaygın bir durum kullandığını unutmayın forya foreach:

for (int i=0; i < 10; i++) // Just one variable
foreach (string x in foo) // And again, despite how it reads out loud

Bunun hakkında daha fazla bilgi için C # 3.0 spesifikasyonunun 7.14.4.2 bölümüne bakın ve kapanışlarla ilgili makalemin daha fazla örneği var.

C # 5 derleyicisi ve ötesinde (C # 'ın önceki bir sürümünü belirtirken bile), davranışın foreachdeğiştiğini ve böylece artık yerel kopya oluşturmanıza gerek olmadığını unutmayın. Daha fazla ayrıntı için bu cevaba bakınız.


32
Jon'un kitabında da bu konuda çok iyi bir bölüm var (mütevazi olmayı bırak, Jon!)
Marc Gravell

35
Diğer insanların fişini takmasına izin verirsem daha iyi görünüyor;) (İtiraf etmeliyim ki cevaplar olsa da oy verme eğilimindeyim.)
Jon Skeet

2
Her zamanki gibi, skeet@pobox.com'a geri bildirim takdir edilecektir :)
Jon Skeet

7
C # 5.0 için davranış farklı (daha makul) Jon
Skeet'in

1
@Florimond: C # 'da kapakların çalışma şekli bu değil. Değerleri değil, değişkenleri yakalarlar . (Bu, döngülerden bağımsız olarak doğrudur ve bir değişkeni yakalayan ve sadece yürütüldüğünde geçerli değeri basan bir lambda ile kolayca gösterilebilir.)
Jon Skeet

23

Yaşadığınız şeyin Kapatma olarak bilinen bir şey olduğuna inanıyorum http://en.wikipedia.org/wiki/Closure_(computer_science) . Lambanızın fonksiyonun dışında yer alan bir değişkene referansı vardır. Lambanız siz çağırana kadar yorumlanmaz ve bir kez değiştiğinde değişkenin yürütme sırasında sahip olduğu değeri alır.


11

Perde arkasında, derleyici yöntem çağrınızın kapanmasını temsil eden bir sınıf oluşturuyor. Döngünün her yinelemesi için closure sınıfının bu tek örneğini kullanır. Kod şöyle görünür, bu da hatanın neden olduğunu görmeyi kolaylaştırır:

void Main()
{
    List<Func<int>> actions = new List<Func<int>>();

    int variable = 0;

    var closure = new CompilerGeneratedClosure();

    Func<int> anonymousMethodAction = null;

    while (closure.variable < 5)
    {
        if(anonymousMethodAction == null)
            anonymousMethodAction = new Func<int>(closure.YourAnonymousMethod);

        //we're re-adding the same function 
        actions.Add(anonymousMethodAction);

        ++closure.variable;
    }

    foreach (var act in actions)
    {
        Console.WriteLine(act.Invoke());
    }
}

class CompilerGeneratedClosure
{
    public int variable;

    public int YourAnonymousMethod()
    {
        return this.variable * 2;
    }
}

Bu aslında örneğinizden derlenmiş kod değil, ama kendi kodumu inceledim ve bu derleyicinin aslında ne üreteceğine benziyor.


8

Bunun yolu, ihtiyacınız olan değeri bir proxy değişkeninde depolamak ve bu değişkenin yakalanmasını sağlamaktır.

IE

while( variable < 5 )
{
    int copy = variable;
    actions.Add( () => copy * 2 );
    ++variable;
}

Düzenlenen cevabımdaki açıklamaya bakın. Spesifikasyonun ilgili parçasını şimdi buluyorum.
Jon Skeet

Haha jon, aslında makaleni okudum: csharpindepth.com/Articles/Chapter5/Closures.aspx İyi iş çıkarıyorsun arkadaşım.
tjlevine

@tjlevine: Çok teşekkürler. Cevabımda buna bir referans ekleyeceğim. Bunu unutmuştum!
Jon Skeet

Ayrıca Jon, çeşitli Java 7 kapatma önerileri hakkındaki düşüncelerinizi okumak isterim. Bir tane yazmak istediğini söylediğini gördüm ama görmedim.
tjlevine

1
@tjlevine: Tamam, yıl sonuna kadar yazmaya çalışacağım söz veriyorum :)
Jon Skeet

6

Bunun döngülerle ilgisi yoktur.

Bu davranış tetiklenir, çünkü () => variable * 2dış kapsamda bir lambda ifadesi kullanırsınız.variable kapsamın aslında iç kapsamında tanımlanmayan .

Lambda ifadeleri (C # 3 + 'da ve C # 2'deki anonim yöntemler) hala gerçek yöntemler oluşturur. Değişkenlerin bu yöntemlere geçirilmesi bazı ikilemleri içerir (değere göre geçiş? Referans ile? C # başvuru ile gider - ancak bu, başvurunun gerçek değişkeni geride bırakabileceği başka bir sorun açar). Tüm bu ikilemleri çözmek için C # 'ın yaptığı şey, lambda ifadelerinde kullanılan yerel değişkenlere karşılık gelen alanlara ve gerçek lambda yöntemlerine karşılık gelen yöntemlere sahip yeni bir yardımcı sınıf ("kapatma") oluşturmaktır. Herhangi bir değişiklikvariableKodunuzdaki , aslında bu koddaki çevrilir.ClosureClass.variable

Böylece while ClosureClass.variabledöngünüz 10'a ulaşıncaya kadar güncellenmeye devam eder , o zaman döngüler için hepsi aynı şekilde çalışan eylemleri yürütürClosureClass.variable .

Beklenen sonucu almak için, döngü değişkeni ile kapatılmakta olan değişken arasında bir ayrım oluşturmanız gerekir. Bunu başka bir değişken girerek yapabilirsiniz, yani:

List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
    var t = variable; // now t will be closured (i.e. replaced by a field in the new class)
    actions.Add(() => t * 2);
    ++variable; // changing variable won't affect the closured variable t
}
foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

Bu ayrımı oluşturmak için kapamayı başka bir yönteme de taşıyabilirsiniz:

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    actions.Add(Mult(variable));
    ++variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

Mult'i lambda ifadesi olarak uygulayabilirsiniz (örtülü kapatma)

static Func<int> Mult(int i)
{
    return () => i * 2;
}

veya gerçek bir yardımcı sınıfla:

public class Helper
{
    public int _i;
    public Helper(int i)
    {
        _i = i;
    }
    public int Method()
    {
        return _i * 2;
    }
}

static Func<int> Mult(int i)
{
    Helper help = new Helper(i);
    return help.Method;
}

Her durumda, "Kapanışlar" döngülerle ilgili bir kavram DEĞİLDİR , bunun yerine anonim yöntemler / lambda ifadeleri yerel kapsamlandırılmış değişkenlerin kullanımıdır - her ne kadar döngülerin bazı dikkatsiz kullanımı kapanış tuzakları gösterir.


5

Evet variable, döngü içinde kapsam yapmanız ve lambda'ya şu şekilde geçirmeniz gerekir:

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    int variable1 = variable;
    actions.Add(() => variable1 * 2);
    ++variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

Console.ReadLine();

5

Aynı durum çoklu iş parçacığında da gerçekleşiyor (C #, .NET 4.0].

Aşağıdaki koda bakın:

Amaç 1,2,3,4,5 sırayla yazdırmaktır.

for (int counter = 1; counter <= 5; counter++)
{
    new Thread (() => Console.Write (counter)).Start();
}

Çıktı ilginç! (21334 gibi olabilir ...)

Tek çözüm yerel değişkenleri kullanmaktır.

for (int counter = 1; counter <= 5; counter++)
{
    int localVar= counter;
    new Thread (() => Console.Write (localVar)).Start();
}

Bu bana yardımcı olmuyor gibi görünüyor. Hala belirleyici değil.
Mladen Mihajlovic

0

Burada kimse ECMA-334'ü doğrudan alıntılamadığı için :

10.4.4.10 İfadeler için

Formun for-ifadesi için kesin atama kontrolü:

for (for-initializer; for-condition; for-iterator) embedded-statement

ifadesi yazılmış gibi yapılır:

{
    for-initializer;
    while (for-condition) {
        embedded-statement;
    LLoop: for-iterator;
    }
}

Spesifikasyonda daha fazla,

12.16.6.3 Yerel değişkenlerin örneklenmesi

Yürütme değişkenin kapsamına girdiğinde yerel değişkenin somutlaştırıldığı kabul edilir.

[Örnek: Örneğin, aşağıdaki yöntem çağrıldığında, yerel değişken xüç kez başlatılır ve döngünün her yinelemesi için bir kez başlatılır.

static void F() {
  for (int i = 0; i < 3; i++) {
    int x = i * 2 + 1;
    ...
  }
}

Bununla birlikte, xdöngü dışının bildirimini hareket ettirmek, aşağıdakilerin tek bir örneğiyle sonuçlanır x:

static void F() {
  int x;
  for (int i = 0; i < 3; i++) {
    x = i * 2 + 1;
    ...
  }
}

son örnek]

Yakalanmadığında, yerel bir değişkenin ne sıklıkta somutlaştırıldığını tam olarak gözlemlemenin bir yolu yoktur; Ancak, anonim bir işlev yerel bir değişkeni yakaladığında, somutlaştırmanın etkileri belirginleşir.

[Örnek: Örnek

using System;

delegate void D();

class Test{
  static D[] F() {
    D[] result = new D[3];
    for (int i = 0; i < 3; i++) {
      int x = i * 2 + 1;
      result[i] = () => { Console.WriteLine(x); };
    }
  return result;
  }
  static void Main() {
    foreach (D d in F()) d();
  }
}

çıktı üretir:

1
3
5

Ancak, bildirimi xdöngü dışına taşındığında:

static D[] F() {
  D[] result = new D[3];
  int x;
  for (int i = 0; i < 3; i++) {
    x = i * 2 + 1;
    result[i] = () => { Console.WriteLine(x); };
  }
  return result;
}

çıktı:

5
5
5

Derleyiciye, üç örneği tek bir delege örneğinde optimize etme izni verilir (ancak zorunlu değildir) (§11.7.2).

For-loop bir yineleme değişkeni bildirirse, bu değişkenin kendisi döngü dışında bildirilir. [Örnek: Dolayısıyla, örnek yineleme değişkeninin kendisini yakalamak için değiştirilirse:

static D[] F() {
  D[] result = new D[3];
  for (int i = 0; i < 3; i++) {
    result[i] = () => { Console.WriteLine(i); };
  }
  return result;
}

çıktıyı üreten yineleme değişkeninin yalnızca bir örneği yakalanır:

3
3
3

son örnek]

Oh evet, sanırım bu değişken C ++ 'da değişken tarafından değer veya başvuru tarafından yakalanıp seçilmediğini seçebilirsiniz çünkü bu sorun oluşmaz belirtilmelidir (bkz: Lambda yakalama ).


-1

Kapatma sorunu denir, sadece bir kopya değişkeni kullanın ve bitti.

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    int i = variable;
    actions.Add(() => i * 2);
    ++ variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

4
Cevabınız yukarıdaki şekilde verilen cevaptan ne şekilde farklıdır?
Thangadurai
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.