it-swarm-ru.tech

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

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

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

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

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

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

42
Scott Dorman

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

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

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

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

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

Дженерики

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

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.a1,.

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: нет 
 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 [.__ .s 02 
 IL_0028: ограничено. System.Collections.Generic.List.Enumerator 
 IL_002E: callvirt System.IDisposable.Dispose 
 IL_0033: нет 
 IL_0034: окончательно 

Таким образом, реализация IDisposable не вызывает каких-либо проблем с производительностью, и (прискорбный) изменяемый аспект перечислителя сохраняется, если метод Dispose действительно что-то делает!

2: double и float являются исключениями из этого правила, где значения NaN не считаются равными.

158
ShuggyCoUk

В некоторых случаях для структуры может быть полезно реализовать интерфейс (если бы он никогда не был полезным, сомнительно, что создатели .net сделали бы это). Если структура реализует интерфейс только для чтения, такой как IEquatable<T>, хранение структуры в месте хранения (переменная, параметр, элемент массива и т.д.) Типа IEquatable<T> потребует его помещения в коробку (каждый тип структуры фактически определяет два вида вещей: тип хранилища, который ведет себя как тип значения, и тип кучи-объекта, который ведет себя как тип класса: первый неявно преобразуется во второй - "бокс" - а второй может быть преобразован в первый посредством явного преобразования - "распаковка"). Однако можно использовать реализацию структуры интерфейса без упаковки, используя так называемые ограниченные обобщения.

Например, если у кого-то есть метод CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>, такой метод может вызывать thing1.Compare(thing2) без необходимости вставлять thing1 или thing2. Если thing1, например, является 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 до enumerator3 будет скопировано в enumerator4, поэтому помеченный оператор будет эффективно продвигаться обаenumerator3 и enumerator4.

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

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

8
supercat

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

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

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

3
Gishu

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

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

3
dotnetengineer

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

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

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

1
Simon Keep

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

0
Joseph Daigle

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

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

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

0
FlySwat