Pregunta

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

public interface Foo { Bar GetBar(); }
public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }
¿Fue útil?

Solución

En esta pregunta pasan varias cosas...

Es posible que una estructura implemente una interfaz, pero existen preocupaciones relacionadas 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 deben usarse para objetos que tienen una semántica de tipo valor.Al implementar una interfaz en una estructura, puede encontrarse con problemas de boxeo a medida que la estructura se desplaza de un lado a otro entre la estructura y la interfaz.Como resultado del boxeo, es posible que las operaciones que cambian el estado interno de la estructura no se comporten correctamente.

Otros consejos

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

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

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

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

Ambas cosas 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 sea que puedan usarse dentro de un genérico contexto con restricciones.Cuando se usa de esta manera, la variable queda 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. Habilitar el uso de la estructura como parámetro de tipo.
    • siempre y cuando no haya otra restricción como new() o class se utiliza.
  2. Permitir evitar el boxeo en estructuras utilizadas de esta manera.

Entonces this.a NO es una referencia de interfaz, por lo que no genera 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 constreñido código de operación:

Si thisType es un tipo de valor y thisType 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 parte de thisType.

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

API de baja fricción

La mayoría de las estructuras deberían tener una semántica similar a la primitiva donde los valores idénticos bit a bit se consideran iguales.2.El tiempo de ejecución proporcionará dicho comportamiento en el modo 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 las 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 T son ellos mismos) para hacer esto más fácil y con mejor rendimiento, así como coherente con el comportamiento de muchos tipos de valores existentes dentro de 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 IFormattable, Además, muchos de los tipos de valores definidos por el sistema, como DateTime, TimeSpan y Guid, también implementan muchos o todos estos.Si está implementando un tipo similar "ampliamente útil", como una estructura numérica compleja o algunos valores textuales de ancho fijo, 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 (como ICollection) entonces implementarlo es una mala idea, ya que significaría que usted hizo que la estructura fuera mutable (lo que lleva a los tipos de errores ya descritos donde las modificaciones ocurren en el valor del cuadro en lugar del original) o confunde a los usuarios al ignorar las implicaciones de los métodos como Add() o lanzar excepciones.

Muchas interfaces NO implican mutabilidad (como IFormattable) y sirve como forma idiomática de exponer cierta funcionalidad de manera consistente.A menudo, al usuario de la estructura no le importará ningún gasto adicional 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 utilizar 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 algunas cosas 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, hacer que esto ocurra a través de una llamada en cuadro eliminaría cualquier beneficio de que el enumerador sea una estructura (de hecho, sería peor).Peor aún, si la llamada a disposición modifica el estado del enumerador de alguna manera, esto sucedería en la instancia en caja y se podrían introducir muchos errores sutiles en casos complejos.Por 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:  call        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:  constrained. 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 (lamentable) aspecto mutable del enumerador se conserva en caso de que el método Dispose realmente haga algo.

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

En algunos casos, puede ser bueno que una estructura implemente una interfaz (si nunca fue útil, es dudoso que los creadores de .net la 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 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 es implícitamente convertible al segundo ("boxing") y el segundo puede convertirse al primero mediante una conversión explícita ("unboxing").Sin embargo, es posible explotar la implementación de una estructura de una interfaz sin boxeo, utilizando lo que se denomina 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 thing1.Compare(thing2) sin tener que boxear thing1 o thing2.Si thing1 resulta ser, por ejemplo, un Int32, el tiempo de ejecución sabrá que cuando genere el código para CompareTwoThings<Int32>(Int32 thing1, Int32 thing2).Dado que sabrá el tipo exacto tanto del elemento que aloja el método como del elemento que se pasa como parámetro, no tendrá que encuadrar ninguno de ellos.

El mayor problema con las estructuras que implementan interfaces es que 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 interfaces de sólo lectura esto generalmente no es un problema, pero para una interfaz mutante como IEnumerator<T> puede producir alguna 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 cebará enumerator1 para leer el primer elemento.El estado de ese enumerador se copiará a enumerator2.La declaración marcada #2 avanzará esa copia para leer el segundo elemento, pero no afectará enumerator1.El estado de ese segundo enumerador luego se copiará a enumerator3, que será avanzado por la declaración marcada #3.Entonces, porque enumerator3 y enumerator4 son ambos tipos de referencia, un REFERENCIA a enumerator3 luego se copiará a enumerator4, por lo que la declaración marcada avanzará efectivamente ambos enumerator3 y enumerator4.

Algunas personas intentan pretender 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 ejemplos de ello.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á a un tipo de referencia, y se comportará como un tipo de referencia.Este ú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 deben tener sus Equals Los métodos prueban la igualdad de referencias, pero no existe 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 se derivan de ValueType con la excepción de System.Enum, sino ambos ValueType y System.Enum son tipos de clase.

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 "encuadrará" en un tipo de referencia, anulando así 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 la estructura hereda una interfaz y pasa interfaces, pierde la naturaleza del tipo de valor de la estructura.También podría convertirlo en una clase si necesita interfaces.

(Bueno, no tengo nada importante que agregar, pero aún no tengo habilidades de edición, así que aquí va...)
Perfectamente seguro.No hay nada ilegal en 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 BOX él.Entonces, penalización por desempeño, etc.

El único escenario válido que se me ocurre 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.

Creo que el problema es que causa boxeo porque las estructuras son tipos de valores, por lo que hay una ligera 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

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

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

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

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

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

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top