2013-03-07 4 views
4

У меня есть следующий простой код:Закрытие захвачена переменная изменяет оригинал, а

static void Main(string[] args) 
{ 
    int j = 0; 
    Func<int> f =() => 
    { 
     for (int i = 0; i < 3; i++) 
     { 
      j += i; 
     } 
     return j; 
    }; 

    int myStr = f(); 
    Console.WriteLine(myStr); 
    Console.WriteLine(j); 
    Console.Read(); 
} 

Из того, что я прочитал, когда затворы участвуют, новый тип создается компилятором, он может хранить захваченное переменную и сохраните ссылку на него. Однако, когда я запускаю следующий код, обе печатные строки показывают 3. Я ожидал 0 и 3, потому что анонимный метод имеет свою собственную переменную в сгенерированном классе компилятором. Так почему же он также изменяет внешнюю переменную?

ответ

9

Внешняя переменная и переменная в замыкании та же переменная. Ваша программа эквивалентна:

private class Closure 
{ 
    public int j; 
    public int Method() 
    { 
     for (int i = 0; i < 3; i++) 
     { 
      this.j += i; 
     } 
     return this.j; 
    } 
} 
static void Main(string[] args) 
{ 
    Closure closure = new Closure(); 
    closure.j = 0; 
    Func<int> f = closure.Method; 
    int myStr = f(); 
    Console.WriteLine(myStr); 
    Console.WriteLine(closure.j); 
    Console.Read(); 
} 

Теперь ясно, почему вы получаете наблюдаемый результат?

+0

Ничего себе, прежде всего, не видели ответа от вас какое-то время. Во-вторых, теперь он очень ясен, хотя я смотрел на ИЛ, его несколько безвольный для меня. У меня не было никакой идеи, как это действует. – Freeman

+1

Эрик, вы сказали, что это поведение может измениться в будущей версии C#. Я знаю, что ты и МС сказали, что ты прощай. Однако изменилось ли это изменение на 5.0 или все еще думает, что это произойдет? –

+5

@ P.Brian.Mackey: поведение, которое лямбда захватывает внешнюю переменную, по дизайну и никогда не изменится. Однако мы вносили изменения в C# 5: переменная цикла * foreach * теперь логически * внутри * тела цикла, а не логически * вне * тела цикла. Поэтому каждый раз, когда вы закрываете переменную цикла foreach, вы получаете переменную * fresh * для закрытия в C# 5.0, а не как общую переменную, как в C# 4.0. –

4

Как работают затворы, они захватывают переменные, а не значения. Таким образом, j будет изменен.

Если вы не хотите этого, вы можете сделать это:

static void Main(string[] args) 
{ 
    int j = 0; 
    Func<int> f =() => 
    { 
     int k = j; 
     for (int i = 0; i < 3; i++) 
     { 
      k += i; 
     } 
     return k; 
    }; 
    int myStr = f(); 
    Console.WriteLine(myStr); 
    Console.WriteLine(j); 
    Console.Read(); 
} 

j еще захвачен закрытия, но не модифицируется. Изменяется только копия k.

Edit:

Вы правильно отметить, что это не будет работать для ссылочных типов. В этом случае k = j хранит копию ссылки на объект. Есть еще одна копия объекта , на которую ссылаются, поэтому любые изменения этого объекта будут влиять на обе переменные.

Вот пример того, как вы будете использовать закрытие для ссылочного типа, а не обновлять исходные переменный:

static void Main(string[] args) 
{ 
    Foo j = new Foo(0); 
    Func<Foo> f =() => 
    { 
     Foo k = new Foo(j.N); // Can't just say k = j; 
     for (int i = 0; i < 3; i++) 
     { 
      k.N += 1; 
     } 
     return k; 
    }; 

    Console.WriteLine(f().N); 
    Console.WriteLine(j.N); 
    Console.Read(); 
} 

public class Foo 
{ 
    public int N { get; set; } 

    public Foo(int n) { N = n; } 
} 

Однако строки будучи неизменных ссылочных типов, вы на самом деле можешь просто сказать k = j , в отличие от произвольных ссылочных типов. Один из способов думать о неизменности заключается в том, что каждый раз, когда вы обновляете значение строки, вы фактически создаете новый экземпляр. Итак, k = k + "1" - это как сказать k = new String(k + "1"). В этот момент он больше не является ссылкой на ту же строку, что и j.

+0

Вначале это выглядит как очень странное поведение, при этом k является типом значения, поэтому его, вероятно, является новой копией j, так что почему только k затронуто? – Freeman

+0

действительно ли копирование переменной предлагает такое же поведение для ссылочных типов? j была строкой, например. – Freeman

+0

@Freeman: Попробуйте! –

3

Спецификация языка говорит:

Анонимные методы подобны лямбда-функциям в языке программирования Lisp. C# 2.0 поддерживает создание «закрытий», когда анонимные методы получают доступ к локальным переменным и параметрам.

И «j» в вашем случае - окружающая переменная.

Смежные вопросы