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

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

public interface Foo { Bar GetBar(); }
public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
97
0
62 829
9
Перейти к ответу Данный вопрос помечен как решенный

Ответы 9

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

За исключением того, что они лишены наследственности.

FlySwat 15.09.2008 19:04

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

Marc Gravell 12.03.2011 18:00

Они неизменны, чрезмерное использование struct сделает вашу память грустной :(

Teoman shipahi 28.07.2014 07:45

@Teomanshipahi Чрезмерное использование экземпляров классов сведет ваш сборщик мусора с ума.

IS4 10.04.2016 00:27

@ IllidanS4 использует IDisposable и оператор using, это сделает ваш сборщик мусора счастливым.

Teoman shipahi 10.04.2016 01:16

Для тех, у кого более 20к репутации, такой ответ просто неприемлем.

Krythic 18.11.2016 21:40

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

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

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

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

Как часто вы передаете IComparable вместо конкретной реализации?

FlySwat 15.09.2008 22:02

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

Andrew Hare 17.08.2009 22:31

@AndrewHare: дженерики с ограничениями позволяют вызывать методы на IComparable<T> в структурах типа T без упаковки.

supercat 14.01.2013 19:48

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

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

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

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

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

Это работает и для примитивов, реализующих интерфейсы?

aoetalks 13.04.2020 21:35
Ответ принят как подходящий

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

Структура может реализовать интерфейс, но есть проблемы, связанные с приведением типов, изменчивостью и производительностью. См. Этот пост для более подробной информации: https://docs.microsoft.com/en-us/archive/blogs/abhinaba/c-structs-and-interface

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

«В результате упаковки операции, которые изменяют внутреннее состояние структуры, могут работать некорректно». Приведите пример и получите ответ.

user1228 15.09.2008 19:36

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

Scott Dorman 15.09.2008 19:38

@Will: CLR Via C# подробно рассматривает эту проблему в главе "Типы ссылок и значений" (3?) ... Я бы напечатал ее, но я ухожу на работу, поэтому, если у вас есть эта книга, взгляните .

FlySwat 15.09.2008 19:48

@ScottDorman: В некоторых случаях наличие структур, реализующих интерфейсы, может помочь боксу избегать. Яркие примеры - IComparable<T> и IEquatable<T>. Сохранение структуры Foo в переменной типа IComparable<Foo> потребует упаковки, но если общий тип T ограничен IComparable<T>, можно сравнить его с другим T, не упаковывая ни один из них, и не имея необходимости знать что-либо о T, кроме того, что он реализует ограничение. Такое выгодное поведение стало возможным только благодаря способности структур реализовывать интерфейсы. Это было сказано ...

supercat 19.08.2013 19:45

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

supercat 19.08.2013 19:55

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

jpmc26 09.07.2017 21:13

Древняя ссылка мертва.

Kevin Krumwiede 08.04.2020 21:51

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

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

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

Если передать Int32 методу, который принимает общий тип T:IComparable<Int32> (который может быть параметром универсального типа для метода или классом метода), этот метод сможет использовать метод Compare для переданного объекта без бокс это.

supercat 14.01.2013 19:50

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

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

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

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

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

Дженерики

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

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

If thisType is a value type and thisType implements method then ptr is passed unmodified as the 'this' pointer to a call method instruction, for the implementation of method by 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), тогда реализация этого будет плохой идеей, так как это будет означать, что вы либо сделали структуру изменяемой (что приводит к видам ошибок, описанным уже, когда изменения происходят в упакованном значении, а не в original), или вы сбиваете с толку пользователей, игнорируя последствия таких методов, как 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 не считаются равными.

Сайт egheadcafe.com был перемещен, но его содержимое не сохранилось. Я пробовал, но не могу найти исходный документ eggheadcafe.com/software/aspnet/31702392/…, не зная OP. (PS +1 за отличное резюме).

Abel 20.10.2014 01:31

Это отличный ответ, но я думаю, что вы можете улучшить его, переместив «Сводку» наверх как «TL; DR». Предоставление заключения сначала помогает читателю понять, к чему вы идете.

Hans 16.02.2015 22:40

При преобразовании struct в interface должно появиться предупреждение компилятора.

Jalal 31.10.2018 12:33

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

Другие вопросы по теме