Поскольку никто не предоставил этого ответа явно, я добавлю следующее:
Реализация интерфейса в структуре не имеет никаких негативных последствий.
Любая переменная типа интерфейса, используемого для хранения структуры, приведет к использованию в штучной упаковке значения этой структуры. Если структура неизменна (это хорошо), то в худшем случае это проблема производительности, если только вы не:
- использование получающегося объекта для блокировки (очень плохая идея в любом случае)
- используя семантику ссылочного равенства и ожидая, что она будет работать для двух значений в штучной упаковке из одной структуры.
Оба эти варианта маловероятны, вместо этого вы, вероятно, будете выполнять одно из следующих действий:
Дженерики
Возможно, многие разумные причины для структур, реализующих интерфейсы, заключаются в том, что они могут использоваться в универсальном контексте с ограничениями . При использовании таким способом переменная выглядит так:
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, он может использовать код операции constrained :
Если thisType является типом значения и thisType реализует метод, то ptr передается неизмененным как указатель 'this' на инструкцию метода вызова для реализации метода thisType.
Это позволяет избежать упаковки, и поскольку тип значения реализует интерфейс, должен реализовать метод, таким образом, никакой упаковки не произойдет. В приведенном выше примере вызов Equals()
выполняется без поля this.a 1 .
API с низким коэффициентом трения
Большинство структур должны иметь примитивоподобную семантику, где битовые идентичные значения считаются равными 2 . Среда выполнения будет обеспечивать такое поведение в неявном Equals()
, но это может быть медленным. Кроме того, это неявное равенство не представляется как реализация IEquatable<T>
и, таким образом, предотвращает легкое использование структур в качестве ключей для словарей, если они сами явно не реализуют его. Поэтому многие публичные типы структур обычно объявляют, что они реализуют IEquatable<T>
(где T
- это они сами), чтобы сделать это проще и лучше, а также в соответствии с поведением многих существующих типов значений в BCL CLR.
Все примитивы в BCL реализуются как минимум:
IComparable
IConvertible
IComparable<T>
IEquatable<T>
(а значит IEquatable
)
Многие также реализуют IFormattable
, кроме того, многие из типов, определенных Системой, таких как DateTime, TimeSpan и Guid, также реализуют многие или все из них. Если вы реализуете аналогично «широко полезный» тип, такой как структура комплексных чисел или некоторые текстовые значения фиксированной ширины, то реализация многих из этих общих интерфейсов (правильно) сделает вашу структуру более полезной и полезной.
1089 * Исключения *
Очевидно, что если интерфейс сильно подразумевает изменчивость (например, ICollection
), то реализация этого является плохой идеей, так как это означало бы, что вы либо сделали структуру изменяемой (приводя к уже описанным типам ошибок) где изменения происходят в упакованном значении, а не в оригинале), или вы путаете пользователей, игнорируя значения таких методов, как Add()
или выбрасывая исключения.
Многие интерфейсы НЕ подразумевают изменчивость (например, IFormattable
) и служат идиоматическим способом последовательного представления определенных функций. Часто пользователь структуры не будет заботиться о каких-либо накладных расходах на бокс для такого поведения. Краткое описание
Когда все сделано разумно, для неизменяемых типов значений, реализация полезных интерфейсов - хорошая идея
Примечания:
1: обратите внимание, что компилятор может использовать это при вызове виртуальных методов для переменных, которые известны , чтобы иметь определенный тип структуры, но в которых требуется вызывать виртуальный метод. Например:
List<int> l = new List<int>();
foreach(var x in l)
;//no-op
Перечислитель, возвращаемый 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: 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
Таким образом, реализация IDisposable не вызывает проблем с производительностью, и (прискорбный) изменяемый аспект перечислителя сохраняется, если метод Dispose действительно что-то делает!
2: double и float являются исключениями из этого правила, когда значения NaN не считаются равными.