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

StackOverflow https://stackoverflow.com/questions/63671

  •  09-06-2019
  •  | 
  •  

Вопрос

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

Тогда this.a НЕ является ссылкой на интерфейс, поэтому он не вызывает поле с тем, что в него помещено.Кроме того, когда компилятор c # компилирует универсальные классы и ему необходимо вставить вызовы методов экземпляра, определенных в экземплярах параметра типа 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

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

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

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

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

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

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

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

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

Однако получение ссылки интерфейса на структуру приведет к появлению ПОЛЯ IT.Итак, снижение производительности и так далее.

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

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

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

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top