it-swarm-pt.tech

É seguro para estruturas implementar interfaces?

Eu me lembro de ler algo sobre como é ruim para estruturas implementar interfaces no CLR via C #, mas não consigo encontrar nada sobre isso. É ruim? Existem consequências não intencionais de fazê-lo?

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

Há várias coisas acontecendo nesta pergunta ...

É possível que uma estrutura implemente uma interface, mas existem preocupações que surgem com a conversão, mutabilidade e desempenho. Consulte esta publicação para obter mais detalhes: http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx

Em geral, as estruturas devem ser usadas para objetos que tenham semântica de tipo de valor. Ao implementar uma interface em uma estrutura, você pode enfrentar problemas de boxe, pois a estrutura é transmitida entre a estrutura e a interface. Como resultado do boxe, operações que alteram o estado interno da estrutura podem não se comportar corretamente.

45
Scott Dorman

Como ninguém mais forneceu explicitamente esta resposta, adicionarei o seguinte:

A implementação de uma interface em uma estrutura não tem consequências negativas.

Qualquer variável do tipo de interface usada para armazenar uma estrutura resultará no uso de um valor em caixa dessa estrutura. Se a estrutura é imutável (uma coisa boa), isso é, na pior das hipóteses, um problema de desempenho, a menos que você seja:

  • usando o objeto resultante para fins de bloqueio (uma ideia imensamente ruim de qualquer maneira)
  • usando semântica de igualdade de referência e esperando que funcione para dois valores em caixa da mesma estrutura.

Seria improvável que ambos os procedimentos sejam os seguintes:

Genéricos

Talvez muitas razões razoáveis ​​para estruturas implementando interfaces sejam para que elas possam ser usadas dentro de um contexto genérico com restrições. Quando usada dessa maneira, a variável é a seguinte:

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 o uso da estrutura como um parâmetro de tipo
    • desde que nenhuma outra restrição como new() ou class seja usada.
  2. Permita evitar o boxe nas estruturas usadas dessa maneira.

Então this.a NÃO é uma referência de interface, portanto, não causa uma caixa do que quer que seja colocado nela. Além disso, quando o compilador c # compila as classes genéricas e precisa inserir invocações dos métodos de instância definidos nas instâncias do parâmetro Type T, pode usar o opcode restrito :

Se thisType é um tipo de valor e thisType implementa o método, então ptr é passado sem modificação como o ponteiro 'this' para uma instrução de método de chamada, para a implementação do método por thisType.

Isso evita o boxe e, como o tipo de valor está implementando a interface é deve implementar o método, portanto, nenhum boxe ocorrerá. No exemplo acima, a chamada Equals() é feita sem nenhuma caixa nisto.a1.

APIs de baixo atrito

A maioria das estruturas deve ter semântica primitiva, onde valores idênticos bit a bit são considerados iguais2. O tempo de execução fornecerá esse comportamento no implícito Equals(), mas isso pode ser lento. Além disso, essa igualdade implícita é não exposta como uma implementação de IEquatable<T> E, assim, evita que as estruturas sejam usadas facilmente como chaves para os dicionários, a menos que eles mesmos implementem explicitamente. Portanto, é comum que muitos tipos de estruturas públicas declarem que implementam IEquatable<T> (Onde T são eles próprios) para tornar isso mais fácil e com melhor desempenho, além de ser consistente com o comportamento de muitos valores existentes tipos dentro da CLR BCL.

Todas as primitivas na BCL implementam no mínimo:

  • IComparable
  • IConvertible
  • IComparable<T>
  • IEquatable<T> (E assim IEquatable)

Muitos também implementam IFormattable, muitos dos tipos de valores definidos pelo Sistema, como DateTime, TimeSpan e Guid, também implementam muitos ou todos eles. Se você estiver implementando um tipo similarmente 'amplamente útil', como uma estrutura numérica complexa ou alguns valores textuais de largura fixa, a implementação de muitas dessas interfaces comuns (corretamente) tornará sua estrutura mais útil e utilizável.

Exclusões

Obviamente, se a interface implica fortemente mutabilidade (como ICollection), implementá-la é uma péssima idéia, pois isso significa que você fez a estrutura mutável (levando a tipos de erros já descritos onde as modificações ocorrem no valor in a box em vez do original) ou você confunde os usuários ignorando as implicações dos métodos como Add() ou lançando exceções.

Muitas interfaces NÃO implicam mutabilidade (como IFormattable) e servem como uma maneira idiomática de expor determinadas funcionalidades de maneira consistente. Freqüentemente, o usuário da estrutura não se importa com nenhuma sobrecarga de boxe para esse comportamento.

Sumário

Quando feita de maneira sensata, em tipos de valor imutáveis, a implementação de interfaces úteis é uma boa ideia


Notas:

1: Observe que o compilador pode usar isso ao invocar métodos virtuais em variáveis ​​que são conhecidas para serem de um tipo de estrutura específico, mas no qual é necessário chamar um método virtual. Por exemplo:

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

O enumerador retornado pela lista é uma estrutura, uma otimização para evitar uma alocação ao enumerar a lista (com algumas interessantes consequências ). No entanto, a semântica do foreach especifica que, se o enumerador implementar IDisposable, Dispose() será chamado assim que a iteração for concluída. Obviamente, fazer com que isso ocorra por meio de uma chamada em caixa eliminaria qualquer benefício do enumerador ser uma estrutura (na verdade, seria pior). Pior, se a chamada de descarte modificar o estado do enumerador de alguma forma, isso aconteceria na instância em caixa e muitos erros sutis poderão ser introduzidos em casos complexos. Portanto, o IL emitido nesse tipo de situação é:

 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: chame System.Collections.Generic.List.get_Current 
 IL_0018: stloc.1 
 IL_0019: ldloca.s 02 
 IL_001B: chame 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: restrito. System.Collections.Generic.List.Enumerator 
 IL_002E: callvirt System.IDisposable.Dispose 
 IL_0033: nop 
 IL_0034: finalmente 

Portanto, a implementação de IDisposable não causa problemas de desempenho e o aspecto mutável (lamentável) do enumerador é preservado, caso o método Dispose realmente faça alguma coisa!

2: double e float são exceções a esta regra em que os valores de NaN não são considerados iguais.

161
ShuggyCoUk

Em alguns casos, pode ser bom para uma estrutura implementar uma interface (se isso nunca foi útil, é duvidoso que os criadores do .net tenham fornecido isso). Se uma estrutura implementa uma interface somente leitura como IEquatable<T>, O armazenamento da estrutura em um local de armazenamento (variável, parâmetro, elemento da matriz etc.) do tipo IEquatable<T> Exigirá que ela esteja em caixa ( cada tipo de estrutura define realmente dois tipos de coisas: um tipo de local de armazenamento que se comporta como um tipo de valor e um tipo de objeto de heap que se comporta como um tipo de classe; o primeiro é implicitamente conversível no segundo - "boxe" - e o o segundo pode ser convertido no primeiro via conversão explícita - "unboxing"). É possível explorar a implementação de uma interface de uma estrutura sem boxe, no entanto, usando o que é chamado de genéricos restritos.

Por exemplo, se alguém tivesse um método CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>, esse método poderia chamar thing1.Compare(thing2) sem ter que caixa thing1 Ou thing2. Se thing1 For, por exemplo, um Int32, O tempo de execução saberá que, quando gerar o código para CompareTwoThings<Int32>(Int32 thing1, Int32 thing2). Como ele saberá o tipo exato da coisa que hospeda o método e da coisa que está sendo passada como parâmetro, ele não precisará encaixotar nenhum deles.

O maior problema com estruturas que implementam interfaces é que uma estrutura que é armazenada em um local do tipo de interface, Object ou ValueType (em oposição a um local de seu próprio tipo) se comportará como um objeto de classe. Para interfaces somente leitura, isso geralmente não é um problema, mas para uma interface mutante como IEnumerator<T>, Pode gerar uma semântica estranha.

Considere, por exemplo, o seguinte 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

A instrução marcada nº 1 iniciará enumerator1 Para ler o primeiro elemento. O estado desse enumerador será copiado para enumerator2. A instrução marcada nº 2 avançará essa cópia para ler o segundo elemento, mas não afetará enumerator1. O estado desse segundo enumerador será copiado para enumerator3, Que será avançado pela instrução marcada # 3. Então, como enumerator3 E enumerator4 São os dois tipos de referência, uma REFERÊNCIA a enumerator3 Será copiado para enumerator4, a instrução marcada avançará efetivamente ambosenumerator3 e enumerator4.

Algumas pessoas tentam fingir que tipos de valor e tipos de referência são os dois tipos de Object, mas isso não é verdade. Tipos de valor real são conversíveis em Object, mas não são instâncias dele. Uma instância de List<String>.Enumerator Que é armazenada em um local desse tipo é um tipo de valor e se comporta como um tipo de valor; copiá-lo para um local do tipo IEnumerator<String> o converterá em um tipo de referência e ele se comportará como um tipo de referência. O último é um tipo de Object, mas o primeiro não é.

BTW, mais algumas notas: (1) Em geral, os tipos de classes mutáveis ​​devem ter seus métodos Equals testando a igualdade de referência, mas não há uma maneira decente de uma estrutura em caixa fazer isso; (2) apesar do nome, ValueType é um tipo de classe, não um tipo de valor; todos os tipos derivados de System.Enum são tipos de valor, assim como todos os tipos derivados de ValueType, com exceção de System.Enum, mas ambos ValueType e System.Enum São tipos de classe.

8
supercat

(Bem, não temos nada a acrescentar, mas ainda não temos habilidades de edição, então aqui vai ..)
Perfeitamente seguro. Nada ilegal na implementação de interfaces em estruturas. No entanto, você deve questionar por que deseja fazê-lo.

No entanto , obter uma referência de interface para uma estrutura irá BOX . Portanto, penalidade de desempenho e assim por diante.

O único cenário válido em que consigo pensar agora é ilustrado no meu post aqui . Quando você deseja modificar o estado de uma estrutura armazenada em uma coleção, é necessário fazê-lo por meio de uma interface adicional exposta na estrutura.

3
Gishu

As estruturas são implementadas como tipos de valor e classes são tipos de referência. Se você possui uma variável do tipo Foo e armazena uma instância do Fubar, ela "encaixota" em um tipo de referência, derrotando a vantagem de usar uma estrutura em primeiro lugar.

A única razão pela qual vejo usar uma estrutura em vez de uma classe é porque será um tipo de valor e não um tipo de referência, mas a estrutura não pode herdar de uma classe. Se você possui a estrutura, herda uma interface e transmite interfaces, perde a natureza do tipo de valor da estrutura. Também pode ser uma classe se você precisar de interfaces.

3
dotnetengineer

Eu acho que o problema é que ele causa boxe, porque as estruturas são tipos de valor, por isso há uma pequena penalidade no desempenho.

Este link sugere que pode haver outros problemas com ele ...

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

1
Simon Keep

Não há consequências para uma estrutura implementando uma interface. Por exemplo, as estruturas do sistema interno implementam interfaces como IComparable e IFormattable.

0
Joseph Daigle

Há muito pouco motivo para um tipo de valor implementar uma interface. Como você não pode subclassificar um tipo de valor, sempre pode se referir a ele como seu tipo concreto.

A menos, é claro, que você tenha várias estruturas implementando a mesma interface, ela pode ser um pouco útil, mas nesse momento eu recomendaria o uso de uma classe e o correto.

É claro que, ao implementar uma interface, você está encaixotando a estrutura, então ela fica na pilha, e você não poderá mais passar por valor ... Isso realmente reforça minha opinião de que você deve usar apenas uma classe nesta situação.

0
FlySwat