Pregunta ¿Es seguro para las estructuras implementar interfaces?


Me parece recordar haber leído algo sobre la dificultad de las estructuras para implementar interfaces en CLR a través de C #, pero parece que no puedo encontrar nada al respecto. ¿Es mala? ¿Hay consecuencias involuntarias de hacerlo?

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

73
2017-09-15 14:59


origen


Respuestas:


Hay varias cosas que están pasando en esta pregunta ...

Es posible que una estructura implemente una interfaz, pero hay preocupaciones que surgen con la conversión, la mutabilidad y el rendimiento. Vea esta publicación para más detalles: http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx

En general, las estructuras deberían usarse para objetos que tengan semántica de tipo de valor. Al implementar una interfaz en una estructura, puede encontrarse con problemas de boxeo a medida que la estructura se envía hacia adelante y hacia atrás entre la estructura y la interfaz. Como resultado del boxeo, las operaciones que cambian el estado interno de la estructura pueden no comportarse correctamente.


37
2017-09-15 15:09



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

Implementar una interfaz en una estructura no tiene consecuencias negativas de ningún tipo.

Alguna variable del tipo de interfaz utilizado para contener una estructura dará como resultado un valor enmarcado de esa estructura. Si la estructura es inmutable (algo bueno), este es, en el peor de los casos, un problema de rendimiento a menos que seas:

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

Ambos serían poco probables, en su lugar es probable que esté haciendo uno de los siguientes:

Genéricos

Quizás muchos motivos razonables para estructurar la implementación de interfaces es que se puedan usar dentro de un genérico contexto con restricciones. Cuando se utiliza de esta manera la variable como tal:

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 struct como un parámetro de tipo
    • siempre que no haya otra restricción como new() o class es usado.
  2. Permitir evitar el boxeo en las estructuras utilizadas de esta manera.

Entonces this.a NO es una referencia de interfaz por lo que no causa una caja de lo que sea que se coloque 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 Tipo T, puede usar el constreñido opcode:

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 boxeo y dado que el tipo de valor está implementando la interfaz es debe implementar el método, por lo tanto, no habrá boxeo. En el ejemplo anterior, Equals() la invocación se realiza sin ninguna casilla en this.a1.

API de baja fricción

La mayoría de las estructuras deberían tener una semántica primitiva en la que los valores idénticos a nivel de bits se consideren iguales2. El tiempo de ejecución proporcionará tal comportamiento en el implícito Equals() pero esto puede ser lento. También esta igualdad implícita es no expuesto como una implementación de IEquatable<T> y, por lo tanto, evita que las estructuras se utilicen fácilmente como claves para Diccionarios a menos que lo implementen ellos mismos explícitamente. Por lo tanto, es común que muchos tipos de estructuras públicas declaren que implementan IEquatable<T> (dónde T es ellos mismos) para hacer que esto sea más fácil y de mejor rendimiento, así como también consistente con el comportamiento de muchos tipos de valores existentes dentro del CLR BCL.

Todas las primitivas en BCL implementan como mínimo:

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

Muchos también implementan IFormattableAdemá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, 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) luego implementarlo es una mala idea, ya que significaría que ha hecho que la estructura sea mutable (llevando al tipo de errores descritos donde 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 lanzando excepciones.

Muchas interfaces NO implican mutabilidad (como IFormattable) y sirven como la forma idiomática de exponer ciertas funciones de manera consistente. A menudo, al usuario de la estructura no le importará ninguna sobrecarga de boxeo para 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 son conocido ser de un tipo de estructura específico pero en el 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 algunos datos interesantes). Consecuencias) Sin embargo, la semántica de foreach especifica que si el enumerador implementa IDisposable entonces Dispose() se llamará una vez que se complete 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 disponer de llamada modifica el estado del enumerador de alguna forma, esto sucedería en la instancia encuadrada y podrían introducirse muchos errores sutiles en casos complejos. Por lo tanto, el IL emitido 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: llama a System.Collections.Generic.List.get_Current
IL_0018: stloc.1
IL_0019: ldloca.s 02
IL_001B: llame a 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 preserva 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.


150
2017-08-17 18:17



En algunos casos, puede ser útil que una estructura implemente una interfaz (si nunca fue útil, es dudoso que los creadores de .net lo hubieran previsto). Si una estructura implementa una interfaz de solo lectura como IEquatable<T>, almacenando la estructura en una ubicación de almacenamiento (variable, parámetro, elemento de matriz, etc.) de tipo IEquatable<T> requerirá que esté encuadrado (cada tipo de estructura realmente 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 es implícitamente convertible al segundo-- "boxeo" - y el segundo se puede convertir al primero a través del lanzamiento explícito - "unboxing"). Sin embargo, es posible explotar la implementación de una interfaz de una interfaz sin boxeo, utilizando lo que se denomina genéricos restringidos.

Por ejemplo, si uno tiene un método CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>, tal método podría llamar thing1.Compare(thing2) sin tener que boxear thing1 o thing2. Si thing1 pasa a ser, por ejemplo, un Int32, el tiempo de ejecución sabrá que cuando genera el código para CompareTwoThings<Int32>(Int32 thing1, Int32 thing2). Como sabrá el tipo exacto de la cosa que hospeda 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 una estructura que se almacena en una ubicación de 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 no es generalmente un problema, sino 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 n. ° 1 se iniciará enumerator1 para leer el primer elemento El estado de ese enumerador se copiará a enumerator2. La declaración marcada n. ° 2 hará avanzar esa copia para leer el segundo elemento, pero no afectará enumerator1. El estado de ese segundo enumerador se copiará a enumerator3, que se avanzará mediante la declaración marcada n. ° 3. Entonces, porque enumerator3 y enumerator4 son ambos tipos de referencia, una REFERENCIA a enumerator3 luego será copiado a enumerator4, por lo tanto, la declaración marcada avanzará efectivamente ambos  enumerator3 y enumerator4.

Algunas personas intentan pretender que los tipos de valores y los tipos de referencia son ambos tipos de Object, pero eso no es realmente cierto. Los tipos de valores reales son convertibles a Object, pero no son ejemplos de eso. 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; copiándolo en una ubicación de tipo IEnumerator<String> lo convertirá en un tipo de referencia, y se comportará como un tipo de referencia. Este último es un tipo de Object, pero el primero no es.

Por cierto, un par de notas más: (1) En general, los tipos de clases mutables deben tener su Equals los métodos prueban la igualdad de referencia, pero no hay una forma decente para que una estructura encuadrada 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, como todos los tipos que se derivan de ValueType con la excepción de System.Enum, sino ambos ValueType y System.Enum son tipos de clase.


6
2018-01-14 16:34



Las estructuras se implementan como tipos de valores y las clases son tipos de referencia. Si tiene una variable de tipo Foo, y almacena una instancia de Fubar en ella, la "Recuperará" en un tipo de referencia, derrotando así la ventaja de usar una estructura en primer lugar.

La única razón por la que veo 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 hereda una interfaz y pasa las interfaces, pierde la naturaleza del tipo de valor de la estructura. Bien podría ser una clase si necesita interfaces.


3
2017-09-15 15:06



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

sin embargo obtener una referencia de interfaz a una estructura será BOX eso. Por lo tanto, pena de rendimiento, etc.

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, tendrá que hacerlo a través de una interfaz adicional expuesta en la estructura.


3
2017-09-15 15:14



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

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

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


1
2017-09-15 15:04



No hay consecuencias para una estructura que implementa una interfaz. Por ejemplo, el sistema incorporado estructura implementa interfaces como IComparable y IFormattable.


0
2017-09-15 15:04