Pourquoi sont délégués types de référence?
Note rapide sur la accepté de répondre à: je suis en désaccord avec une petite partie de Jeffrey répondre, à savoir le point que depuis Delegate
devait être un type de référence, il s'ensuit que tous les délégués sont des types référence. (Il n'est tout simplement pas vrai qu'un multi-niveau de la chaîne d'héritage sur les règles types de valeur; tous les types d'énumérations, par exemple, hériter de System.Enum
, qui hérite de System.ValueType
, qui hérite de l' System.Object
, tous types de référence.) Cependant, je pense que le fait que, fondamentalement, tous les délégués en fait hériter pas seulement de Delegate
mais de MulticastDelegate
est la critique de la réalisation ici. Comme Raymond souligne dans un commentaire à son réponse, une fois que vous avez déterminé à soutenir plusieurs abonnés, il n'y a vraiment aucun point en pas à l'aide d'un type de référence pour le délégué lui-même, compte tenu de la nécessité d'un tableau quelque part.
Voir mise à jour en bas.
Il a toujours semblé étrange pour moi que si je fais ceci:
Action foo = obj.Foo;
Je suis entrain de créer un nouveau Action
objet, à chaque fois. Je suis sûr que le coût est minime, mais il implique une allocation de la mémoire pour plus tard être nettoyée.
Donné que les délégués sont intrinsèquement eux-mêmes immuable, je me demande pourquoi ils ne pouvaient pas être des types de valeur? Puis une ligne de code comme celui ci-dessus entraînerait rien de plus qu'une simple affectation d'une adresse de mémoire sur la pile*.
Même en tenant compte des fonctions anonymes, il me semble (à moi) ce serait le travail. Considérons l'exemple simple suivant.
Action foo = () => { obj.Foo(); };
Dans ce cas foo
ne constitue pas un fermeture, oui. Et dans de nombreux cas, j'imagine que cela ne nécessite une réelle type de référence (comme lorsque les variables locales sont fermés et sont modifiés à l'intérieur de la fermeture). Mais dans certains cas, il ne devrait pas. Par exemple, dans le cas ci-dessus, il semble qu'un type à l'appui de la fermeture pourrait ressembler à ceci: je reprends mon premier point à ce sujet. Le ci-dessous n'a vraiment besoin d'être un type de référence (ou: il n'a pas besoin être, mais si c'est un struct
il va juste pour obtenir la boîte de toute façon). Donc, le mépris de l'exemple de code ci-dessous. Je le laisse seul à fournir un contexte pour les réponses le specfically le mentionner.
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 de sens? Comme je le vois, il y a deux explications possibles:
- La mise en œuvre de délégués correctement comme des types de valeur aurait nécessité un travail supplémentaire ou de la complexité, puisque le soutien pour des choses comme des fermetures qui ne modifier les valeurs des variables locales aurait exigé généré par le compilateur types de référence, de toute façon.
- Il y a quelques autres raisons pour lesquelles, sous le capot, les délégués ont simplement ne peut pas être mis en œuvre comme des types de valeur.
En fin de compte, je ne suis pas de perdre le sommeil plus de cela; c'est juste quelque chose que je ai été curieux de connaître un peu de temps.
Mise à jour: En réponse à l'Ani du commentaire, je ne vois pourquoi le CompilerGenerated
type dans mon exemple ci-dessus pourrait tout aussi bien être un type de référence, car si un délégué va se composent d'un pointeur de fonction et d'un pointeur d'objet c'aurez besoin d'un type de référence, de toute façon (au moins pour les fonctions anonymes en utilisant les fermetures, car même si vous avez mis en place un supplément de paramètre de type générique—par exemple, Action<TCaller>
—ce ne serait pas des types de couvert qui ne peuvent pas être nommés!). Cependant, tout cela est une sorte de me faire regretter d'amener la question de l'généré par le compilateur types de fermetures dans le débat à tous! Ma principale question est à propos de délégués, c'est à dire, la chose avec le pointeur de fonction et le pointeur d'objet. Il semble encore pour moi que pourrait être un type de la valeur.
En d'autres termes, même si cette...
Action foo = () => { obj.Foo(); };
...nécessite la création de un de référence de type object (à l'appui de la fermeture, et de donner le délégué quelque chose de référence), pourquoi faut-il de la création de deux (la fermeture-le soutien de l'objet plus la Action
délégué)?
*Oui, oui, la mise en œuvre du détail, je sais! Tout ce que je veux dire, c'est à court terme de la mémoire de stockage.
- La première explication possible des sons plus que de raison suffisante pour moi.
- Ok, disons que vous souhaitez mettre en œuvre un délégué comme une valeur de type avec un pointeur de fonction et d'un pointeur d'objet. Dans votre fermeture exemple, où serait le pointeur d'objet point d'? Vous serait presque certainement besoin de cocher la
CompilerGenerated
struct exemple et le mettre sur le tas (avec évacuation d'analyse, ce qui pourrait être évitée dans certains cas). - Ah, je vois votre point de vue. Vous pourriez peut-être développer ce commentaire dans le formulaire de réponse?
- Avez-vous vraiment envie de travailler avec des valeurs null<Action> ?
- Avez-vous vraiment envie de travailler avec
null
? La possibilité de valeur null n'est pas toujours souhaité. - Jon Skeet a répondu ceci : stackoverflow.com/questions/2324224/...
- Si un délégué est une structure qui contient un pointeur de fonction et pointeur d'objet, la construction d'une clôture ne nécessiterait la création d'un nouveau segment de l'objet plutôt que deux. Si les délégués étaient des types d'interface (qui est ce que je pense qu'ils devraient être), une clôture ne nécessiterait la création d'un unique objet tas de tenir à la fois la fermeture de données et de sa méthode.
Vous devez vous connecter pour publier un commentaire.
La question se résume à ceci: la CLI (Common Language Infrastructure) spécification indique que les délégués sont des types référence. Pourquoi est-ce donc?
Une raison à cela est clairement visible dans le .NET Framework d'aujourd'hui. Dans la conception originale, il y avait deux sortes de délégués: normal délégués et "multicast" les délégués, ce qui pourrait avoir plus d'une cible dans leur liste d'invocation. Le
MulticastDelegate
classe hérite deDelegate
. Puisque vous ne pouvez pas hériter d'un type de valeur,Delegate
devait être un type de référence.En fin de compte, tous les délégués ont fini par être multidiffusion délégués, mais à ce stade du processus, il était trop tard pour fusionner les deux classes. Voir ce post de blog sur ce sujet:
En outre, les délégués ont actuellement de 4 à 6 champs, tous les pointeurs. 16 octets est généralement considérée comme la limite supérieure où d'économiser la mémoire encore des gains supplémentaires de la copie. Un 64 bits
MulticastDelegate
prend en hausse de 48 octets. Compte tenu de cela, et le fait qu'ils ont été en utilisant l'héritage suggère qu'une classe était le choix naturel.MulticastDelegate
etDelegate
et le premier étant une sous-classe de ce dernier?Delegate
est un type de référence, tous les délégués doivent être des types de référence, à droite? Je veux dire, pensezSystem.Enum
: c'est un type de référence, et tous les types enum héritent de celui-ci; et, pourtant, les énumérations sont des types de valeur. C'est légale dans la CLI et clairement possible de le compilateur de la fin. Donc, encore faut-il qu'il y ait de nouvelles raisons de la décision que tous les types délégués sont des types référence.Enum
.System.Enum
est pas un type de valeur! C'est une classe abstraite; voir par vous-même: msdn.microsoft.com/en-us/library/system.enum.aspx.System.ValueType
(qui est, paradoxalement, un type de référence).Delegate
type référence type de nous a un peu de hors piste, mais il semble que l'invocation de la liste utilisée parMulticastDelegate
est au cœur de la question ici. Et j'aime bien le contexte historique que vous avez fournis dans cette réponse. Merci!Delegate
besoin de plus de deux champs (pour tenir la Méthode et de la Cible). Étant donné deux "unicast" les délégués, on pourrait former un délégué multicast en ayantTarget
référence à un tableau contenant les deux autres délégués, etMethod
point d'une méthode statique prend un tableau en premier paramètre et appelle les délégués qui y sont. Notez que la construction d'un délégué auprès de l'Method
etTarget
d'un tel délégué multicast serait de produire correctement formé délégué multicast--unlikes la situation actuelle. Notez également que...Delegate.Combine
pu vérifier laMethod
de tout délégué pour voir si c'était la multidiffusion invocateur, et de produire un combiné liste d'invocation si.Il n'y a qu'une seule raison pour que Délégué doit être une classe, mais c'est un gros: tout en un délégué pourrait être assez petit pour permettre le stockage efficace comme un type de valeur (8 octets sur les systèmes 32 bits, soit 16 octets sur les systèmes 64 bits), il n'y a aucune manière qu'il pourrait être assez petit pour garantir de façon efficace si un thread tente d'écrire un délégué tandis qu'un autre thread tente de l'exécuter, le dernier thread ne serait pas la fin, soit en invoquant la méthode ancienne sur la nouvelle cible, ou la nouvelle méthode sur l'ancienne cible. Permettant une telle chose de se produire serait un trou de sécurité majeur. Avoir des délégués être de types de référence permet d'éviter ce risque.
En fait, même mieux que d'avoir les délégués types de structure serait d'avoir entre eux des interfaces. La création d'une fermeture nécessite la création de deux tas d'objets: une générées par le compilateur objet de détenir tout fermé sur les variables, et d'un délégué pour appeler la bonne méthode sur cet objet. Si les délégués étaient des interfaces, l'objet qui a tenu le plus de variables, il peut être utilisé en tant que délégué, avec aucun autre objet requis.
TypedReference
,ArgIterator
, et diverses poignées, qui représentent également une référence à quelque chose, ils sont tous des types de valeur.Imaginez si les délégués étaient des types valeur.
Par votre proposition, ce serait à l'interne être converti à
Si
delegate
étaient un type de valeur, puisSignalEvent
serait d'obtenir une copie dehandler
, ce qui signifie qu'une nouvelleCompilerGenerated
serait créé (une copie dehandler
) et transmis àSignalEvent
.SignalTwice
à l'exécution de l'délégué deux fois, ce qui augmente lecounter
deux fois dans la copie. Et puisSignalTwice
retourne, et la fonction affiche de 0, car l'original n'a pas été modifié.CompilerGenerated
dans ce cas pourrait être un type de la valeur. Mais il y a encore la question de la déléguée lui-même. Dans votre exemple, plutôt que deNotify handler = new CompilerGenerated()
, ne serait pas la "vraie" sortie sera plus commeNotify handler = new CompilerGenerated().Execute
? Et dans ce cas, ce que j'essaie de comprendre c'est que même siCompilerGenerated
doit être un type de référence,Notify
n' pas. UnNotify
instance peut désigner unCompilerGenerated
et une fonction (Execute
) et c'est tout.Voici un mal informés deviner:
Si les délégués ont été mises en œuvre comme des types de valeur, les instances serait très coûteux pour copier autour depuis un délégué instance est relativement lourd. Peut-être que MME d'avis qu'il serait plus sûr de les concevoir comme immuable référence les types de machine à copier-parole de la taille des références à des instances sont relativement bon marché.
Une instance de délégué besoins, à tout le moins:
Supposons que la valeur de type délégués ont été mises en œuvre de manière similaire à la référence actuelle-type de mise en œuvre (c'est peut-être un peu déraisonnable; une conception différente peut très bien avoir été choisi pour garder la taille vers le bas) pour illustrer. À l'aide de Réflecteur, voici les champs requis dans une instance de délégué:
Si elles sont appliquées comme un struct (pas d'en-tête objet), ce serait ajouter jusqu'à 24 octets sur x86 et 48 octets sur x64, ce qui est énorme pour un struct.
Sur une autre note, je veux vous demander comment, dans votre projet de la conception, la fabrication, la
CompilerGenerated
fermeture-type d'une structure d'aide en quelque sorte. Où serait le créé délégué du pointeur d'objet point d'? Laissant la fermeture d'instance de type sur la pile sans échapper à l'analyse de extrêmement entreprise risquée.CompilerGenerated
un type de valeur: vous avez raison, il ne l'aide pas. Mais la question est toujours debout sur les délégués eux-mêmes. Je pense que votre hypothèse sur le raisonnement ici fait sens.Je peux dire que les décisions des délégués des types de référence est certainement un mauvais choix de conception. Ils pourraient être les types valeur et encore le support multi-cast délégués.
Imaginer que le Délégué est une structure composée de, disons:
objet cible;
pointeur de la méthode
Il peut être un struct, droit?
La boxe ne se fera que si la cible est un struct (mais le délégué lui-même ne sera pas en boîte).
Vous pouvez penser, il ne prendra pas en charge MultiCastDelegate, mais alors nous pouvons:
Créer un nouvel objet qui va contenir le tableau de normal de délégués.
De retour d'un Délégué (comme struct) à ce nouvel objet, qui mettra en œuvre les Invoquer une itération sur l'ensemble de ses valeurs et de l'appelant Invoque sur eux.
Donc, pour normal de délégués, qui ne vont jamais à l'appel de deux ou plusieurs gestionnaires, il pourrait fonctionner comme une structure.
Malheureusement, cela ne va pas changer dans .Net.
Comme une note de côté, la variance ne requiert pas la Déléguer à des types référence. Les paramètres du délégué doit être de types de référence. Après tout, si vous passez une chaîne de caractères est un objet qui est requis (pour l'entrée, pas de ref ou à l'extérieur), alors aucune conversion n'est nécessaire, comme une chaîne de caractères est déjà un objet.
J'ai vu cette intéressante conversation sur Internet:
Et la réponse:
Prises de ici
Donc, à mon avis, la décision a été prise par la langue de l'utilisabilité des aspects, et non par le compilateur de difficultés technologiques. J'aime nullable délégués.
using
de faire une copie,obj.valueTypeInstance
de faire une copie, l'obtention d'unSpinLock
(valeur type) à partir d'une liste d'effectuer une copie, etc. tous semblent parfaitement en ligne avec les types de valeurs de' sémantique que j'ai dans ma tête. Que les feuilles que beaucoup de gens n'ont pas entièrement groked valeur sémantique.default(Action)
qui n'a rien fait (ou même jeté unNullReferenceException
que le pointeur de fonction serait rien) serait très bien fonctionner.Je suppose que l'une des raisons est le support du multi cast délégués Multi cast délégués sont plus complexes que simplement quelques champs indiquant la cible et de la méthode.
Une autre chose qui n'est possible que dans ce formulaire est délégué de la variance. Ce genre de variance nécessite une conversion de référence entre les deux types.
Il est intéressant de noter F# définit la fonction pointeur de type similaire à celui des délégués, mais plus léger. Mais je ne suis pas sûr si c'est une valeur ou d'un type de référence.