Чи безпечно для структур застосовувати інтерфейси?


92

Здається, я пам’ятаю, що читав щось про те, як погано для структур реалізовувати інтерфейси в CLR через C #, але, здається, я не можу про це нічого знайти. Це погано? Чи є непередбачені наслідки від цього?

public interface Foo { Bar GetBar(); }
public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }

Відповіді:


45

У цьому питанні відбувається кілька речей ...

Структура може реалізувати інтерфейс, але є проблеми, пов’язані з кастингом, змінністю та продуктивністю. Детальніше див. У цій публікації: https://docs.microsoft.com/en-us/archive/blogs/abhinaba/c-structs-and-interface

Загалом, структури слід використовувати для об'єктів, що мають семантику типового значення. Реалізуючи інтерфейс на структурі, ви можете зіткнутися з проблемами боксу, оскільки структура перекидається між структурою та інтерфейсом. В результаті боксу операції, що змінюють внутрішній стан структури, можуть не поводитися належним чином.


3
"В результаті боксу операції, що змінюють внутрішній стан структури, можуть не поводитися належним чином." Наведіть приклад і отримайте відповідь.

2
@Will: Не впевнений, на що ви посилаєтесь у своєму коментарі. У дописі в блозі, на який я посилався, є приклад, який показує, коли виклик методу інтерфейсу на структурі насправді не змінює внутрішнє значення.
Скотт Дорман,

12
@ScottDorman: У деяких випадках наявність структур, що реалізують інтерфейси, може допомогти уникнути боксу. Основними прикладами є IComparable<T>та IEquatable<T>. Зберігання структури Fooу змінній типу IComparable<Foo>зажадало б боксу, але якщо загальний тип Tобмежений IComparable<T>одним, можна порівняти його з іншим, Tне знаходячи жодного з них і не знаючи нічого про Tінше, крім того, що він реалізує обмеження. Така вигідна поведінка стає можливою лише завдяки здатності структур реалізовувати інтерфейси. Це вже сказано ...
суперкіт

3
... могло б бути приємно, якби існували засоби, що заявляють, що певний інтерфейс слід вважати застосовним лише до невкладених структур, оскільки існують деякі контексти, коли для об’єкта класу або коробчатої структури не було б можливості мати бажаний поведінки.
supercat

2
"структури повинні використовуватися для об'єктів, що мають семантику типового значення. ... операції, що змінюють внутрішній стан структури, можуть не поводитися належним чином." Чи не справжня проблема полягає в тому, що семантика ціннісного типу та мінливість не поєднуються добре?
jpmc26,

183

Оскільки ніхто іншої прямо не вказав цю відповідь, я додаю наступне:

Реалізація інтерфейсу на структурі не має жодних негативних наслідків.

Будь-яка змінна типу інтерфейсу, яка використовується для утримання структури, призведе до того, що значення цієї структури буде розміщено у вікні. Якщо структура незмінна (добре), то це найгірше проблема продуктивності, якщо ви не:

  • використання отриманого об'єкта для блокування (надзвичайно погана ідея в будь-якому випадку)
  • використовуючи семантику довідкової рівності та очікуючи, що вона спрацює для двох значень з однієї структури.

І те, і інше було б малоймовірним, натомість ви, швидше за все, зробите одне з наступного:

Дженерики

Можливо, багато розумних причин для конструкцій, що реалізують інтерфейси, полягає в тому, що їх можна використовувати в загальному контексті з обмеженнями . При використанні таким чином змінна виглядає так:

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. Дозвольте використовувати структуру як параметр типу
    • до тих пір, поки не застосовується будь-яке інше обмеження, подібне new()або classвикористовуване.
  2. Дозвольте уникати боксу на конструкціях, що використовуються таким чином.

Тоді 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)
    ;//no-op

Перечислювач, що повертається Списком, є структурою, оптимізацією для уникнення розподілу при перерахуванні списку (з деякими цікавими наслідками ). Однак семантика 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 не вважаються рівними.


1
Сайт egheadcafe.com перемістився, але не зміг зберегти його вміст. Я спробував, але не можу знайти оригінальний документ eggheadcafe.com/software/aspnet/31702392/… , не маючи знань про ОП. (PS +1 за чудовий підсумок).
Абель

2
Це чудова відповідь, але я думаю, що ви можете її покращити, перемістивши "Підсумок" угорі як "TL; DR". Перш за все надання висновку допомагає читачеві зрозуміти, куди ви рухаєтесь.
Ганс,

Потрібно мати попередження компілятора при structпередачі a до interface.
Джалал,

8

У деяких випадках може бути добре для структури реалізувати інтерфейс (якщо він ніколи не був корисним, сумнівно, це створили б .net). Якщо структура реалізує інтерфейс, призначений лише для читання, наприклад IEquatable<T>, зберігання структури в місці зберігання (змінної, параметру, елементі масиву тощо) типу IEquatable<T>вимагатиме, щоб вона була встановлена ​​в коробку (кожен тип структури насправді визначає два типи речей: сховище тип розташування, який поводиться як тип значення, і тип об'єкта кучі, який поводиться як тип класу; перший неявно конвертується у другий - "бокс" - а другий може бути перетворений у перший за допомогою явного приведення - "розпакування"). Можна використовувати реалізацію структури інтерфейсу без боксу, однак, використовуючи так звані обмежені дженерики.

Наприклад, якби хтось мав метод CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>, такий метод міг би зателефонувати thing1.Compare(thing2)без необхідності встановлювати бокс thing1або thing2. Якщо thing1трапляється, наприклад, an Int32, час виконання знатиме, що коли він генерує код для CompareTwoThings<Int32>(Int32 thing1, Int32 thing2). Оскільки він буде знати точний тип і того, що розміщує метод, і того, що передається як параметр, йому не доведеться встановлювати жоден із них.

Найбільша проблема структур, що реалізують інтерфейси, полягає в тому, що структура, яка зберігається у розташуванні типу інтерфейсу Object, або ValueType(на відміну від розташування власного типу) буде поводитися як об'єкт класу. Для інтерфейсів, призначених лише для читання, це, як правило, не є проблемою, але для такого мутуючого інтерфейсу, як IEnumerator<T>він, може виникнути дивна семантика.

Розглянемо, наприклад, такий код:

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

Позначене твердження №1 буде основним enumerator1 для читання першого елемента. Буде скопійовано стан цього перелічувача enumerator2. Позначене твердження №2 перенесе цю копію на читання другого елемента, але не вплине enumerator1. Потім буде скопійовано стан цього другого перелічувача enumerator3, який буде просунутий за допомогою позначеного оператора №3. Потім, оскільки enumerator3і enumerator4є обома посилальними типами, ДОСИЛАННЯ до enumerator3буде скопійовано до enumerator4, тому позначений оператор фактично просуне обидва enumerator3 та enumerator4.

Деякі люди намагаються робити вигляд, що типи значень та типи посилань є обома видами Object, але це насправді не так. Типи реальних значень можна конвертувати Object, але не є їх екземплярами. Екземпляр List<String>.Enumeratorякого зберігається в розташуванні цього типу є типом значення і поводиться як тип значення; копіюючи його в розташування типу, IEnumerator<String>він перетворить його на еталонний тип, і він буде вести себе як еталонний тип . Останнє є свого роду Object, але перше - ні.

До речі, ще кілька приміток: (1) Взагалі, для змінних типів класів методи повинні Equalsперевіряти еталонну рівність, але для коробчатої структури це не має гідного способу; (2) незважаючи на свою назву, ValueTypeє типом класу, а не типом значення; всі типи, похідні відSystem.Enum є типами значень, як і всі типи, які походять від, ValueTypeза винятком System.Enum, але обидва ValueTypeі System.Enumє типами класів.


3

Структури реалізовані як типи значень, а класи - еталонні типи. Якщо у вас є змінна типу Foo, і ви зберігаєте в ній екземпляр Fubar, вона буде "упаковувати її" в контрольний тип, тим самим перемагаючи перевагу використання структури в першу чергу.

Єдина причина, по якій я бачу використовувати структуру замість класу, полягає в тому, що це буде тип значення, а не тип посилання, але структура не може успадкувати від класу. Якщо у вас є структура, яка успадковує інтерфейс, і ви обмінюєте інтерфейси, ви втрачаєте характер цього типу структури. Можливо, просто зробіть це класом, якщо вам потрібні інтерфейси.


Це працює так само для примітивів, які реалізують інтерфейси?
aoetalks

3

(Ну нічого серйозного додати, але ще не маю майстерності редагування, отже, тут ..)
Цілком безпечно. Нічого незаконного з реалізацією інтерфейсів на структурах. Однак слід запитати, чому ви хочете це зробити.

Однак отримання посилання на інтерфейс до структури буде BOX . Тож покарання за продуктивність тощо.

Єдиний дійсний сценарій, про який я можу подумати зараз, проілюстрований у моєму дописі тут . Коли ви хочете змінити стан структури, що зберігається у колекції, вам доведеться це зробити за допомогою додаткового інтерфейсу, що виставляється на структурі.


Якщо хтось передає Int32метод, який приймає загальний тип T:IComparable<Int32>(який може бути або загальним параметром типу методу, або класом методу), цей метод зможе використовувати Compareметод на переданому об'єкті, не встановлюючи його.
supercat


0

Структура, що реалізує інтерфейс, не має наслідків. Наприклад, вбудовані системні структури реалізують такі інтерфейси, як IComparableі IFormattable.


0

Існує дуже мало причин для типу значення для реалізації інтерфейсу. Оскільки ви не можете підкласувати тип значення, ви завжди можете назвати його конкретним типом.

Якщо, звичайно, у вас є кілька структур, які реалізують один і той же інтерфейс, це може бути незначно корисним, але на той момент я рекомендую використовувати клас і робити це правильно.

Звичайно, впроваджуючи інтерфейс, ви боксуєте структуру, тому вона тепер сидить у купі, і ви більше не зможете передавати її за значенням ... Це справді підсилює мою думку, що вам слід просто використовувати клас в цій ситуації.


Як часто ви передаєте IComparable замість конкретної реалізації?
FlySwat

Вам не потрібно передавати, щоб вказати IComparableзначення. Просто викликавши метод, який очікується, IComparableіз типом значення, який його реалізує, ви неявно встановите тип значення.
Ендрю Заєць

1
@AndrewHare: Обмежені дженерики дозволяють IComparable<T>викликати методи на структурах типу Tбез боксу.
supercat

-10

Структури подібні до класів, які живуть у стосі. Я не бачу причин, чому вони повинні бути "небезпечними".


За винятком того, що їм бракує спадщини.
FlySwat

7
Я повинен не погодитися з кожною частиною цієї відповіді; вони не обов’язково живуть у стеку, а семантика копіювання сильно відрізняється від класів.
Марк Гравелл

1
Вони незмінні, надмірне використання структури зробить вашу пам’ять сумною :(
Teoman shipahi

1
@Teomanshipahi Надмірне використання екземплярів класу зведе ваш збирач сміття з розуму.
IllidanS4 підтримує Моніку

4
Для тих, хто має 20 тис. Повторень, ця відповідь просто неприйнятна.
Krythic
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.