É seguro para structs implementar interfaces?


91

Parece que me lembro de ter lido algo sobre como é ruim para structs implementar interfaces em CLR via C #, mas não consigo encontrar nada sobre isso. É ruim? Existem consequências indesejadas de fazer isso?

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

Respostas:


46

Há várias coisas acontecendo nesta questão ...

É possível para uma estrutura implementar uma interface, mas existem preocupações que surgem com a conversão, mutabilidade e desempenho. Veja esta postagem para mais detalhes: https://docs.microsoft.com/en-us/archive/blogs/abhinaba/c-structs-and-interface

Em geral, structs devem ser usados ​​para objetos que têm semântica de tipo de valor. Ao implementar uma interface em uma estrutura, você pode ter problemas de boxing, pois a estrutura é lançada para frente e para trás entre a estrutura e a interface. Como resultado do boxing, as operações que alteram o estado interno da estrutura podem não se comportar adequadamente.


3
"Como resultado do boxing, as operações que alteram o estado interno da estrutura podem não se comportar adequadamente." Dê um exemplo e obtenha a resposta.

2
@Will: Não tenho certeza do que você está se referindo em seu comentário. A postagem do blog que mencionei tem um exemplo que mostra onde chamar um método de interface na estrutura não altera realmente o valor interno.
Scott Dorman,

11
@ScottDorman: Em alguns casos, fazer com que as estruturas implementem interfaces pode ajudar a evitar o boxe. Os principais exemplos são IComparable<T>e IEquatable<T>. Armazenar uma estrutura Fooem uma variável de tipo IComparable<Foo>exigiria encaixotamento, mas se um tipo genérico Tfor restrito a IComparable<T>um, podemos compará-lo a outro Tsem precisar encaixar nenhum dos dois e sem precisar saber nada sobre a Tnão ser que ele implementa a restrição. Esse comportamento vantajoso só é possível pela capacidade dos structs de implementar interfaces. Dito isto ...
supercat

3
... poderia ter sido bom se houvesse um meio de declarar que uma determinada interface só deveria ser considerada aplicável a estruturas unboxed, uma vez que existem alguns contextos onde não seria possível para um objeto de classe ou estrutura boxed ter o desejado comportamentos.
supercat

2
"structs devem ser usados ​​para objetos que têm semântica de tipo de valor. ... operações que alteram o estado interno da struct podem não se comportar corretamente." O verdadeiro problema aí não é o fato de que a semântica do tipo de valor e a mutabilidade não combinam bem?
jpmc26

180

Uma vez que ninguém mais forneceu explicitamente esta resposta, acrescentarei o seguinte:

Implementar uma interface em uma estrutura não tem consequências negativas de qualquer espécie.

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

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

Ambos seriam improváveis, em vez disso, você provavelmente fará um dos seguintes:

Genéricos

Talvez muitos motivos razoáveis ​​para structs implementando interfaces sejam para que eles possam ser usados ​​em um contexto genérico com restrições . Quando usada desta forma, a variável assim:

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 o uso da estrutura como um parâmetro de tipo
    • desde que nenhuma outra restrição como new()ou classseja usada.
  2. Permite evitar o boxe nas estruturas utilizadas desta forma.

Então this.a NÃO é uma referência de interface, portanto, não causa uma caixa com o 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, ele pode usar o opcode restrito :

Se thisType for um tipo de valor e thisType implementar o método, ptr será passado sem modificações 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 boxing e como o tipo de valor está implementando a interface é necessário implementar o método, portanto, nenhum boxing ocorrerá. No exemplo acima, a Equals()invocação é feita sem nenhuma caixa neste.a 1 .

APIs de baixa fricção

A maioria dos structs deve ter uma semântica de tipo primitivo, em que valores idênticos bit a bit são considerados iguais 2 . O tempo de execução fornecerá esse comportamento 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, portanto, evita que as estruturas sejam usadas facilmente como chaves para Dicionários, a menos que elas mesmas as implementem explicitamente. Portanto, é comum que muitos tipos de estruturas públicas declarem que implementam IEquatable<T>(onde Testão eles próprios) para tornar isso mais fácil e com melhor desempenho, bem como consistentes com o comportamento de muitos tipos de valor existentes no CLR BCL.

Todas as primitivas no BCL implementam no mínimo:

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

Muitos também implementam IFormattable, além de muitos dos tipos de valor definidos pelo sistema, como DateTime, TimeSpan e Guid, implementam muitos ou todos eles também. Se você estiver implementando um tipo similarmente 'amplamente útil', como uma estrutura de número complexo 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 em mutabilidade (como ICollection), implementá-la é uma má ideia, pois significaria que você tornou a estrutura mutável (levando aos tipos de erros já descritos onde as modificações ocorrem no valor em caixa 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 em mutabilidade (como IFormattable) e servem como a forma idiomática de expor certas funcionalidades de maneira consistente. Freqüentemente, o usuário da estrutura não se preocupa com qualquer sobrecarga de boxing para tal comportamento.

Resumo

Quando feito 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 por serem de um tipo de estrutura específico, mas nas quais é necessário invocar 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 consequências interessantes ). No entanto, a semântica de foreach especifica que, se o enumerador implementar, IDisposableentão Dispose()será chamado assim que a iteração for concluída. Obviamente, isso ocorrer por meio de uma chamada em caixa eliminaria qualquer benefício de o enumerador ser uma estrutura (na verdade, seria pior). Pior, se a chamada dispose modifica o estado do enumerador de alguma forma, isso aconteceria na instância em caixa e muitos bugs sutis podem ser introduzidos em casos complexos. Portanto, o IL emitido neste 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 nenhum problema 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 onde os valores NaN não são considerados iguais.


1
O site egheadcafe.com mudou, mas não fez um bom trabalho ao reter seu conteúdo. Tentei, mas não consigo encontrar o documento original de eggheadcafe.com/software/aspnet/31702392/… , sem o conhecimento do OP. (PS +1 para um excelente resumo).
Abel de

2
Esta é uma ótima resposta, mas acho que você pode melhorá-la movendo o "Resumo" para o topo como "TL; DR". Fornecer a conclusão primeiro ajuda o leitor a saber onde você está indo com as coisas.
Hans

Deve haver um aviso do compilador ao converter um structpara um interface.
Jalal,

8

Em alguns casos, pode ser bom para uma estrutura implementar uma interface (se nunca foi útil, é duvidoso que os criadores de .net a teriam fornecido). Se uma estrutura implementa uma interface somente leitura como IEquatable<T>, armazenar a estrutura em um local de armazenamento (variável, parâmetro, elemento de matriz, etc.) do tipo IEquatable<T>exigirá que seja encaixotada (cada tipo de estrutura na verdade define dois tipos de coisas: um armazenamento tipo de localização que se comporta como um tipo de valor e um tipo de objeto heap que se comporta como um tipo de classe; o primeiro é implicitamente conversível para o segundo - "boxing" - e o segundo pode ser convertido para o primeiro por meio de conversão explícita - "unboxing"). É possível explorar a implementação de uma interface de uma estrutura sem boxing, entretanto, usando o que são chamados de genéricos restritos.

Por exemplo, se alguém tivesse um método CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>, tal método poderia chamar thing1.Compare(thing2)sem a necessidade de box thing1ou thing2. Se thing1acontecer de ser, por exemplo, um Int32, o tempo de execução saberá disso 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, não precisará encaixotar nenhum deles.

O maior problema com structs que implementam interfaces é que um struct que é armazenado em um local do tipo de interface Object, ou ValueType(ao contrário de 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>ela pode produzir algumas semânticas estranhas.

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 irá preparar enumerator1para 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á então copiado para enumerator3, que será avançado pela declaração marcada # 3. Então, como enumerator3e enumerator4são tipos de referência, um REFERENCE to enumerator3será copiado para enumerator4, portanto, a instrução marcada avançará efetivamente ambos enumerator3 e enumerator4.

Algumas pessoas tentam fingir que tipos de valor e tipos de referência são ambos tipos de Object, mas isso não é realmente verdade. Tipos de valor real são conversíveis em Object, mas não são instâncias dele. Uma instância List<String>.Enumeratorarmazenada 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>irá convertê-lo em um tipo de referência e ele se comportará como um tipo de referência . O último é uma espécie de Object, mas o primeiro não é.

A propósito, mais algumas observações: (1) Em geral, os tipos de classes mutáveis ​​devem ter seus Equalsmétodos para testar 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.Enumsão tipos de valor, assim como todos os tipos derivados de, ValueTypecom exceção de System.Enum, mas ambos ValueTypee System.Enumsão tipos de classe.


3

As estruturas são implementadas como tipos de valor e as classes são tipos de referência. Se você tiver uma variável do tipo Foo e armazenar uma instância de Fubar nela, ela irá "encaixá-la" em um tipo de referência, anulando assim a vantagem de usar uma estrutura em primeiro lugar.

A única razão que vejo para 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ê fizer com que a estrutura herde uma interface e passar interfaces, você perderá a natureza do tipo de valor da estrutura. Pode muito bem torná-lo uma classe se você precisar de interfaces.


Funciona assim para primitivas que implementam interfaces também?
Aoetalks

3

(Bem, não tenho nada de importante a acrescentar, mas ainda não tenho habilidade de edição, então aqui vai ...)
Perfeitamente seguro. Nada ilegal com a implementação de interfaces em structs. No entanto, você deve questionar por que deseja fazer isso.

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

O único cenário válido que posso pensar agora é ilustrado em meu post aqui . Quando você quiser modificar o estado de um struct armazenado em uma coleção, terá que fazer isso por meio de uma interface adicional exposta no struct.


Se alguém passar um Int32para um método que aceita um tipo genérico T:IComparable<Int32>(que pode ser um parâmetro de tipo genérico do método ou a classe do método), esse método será capaz de usar o Comparemétodo no objeto passado sem encaixá-lo.
supercat


0

Não há consequências para uma estrutura que implementa uma interface. Por exemplo, as estruturas de sistema integradas implementam interfaces como IComparablee IFormattable.


0

Há muito pouca razão para um tipo de valor implementar uma interface. Visto que você não pode criar uma subclasse de um tipo de valor, você sempre pode se referir a ele como seu tipo concreto.

A menos, é claro, que você tenha vários structs, todos implementando a mesma interface, pode ser marginalmente útil, mas nesse ponto eu recomendo usar uma classe e fazê-lo direito.

Claro, ao implementar uma interface, você está encaixotando a estrutura, então ela agora fica no heap e você não será mais capaz de passá-la por valor ... Isso realmente reforça minha opinião de que você deve apenas usar uma classe nesta situação.


Com que frequência você passa IComparable em vez da implementação concreta?
FlySwat

Você não precisa repassar IComparablepara encaixotar o valor. Simplesmente chamando um método que espera IComparablecom um tipo de valor que o implementa, você irá encaixotar implicitamente o tipo de valor.
Andrew Hare

1
@AndrewHare: Os genéricos restritos permitem que métodos on IComparable<T>sejam chamados em estruturas do tipo Tsem encaixe.
supercat

-10

Structs são como classes que vivem na pilha. Não vejo razão para que sejam "inseguros".


Exceto que eles não têm herança.
FlySwat

7
Tenho que discordar de todas as partes desta resposta; eles não vivem necessariamente na pilha e a semântica de cópia é muito diferente das classes.
Marc Gravell

1
Eles são imutáveis, o uso excessivo de struct deixará sua memória triste :(
Teoman shipahi

1
@Teomanshipahi O uso excessivo de instâncias de classe deixará seu coletor de lixo louco.
IllidanS4 quer Monica de volta em

4
Para alguém que tem mais de 20.000 repetições, essa resposta é simplesmente inaceitável.
Krythic
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.