¿Es seguro que las estructuras implementen interfaces?

94

Me parece recordar haber leído algo sobre cómo es malo para las estructuras implementar 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(); } }
nawfal
fuente

Respuestas:

45

Hay varias cosas que suceden en esta pregunta ...

Es posible que una estructura implemente una interfaz, pero existen preocupaciones que surgen con la conversión, la mutabilidad y el rendimiento. Consulte esta publicación para obtener más detalles: https://docs.microsoft.com/en-us/archive/blogs/abhinaba/c-structs-and-interface

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

Scott Dorman
fuente
3
"Como resultado del boxing, las operaciones que cambian el estado interno de la estructura pueden no comportarse correctamente". Da un ejemplo y obtén la respuesta.
2
@Will: No estoy seguro de a qué te refieres en tu comentario. La publicación de blog a la que hice referencia tiene un ejemplo que muestra dónde llamar a un método de interfaz en la estructura en realidad no cambia el valor interno.
Scott Dorman
12
@ScottDorman: En algunos casos, tener estructuras que implementen interfaces puede ayudar a evitar el boxeo. Los mejores ejemplos son IComparable<T>y IEquatable<T>. Almacenar una estructura Fooen una variable de tipo IComparable<Foo>requeriría encuadrar, pero si un tipo genérico Testá restringido a IComparable<T>uno, puede compararlo con otro Tsin tener que enmarcar ninguno de ellos, y sin tener que saber nada Tmás que implementa la restricción. Este comportamiento ventajoso solo es posible gracias a la capacidad de las estructuras para implementar interfaces. Habiendo dicho eso ...
supercat
3
... podría haber sido bueno si hubiera un medio de declarar que una interfaz en particular solo debería considerarse aplicable a estructuras sin caja, ya que hay algunos contextos en los que no sería posible que un objeto de clase o una estructura en caja tenga el deseado comportamientos.
supercat
2
"Las estructuras deben usarse para objetos que tienen semántica de tipo valor ... las operaciones que cambian el estado interno de la estructura pueden no comportarse correctamente". ¿No es el verdadero problema el hecho de que la semántica de tipo valor y la mutabilidad no se mezclan bien?
jpmc26
185

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

La implementación de una interfaz en una estructura no tiene ninguna consecuencia negativa.

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

  • usar el objeto resultante para fines de bloqueo (una idea inmensamente mala de cualquier manera)
  • utilizando semántica de igualdad de referencia y esperando que funcione para dos valores en caja de la misma estructura.

Ambos serían poco probables, en cambio, es probable que esté haciendo una de las siguientes cosas:

Genéricos

Quizás muchas razones razonables para que las estructuras implementen interfaces es que pueden usarse dentro de un contexto genérico con restricciones . Cuando se usa de esta manera, la variable 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 parámetro de tipo
    • siempre que no se utilice ninguna otra restricción como new()o class.
  2. Permita evitar el boxeo en estructuras utilizadas de esta manera.

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

Si thisType es un tipo de valor y thisType implementa el método, 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 thisType.

Esto evita el boxing y dado que el tipo de valor está implementando la interfaz, se debe implementar el método, por lo que no ocurrirá boxing. En el ejemplo anterior, la Equals()invocación se realiza sin ningún recuadro sobre esto . A 1 .

API de baja fricción

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

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

  • IComparable
  • IConvertible
  • IComparable<T>
  • IEquatable<T>(Y así IEquatable)

Muchos también implementan IFormattable, además, 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 similarmente 'ampliamente útil' como una estructura numérica compleja o algunos valores textuales de ancho fijo, entonces implementar muchas de estas interfaces comunes (correctamente) hará que su estructura sea más útil y utilizable.

Exclusiones

Obviamente, si la interfaz implica fuertemente mutabilidad (por ejemplo ICollection), implementarla es una mala idea, ya que significaría que hizo que la estructura fuera mutable (lo que lleva a los tipos de errores descritos ya en los que las modificaciones ocurren en el valor en caja en lugar del original ) o confunde a los usuarios al ignorar las implicaciones de los métodos como Add()o al lanzar excepciones.

Muchas interfaces NO implican mutabilidad (como IFormattable) y sirven como la forma idiomática de exponer cierta funcionalidad de manera consistente. A menudo, al usuario de la estructura no le importará ninguna sobrecarga de 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 al invocar métodos virtuales en variables que se sabe que son de un tipo de estructura específico 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 consecuencias interesantes ). Sin embargo la semántica de foreach especifican que si los implementos empadronador IDisposablea continuación Dispose()se llamará una vez completada la iteración. Obviamente, que esto ocurra a través de una llamada en caja 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 podrían introducirse muchos errores sutiles en casos complejos. Por tanto, el IL emitido en este tipo de situaciones 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: llamar System.Collections.Generic.List.get_Current
IL_0018: stloc.1     
IL_0019: ldloca.s 02 
IL_001B: llamar 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.

ShuggyCoUk
fuente
1
El sitio egheadcafe.com se ha movido, pero no hizo un buen trabajo conservando su contenido. Lo intenté, pero no puedo encontrar el documento original de eggheadcafe.com/software/aspnet/31702392/… , sin el conocimiento del OP. (PS +1 para un excelente resumen).
Abel
2
Esta es una gran respuesta, pero creo que puede mejorarla moviendo el "Resumen" a la parte superior como "TL; DR". Proporcionar la conclusión primero ayuda al lector a saber adónde va con las cosas.
Hans
Debería haber una advertencia del compilador al convertir un structen un interface.
Jalal
8

En algunos casos, puede ser bueno que una estructura implemente una interfaz (si nunca fue útil, es dudoso que los creadores de .net la hayan proporcionado). Si una estructura implementa una interfaz de solo lectura como IEquatable<T>, almacenar la estructura en una ubicación de almacenamiento (variable, parámetro, elemento de matriz, etc.) de tipo IEquatable<T>requerirá que esté en caja (cada tipo de estructura en realidad define dos tipos de cosas: un almacenamiento tipo de ubicación 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 es implícitamente convertible al segundo - "boxing" - y el segundo puede convertirse al primero a través de una conversión explícita-- "unboxing"). Sin embargo, es posible explotar la implementación de una estructura de una interfaz sin encajonar, utilizando lo que se denominan genéricos restringidos.

Por ejemplo, si uno tuviera un método CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>, dicho método podría llamar thing1.Compare(thing2)sin tener que box thing1o thing2. Si thing1resulta ser, por ejemplo, an 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 tanto de la cosa que aloja el método como de la cosa 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(a diferencia de 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>esta 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 se preparará enumerator1para leer el primer elemento. Se copiará el estado de ese enumerador enumerator2. La declaración marcada # 2 avanzará esa copia para leer el segundo elemento, pero no afectará enumerator1. Luego se copiará el estado de ese segundo enumerador enumerator3, que se avanzará mediante la declaración marcada # 3. Entonces, debido enumerator3y enumerator4son los dos tipos de referencia, una referencia al enumerator3entonces será copiado a enumerator4, tan marcada comunicado avanzará de manera efectiva tanto enumerator3 y enumerator4.

Algunas personas intentan fingir que los tipos de valores y los tipos de referencia son ambos tipos 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 la List<String>.Enumeratorcual se almacena en una ubicación de ese tipo es un tipo de valor y se comporta como un tipo de valor; copiarlo en 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 una especie de Object, pero el primero no lo es.

Por cierto, un par de notas más: (1) En general, los tipos de clases mutables deberían tener sus Equalsmétodos para probar la igualdad de referencia, pero no hay una forma decente para que una estructura en caja lo haga; (2) a pesar de su nombre, ValueTypees un tipo de clase, no un tipo de valor; todos los tipos derivados de System.Enumson tipos de valor, al igual que todos los tipos que derivan de ValueTypecon la excepción de System.Enum, pero ambos ValueTypey System.Enumson tipos de clase.

Super gato
fuente
3

Las estructuras se implementan como 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, la "encajará" en un tipo de referencia, anulando así la ventaja de usar una estructura en primer lugar.

La única razón por la 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 heredando una interfaz y pasa interfaces, pierde esa naturaleza de tipo de valor de la estructura. También podría convertirlo en una clase si necesita interfaces.

ingeniero
fuente
¿Funciona así también para primitivas que implementan interfaces?
aoetalks
3

(Bueno, no tengo nada importante que agregar, pero aún no tengo destreza en la 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, la obtención de una referencia de interfaz a una estructura la ENCUENTRA . Entonces, penalización de rendimiento y así sucesivamente.

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

Gishu
fuente
Si se pasa un Int32a un método que acepta un tipo genérico T:IComparable<Int32>(que puede ser un parámetro de tipo genérico del método o la clase del método), ese método podrá usar el Comparemétodo en el objeto pasado sin encasillarlo.
supercat
0

No hay consecuencias para una estructura que implemente una interfaz. Por ejemplo, las estructuras del sistema integradas implementan interfaces como IComparabley IFormattable.

Joseph Daigle
fuente
0

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

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

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

FlySwat
fuente
¿Con qué frecuencia pasa IComparable en lugar de la implementación concreta?
FlySwat
No es necesario pasar IComparablepara marcar el valor. Simplemente llamando a un método que espera IComparablecon un tipo de valor que lo implementa, implícitamente encuadrará el tipo de valor.
Andrew Hare
1
@AndrewHare: Los genéricos restringidos permiten IComparable<T>invocar métodos en estructuras de tipo Tsin recuadro.
supercat
-10

Las estructuras son como clases que viven en la pila. No veo ninguna razón por la que deberían ser "inseguros".

Sklivvz
fuente
Excepto que carecen de herencia.
FlySwat
7
Tengo que estar en desacuerdo con cada parte de esta respuesta; que no necesariamente viven en la pila, y la copia-semántica es muy diferente a las clases.
Marc Gravell
1
Son inmutables, el uso excesivo de la estructura entristecerá su memoria :(
Teoman shipahi
1
@Teomanshipahi El uso excesivo de instancias de clase hará que su recolector de basura se enoje.
IllidanS4 apoya a Monica
4
Para alguien que tiene más de 20.000 repeticiones, esta respuesta es simplemente inaceptable.
Krythic