Есть ли веские причины, по которым замыкания не являются неизменными в C #? - PullRequest
6 голосов
/ 27 января 2009

Я много раз повторял это в своей голове, и я не могу придумать вескую причину, по которой замыкания в C # изменчивы. Это просто хороший способ получить непредвиденные последствия, если вы не знаете точно, что происходит.

Может быть, кто-то, кто немного более осведомлен, может пролить свет на то, почему разработчики C # допускают изменение состояния в замыкании?

Пример:

var foo = "hello";
Action bar = () => Console.WriteLine(foo);
bar();
foo = "goodbye";
bar();

Будет напечатано «привет» для первого звонка, но внешнее состояние изменится для второго звонка, напечатав «до свидания». Состояние замыкания было обновлено, чтобы отразить изменения в локальной переменной.

Ответы [ 6 ]

9 голосов
/ 27 января 2009

C # и JavaScript, а также O'Caml и Haskell и многие другие языки имеют так называемое лексическое замыкание . Это означает, что внутренние функции могут получать доступ к именам локальных переменных в окружающих функциях, а не только к копиям значений . Конечно, в языках с неизменяемыми символами, таких как O'Caml или Haskell, закрытие по именам идентично закрытию по значениям, поэтому разница между двумя типами закрытия исчезает; тем не менее эти языки имеют лексические замыкания, такие как C # и JavaScript.

3 голосов
/ 27 января 2009

Не все замыкания ведут себя одинаково. Есть различия в семантике .

Обратите внимание, что первая представленная идея соответствует поведению C # ... ваша концепция семантики замыкания может не быть доминирующей концепцией.

Что касается причин: я думаю, что ключевым моментом здесь является ECMA, группа стандартов. В этом случае Microsoft просто следует их семантике.

2 голосов
/ 27 января 2009

Это на самом деле фантастическая особенность. Это позволяет вам иметь замыкание, которое обращается к чему-то обычно скрытому, скажем, к закрытой переменной класса, и позволяет ему управлять им контролируемым образом как ответ на что-то вроде события.

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

1 голос
/ 27 января 2009

Вы также должны помнить, что в C # действительно нет понятия неизменяемых типов. Поскольку целые объекты в .Net Framework просто не копируются (вы должны явно реализовать ICloneable и т. Д.), Этот код выведет «до свидания», даже если в замыкании скопирован «указатель» foo:

class Foo
{
    public string Text;
}    
var foo = new Foo();
foo.Text = "Hello";
Action bar = () => Console.WriteLine(foo.Text);
bar();
foo.Text = "goodbye";
bar();

Так что сомнительно, если в текущем поведении легче получить непредвиденные последствия.

0 голосов
/ 09 ноября 2009

Что касается , почему являются изменяемыми замыканиями в C #, вы должны спросить: «Вам нужна простота (Java) или мощность со сложностью (C #)?»)

Изменяемые замыкания позволяют определить один раз и использовать повторно. Пример:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ClosureTest
{
    class Program
    {   
        static void Main(string[] args)
        {
            string userFilter = "C";            
            IEnumerable<string> query = (from m in typeof(String).GetMethods()
                                         where m.Name.StartsWith(userFilter)
                                         select m.Name.ToString()).Distinct();

            while(userFilter.ToLower() != "q")
            {
                DiplayStringMethods(query, userFilter);
                userFilter = GetNewFilter();
            }
        }

        static void DiplayStringMethods(IEnumerable<string> methodNames, string userFilter)
        {
            Console.WriteLine("Here are all of the String methods starting with the letter \"{0}\":", userFilter);
            Console.WriteLine();

            foreach (string methodName in methodNames)
                Console.WriteLine("  * {0}", methodName);
        }

        static string GetNewFilter()
        {
            Console.WriteLine();
            Console.Write("Enter a new starting letter (type \"Q\" to quit): ");
            ConsoleKeyInfo cki = Console.ReadKey();
            Console.WriteLine();
            return cki.Key.ToString();
        }
    }
}

Если вы не хотите определять один раз и использовать повторно, поскольку вы беспокоитесь о непредвиденных последствиях, вы можете просто использовать копию переменной. Измените приведенный выше код следующим образом:

        string userFilter = "C";
        string userFilter_copy = userFilter;
        IEnumerable<string> query = (from m in typeof(String).GetMethods()
                                     where m.Name.StartsWith(userFilter_copy)
                                     select m.Name.ToString()).Distinct();

Теперь запрос будет возвращать один и тот же результат, независимо от того, что равно userFilter.

Джон Скит отлично знакомит с различиями между Java и C # замыканиями .

0 голосов
/ 27 января 2009

Когда вы создаете замыкание, компилятор создает для вас тип, в котором есть члены для каждой захваченной переменной. В вашем примере компилятор сгенерирует что-то вроде этого:

[CompilerGenerated]
private sealed class <>c__DisplayClass1
{
    public string foo;

    public void <Main>b__0()
    {
        Console.WriteLine(this.foo);
    }
}

Вашему делегату дается ссылка на этот тип, чтобы он мог использовать захваченные переменные позже. К сожалению, локальный экземпляр foo также изменен, чтобы указать здесь, поэтому любые изменения локально будут влиять на делегата, поскольку они используют один и тот же объект.

Как видите, постоянство foo обрабатывается открытым полем, а не свойством, поэтому в текущей реализации здесь даже нет опции неизменяемости. Я думаю, что вы хотите, должно быть что-то вроде этого:

var foo = "hello";
Action bar = [readonly foo]() => Console.WriteLine(foo);
bar();
foo = "goodbye";
bar();

Прошу прощения за неуклюжий синтаксис, но идея состоит в том, чтобы обозначить, что foo захвачен readonly способом, который затем намекает компилятору выводить этот сгенерированный тип:

[CompilerGenerated]
private sealed class <>c__DisplayClass1
{
    public readonly string foo;

    public <>c__DisplayClass1(string foo)
    {
        this.foo = foo;
    }

    public void <Main>b__0()
    {
        Console.WriteLine(this.foo);
    }
}

Это даст вам то, что вы хотите определенным образом, но потребует обновления компилятора.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...