Безопасно ли для структур реализовывать интерфейсы?
Я, кажется, помню, что читал что-то о том, как плохо для структур реализовывать интерфейсы в CLR через C#, но я ничего не могу найти об этом. Это плохо? Есть ли непреднамеренные последствия этого?
public interface Foo { Bar GetBar(); }
public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }
9 ответов:
в этом вопросе происходит несколько вещей...
структура может реализовать интерфейс, но есть проблемы, связанные с литьем, изменчивостью и производительностью. См. этот пост для более подробной информации: http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx
В общем случае структуры должны использоваться для объектов, имеющих семантику типа значения. Реализуя интерфейс в структуре, вы можете столкнуться с проблемами бокса поскольку структура отбрасывается назад и вперед между структурой и интерфейсом. В результате бокса операции, которые изменяют внутреннее состояние структуры, могут вести себя неправильно.
поскольку никто другой явно не предоставил этот ответ, я добавлю следующее:
реализация интерфейс по структуре, не имеет никаких негативных последствий.
любой переменная типа интерфейса, используемого для хранения структуры, приведет к использованию коробочного значения этой структуры. Если структура неизменна (хорошая вещь), то это в худшем случае проблема производительности, если вы:
- использование результирующий объект для целей блокировки (очень плохая идея в любом случае)
- используя семантику равенства ссылок и ожидая, что она будет работать для двух коробочных значений из одной и той же структуры.
оба из них были бы маловероятны, вместо этого вы, вероятно, будете делать одно из следующих действий:
дженериков
возможно, многие разумные причины для структур, реализующих интерфейсы так, что их можно использовать внутри generic контексте с ограничения. При использовании таким образом переменная выглядит так:
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()или есть.- позвольте избежать бокса на структурах, используемых таким образом.
затем эта.a не является ссылкой на интерфейс, поэтому он не вызывает поле того, что помещается в нее. Далее, когда компилятор c# компилирует универсальные классы и должен вставлять вызовы методов экземпляра, определенных на экземплярах параметра типа T, он может использовать ограниченного код:
если thisType является типом значения и thistype реализует метод, то ptr передается немодифицированным как указатель' this ' на инструкцию метода вызова, для реализации метода thisType.
это позволяет избежать бокса и поскольку тип значения реализует интерфейс должны реализовать метод, таким образом, бокс не будет происходить. В приведенном выше примере
Equals()вызов выполняется без коробки на этом.а1.низкое трение APIs
большинство структур должны иметь примитивную семантику, где побитовые одинаковые значения считаются равными2. Среда выполнения будет предоставлять такое поведение в неявном
Equals()но это может быть медленным. Также это неявное равенство-это не выставляется как реализацияIEquatable<T>и таким образом предотвращает использование структур легко в качестве ключей для словарей, если они явно не реализуют его сами. Поэтому многие типы публичных структур обычно объявляют, что они реализуютIEquatable<T>(гдеTis them self), чтобы сделать это проще и лучше, а также в соответствии с поведением многих существующих типов значений в среде CLR BCL.все примитивы в реализация BCL как минимум:
IComparableIConvertibleIComparable<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 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 не считаются равными.
в некоторых случаях для структуры может быть полезно реализовать интерфейс (если он никогда не был полезен, сомнительно, что создатели .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оба ссылочными типами, а ссылка до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, он будет "упаковывать его" в ссылочный тип, тем самым уничтожая преимущество использования структуры в первую очередь.
единственная причина, по которой я вижу, чтобы использовать структуру вместо класса, потому что это будет тип значения, а не ссылочный тип, но структура не может наследовать от класса. Если у вас есть структура наследовать интерфейс, и вы передаете интерфейсы, вы теряете эту природу типа значения структуры. Может также просто сделать его классом, если вам нужны интерфейсы.
(Ну нет ничего серьезного, чтобы добавить, но пока нет мастерства редактирования, так что здесь идет..)
получение ссылки на интерфейс к структуре будет BOX его. Так штраф и так далее.
Совершенно Безопасно. Ничего противозаконного в реализации интерфейсов на структурах нет. Однако вы должны спросить, почему вы хотите это сделать.единственный допустимый сценарий, который я могу придумать прямо сейчас показано в моем посте здесь. Если вы хотите изменить состояние структуры, хранящейся в коллекции, вам придется сделать это через дополнительный интерфейс, открытый в структуре.
Я думаю, что проблема в том, что это вызывает бокс, потому что структуры являются типами значений, поэтому есть небольшое снижение производительности.
эта ссылка предполагает, что могут быть и другие проблемы с ним...
http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx
нет никаких последствий для структуры, реализующей интерфейс. Например, встроенные системные структуры реализуют интерфейсы типа
IComparableиIFormattable.
существует очень мало причин для типа значения для реализации интерфейса. Поскольку вы не можете подкласс тип значения, вы всегда можете обратиться к нему, как его конкретный тип.
Если, конечно, у вас есть несколько структур, реализующих один и тот же интерфейс, это может быть немного полезно, но в этот момент я бы рекомендовал использовать класс и делать это правильно.
конечно, реализуя интерфейс, вы боксируете структуру, поэтому теперь она сидит в куче, и вы не будете быть в состоянии передать его по значению больше...Это действительно укрепляет мое мнение, что вы должны просто использовать класс в этой ситуации.
структуры так же, как классы, которые живут в стеке. Я не вижу причин, почему они должны быть "небезопасно".
Comments