Оскільки ніхто іншої прямо не вказав цю відповідь, я додаю наступне:
Реалізація інтерфейсу на структурі не має жодних негативних наслідків.
Будь-яка змінна типу інтерфейсу, яка використовується для утримання структури, призведе до того, що значення цієї структури буде розміщено у вікні. Якщо структура незмінна (добре), то це найгірше проблема продуктивності, якщо ви не:
- використання отриманого об'єкта для блокування (надзвичайно погана ідея в будь-якому випадку)
- використовуючи семантику довідкової рівності та очікуючи, що вона спрацює для двох значень з однієї структури.
І те, і інше було б малоймовірним, натомість ви, швидше за все, зробите одне з наступного:
Дженерики
Можливо, багато розумних причин для конструкцій, що реалізують інтерфейси, полягає в тому, що їх можна використовувати в загальному контексті з обмеженнями . При використанні таким чином змінна виглядає так:
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 # компілює загальні класи і йому потрібно вставити виклики методів екземпляра, визначених на екземплярах параметра Типу T, він може використовувати обмежений код коду:
Якщо thisType є типом значення, і thisType реалізує метод, тоді ptr передається немодифікованим як вказівник 'this' до інструкції методу виклику для реалізації методу thisType.
Це дозволяє уникнути боксу, а оскільки тип значення реалізує інтерфейс, він повинен реалізувати метод, отже, боксу не буде. У наведеному вище прикладі Equals()
виклик виконується без позначки . 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)
;
Перечислювач, що повертається Списком, є структурою, оптимізацією для уникнення розподілу при перерахуванні списку (з деякими цікавими наслідками ). Однак семантика 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: залишити. 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 не вважаються рівними.