Безопасно ли для структур реализовать интерфейсы? - PullRequest
81 голосов
/ 15 сентября 2008

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

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

Ответы [ 9 ]

158 голосов
/ 17 августа 2009

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

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

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

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

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

Дженерики

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

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, он может использовать код операции constrained :

Если thisType является типом значения и thisType реализует метод, то ptr передается неизмененным как указатель 'this' на инструкцию метода вызова для реализации метода thisType.

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

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

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

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

  • IComparable
  • IConvertible
  • IComparable<T>
  • IEquatable<T> (а значит IEquatable)

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

1089 * Исключения * Очевидно, что если интерфейс сильно подразумевает изменчивость (например, 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:  call        System.Collections.Generic.List.get_Current
IL_0018:  stloc.1     
IL_0019:  ldloca.s    02 
IL_001B:  call        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:  constrained. System.Collections.Generic.List.Enumerator
IL_002E:  callvirt    System.IDisposable.Dispose
IL_0033:  nop         
IL_0034:  endfinally  

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

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

42 голосов
/ 15 сентября 2008

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

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

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

8 голосов
/ 14 января 2013

В некоторых случаях для структуры может быть полезно реализовать интерфейс (если бы он никогда не был полезным, сомнительно, что создатели .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 являются типами классов.

3 голосов
/ 15 сентября 2008

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

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

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

3 голосов
/ 15 сентября 2008

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

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

1 голос
/ 15 сентября 2008

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

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

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

0 голосов
/ 15 сентября 2008

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

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

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

0 голосов
/ 15 сентября 2008

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

0 голосов
/ 15 сентября 2008

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...