Existe-t-il de bonnes raisons pour que les fermetures ne soient pas immuables en C #?

StackOverflow https://stackoverflow.com/questions/483944

  •  20-08-2019
  •  | 
  •  

Question

Cela m'est souvent reproché, et je n'arrive pas à trouver une bonne raison pour laquelle les fermetures en C # sont mutables. Cela semble être un bon moyen d’obtenir des conséquences inattendues si vous ne savez pas exactement ce qui se passe.

Peut-être que quelqu'un qui en sait un peu plus peut expliquer pourquoi les concepteurs de C # autoriseraient un changement d'état dans une fermeture?

Exemple:

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

Ceci imprimera " bonjour " pour le premier appel, mais l'état extérieur change pour le deuxième appel, en imprimant & «Au revoir. &»; L'état de la fermeture a été mis à jour pour refléter les modifications apportées à la variable locale.

Était-ce utile?

La solution

C # et JavaScript, ainsi que O'Caml et Haskell, ainsi que de nombreux autres langages, ont ce qu'on appelle des fermetures lexicales . Cela signifie que les fonctions internes peuvent accéder aux noms des variables locales dans les fonctions englobantes, et pas seulement aux copies des valeurs . Bien sûr, dans les langues avec des symboles immuables, tels que O'Caml ou Haskell, la fermeture de noms est identique à la fermeture de valeurs, de sorte que la différence entre les deux types de fermeture disparaît; ces langages ont néanmoins des fermetures lexicales, tout comme C # et JavaScript.

Autres conseils

Toutes les fermetures ne se comportent pas de la même manière. Il existe des différences de sémantique .

Notez que la première idée présentée correspond au comportement de C # ... votre concept de sémantique de fermeture peut ne pas être le concept prédominant.

En ce qui concerne les raisons: je pense que la clé ici est ECMA, un groupe de normalisation. Microsoft ne fait que suivre leur sémantique dans ce cas.

C’est en fait une fonctionnalité fantastique. Cela vous permet d’avoir une fermeture qui accède à quelque chose qui est normalement caché, par exemple une variable de classe privée, et de la manipuler de manière contrôlée en réponse à quelque chose comme un événement.

Vous pouvez facilement simuler ce que vous voulez en créant une copie locale de la variable et en l'utilisant.

Vous devez également vous rappeler que dans C #, il n’existe vraiment aucun concept de types immuables. Parce que les objets entiers du framework .Net ne sont tout simplement pas copiés (vous devez implémenter explicitement ICloneable, etc.), ce code afficherait & "Au revoir &"; même si le " pointeur " foo a été copié dans la fermeture:

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

On peut donc se demander si, dans le comportement actuel, il est plus facile d’obtenir des conséquences inattendues.

Lorsque vous créez une fermeture, le compilateur crée pour vous un type comportant des membres pour chaque variable capturée. Dans votre exemple, le compilateur générerait quelque chose comme ceci:

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

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

Une référence à ce type est donnée à votre délégué afin qu'il puisse utiliser les variables capturées ultérieurement. Malheureusement, l'instance locale de foo est également modifiée pour pointer ici. Toute modification locale affectera le délégué car il utilise le même objet.

Comme vous pouvez le constater, la persistance de readonly est gérée par un champ public plutôt que par une propriété. Il n'y a donc pas d'option d'immuabilité dans l'implémentation actuelle. Je pense que ce que vous voulez devrait être quelque chose comme ceci:

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

Pardonnez la syntaxe maladroite, mais l’idée est de noter que <=> est capturé de manière <=>, ce qui indiquerait ensuite au compilateur de générer le type généré:

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

Cela vous donnerait ce que vous vouliez d'une certaine manière mais nécessiterait des mises à jour du compilateur.

En ce qui concerne pourquoi les fermetures sont-elles modifiables en C #, vous devez demander & "Voulez-vous la simplicité (Java) ou la puissance avec la complexité (C #)? &" ;

Les fermetures mutables vous permettent de définir une fois et de les réutiliser. Exemple:

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 vous ne souhaitez pas définir une seule fois et le réutiliser, car vous craignez des conséquences imprévues, vous pouvez simplement utiliser une copie de la . Modifiez le code ci-dessus comme suit:

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

Maintenant, la requête retournera le même résultat, quelle que soit userFilter égale.

Jon Skeet a une excellente introduction aux différences entre les fermetures Java et C # .

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top