41 votes

C# Impossible d'utiliser un paramètre ref ou out dans le corps d'une méthode anonyme.

J'essaie de créer une fonction qui peut créer une action qui incrémente le nombre entier qui lui est passé. Cependant, ma première tentative me donne une erreur "cannot use ref or out parameter inside an anonymous method body".

public static class IntEx {
    public static Action CreateIncrementer(ref int reference) {
        return () => {
            reference += 1;
        };
    }
}

Je comprends pourquoi le compilateur n'aime pas cela, mais néanmoins j'aimerais avoir une manière gracieuse de fournir une belle usine d'incrémenteur qui peut pointer vers n'importe quel entier. La seule façon que je vois de le faire est quelque chose comme ce qui suit :

public static class IntEx {
    public static Action CreateIncrementer(Func<int> getter, Action<int> setter) {
        return () => setter(getter() + 1);
    }
}

Mais bien sûr, cela est plus pénible à utiliser pour l'appelant, qui doit créer deux lambdas au lieu de simplement passer une référence. Existe-t-il un moyen plus gracieux de fournir cette fonctionnalité, ou dois-je me contenter de l'option à deux lambdas ?

31voto

Dax Fohl Points 3616

Ok, j'ai découvert qu'en fait es possible avec les pointeurs si dans un contexte non sécurisé :

public static class IntEx {
    unsafe public static Action CreateIncrementer(int* reference) {
        return () => {
            *reference += 1;
        };
    }
}

Cependant, le ramasseur d'ordures peut faire des ravages en déplaçant votre référence pendant le ramassage des ordures, comme l'indique l'exemple suivant :

class Program {
    static void Main() {
        new Program().Run();
        Console.ReadLine();
    }

    int _i = 0;
    public unsafe void Run() {
        Action incr;
        fixed (int* p_i = &_i) {
            incr = IntEx.CreateIncrementer(p_i);
        }
        incr();
        Console.WriteLine(_i); // Yay, incremented to 1!
        GC.Collect();
        incr();
        Console.WriteLine(_i); // Uh-oh, still 1!
    }
}

On peut contourner ce problème en plaçant la variable à un endroit précis de la mémoire. Cela peut être fait en ajoutant ce qui suit au constructeur :

    public Program() {
        GCHandle.Alloc(_i, GCHandleType.Pinned);
    }

Cela empêche le ramasseur de déchets de déplacer l'objet, ce qui est exactement ce que nous recherchons. Cependant, il faut ensuite ajouter un destructeur pour libérer la broche, ce qui fragmente la mémoire pendant toute la durée de vie de l'objet. Ce n'est pas vraiment plus facile. Cela aurait plus de sens en C++, où les objets ne sont pas déplacés et où la gestion des ressources est normale, mais pas tellement en C# où tout cela est censé être automatique.

Il semble donc que la morale de l'histoire soit la suivante : il suffit d'envelopper ce membre int dans un type de référence et d'en finir avec lui.

(Et oui, c'est ainsi que je l'avais fait fonctionner avant de poser la question, mais j'essayais juste de savoir s'il y avait un moyen de me débarrasser de toutes mes variables membres Reference<int> et d'utiliser simplement des ints ordinaires. Oh bien).

24voto

SLaks Points 391154

Ce n'est pas possible.

Le compilateur transformera toutes les variables locales et les paramètres utilisés par les méthodes anonymes en champs dans une classe de fermeture générée automatiquement.

Le CLR ne permet pas ref les types à stocker dans les champs.

Par exemple, si vous passez un type de valeur dans une variable locale comme un tel ref la durée de vie de la valeur s'étendrait au-delà de son cadre de pile.

2voto

supercat Points 25534

Il aurait pu être utile que le runtime permette la création de références de variables avec un mécanisme empêchant leur persistance ; une telle fonctionnalité aurait permis à un indexeur de se comporter comme un tableau (par exemple, pour qu'un Dictionary<Int32, Point> soit accessible via "myDictionary[5].X = 9 ;"). Je pense qu'une telle fonctionnalité aurait pu être fournie en toute sécurité si de telles références ne pouvaient pas être downcastées vers d'autres types d'objets, ni utilisées comme champs, ni transmises par référence elle-même (puisque tout endroit où une telle référence pourrait être stockée sortirait de la portée avant la référence elle-même). Malheureusement, le CLR ne fournit pas une telle fonctionnalité.

Pour mettre en œuvre ce que vous recherchez, il faudrait que la appelant de toute fonction qui utilise un paramètre de référence dans une fermeture doit envelopper dans une fermeture toute variable qu'elle veut passer à une telle fonction. S'il y avait une déclaration spéciale pour indiquer qu'un paramètre serait utilisé de cette manière, il pourrait être pratique pour un compilateur d'implémenter le comportement requis. Peut-être dans un compilateur .net 5.0, mais je ne suis pas sûr de son utilité.

Par ailleurs, je crois savoir que les fermetures en Java utilisent une sémantique par valeur, tandis que celles en .net sont par référence. Je peux comprendre certaines utilisations occasionnelles de la sémantique par référence, mais l'utilisation de la référence par défaut semble une décision douteuse, analogue à l'utilisation de la sémantique de passage de paramètres par référence par défaut pour les versions VB jusqu'à VB6. Si l'on veut capturer la valeur d'une variable lors de la création d'un délégué pour appeler une fonction (par exemple, si l'on veut qu'un délégué appelle MyFunction(X) en utilisant la valeur de X lors de la création du délégué), est-il préférable d'utiliser une lambda avec un temp supplémentaire, ou est-il préférable d'utiliser simplement une usine de délégués et de ne pas s'embêter avec les expressions lambda.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X