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

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

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

Ответ 1

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

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

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

Ответ 2

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

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

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

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

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

Дженерики

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

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);
    }
}
  • Разрешить использование структуры как параметра типа
    • пока не используется никакое другое ограничение, например new() или class.
  • Разрешить избегать бокса в структурах, используемых таким образом.

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

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

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

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

Большинство структур должны иметь примитивно-подобную семантику, где побито одинаковые значения считаются равными 2. Время выполнения будет обеспечивать такое поведение в неявном Equals(), но это может быть медленным. Кроме того, это неявное равенство не рассматривается как реализация IEquatable<T> и, таким образом, предотвращает использование structs как ключей для словарей, если они явно не реализуют его сами. Поэтому многие публичные типы структур объявляют, что они реализуют 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 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:  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 не считаются равными.

Ответ 3

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

Например, если у вас был метод 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 являются ссылочными типами, ССЫЛКА на enumerator3 затем будет скопирована в enumerator4, поэтому отмеченная инструкция будет эффективно продвигать как enumerator3, так и enumerator4.

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

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

Ответ 4

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

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

Ответ 5

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

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

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

Ответ 6

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

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

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

Ответ 7

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

Ответ 8

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

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

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

Ответ 9

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