Pregunta

He estado repitiendo esto una y otra vez en mi cabeza, y parece que no puedo encontrar una buena razón por la cual los cierres de C # sean mutables. Simplemente parece una buena manera de obtener algunas consecuencias no deseadas si no eres consciente de lo que está sucediendo exactamente.

¿Quizás alguien que tenga un poco más de conocimiento pueda arrojar algo de luz sobre por qué los diseñadores de C # permitirían que el estado cambie en un cierre?

Ejemplo:

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

Esto imprimirá " hola " para la primera llamada, pero el estado externo cambia para la segunda llamada, imprimiendo " adiós. " El estado del cierre se actualizó para reflejar los cambios en la variable local.

¿Fue útil?

Solución

C # y JavaScript, así como O'Caml y Haskell, y muchos otros lenguajes, tienen lo que se conoce como cierres léxicos . Esto significa que las funciones internas pueden acceder a los nombres de las variables locales en las funciones adjuntas, no solo a las copias de los valores . En lenguajes con símbolos inmutables, como O'Caml o Haskell, cerrar los nombres es idéntico a cerrar los valores, por lo que desaparece la diferencia entre los dos tipos de cierre; Sin embargo, estos lenguajes tienen cierres léxicos como C # y JavaScript.

Otros consejos

No todos los cierres se comportan igual. Hay diferencias en la semántica .

Tenga en cuenta que la primera idea presentada coincide con el comportamiento de C # ... su concepto de semántica de cierre puede no ser el concepto predominante.

En cuanto a las razones: creo que la clave aquí es ECMA, un grupo de estándares. Microsoft solo está siguiendo su semántica en este caso.

Esta es realmente una característica fantástica. Esto le permite tener un cierre que accede a algo normalmente oculto, por ejemplo, una variable de clase privada, y le permite manipularlo de forma controlada como respuesta a algo como un evento.

Puede simular lo que quiere con bastante facilidad creando una copia local de la variable y usándola.

También debe recordar que en C # realmente no existe el concepto de tipos inmutables. Debido a que los objetos completos en el marco .Net simplemente no se copian (debe implementar explícitamente ICloneable, etc.), este código imprimirá & Quot; goodbye & Quot; incluso si el " puntero " foo fue copiado en el cierre:

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

Entonces es cuestionable si en el comportamiento actual es más fácil obtener consecuencias no deseadas.

Cuando crea un cierre, el compilador crea un tipo para usted que tiene miembros para cada variable capturada. En su ejemplo, el compilador generaría algo como esto:

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

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

Su delegado recibe una referencia a este tipo para que pueda usar las variables capturadas más adelante. Desafortunadamente, la instancia local de foo también se cambia para apuntar aquí, por lo que cualquier cambio local afectará al delegado ya que usa el mismo objeto.

Como puede ver, la persistencia de readonly es manejada por un campo público en lugar de una propiedad, por lo que ni siquiera hay una opción de inmutabilidad aquí con la implementación actual. Creo que lo que quieres sería algo como esto:

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

Disculpe la sintaxis torpe, pero la idea es denotar que <=> se captura de una manera <=> que luego le indicaría al compilador que muestre este tipo generado:

[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);
    }
}

Esto le daría lo que quería de cierta manera, pero requeriría actualizaciones para el compilador.

Con respecto a por qué los cierres son mutables en C #, debe preguntar, " ¿Quiere simplicidad (Java) o potencia con complejidad (C #)? " ;

Los cierres mutables le permiten definir una vez y reutilizar. Ejemplo:

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();
        }
    }
}

Si no desea definir una vez y reutilizar, porque le preocupan las consecuencias no deseadas, simplemente puede usar una copia de la variable. Cambie el código anterior de la siguiente manera:

        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();

Ahora la consulta devolverá el mismo resultado, independientemente de lo que sea userFilter igual.

Jon Skeet tiene una excelente introducción a las diferencias entre los cierres de Java y C # .

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top