28 votes

Pourquoi les délégués sont-ils des types de référence ?

Petite note sur la réponse acceptée : Je ne suis pas d'accord avec une petite partie de Réponse de Jeffrey , à savoir le fait que, puisque Delegate devait être un type de référence, il s'ensuit que tous les délégués sont des types de référence. (Il n'est tout simplement pas vrai qu'une chaîne d'héritage à plusieurs niveaux exclut les types de valeur ; tous les types enum, par exemple, héritent de System.Enum qui hérite à son tour de System.ValueType qui hérite de System.Object , tous de référence). Cependant, je pense que le fait que, fondamentalement, tous les délégués héritent en fait non seulement de Delegate mais de MulticastDelegate est la réalisation critique ici. En tant que Raymond souligne dans un commentaire à son une fois que vous vous êtes engagé à prendre en charge plusieurs abonnés, il n'y a vraiment aucune raison pour que vous vous engagiez dans un programme de formation. no l'utilisation d'un type de référence pour le délégué lui-même, étant donné la nécessité d'un tableau quelque part.


Voir la mise à jour en bas de page.

Il m'a toujours semblé étrange que si je faisais cela :

Action foo = obj.Foo;

Je crée un nouveau Action à chaque fois. Je suis sûr que le coût est minime, mais il implique l'allocation de mémoire qui sera ensuite ramassée par les éboueurs.

Étant donné que les délégués sont par nature eux-mêmes immuables, je me demande pourquoi ils ne pourraient pas être des types de valeurs ? Dans ce cas, une ligne de code comme celle qui précède n'entraînerait rien de plus qu'une simple affectation à une adresse mémoire sur la pile*.

Même en considérant les fonctions anonymes, il semble (pour moi ), cela fonctionnerait. Prenons l'exemple simple suivant.

Action foo = () => { obj.Foo(); };

Dans ce cas foo constitue une fermeture , oui. Et dans de nombreux cas, j'imagine que cela nécessite un type de référence réel (comme lorsque les variables locales sont fermées et modifiées dans la fermeture). Mais dans certains cas, cela ne devrait pas être le cas. Par exemple, dans le cas ci-dessus, il semble qu'un type supportant la fermeture pourrait ressembler à ceci : Je retire ma remarque initiale à ce sujet. Le type ci-dessous doit vraiment être un type de référence (ou : il ne doit pas être un type de référence). besoin mais s'il s'agit d'un struct il sera de toute façon mis en boîte). Ne tenez donc pas compte de l'exemple de code ci-dessous. Je le laisse uniquement pour fournir un contexte aux réponses qui le mentionnent spécifiquement.

struct CompilerGenerated
{
    Obj obj;

    public CompilerGenerated(Obj obj)
    {
        this.obj = obj;
    }

    public void CallFoo()
    {
        obj.Foo();
    }
}

// ...elsewhere...

// This would not require any long-term memory allocation
// if Action were a value type, since CompilerGenerated
// is also a value type.
Action foo = new CompilerGenerated(obj).CallFoo;

Cette question a-t-elle un sens ? Selon moi, il y a deux explications possibles :

  • L'implémentation correcte des délégués en tant que types de valeurs aurait nécessité un surcroît de travail et de complexité, étant donné que la prise en charge de choses telles que les fermetures que les faire modifier les valeurs des variables locales aurait de toute façon nécessité des types de référence générés par le compilateur.
  • Il y a quelques autres raisons pour lesquelles, sous le capot, les délégués ne peut être mis en œuvre en tant que types de valeurs.

En fin de compte, je n'en perds pas le sommeil ; c'est juste quelque chose qui m'intrigue depuis un petit moment.


Mise à jour : En réponse au commentaire d'Ani, je comprends pourquoi les CompilerGenerated dans mon exemple ci-dessus pourrait tout aussi bien être un type de référence, puisque si un délégué doit comprendre un pointeur de fonction et un pointeur d'objet, il aura de toute façon besoin d'un type de référence (au moins pour les fonctions anonymes utilisant des fermetures, puisque même si vous introduisez un paramètre de type générique supplémentaire - par exemple, Action<TCaller> -(ce qui ne couvrirait pas les types qui ne peuvent pas être nommés). Cependant Tout cela me fait regretter d'avoir introduit dans la discussion la question des types générés par le compilateur pour les fermetures ! Ma principale question concerne délégués c'est-à-dire la chose avec le pointeur de fonction et le pointeur d'objet. Il me semble toujours que que peut être un type de valeur.

En d'autres termes, même si cette...

Action foo = () => { obj.Foo(); };

...nécessite la création de un (pour soutenir la fermeture et donner au délégué quelque chose à référencer), pourquoi exige-t-il la création d'un objet de type "référence" ? deux (l'objet supportant la fermeture plus les Action délégué) ?

*Oui, oui, détail de la mise en œuvre, je sais ! Tout ce que je veux dire, c'est <em>stockage de la mémoire à court terme </em>.

13voto

Jeffrey Sax Points 6512

La question se résume à ceci : la spécification CLI (Common Language Infrastructure) indique que les délégués sont des types de référence. Pourquoi en est-il ainsi ?

L'une des raisons est clairement visible dans le cadre .NET actuel. Dans la conception originale, il y avait deux types de délégués : les délégués normaux et les délégués "multicast", qui pouvaient avoir plus d'une cible dans leur liste d'invocation. Les délégués "multicast" peuvent avoir plus d'une cible dans leur liste d'invocation. MulticastDelegate hérite de la classe Delegate . Puisqu'il n'est pas possible d'hériter d'un type de valeur, Delegate doit être un type de référence.

En fin de compte, tous les délégués réels ont fini par être des délégués multicast, mais à ce stade du processus, il était trop tard pour fusionner les deux classes. Voir ce qui suit article de blog sur ce sujet précis :

Nous avons abandonné la distinction entre Délégué et Mul vers la fin de la V1. À l'époque, il aurait été massif de fusionner les deux classes, nous ne l'avons donc pas fait. Vous devriez prétendre qu'elles sont fusionnées et que seul MulticastDelegate existe.

En outre, les délégués disposent actuellement de 4 à 6 champs, tous des pointeurs. 16 octets est généralement considéré comme la limite supérieure où l'économie de mémoire l'emporte encore sur la copie supplémentaire. Un système de 64 bits MulticastDelegate occupe 48 octets. Compte tenu de ces éléments et du fait qu'ils utilisent l'héritage, le choix d'une classe s'impose naturellement.

6voto

Raymond Chen Points 27887

Imaginez que les délégués soient des types de valeurs.

public delegate void Notify();

void SignalTwice(Notify notify) { notify(); notify(); }

int counter = 0;
Notify handler = () => { counter++; }
SignalTwice(handler);
System.Console.WriteLine(counter); // what should this print?

Selon votre proposition, il serait converti en interne en

struct CompilerGenerated
{
    int counter = 0;
    public Execute() { ++counter; }
};

Notify handler = new CompilerGenerated();
SignalTwice(handler);
System.Console.WriteLine(counter); // what should this print?

Si delegate était un type de valeur, alors SignalEvent obtiendrait une copie de handler , ce qui signifie qu'un tout nouveau CompilerGenerated serait créée (une copie de handler ) et transmis à SignalEvent . SignalTwice exécuterait le délégué deux fois, ce qui incrémenterait la valeur de l'élément counter deux fois dans la copie . Et puis SignalTwice revient, et la fonction imprime 0, car l'original n'a pas été modifié.

4voto

Ani Points 59747

Voici une supposition non informée :

Si les délégués étaient mis en œuvre comme des types de valeurs, les instances seraient très coûteuses à copier, car une instance de délégué est relativement lourde. MS a peut-être estimé qu'il serait plus sûr de les concevoir comme immuables référence les types - la copie de références aux instances de la taille d'un mot machine est relativement peu coûteuse.

Une instance de délégué a besoin, au minimum, des éléments suivants

  • Une référence d'objet (la référence "this" de la méthode enveloppée s'il s'agit d'une méthode d'instance).
  • Un pointeur sur la fonction enveloppée.
  • Une référence à l'objet contenant la liste des invocations de multidiffusion. Notez qu'un type de délégué doit prendre en charge, par conception, la multidiffusion à l'aide de la fonction même type de délégué.

Supposons que les délégués de type valeur aient été mis en œuvre d'une manière similaire à la mise en œuvre actuelle des délégués de type référence (ce qui est peut-être quelque peu déraisonnable ; une conception différente peut très bien avoir été choisie pour réduire la taille) pour illustrer. En utilisant Reflector, voici les champs requis dans une instance de délégué :

System.Delegate: _methodBase, _methodPtr, _methodPtrAux, _target
System.MulticastDelegate: _invocationCount, _invocationList

Si elles étaient mises en œuvre sous la forme d'une structure (sans en-tête d'objet), elles représenteraient 24 octets sur x86 et 48 octets sur x64, ce qui est énorme pour une structure.


Dans un autre ordre d'idées, je voudrais savoir comment, dans le cadre de la conception que vous proposez, le fait de rendre les CompilerGenerated la fermeture d'un type de structure aide de quelque manière que ce soit. Où pointerait le pointeur d'objet du délégué créé ? Laisser l'instance du type de fermeture sur la pile sans analyse d'échappement appropriée serait extrêmement une activité risquée.

4voto

supercat Points 25534

Il n'y a qu'une seule raison pour laquelle Delegate doit être une classe, mais elle est de taille : alors qu'un délégué pourrait être suffisamment petit pour permettre un stockage efficace en tant que type de valeur (8 octets sur les systèmes 32 bits, ou 16 octets sur les systèmes 64 bits), il est impossible qu'il soit suffisamment petit pour garantir efficacement que si un thread tente d'écrire un délégué tandis qu'un autre thread tente de l'exécuter, ce dernier thread ne finira pas par invoquer l'ancienne méthode sur la nouvelle cible, ou la nouvelle méthode sur l'ancienne cible. Permettre une telle chose serait une faille de sécurité majeure. Le fait que les délégués soient des types de référence évite ce risque.

En fait, il serait encore mieux de faire des délégués des types de structures que des interfaces. La création d'une fermeture nécessite la création de deux objets du tas : un objet généré par le compilateur pour contenir toutes les variables fermées, et un délégué pour invoquer la méthode appropriée sur cet objet. Si les délégués étaient des interfaces, l'objet qui contient les variables fermées pourrait lui-même être utilisé comme délégué, sans qu'aucun autre objet ne soit nécessaire.

2voto

Daniel Peñalba Points 8548

J'ai vu cette conversation intéressante sur Internet :

Immuable ne signifie pas qu'il doit être un type de valeur. Et quelque chose qui est un type de valeur n'a pas besoin d'être immuable. Les deux vont souvent de pair, mais ce n'est pas la même chose. souvent main dans la main, mais ce n'est pas la même chose, et il existe des en fait des contre-exemples de chacun dans le cadre .NET (la classe String, par exemple). par exemple).

Et la réponse :

La différence est que, alors que les types de référence immuables sont raisonnablement communs et parfaitement raisonnables, rendre les types de valeurs mutables est presque toujours une mauvaise idée, et peut entraîner des conséquences très graves. très déroutant !

Tiré de ici

Ainsi, à mon avis, la décision a été prise en fonction de la facilité d'utilisation de la langue et non des difficultés technologiques du compilateur. J'aime beaucoup les délégués nullables.

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