Безопасно ли для структур создавать интерфейсы?

Я, кажется, помню, что читал что-то о том, как плохо для structs реализовать интерфейсы в CLR через C #, но я не могу найти ничего об этом. Это плохо? Имеются ли непредвиденные последствия?

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

В этом вопросе происходит несколько вещей …

Структуру можно реализовать с помощью интерфейса, но есть проблемы, связанные с кастингом, изменчивостью и производительностью. См. Это сообщение для получения дополнительной информации: http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx

В общем случае структуры должны использоваться для объектов, имеющих семантику значений. Внедряя интерфейс в структуру, вы можете столкнуться с проблемами бокса, поскольку структура передается между структурой и интерфейсом. В результате бокса операции, которые изменяют внутреннее состояние структуры, могут вести себя неправильно.

Поскольку никто другой явно не предоставил этот ответ, я добавлю следующее:

Внедрение интерфейса в структуре не имеет никаких негативных последствий.

Любая переменная типа интерфейса, используемая для удержания структуры, будет иметь значение в штучной упаковке этой структуры. Если структура неизменна (хорошая вещь), то это в худшем случае проблема с производительностью, если вы не находитесь:

  • используя результирующий объект для целей блокировки (очень плохая идея любым способом)
  • используя ссылочную семантику равенства и ожидая, что она будет работать для двух вставных значений из одной и той же структуры.

Оба варианта были бы маловероятными, вместо этого вы, вероятно, будете выполнять одно из следующих действий:

Дженерики

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

 class Foo : IEquatable> where T : IEquatable { private readonly T a; public bool Equals(Foo other) { return this.a.Equals(other.a); } } 
  1. Включить использование структуры как параметра типа
    • пока не используются никакие другие ограничения, такие как new() или class .
  2. Разрешите избегать бокса в структурах, используемых таким образом.

Тогда this.a НЕ является ссылкой на интерфейс, поэтому он не вызывает коробку того, что помещается в нее. Далее, когда компилятор c # компилирует общие classы и должен вставлять вызовы методов экземпляра, определенных в экземплярах параметра T параметра T, он может использовать ограниченный код операции:

Если thisType является типом значения, и thisType реализует метод, тогда ptr передается немодифицированным как «этот» указатель на инструкцию метода вызова для реализации метода этим типом.

Это позволяет избежать бокса, и поскольку тип значения реализует интерфейс, он должен реализовать метод, поэтому никакого бокса не произойдет. В приведенном выше примере вызов Equals() выполняется без поля на этом.a 1 .

API с низким коэффициентом трения

Большинство структур должны иметь примитивно-подобную семантику, где побитовое одинаковое значение считается равным 2 . Время выполнения будет обеспечивать такое поведение в неявном Equals() но это может быть медленным. Кроме того, это неявное равенство не рассматривается как реализация IEquatable и, таким образом, предотвращает использование structs как ключей для словарей, если они явно не реализуют его сами. Поэтому многие публичные типы структур обычно заявляют, что они реализуют IEquatable (где T является им самим), чтобы сделать это более простым и эффективным, а также совместимым с поведением многих существующих типов значений в CLR CLL.

Все примитивы в BCL реализуются как минимум:

  • IComparable
  • IConvertible
  • IComparable
  • IEquatable (и, следовательно, IEquatable )

Многие также реализуют IFormattable , и многие из системных типов значений, таких как DateTime, TimeSpan и Guid, реализуют многие или все из них. Если вы реализуете аналогично «широко используемый» тип, например, сложную структуру числа или некоторые текстовые значения фиксированной ширины, тогда реализация многих из этих общих интерфейсов (правильно) сделает вашу структуру более полезной и полезной.

Исключения

Очевидно, что если интерфейс сильно подразумевает изменчивость (например, ICollection ), то реализация это плохая идея, так как это означало бы, что вы либо ICollection структуру (приводя к подобным ошибкам, описанным уже там, где изменения происходят по размеру в штучной упаковке, оригинал) или вы путаете пользователей, игнорируя последствия таких методов, как Add() или исключая исключения.

Многие интерфейсы НЕ подразумевают изменчивость (например, IFormattable ) и служат в качестве идиоматического способа обеспечить определенную функциональность согласованным образом. Часто пользователю этой структуры не нужно беспокоиться о каких-либо накладных расходах на бокс для такого поведения.

Резюме

Когда сделано разумно, по неизменяемым типам значений, реализация полезных интерфейсов – хорошая идея


Заметки:

1: Обратите внимание, что компилятор может использовать это при вызове виртуальных методов для переменных, которые, как известно, имеют определенный тип структуры, но в которых требуется вызвать виртуальный метод. Например:

 List l = new List(); foreach(var x in l) ;//no-op 

Перечислитель, возвращаемый List, представляет собой структуру, оптимизацию, чтобы избежать выделения при перечислении списка (с некоторыми интересными последствиями ). Однако семантика foreach указывает, что если перечислитель реализует IDisposable то Dispose() вызывается после завершения итерации. Очевидно, что если это произойдет через бокс-вызов, это исключит любое преимущество перечислителя, являющегося структурой (на самом деле это будет хуже). Хуже того, если dispose call каким-то образом изменяет состояние перечислителя, это произойдет в экземпляре в штучной упаковке, и многие сложные ошибки могут быть введены в сложных случаях. Поэтому ИЛ, испускаемый в такой ситуации:

 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 не считаются равными.

В некоторых случаях может быть хорошо для структуры реализовать интерфейс (если он никогда не был полезным, вряд ли создатели .net могли бы его предоставить). Если структура реализует интерфейс только для чтения, такой как IEquatable , сохранение структуры в хранилище (переменная, параметр, элемент массива и т. Д.) Типа IEquatable потребует, чтобы он был помещен в коробку (каждый тип структуры фактически определяет два типа вещей: тип местоположения хранилища, который ведет себя как тип значения и тип объекта кучи, который ведет себя как тип classа, первый неявно конвертируется во второй – «бокс» – и второй может быть преобразован к первому через явное приведение – «unboxing»). Можно использовать реализацию структуры интерфейса без бокса, однако, используя так называемые ограниченные дженерики.

Например, если у вас был метод CompareTwoThings(T thing1, T thing2) where T:IComparable , такой метод мог бы вызвать thing1.Compare(thing2) без необходимости thing1 или thing2 . Если thing1 является, например, Int32 , время выполнения будет знать, что когда он генерирует код для CompareTwoThings(Int32 thing1, Int32 thing2) . Поскольку он будет знать точный тип как объекта, в котором находится метод, так и того, что передается в качестве параметра, ему не нужно будет вставлять ни один из них.

Самая большая проблема с структурами, реализующими интерфейсы, заключается в том, что структура, которая хранится в местоположении типа интерфейса, Object или ValueType (в отличие от местоположения его собственного типа), будет вести себя как объект classа. Для интерфейсов только для чтения это обычно не проблема, но для мутирующего интерфейса, такого как IEnumerator это может привести к некоторой странной семантике.

Рассмотрим, например, следующий код:

 List myList = [list containing a bunch of strings] var enumerator1 = myList.GetEnumerator(); // Struct of type List.IEnumerator enumerator1.MoveNext(); // 1 var enumerator2 = enumerator1; enumerator2.MoveNext(); // 2 IEnumerator enumerator3 = enumerator2; enumerator3.MoveNext(); // 3 IEnumerator enumerator4 = enumerator3; enumerator4.MoveNext(); // 4 

Отмеченная заявка # 1 будет enumerator1 первый enumerator1 для чтения первого элемента. Состояние этого перечислителя будет скопировано в enumerator2 . Отмеченная заявка # 2 переведет эту копию для чтения второго элемента, но не повлияет на enumerator1 . Состояние этого второго перечислителя затем будет скопировано в enumerator3 , который будет расширен с помощью отмеченного оператора # 3. Затем, поскольку enumerator3 и enumerator4 являются ссылочными типами, REFERENCE для enumerator3 затем будет скопирован в enumerator4 , поэтому отмеченная инструкция будет эффективно продвигать как enumerator3 и enumerator4 .

Некоторые люди пытаются притвориться, что типы значений и ссылочные типы являются обоими видами Object , но это не так. Реальные типы значений конвертируются в Object , но не являются экземплярами. Экземпляр List.Enumerator который хранится в местоположении этого типа, является типом значения и ведет себя как тип значения; копируя его в местоположение типа IEnumerator , преобразует его в ссылочный тип и будет вести себя как ссылочный тип . Последнее является своего рода Object , но первое – нет.

BTW, еще несколько заметок: (1) В целом, изменяемые типы classов должны иметь свои методы Equals проверки ссылочного равенства, но для построения в штучной коробке нет подходящего способа; (2) несмотря на свое имя, ValueType – это тип classа, а не тип значения; все типы, полученные из System.Enum являются типами значений, как и все типы, которые производятся из ValueType за исключением System.Enum , но оба типа ValueType и System.Enum являются типами classов.

Структуры реализуются как типы значений, а classы – ссылочные типы. Если у вас есть переменная типа Foo, и вы храните в ней экземпляр Fubar, он будет «вставлять» его в ссылочный тип, тем самым побеждая преимущество использования структуры в первую очередь.

Единственная причина, по которой я вижу использование структуры вместо classа, заключается в том, что это будет тип значения, а не ссылочный тип, но структура не может наследовать от classа. Если у вас есть структура, наследующая интерфейс, и вы обходите интерфейсы, вы теряете характер типа структуры. Возможно, просто сделайте это classом, если вам нужны интерфейсы.

(Ну, нечего добавить, но у меня нет прав на редактирование, но вот идет …)
Совершенно безопасно. Ничего незаконного с внедрением интерфейсов в структурах. Однако вы должны задать вопрос, почему вы хотите это сделать.

Однако получение ссылки на структуру структуры будет BOX . Таким образом, штраф за исполнение и так далее.

Единственный допустимый сценарий, о котором я могу думать сейчас, проиллюстрирован в моем сообщении здесь . Если вы хотите изменить состояние структуры, хранящееся в коллекции, вам нужно будет сделать это через дополнительный интерфейс, открытый в структуре.

Я думаю, проблема заключается в том, что она вызывает бокс, потому что структуры являются типами значений, поэтому есть небольшое ограничение производительности.

Эта ссылка предполагает, что могут быть другие проблемы с этим …

http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx

Для структуры, реализующей интерфейс, нет никаких последствий. Например, встроенные системные структуры реализуют такие интерфейсы, как IComparable и IFormattable .

Существует очень мало оснований для того, чтобы тип значения реализовал интерфейс. Поскольку вы не можете подclassифицировать тип значения, вы всегда можете ссылаться на него как на его конкретный тип.

Если, конечно, у вас несколько структур, реализующих один и тот же интерфейс, это может быть немного полезно тогда, но в этот момент я бы рекомендовал использовать class и сделать это правильно.

Конечно, реализуя интерфейс, вы боксируете структуру, поэтому теперь она сидит на куче, и вы больше не сможете передавать ее по значению … Это действительно подтверждает мое мнение о том, что вы должны просто использовать class в этой ситуации.

Структуры – это как classы, которые живут в стеке. Я не вижу причин, почему они должны быть «небезопасными».

  • Почему GCC не оптимизирует структуры?
  • Изменить переменную Struct в словаре
  • Как включить динамический массив INSIDE a struct в C?
  • Разница между 'struct' и 'typedef struct' в C ++?
  • Почему C имеет различие между -> и.?
  • как устанавливать и получать поля в структурах Golang?
  • C: sizeof single struct member
  • Копирование одной структуры в другую
  • Как скомпилировать C-код с анонимными структурами / объединениями?
  • Почему C ++ поддерживает поэтапное назначение массивов внутри структур, но не в целом?
  • Класс смешивания и структура
  • Давайте будем гением компьютера.