c# - usar - tipos de delegates
¿Por qué los delegados hacen referencia a los tipos? (7)
Aquí hay una suposición desinformada:
Si los delegados se implementaran como tipos de valor, sería muy costoso copiar las instancias, ya que una instancia de delegado es relativamente pesada. Tal vez MS consideró que sería más seguro diseñarlos como tipos de referencia inmutables: copiar las referencias de tamaño de la máquina a las instancias es relativamente barato.
Una instancia de delegado necesita, al menos:
- Una referencia de objeto (la referencia "this" para el método envuelto si es un método de instancia).
- Un puntero a la función envuelta.
- Una referencia al objeto que contiene la lista de invocación de multidifusión. Tenga en cuenta que un tipo de delegado debe admitir, por diseño, la multidifusión usando el mismo tipo de delegado.
Supongamos que los delegados de tipo de valor se implementaron de manera similar a la implementación de tipo de referencia actual (esto es quizás algo irrazonable, un diseño diferente bien puede haber sido elegido para mantener el tamaño reducido) para ilustrar. Usando Reflector, aquí están los campos requeridos en una instancia de delegado:
System.Delegate: _methodBase, _methodPtr, _methodPtrAux, _target
System.MulticastDelegate: _invocationCount, _invocationList
Si se implementa como una estructura (sin encabezado de objeto), estos agregarían hasta 24 bytes en x86 y 48 bytes en x64, que es masivo para una estructura.
En otra nota, quiero preguntar cómo, en el diseño propuesto, hacer que CompilerGenerated
-Generated-type sea una estructura ayuda de alguna manera. ¿A dónde apunta el puntero de objeto del delegado creado? Dejar la instancia del tipo de cierre en la pila sin un análisis de escape adecuado sería un negocio extremadamente arriesgado.
Nota rápida sobre la respuesta aceptada : No estoy de acuerdo con una pequeña parte de la respuesta de Jeffrey , a saber, el hecho de que, como Delegate
tenía que ser un tipo de referencia, se deduce que todos los delegados son tipos de referencia. (Simplemente, no es cierto que una cadena de herencia de varios niveles System.ValueType
tipos de valores; todos los tipos enum, por ejemplo, heredan de System.Enum
, que a su vez hereda de System.ValueType
, que hereda de System.Object
, toda referencia tipos.) Sin embargo, creo que el hecho de que, fundamentalmente, todos los delegados hereden no solo del Delegate
sino de MulticastDelegate
es la realización crítica aquí. Como señala Raymond en un comentario de su respuesta, una vez que te has comprometido a admitir múltiples suscriptores, no tiene sentido no usar un tipo de referencia para el delegado, dada la necesidad de una matriz en algún lugar.
Ver actualización en la parte inferior.
Siempre me ha parecido extraño que si hago esto:
Action foo = obj.Foo;
Estoy creando un nuevo objeto de Action
, todo el tiempo. Estoy seguro de que el costo es mínimo, pero implica la asignación de memoria para luego ser recolectada.
Dado que los delegados son intrínsecamente inmutables, me pregunto por qué no podrían ser tipos de valor. Entonces, una línea de código como la anterior no incurriría en nada más que una simple asignación a una dirección de memoria en la pila *.
Incluso considerando funciones anónimas, parece (a mí ) que esto funcionaría. Considere el siguiente ejemplo sencillo.
Action foo = () => { obj.Foo(); };
En este caso, foo
constituye un cierre , sí. Y en muchos casos, imagino que esto requiere un tipo de referencia real (como cuando las variables locales se cierran y se modifican dentro del cierre). Pero en algunos casos, no debería. Por ejemplo, en el caso anterior, parece que un tipo para apoyar el cierre podría ser así: Retiro mi punto original sobre esto. Lo siguiente realmente necesita ser un tipo de referencia (o: no necesita serlo, pero si es una struct
se va a encasillar de todos modos). Por lo tanto, ignore el ejemplo de código siguiente. Lo dejo solo para dar contexto a las respuestas, lo menciono específicamente.
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;
¿Esta pregunta tiene sentido? Como lo veo, hay dos posibles explicaciones:
- Implementar delegados correctamente como tipos de valor habría requerido trabajo / complejidad adicional, ya que el soporte para cosas como cierres que sí modifican los valores de las variables locales habría requerido tipos de referencia generados por el compilador de todos modos.
- Existen otros motivos por los cuales, bajo el paraguas, los delegados simplemente no pueden implementarse como tipos de valor.
Al final, no voy a perder ningún sueño por esto; es algo que he tenido curiosidad por un tiempo.
Actualización : en respuesta al comentario de Ani, veo por qué el tipo CompilerGenerated
por CompilerGenerated
en mi ejemplo anterior podría ser un tipo de referencia, ya que si un delegado comprenderá un puntero de función y un puntero de objeto necesitará un tipo de referencia de todos modos ( al menos para funciones anónimas que usan cierres, ya que incluso si introdujes un parámetro de tipo genérico adicional, por ejemplo, Action<TCaller>
esto no cubriría los tipos que no pueden nombrarse!). Sin embargo , todo lo que hace es hacer que me arrepienta de haber planteado la cuestión de los tipos generados por el compilador para los cierres en la discusión. Mi pregunta principal es acerca de los delegados , es decir, la cosa con el puntero de la función y el puntero del objeto. Todavía me parece que podría ser un tipo de valor.
En otras palabras, incluso si esto ...
Action foo = () => { obj.Foo(); };
... requiere la creación de un objeto de tipo de referencia (para apoyar el cierre, y darle al delegado algo para referenciar), ¿por qué requiere la creación de dos (el objeto de apoyo de cierre más el delegado de Action
)?
* Sí, sí, detalle de implementación, lo sé! Lo único que quiero decir es almacenamiento de memoria a corto plazo .
Imagine si los delegados fueran tipos de valor.
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?
Según su propuesta, esto se convertiría internamente a
struct CompilerGenerated
{
int counter = 0;
public Execute() { ++counter; }
};
Notify handler = new CompilerGenerated();
SignalTwice(handler);
System.Console.WriteLine(counter); // what should this print?
Si el delegate
fuera un tipo de valor, entonces SignalEvent
obtendría una copia del handler
, lo que significa que se crearía un nuevo CompilerGenerated
generado (una copia del handler
) y se pasaría a SignalEvent
. SignalTwice
ejecutará el delegado dos veces, lo que incrementa el counter
dos veces en la copia . Y luego SignalTwice
regresa, y la función imprime 0, porque el original no fue modificado.
La pregunta se reduce a esto: la especificación CLI (Common Language Infrastructure) dice que los delegados son tipos de referencia. ¿Por qué esto es tan?
Una razón es claramente visible en .NET Framework hoy. En el diseño original, había dos tipos de delegados: delegados normales y delegados de "multidifusión", que podían tener más de un objetivo en su lista de invocación. La clase MulticastDelegate
hereda de Delegate
. Como no puede heredar de un tipo de valor, Delegate
debe ser un tipo de referencia.
Al final, todos los delegados reales terminaron siendo delegados de multidifusión, pero en esa etapa del proceso, ya era demasiado tarde para fusionar las dos clases. Vea esta publicación en el blog sobre este tema exacto:
Abandonamos la distinción entre Delegate y MulticastDelegate hacia el final de V1. En ese momento, habría sido un cambio masivo fusionar las dos clases, así que no lo hicimos. Debe fingir que están fusionados y que solo existe MulticastDelegate.
Además, los delegados actualmente tienen de 4 a 6 campos, todos punteros. Normalmente, 16 bytes se considera el límite superior donde la memoria de guardado aún gana con el copiado adicional. Un MulticastDelegate
64 bits ocupa 48 bytes. Dado esto, y el hecho de que estaban usando herencia sugiere que una clase fue la elección natural.
Puedo decir que hacer delegados como tipos de referencia definitivamente es una mala elección de diseño. Podrían ser tipos de valor y aún así admitir delegados de multidifusión.
Imagine que Delegate es una estructura compuesta, digamos, de object object; puntero al método
Puede ser una estructura, ¿verdad? El boxeo solo ocurrirá si el objetivo es una estructura (pero el delegado en sí mismo no estará encuadrado).
Puede pensar que no admitirá MultiCastDelegate, pero luego podemos: Crear un nuevo objeto que contendrá la matriz de delegados normales. Devuelve un Delegado (como struct) a ese nuevo objeto, que implementará Invoke iterando sobre todos sus valores y llamando a Invoke sobre ellos.
Entonces, para los delegados normales, que nunca van a llamar a dos o más controladores, podría funcionar como una estructura. Desafortunadamente, eso no va a cambiar en .Net.
Como nota al margen, la varianza no requiere que el Delegado sea un tipo de referencia. Los parámetros del delegado deben ser tipos de referencia. Después de todo, si pasa una cuerda si se requiere un objeto (para entrada, no ref o fuera), entonces no se necesita molde, ya que la cuerda ya es un objeto.
Solo hay una razón por la cual el Delegado necesita ser una clase, pero es grande: mientras que un delegado podría ser lo suficientemente pequeño como para permitir un almacenamiento eficiente como un tipo de valor (8 bytes en sistemas de 32 bits o 16 bytes en 64 bits) sistemas), no hay forma de que sea lo suficientemente pequeño como para garantizar de manera eficiente si un hilo intenta escribir un delegado mientras otro hilo intenta ejecutarlo, el último hilo no terminaría invocando el método anterior en el nuevo objetivo, o el nuevo método en el viejo objetivo. Permitir que tal cosa ocurra sería un gran agujero de seguridad. Tener delegados como tipos de referencia evita este riesgo.
En realidad, incluso mejor que tener delegados, los tipos de estructura serían tenerlos como interfaces. Crear un cierre requiere crear dos objetos de montón: un objeto generado por el compilador para contener cualquier variable cerrada, y un delegado para invocar el método apropiado en ese objeto. Si los delegados fueran interfaces, el objeto que contenía las variables cerradas podría utilizarse como delegado, sin necesidad de otro objeto.
Supongo que una de las razones es el soporte para los delegados de reparto múltiple Los delegados de reparto múltiple son más complejos que simplemente unos pocos campos que indican el objetivo y el método.
Otra cosa que solo es posible en esta forma es la variación del delegado. Este tipo de varianza requiere una conversión de referencia entre los dos tipos.
Curiosamente, F # define su propio tipo de puntero de función que es similar a los delegados, pero más liviano. Pero no estoy seguro de si es un valor o un tipo de referencia.
Vi esta conversación interesante en Internet:
Inmutable no significa que tiene que ser un tipo de valor. Y no se requiere que algo que sea un tipo de valor sea inmutable. Los dos suelen ir de la mano, pero en realidad no son la misma cosa, y de hecho hay contraejemplos de cada uno en .NET Framework (la clase String, por ejemplo).
Y la respuesta:
La diferencia es que, si bien los tipos de referencia inmutables son razonablemente comunes y perfectamente razonables, hacer que los tipos de valor sean mutables es casi siempre una mala idea y puede dar como resultado un comportamiento muy confuso.
Tomado de here
Entonces, en mi opinión, la decisión fue tomada por los aspectos de usabilidad del lenguaje, y no por las dificultades tecnológicas del compilador. Me encantan los delegados que aceptan nulos.