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

94

Кажется, я помню, как читал что-то о том, как плохо для структур реализовывать интерфейсы в 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кроме того, что он реализует ограничение. Такое выгодное поведение стало возможным только благодаря способности структур реализовывать интерфейсы. Как было сказано ...
supercat
3
... было бы неплохо, если бы существовали средства объявления того, что конкретный интерфейс следует считать применимым только к неупакованным структурам, поскольку есть некоторые контексты, в которых объект класса или упакованная структура не могут иметь желаемое поведение.
supercat
2
«структуры должны использоваться для объектов, которые имеют семантику типа значения. ... операции, которые изменяют внутреннее состояние структуры, могут не вести себя должным образом». Разве настоящая проблема не в том, что семантика типа значения и изменчивость плохо сочетаются?
jpmc26 09
185

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

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

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

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

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

Дженерики

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

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 # компилирует универсальные классы и ему необходимо вставить вызовы методов экземпляра, определенных в экземплярах параметра 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)
    ;//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: вызов 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 не считаются равными.

ShuggyCoUk
источник
1
Сайт egheadcafe.com был перемещен, но его содержание не удалось сохранить. Я пробовал, но не могу найти исходный документ eggheadcafe.com/software/aspnet/31702392/… , не зная OP. (PS +1 за отличное резюме).
Abel
2
Это отличный ответ, но я думаю, что вы можете улучшить его, переместив «Сводку» наверх как «TL; DR». Предоставление заключения сначала помогает читателю понять, к чему вы идете.
Ганс,
При structпреобразовании в 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являются ссылочными типами, REFERENCE to 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, она «упакует» ее в ссылочный тип, таким образом сводя на нет преимущество использования структуры в первую очередь.

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

dotnetengineer
источник
Это работает и для примитивов, реализующих интерфейсы?
aoetalks
3

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

Однако получение ссылки на интерфейс для структуры поместит ее в коробку . Итак, штраф за производительность и так далее.

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

Гишу
источник
Если передать Int32методу, который принимает универсальный тип T:IComparable<Int32>(который может быть параметром универсального типа метода или классом метода), этот метод сможет использовать Compareметод для переданного объекта без его упаковки.
supercat
1

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

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

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

Саймон Кип
источник
0

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

Джозеф Дейгл
источник
0

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

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

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

FlySwat
источник
Как часто вы передаете IComparable вместо конкретной реализации?
FlySwat
Вам не нужно передавать значение IComparableв бокс. Просто вызывая метод, который ожидает IComparableс типом значения, который его реализует, вы неявно помещаете тип значения в коробку.
Эндрю Хэйр
1
@AndrewHare: дженерики с ограничениями позволяют IComparable<T>вызывать методы для структур типа Tбез упаковки.
supercat
-10

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

Скливвз
источник
Только у них нет наследственности.
FlySwat
7
Я не могу согласиться с каждой частью этого ответа; они не обязательно находятся в стеке, а семантика копирования сильно отличается от классов.
Марк Гравелл
1
Они неизменны, чрезмерное использование struct сделает вашу память грустной :(
Teoman shipahi
1
@Teomanshipahi Чрезмерное использование экземпляров класса сведет ваш сборщик мусора с ума.
IllidanS4 поддерживает Монику
4
Для тех, у кого более 20к репутации, такой ответ просто неприемлем.
Krythic