Pergunta

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

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

Solução

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

É possível que uma estrutura implemente uma interface, mas existem preocupações relacionadas à conversão, mutabilidade e desempenho.Veja este post para mais detalhes: http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx

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

Outras dicas

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

Implementando uma interface em uma estrutura não tem nenhuma consequência negativa.

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

  • usar o objeto resultante para fins de bloqueio (de qualquer forma, uma ideia imensamente ruim)
  • usando 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, é provável que você esteja fazendo o seguinte:

Genéricos

Talvez muitas razões razoáveis ​​para estruturas implementarem interfaces sejam para que elas possam ser usadas dentro de um genérico contexto com restrições.Quando usada desta forma, a variável fica 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 struct como parâmetro de tipo
    • desde que nenhuma outra restrição como new() ou class é usado.
  2. Permitir evitar boxe em estruturas usadas 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 restrito Código de operação:

Se thisType for um tipo de valor e thisType implementar o método, então ptr será 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 o Equals() a invocação é feita sem nenhuma caixa.a1.

APIs de baixo atrito

A maioria das estruturas deve ter semântica semelhante a primitiva, onde valores idênticos bit a bit são considerados iguais2.O tempo de execução fornecerá tal comportamento no implícito Equals() mas isso pode ser lento.Além disso, esta igualdade implícita é não exposto como uma implementação de IEquatable<T> e, portanto, evita que estruturas sejam usadas facilmente como chaves para dicionários, a menos que elas próprias as implementem explicitamente.Portanto, é comum que muitos tipos de estruturas públicas declarem que implementam IEquatable<T> (onde T são eles mesmos) para tornar isso mais fácil e com melhor desempenho, bem como consistente com o comportamento de muitos tipos de valor existentes dentro do 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, além disso, muitos dos tipos de valores 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 mutabilidade (como ICollection), então implementá-lo é 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 confundiu os usuários ignorando as implicações de os métodos como Add() ou lançando exceções.

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

Resumo

Quando feita de forma sensata, em tipos de valores 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 conhecido ser de um tipo de estrutura específico, mas no qual é 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 struct, uma otimização para evitar uma alocação ao enumerar a lista (Com alguns detalhes interessantes consequências).No entanto, a semântica do foreach especifica que se o enumerador implementar IDisposable então Dispose() será chamado assim que a iteração for concluída.Obviamente, fazer isso ocorrer por meio de uma chamada em caixa eliminaria qualquer benefício do enumerador ser uma estrutura (na verdade, seria pior).Pior ainda, se a chamada de descarte modificar o estado do enumerador de alguma forma, isso aconteceria na instância em caixa e muitos bugs sutis poderiam 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:  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  

Assim, 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.

Em alguns casos, pode ser bom para uma estrutura implementar uma interface (se nunca fosse útil, é duvidoso que os criadores do .net a tivessem fornecido).Se uma estrutura implementa uma interface somente leitura como IEquatable<T>, armazenando a estrutura em um local de armazenamento (variável, parâmetro, elemento de array, etc.) do tipo IEquatable<T> exigirá que seja encaixotado (cada tipo de estrutura na verdade define dois tipos de coisas:um tipo de local de armazenamento 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 no segundo - "boxing" - e o segundo pode ser convertido no primeiro por meio de conversão explícita - "unboxing").É possível explorar a implementação de uma interface em uma estrutura sem boxe, entretanto, 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>, tal método poderia chamar thing1.Compare(thing2) sem precisar encaixotar thing1 ou thing2.Se thing1 acontece de ser, 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, não será necessário encaixotar nenhum deles.

O maior problema com estruturas que implementam interfaces é que uma estrutura que é armazenada em um local do tipo 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 alguma 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 declaração marcada nº 1 será prime 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á então copiado para enumerator3, que será avançado pela instrução marcada nº 3.Então, porque enumerator3 e enumerator4 são ambos tipos de referência, um REFERÊNCIA para enumerator3 será então copiado para enumerator4, então a declaração marcada avançará efetivamente ambos enumerator3 e enumerator4.

Algumas pessoas tentam fingir que os tipos de valor e os tipos de referência são ambos tipos de Object, mas isso não é verdade.Os tipos de valor real são conversíveis em Object, mas não são instâncias disso.Uma instância de List<String>.Enumerator que está armazenado em um local desse tipo é um tipo de valor e se comporta como um tipo de valor;copiando-o 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.Este último é uma espécie de Object, mas o primeiro não é.

Aliás, mais algumas notas:(1) Em geral, os tipos de classes mutáveis ​​devem ter seus Equals os métodos testam 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 que derivam de ValueType com a exceção de System.Enum, mas ambos ValueType e System.Enum são tipos de classe.

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 será "encaixotada" 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 ela 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 passe interfaces, você perderá a natureza do tipo de valor da estrutura.É melhor torná-lo uma classe se você precisar de interfaces.

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

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

O único cenário válido em que consigo pensar neste momento é ilustrado no meu post aqui.Quando você quiser modificar o estado de uma estrutura armazenada em uma coleção, terá que fazer isso por meio de uma interface adicional exposta na estrutura.

Acho que o problema é que isso causa boxe porque as estruturas são tipos de valor, portanto 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

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

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

A menos, é claro, que você tenha várias estruturas, todas implementando a mesma interface, isso pode ser um pouco útil, mas nesse ponto eu recomendo usar uma classe e fazer isso da maneira certa.

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

Estruturas são como classes que residem na pilha.Não vejo razão para que eles sejam "inseguros".

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top