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);
}
}
- Habilite o uso da estrutura como um parâmetro de tipo
- desde que nenhuma outra restrição como
new()
ou class
seja usada.
- 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 T
estã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, IDisposable
entã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.