Поскольку этот ответ явно никто не дал, я добавлю следующее:
Реализация интерфейса в структуре не имеет никаких негативных последствий.
Любая переменная типа интерфейса, используемая для хранения структуры, приведет к использованию значения этой структуры в рамке. Если структура неизменяема (хорошо), то в худшем случае это проблема производительности, если только вы:
- использование полученного объекта для целей блокировки (в любом случае очень плохая идея)
- используя семантику ссылочного равенства и ожидая, что она будет работать для двух упакованных значений из одной и той же структуры.
Оба варианта маловероятны, вместо этого вы, скорее всего, выполните одно из следующих действий:
Дженерики
Возможно, многие разумные причины для структур, реализующих интерфейсы, заключаются в том, что их можно использовать в общем контексте с ограничениями . При таком использовании переменная выглядит так:
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);
}
}
- Включить использование структуры в качестве параметра типа
- до тех пор, пока не используется никакое другое ограничение, подобное
new()
или class
.
- Позвольте избегать боксов на структурах, используемых таким образом.
Тогда this.a НЕ является ссылкой на интерфейс, поэтому он не приводит к тому, что в него помещается ящик. Кроме того, когда компилятор C # компилирует универсальные классы и ему необходимо вставить вызовы методов экземпляра, определенных в экземплярах параметра Type T, он может использовать ограниченный код операции:
Если thisType является типом значения и thisType реализует метод, тогда ptr передается без изменений как указатель this на инструкцию вызова метода для реализации метода с помощью thisType.
Это позволяет избежать упаковки, и поскольку тип значения реализует интерфейс, он должен реализовывать метод, поэтому упаковки не произойдет. В приведенном выше примере Equals()
вызов выполняется без поля this . A 1 .
API с низким коэффициентом трения
Большинство структур должны иметь примитивную семантику, в которой побитовые одинаковые значения считаются равными 2 . Среда выполнения предоставит такое поведение неявно, Equals()
но это может быть медленным. Кроме того, это неявное равенство не отображается как реализация IEquatable<T>
и, таким образом, предотвращает легкое использование структур в качестве ключей для словарей, если только они явно не реализуют его сами. Поэтому многие общедоступные типы структур обычно объявляют, что они реализуют IEquatable<T>
(где T
они сами), чтобы упростить и повысить производительность, а также согласовать с поведением многих существующих типов значений в CLR BCL.
Все примитивы в BCL реализуют как минимум:
IComparable
IConvertible
IComparable<T>
IEquatable<T>
(И таким образом IEquatable
)
Многие также реализуют IFormattable
, кроме того, многие типы значений, определенные Системой, такие как DateTime, TimeSpan и Guid, также реализуют многие или все из них. Если вы реализуете аналогичный «широко полезный» тип, такой как структура со сложным числом или некоторые текстовые значения фиксированной ширины, то реализация многих из этих общих интерфейсов (правильная) сделает вашу структуру более полезной и удобной.
Исключения
Очевидно, что если интерфейс явно подразумевает изменяемость (например, ICollection
), то реализация этого - плохая идея, так как это будет означать, что вы либо сделали структуру изменяемой (что приводит к видам ошибок, описанным уже, когда изменения происходят в упакованном значении, а не в исходном ) или вы сбиваете с толку пользователей, игнорируя последствия таких методов, как Add()
исключения или выдачу исключений.
Многие интерфейсы НЕ предполагают изменчивости (например, IFormattable
) и служат идиоматическим способом согласованного предоставления определенных функций. Часто пользователь структуры не заботится о накладных расходах для такого поведения.
Резюме
Когда все сделано разумно, для неизменяемых типов значений хорошей идеей является реализация полезных интерфейсов.
Примечания:
1: Обратите внимание, что компилятор может использовать это при вызове виртуальных методов для переменных, которые, как известно, относятся к определенному типу структуры, но в которых требуется вызвать виртуальный метод. Например:
List<int> l = new List<int>();
foreach(var x in l)
;
Перечислитель, возвращаемый List, представляет собой структуру, оптимизацию, позволяющую избежать выделения при перечислении списка (с некоторыми интересными последствиями ). Однако семантика foreach указывает, что если перечислитель реализует, IDisposable
то Dispose()
будет вызываться после завершения итерации. Очевидно, что если это произойдет через упакованный вызов, это исключит любую выгоду от того, что перечислитель является структурой (на самом деле это было бы хуже). Хуже того, если вызов dispose каким-либо образом изменяет состояние перечислителя, это может произойти с экземпляром в штучной упаковке, и в сложных случаях может появиться множество мелких ошибок. Следовательно, ИЛ, излучаемый в такой ситуации:
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: вызов System.Collections.Generic.List.get_Current
IL_0018: stloc.1
IL_0019: ldloca.s 02
IL_001B: вызов 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: ограничено. System.Collections.Generic.List.Enumerator
IL_002E: callvirt System.IDisposable.Dispose
IL_0033: nop
IL_0034: в конце
Таким образом, реализация IDisposable не вызывает никаких проблем с производительностью, и (прискорбно) изменяемый аспект перечислителя сохраняется, если метод Dispose действительно что-то сделает!
2: double и float - исключения из этого правила, где значения NaN не считаются равными.