it-swarm-es.tech

¿Es seguro que las estructuras implementen interfaces?

Parece recordar haber leído algo acerca de cómo es malo que las estructuras implementen interfaces en CLR a través de C #, pero parece que no puedo encontrar nada al respecto. ¿Es malo? ¿Hay consecuencias no deseadas de hacerlo?

public interface Foo { Bar GetBar(); }
public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }
81
Will

Hay varias cosas en esta pregunta ...

Es posible que una estructura implemente una interfaz, pero existen inquietudes relacionadas con el lanzamiento, la mutabilidad y el rendimiento. Consulte esta publicación para obtener más detalles: http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx

En general, las estructuras se deben usar para objetos que tienen semántica de tipo valor. Al implementar una interfaz en una estructura, puede encontrarse con problemas de boxeo, ya que la estructura se repite entre la estructura y la interfaz. Como resultado del boxeo, las operaciones que cambian el estado interno de la estructura pueden no comportarse correctamente.

42
Scott Dorman

Como nadie más proporcionó explícitamente esta respuesta, agregaré lo siguiente:

Implementar una interfaz en una estructura no tiene consecuencias negativas en absoluto.

Cualquier variable del tipo de interfaz utilizado para mantener una estructura dará como resultado un valor en caja de esa estructura que se está utilizando. Si la estructura es inmutable (algo bueno), esto es, en el peor de los casos, un problema de rendimiento a menos que usted sea:

  • usando el objeto resultante para propósitos de bloqueo (una idea inmensamente mala)
  • usando la semántica de igualdad de referencia y esperando que funcione para dos valores encuadrados de la misma estructura.

Ambos de estos serían improbables, en lugar de eso, es probable que esté realizando una de las siguientes acciones:

Genéricos

Quizás muchas razones razonables para que las estructuras implementen interfaces es que se pueden usar en un contexto generic con restricciones. Cuando se usa de esta manera, la variable es así:

class Foo<T> : IEquatable<Foo<T>> where T : IEquatable<T>
{
    private readonly T a;

    public bool Equals(Foo<T> other)
    {
         return this.a.Equals(other.a);
    }
}
  1. Habilite el uso de la estructura como un parámetro de tipo
    • siempre que no se utilice ninguna otra restricción como new() o class.
  2. Permitir evitar el boxeo en las estructuras utilizadas de esta manera.

Entonces this.a NO es una referencia de interfaz, por lo tanto no causa una caja de lo que se coloca en ella. Además, cuando el compilador de c # compila las clases genéricas y necesita insertar invocaciones de los métodos de instancia definidos en las instancias del parámetro Type T, puede usar el código restringido opcode:

Si este Tipo es un tipo de valor y este Tipo implementa el método, entonces ptr se pasa sin modificar como el puntero 'this' a una instrucción de método de llamada, para la implementación del método por este Tipo.

Esto evita el boxeo y dado que el tipo de valor está implementando la interfaz es debe implementar el método, por lo tanto, no se producirá ningún boxeo. En el ejemplo anterior, la invocación Equals() se realiza sin ningún recuadro en esto.a1.

APIs de baja fricción

La mayoría de las estructuras deben tener una semántica de tipo primitivo donde los valores idénticos a nivel de bits se consideran iguales2. El tiempo de ejecución proporcionará dicho comportamiento en la Equals() implícita, pero esto puede ser lento. Además, esta igualdad implícita está no expuesta como una implementación de IEquatable<T> y, por lo tanto, evita que las estructuras se usen fácilmente como claves para los diccionarios a menos que se implementen explícitamente ellos mismos. Por lo tanto, es común que muchos tipos de estructuras públicas declaren que implementan IEquatable<T> (donde T es ellos mismos) para hacer esto más fácil y mejor, así como para que sea consistente con el comportamiento de muchos tipos de valores existentes dentro del CLR BCL.

Todas las primitivas en el BCL implementan como mínimo:

  • IComparable
  • IConvertible
  • IComparable<T>
  • IEquatable<T> (y por lo tanto IEquatable)

Muchos también implementan IFormattable, y muchos de los tipos de valores definidos por el Sistema, como DateTime, TimeSpan y Guid, implementan muchos o todos estos también. Si está implementando un tipo similar 'ampliamente útil' como una estructura de números compleja o algunos valores textuales de ancho fijo, la implementación de muchas de estas interfaces comunes (correctamente) hará que su estructura sea más útil y utilizable.

Exclusiones

Obviamente, si la interfaz implica fuertemente mutabilidad (como ICollection), implementarlo es una mala idea, ya que significaría que cualquiera de los dos hizo que la estructura sea mutable (lo que lleva a la clase de errores descritos ya donde ocurren las modificaciones). el valor en caja en lugar del original) o confunde a los usuarios ignorando las implicaciones de los métodos como Add() o lanzando excepciones.

Muchas interfaces NO implican mutabilidad (como IFormattable) y sirven como la manera idiomática de exponer cierta funcionalidad de una manera consistente. A menudo, el usuario de la estructura no se preocupará por los gastos generales del boxeo por tal comportamiento.

Resumen

Cuando se hace con sensatez, en tipos de valores inmutables, la implementación de interfaces útiles es una buena idea


Notas:

1: Tenga en cuenta que el compilador puede usar esto cuando invoca métodos virtuales en variables que son conocido para ser de un tipo de estructura específica pero en las que se requiere invocar un método virtual. Por ejemplo:

List<int> l = new List<int>();
foreach(var x in l)
    ;//no-op

El enumerador devuelto por la Lista es una estructura, una optimización para evitar una asignación al enumerar la lista (Con algunas interesantes consecuencias ). Sin embargo, la semántica de foreach especifica que si el enumerador implementa IDisposable, entonces Dispose() será llamado una vez que se complete la iteración. Obviamente, si esto ocurre a través de una llamada en caja, se eliminaría cualquier beneficio de que el enumerador sea una estructura (de hecho, sería peor). Peor aún, si dispose call modifica el estado del enumerador de alguna manera, esto sucedería en la instancia en caja y muchos errores sutiles podrían introducirse en casos complejos. Por lo tanto la IL emitida en este tipo de situación es:

 IL_0001: newobj System.Collections.Generic.List..ctor 
 IL_0006: stloc.0 
 IL_0007: nop 
 IL_0008: ldloc.0 
 IL_0009: Callvirt System.Collections.Generic.List.GetEnumerator 
 IL_000E: stloc.2 
 IL_000F: br.s IL_0019 
 IL_0011: ldloca.s 02 
 IL_0013: llame a System.Collections.Generic.List.get_Current 
 IL_0018: stloc.1 
 IL_0019: ldloca.s 02 
 IL_001B: call System.Collections.Generic.List.MoveNext 
 IL_0020: stloc.3 
 IL_0021: ldloc.3 
 IL_0022: brtrue.s IL_0011 
 IL_0024: leave.s IL_0035 
 IL_0026: ldloca .s 02 
 IL_0028: restringido. System.Collections.Generic.List.Enumerator 
 IL_002E: callvirt System.IDisposable.Dispose 
 IL_0033: nop 
 IL_0034: endfinally 

Por lo tanto, la implementación de IDisposable no causa ningún problema de rendimiento y el aspecto mutable (lamentable) del enumerador se conserva si el método Dispose realmente hace algo.

2: double y float son excepciones a esta regla donde los valores de NaN no se consideran iguales.

158
ShuggyCoUk

En algunos casos, puede ser bueno para una estructura implementar una interfaz (si nunca fue útil, es dudoso que los creadores de .net lo hayan proporcionado). Si una estructura implementa una interfaz de solo lectura como IEquatable<T>, el almacenamiento de la estructura en una ubicación de almacenamiento (variable, parámetro, elemento de matriz, etc.) del tipo IEquatable<T> requerirá que esté encuadrada (cada tipo de estructura en realidad define dos tipos de cosas: un tipo de ubicación de almacenamiento que se comporta como un tipo de valor y un tipo de objeto de montón que se comporta como un tipo de clase; el primero se puede convertir implícitamente al segundo "boxeo" y el segundo se puede convertir al primero por medio de una conversión explícita - "Unboxing"). Es posible explotar la implementación de una estructura de una interfaz sin boxeo, sin embargo, utilizando lo que se llaman genéricos restringidos.

Por ejemplo, si uno tuviera un método CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>, tal método podría llamar a thing1.Compare(thing2) sin tener que marcar thing1 o thing2. Si resulta que thing1 es, por ejemplo, un Int32, el tiempo de ejecución lo sabrá cuando genere el código para CompareTwoThings<Int32>(Int32 thing1, Int32 thing2). Dado que sabrá el tipo exacto de lo que contiene el método y lo que se pasa como parámetro, no tendrá que marcar ninguno de ellos.

El mayor problema con las estructuras que implementan interfaces es que una estructura que se almacena en una ubicación del tipo de interfaz, Object, o ValueType (en oposición a una ubicación de su propio tipo) se comportará como un objeto de clase. Para las interfaces de solo lectura, esto generalmente no es un problema, pero para una interfaz mutante como IEnumerator<T> puede producir una semántica extraña.

Considere, por ejemplo, el siguiente código:

List<String> myList = [list containing a bunch of strings]
var enumerator1 = myList.GetEnumerator();  // Struct of type List<String>.IEnumerator
enumerator1.MoveNext(); // 1
var enumerator2 = enumerator1;
enumerator2.MoveNext(); // 2
IEnumerator<string> enumerator3 = enumerator2;
enumerator3.MoveNext(); // 3
IEnumerator<string> enumerator4 = enumerator3;
enumerator4.MoveNext(); // 4

La declaración marcada # 1 cebará enumerator1 para leer el primer elemento. El estado de ese enumerador se copiará en enumerator2. La declaración marcada # 2 avanzará esa copia para leer el segundo elemento, pero no afectará a enumerator1. El estado de ese segundo enumerador se copiará en enumerator3, que se avanzará con la declaración marcada # 3. Luego, como enumerator3 y enumerator4 son tipos de referencia, una REFERENCE a enumerator3 se copiará en enumerator4, así que la instrucción marcada avanzará efectivamente ambosenumerator3 y enumerator4.

Algunas personas intentan fingir que los tipos de valor y los tipos de referencia son ambos tipos de Object, pero eso no es realmente cierto. Los tipos de valor real son convertibles a Object, pero no son instancias de él. Una instancia de List<String>.Enumerator que se almacena en una ubicación de ese tipo es un tipo de valor y se comporta como un tipo de valor; copiarlo a una ubicación de tipo IEnumerator<String> lo convertirá en un tipo de referencia, y se comportará como un tipo de referencia. El último es un tipo de Object, pero el primero no lo es.

Por cierto, un par de notas más: (1) En general, los tipos de clase mutables deben tener sus métodos Equals de prueba de igualdad de referencia, pero no hay una forma decente para que una estructura en caja lo haga; (2) a pesar de su nombre, ValueType es un tipo de clase, no un tipo de valor; todos los tipos derivados de System.Enum son tipos de valor, al igual que todos los tipos que derivan de ValueType con la excepción de System.Enum, pero ambos ValueType y System.Enum son tipos de clase.

8
supercat

(Bueno, no tengo nada importante que agregar, pero no tengo destreza de edición, así que aquí va ...)
Perfectamente seguro. Nada ilegal con la implementación de interfaces en estructuras. Sin embargo, deberías preguntarte por qué querrías hacerlo.

Sin embargo, obtener una referencia de interfaz a una estructura será BOX it. Así que la penalización del rendimiento y así sucesivamente.

El único escenario válido en el que puedo pensar ahora es ilustrado en mi publicación aquí . Cuando desee modificar el estado de una estructura almacenada en una colección, deberá hacerlo a través de una interfaz adicional expuesta en la estructura.

3
Gishu

Las estructuras se implementan ya que los tipos de valor y las clases son tipos de referencia. Si tiene una variable de tipo Foo, y almacena una instancia de Fubar en ella, se "encajonará" en un tipo de referencia, eliminando la ventaja de usar una estructura en primer lugar.

La única razón que veo para usar una estructura en lugar de una clase es porque será un tipo de valor y no un tipo de referencia, pero la estructura no puede heredar de una clase. Si tiene la estructura heredada de una interfaz y pasa por las interfaces, perderá la naturaleza de tipo de valor de la estructura. También podría ser una clase si necesita interfaces.

3
dotnetengineer

Creo que el problema es que causa el boxeo porque las estructuras son tipos de valor, por lo que hay una leve penalización en el rendimiento.

Este enlace sugiere que podría haber otros problemas con él ...

http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx

1
Simon Keep

No hay consecuencias para una estructura que implementa una interfaz. Por ejemplo, las estructuras de sistema integradas implementan interfaces como IComparable y IFormattable.

0
Joseph Daigle

Hay muy pocas razones para que un tipo de valor implemente una interfaz. Como no puede subclasificar un tipo de valor, siempre puede referirse a él como su tipo concreto.

Por supuesto, a menos que tenga varias estructuras que implementen la misma interfaz, podría ser marginalmente útil en ese momento, pero en ese momento recomendaría usar una clase y hacerlo bien.

Por supuesto, al implementar una interfaz, estás encajonando la estructura, por lo que ahora está en el montón, y ya no podrás pasarla por valor ... Esto realmente refuerza mi opinión de que solo debes usar una clase en esta situación.

0
FlySwat