После обсуждений здесь, посвященных SO, я уже несколько раз читал замечание о том, что изменяемые структуры являются «злыми» (как в ответе на этот вопрос ).
Какова реальная проблема с изменчивостью и структурами в C #?
c#
struct
immutability
mutable
Дирк Воллмар
источник
источник
int
s,bool
s и все другие типы значений являются злом. Есть случаи изменчивости и неизменности. Эти случаи зависят от роли, которую играют данные, а не от типа распределения / распределения памяти.int
и неbool
являются изменяемыми ...
-Синтаксис, заставляющий операции с данными с типами ref и данными с типами значений выглядеть одинаково, даже если они явно отличаются. Это ошибка свойств C #, а не структур - некоторые языки предлагают альтернативныйa[V][X] = 3.14
синтаксис для мутирования на месте. В C # лучше предлагать методы-мутаторы struct-member, такие как MutateV (Action <ref Vector2> mutator), и использовать их какa.MutateV((v) => { v.X = 3; })
(пример слишком упрощен из-за ограничений, которые C # имеет в отношенииref
ключевого слова, но с некоторыми обходные пути должны быть возможными) .x
) эта единственная операция состоит из 4 инструкций:ldloc.0
(загружает переменную с 0 индексами в ...T
это тип. Ref - это просто ключевое слово, которое заставляет переменную передаваться самому методу, а не его копии. Это также имеет смысл для ссылочных типов, поскольку мы можем изменить переменную , т.е. ссылка вне метода будет указывать на другой объект после изменения в методе. Посколькуref T
это не тип, а способ передачи параметра метода, вы не можете поместить его в него<>
, поскольку в него можно помещать только типы. Так что это просто неверно. Может быть, это было бы удобно, может быть, команда C # могла бы сделать это для какой-то новой версии, но сейчас они работают над некоторыми ...Ответы:
Структуры являются типами значений, что означает, что они копируются при передаче.
Поэтому, если вы меняете копию, вы изменяете только эту копию, а не оригинал и любые другие копии, которые могут быть рядом.
Если ваша структура неизменна, то все автоматические копии, полученные в результате передачи по значению, будут одинаковыми.
Если вы хотите изменить его, вы должны сделать это сознательно, создав новый экземпляр структуры с измененными данными. (не копия)
источник
С чего начать ;-p
Блог Эрика Липперта всегда хорош для цитаты:
Во-первых, вы, как правило, легко теряете изменения ... например, вынимаете вещи из списка:
что это изменило? Ничего полезного ...
То же самое со свойствами:
заставляя вас делать:
менее критично, есть проблема размера; изменчивые объекты обычно имеют несколько свойств; тем не менее, если у вас есть структура с двумя
int
s, astring
, aDateTime
и abool
, вы можете очень быстро прожечь много памяти. С классом несколько абонентов могут совместно использовать ссылку на один и тот же экземпляр (ссылки маленькие).источник
++
оператора. В этом случае компилятор просто пишет явное присваивание вместо того, чтобы толкать программиста.Я бы не сказал зло, но изменчивость часто является признаком чрезмерного стремления программиста обеспечить максимальную функциональность. На самом деле, это часто не нужно, и это, в свою очередь, делает интерфейс меньше, проще в использовании и труднее использовать неправильно (= более надежно).
Одним из примеров этого являются конфликты чтения / записи и записи / записи в условиях гонки. Это просто не может произойти в неизменяемых структурах, так как запись не является допустимой операцией.
Кроме того, я утверждаю, что изменчивость практически никогда не нужна , программист просто думает, что это может произойти в будущем. Например, просто не имеет смысла менять дату. Скорее, создайте новую дату на основе старой. Это дешевая операция, поэтому производительность не учитывается.
источник
float
компонентов графического преобразования. Если такой метод возвращает структуру открытого поля с шестью компонентами, очевидно, что изменение полей структуры не изменит графический объект, от которого она была получена. Если такой метод возвращает объект изменяемого класса, возможно, изменение его свойств изменит базовый графический объект, а может и нет - никто не знает.Изменчивые структуры не являются злом.
Они абсолютно необходимы в условиях высокой производительности. Например, когда строки кэша и / или сборка мусора становятся узким местом.
Я бы не назвал использование неизменяемой структуры в этих совершенно правильных сценариях использования «злом».
Я вижу смысл в том, что синтаксис C # не помогает различать доступ к элементу типа значения или ссылочного типа, поэтому я полностью предпочитаю неизменяемые структуры, которые обеспечивают неизменность, а не изменяемые структуры.
Однако вместо простой маркировки неизменяемых структур как «злых» я бы посоветовал принять язык и отстаивать более полезное и конструктивное правило.
Например: «структуры являются типами значений, которые копируются по умолчанию. Вам нужна ссылка, если вы не хотите их копировать» или «попробуйте сначала работать с структурами только для чтения» .
источник
struct
с открытыми полями), чем определять класс, который можно использовать, неуклюже, для достижения тех же целей, или добавить кучу мусора в структуру, чтобы он эмулировал такой класс (вместо того, чтобы иметь его) вести себя как набор переменных, склеенных клейкой лентой, что иСтруктуры с открытыми изменяемыми полями или свойствами не являются злом.
Методы структуры (в отличие от установщиков свойств), которые изменяют «это», являются чем-то злым, только потому, что .net не предоставляет средства отличить их от методов, которые этого не делают. Методы структуры, которые не изменяют «this», должны вызываться даже для структур только для чтения без необходимости защитного копирования. Методы, которые изменяют "this", вообще не должны вызываться в структурах только для чтения. Поскольку .net не хочет запрещать использование структурных методов, которые не изменяют «this», для структур, доступных только для чтения, но не хочет разрешать изменение структур, доступных только для чтения, он защищенно копирует структуры в режиме чтения. только контексты, возможно получая худшее из обоих миров.
Тем не менее, несмотря на проблемы с обработкой самопутывающихся методов в контекстах только для чтения, изменяемые структуры часто предлагают семантику, намного превосходящую изменяемые типы классов. Рассмотрим следующие три сигнатуры метода:
Для каждого метода ответьте на следующие вопросы:
ответы:
Method1 не может изменить foo и никогда не получает ссылку. Method2 получает кратковременную ссылку на foo, которую он может использовать для изменения полей foo любое количество раз в любом порядке, пока не вернется, но он не может сохранить эту ссылку. Перед возвратом Method2, если он не использует небезопасный код, все копии, которые могли быть сделаны из его ссылки 'foo', исчезнут. Метод 3, в отличие от метода 2, получает беспорядочную ссылку на foo, и никто не знает, что он может с этим делать. Это может вообще не изменить foo, это может изменить foo и затем вернуться, или это может дать ссылку на foo другому потоку, который может каким-то образом изменить его в некоторый произвольный момент времени в будущем.
Массивы структур предлагают прекрасную семантику. Учитывая RectArray [500] типа Rectangle, становится ясным и очевидным, как, например, скопировать элемент 123 в элемент 456, а затем через некоторое время установить ширину элемента 123 в 555, не нарушая элемент 456. "RectArray [432] = RectArray [321 ]; ...; RectArray [123] .Width = 555; ". Знание того, что Rectangle - это структура с целочисленным полем Width, расскажет все, что нужно знать о приведенных выше утверждениях.
Теперь предположим, что RectClass был классом с теми же полями, что и Rectangle, и один хотел выполнить те же операции над RectClassArray [500] типа RectClass. Возможно, массив должен содержать 500 предварительно инициализированных неизменяемых ссылок на изменяемые объекты RectClass. в этом случае правильный код будет выглядеть примерно так: «RectClassArray [321] .SetBounds (RectClassArray [456]); ...; RectClassArray [321] .X = 555;». Возможно, предполагается, что массив содержит экземпляры, которые не будут изменяться, поэтому правильный код будет больше похож на "RectClassArray [321] = RectClassArray [456]; ...; RectClassArray [321] = New RectClass (RectClassArray [321 ]); RectClassArray [321] .X = 555; " Чтобы знать, что нужно делать, нужно знать гораздо больше о RectClass (например, поддерживает ли он конструктор копирования, метод копирования и т. Д.). ) и предполагаемое использование массива. Нигде не так чисто, как использование структуры.
Безусловно, для любого класса контейнера, кроме массива, нет хорошего способа предложить чистую семантику массива структуры. Лучшее, что можно было бы сделать, если бы кто-то хотел, чтобы коллекция была проиндексирована, например, строкой, вероятно, было бы предложить универсальный метод ActOnItem, который бы принимал строку для индекса, универсальный параметр и делегат, который был бы передан. по ссылке и общий параметр, и элемент коллекции. Это позволило бы использовать почти ту же семантику, что и структурные массивы, но если люди vb.net и C # не смогут предложить хороший синтаксис, код будет выглядеть неуклюже, даже если он имеет разумную производительность (передача универсального параметра разрешить использование статического делегата и избежать необходимости создавать какие-либо временные экземпляры классов).
Лично меня раздражает ненависть Эрика Липперта и других. выбросить в отношении изменяемых типов значений. Они предлагают намного более чистую семантику, чем беспорядочные ссылочные типы, которые используются повсеместно. Несмотря на некоторые ограничения, связанные с поддержкой .net для типов значений, во многих случаях изменяемые типы значений лучше подходят, чем любой другой тип объектов.
источник
Rectangle
я легко смогу придумать общую ситуацию, когда вы будете вести себя крайне неясно . Учтите, что WinForms реализует изменяемыйRectangle
тип, используемый вBounds
свойстве формы . Если я хочу изменить границы, я бы хотел использовать ваш красивый синтаксис:form.Bounds.X = 10;
однако это точно ничего не меняет в форме (и выдает прекрасную ошибку, сообщающую вам о таком). Несоответствие - проклятие программирования и именно поэтому нужна неизменность.Типы значений в основном представляют собой неизменные понятия. Fx, не имеет смысла иметь математическое значение, такое как целое число, вектор и т. Д., А затем иметь возможность изменять его. Это было бы как переопределение значения стоимости. Вместо изменения типа значения имеет смысл назначить другое уникальное значение. Подумайте о том, что типы значений сравниваются путем сравнения всех значений его свойств. Дело в том, что если свойства одинаковы, то это одно и то же универсальное представление этого значения.
Как упоминает Конрад, также нет смысла менять дату, поскольку значение представляет этот уникальный момент времени, а не экземпляр объекта времени, который имеет какое-либо состояние или контекст-зависимость.
Надеется, что это имеет какой-то смысл для вас. Конечно, это скорее концепция, которую вы пытаетесь охватить типами значений, чем практические детали.
источник
int
итератор, который был бы совершенно бесполезным, если бы он был неизменным. Я думаю, что вы объединяете «реализации типов значений компилятора / среды выполнения» с «переменными, типизированными для типа значения» - последнее, безусловно, изменяемо для любого из возможных значений.Есть пара других крайних случаев, которые могут привести к непредсказуемому поведению с точки зрения программиста.
Типы неизменяемых значений и поля только для чтения
Типы изменяемых значений и массив
Предположим, у нас есть массив нашей
Mutable
структуры, и мы вызываемIncrementI
метод для первого элемента этого массива. Какое поведение вы ожидаете от этого звонка? Должно ли это изменить значение массива или только копию?Таким образом, изменчивые структуры не являются злом, если вы и остальная часть команды четко понимаете, что вы делаете. Но есть слишком много угловых случаев, когда поведение программы будет отличаться от ожидаемого, что может привести к едва заметным трудным для понимания и трудным для понимания ошибкам.
источник
T[]
и целочисленный индекс, и обеспечения того, что доступ к свойству типаArrayRef<T>
будет интерпретироваться как доступ к соответствующему элементу массива) [если класс хочет предоставить объектArrayRef<T>
для какой-либо другой цели, он может предоставить метод - в отличие от свойства - для его извлечения]. К сожалению, таких положений не существует.Если вы когда-либо программировали на языке, подобном C / C ++, структуры можно использовать как изменяемые. Просто передайте их с реф, вокруг, и нет ничего, что может пойти не так. Единственная проблема, которую я нахожу, это ограничения компилятора C # и то, что в некоторых случаях я не могу заставить глупую вещь использовать ссылку на структуру вместо копии (например, когда структура является частью класса C #) ).
Таким образом, изменчивые структуры не являются злом, C # сделал их злыми. Я все время использую изменяемые структуры в C ++, и они очень удобны и интуитивно понятны. Напротив, C # заставил меня полностью отказаться от структур как членов классов из-за того, как они обрабатывают объекты. Их удобство стоило нам нашего.
источник
readonly
, но если кто-то избегает делать это, классовые поля структурных типов просто хороши. Единственное действительно фундаментальное ограничение структур состоит в том, что поле структуры типа изменяемого класса, такого как,int[]
может инкапсулировать идентичность или неизменный набор значений, но не может использоваться для инкапсуляции изменяемых значений без инкапсуляции нежелательной идентичности.Если вы будете придерживаться того, для чего предназначены структуры (в C #, Visual Basic 6, Pascal / Delphi, типе структуры C ++ (или классах), когда они не используются в качестве указателей), вы обнаружите, что структура не больше, чем составная переменная , Это означает: вы будете рассматривать их как упакованный набор переменных под общим именем (переменная записи, на которую вы ссылаетесь, члены).
Я знаю, что это сбило бы с толку многих людей, глубоко привыкших к ООП, но этого недостаточно для того, чтобы говорить, что такие вещи по своей сути злые, если их правильно использовать. Некоторые структуры являются неизменяемыми по своему назначению (это в случае с Python
namedtuple
), но это еще одна парадигма, которую следует рассмотреть.Да, структуры включают в себя много памяти, но при этом не будет больше памяти:
по сравнению с:
Потребление памяти будет, по крайней мере, таким же или даже больше в неизменяемом случае (хотя этот случай будет временным, для текущего стека, в зависимости от языка).
Но, наконец, структуры - это структуры , а не объекты. В POO основным свойством объекта является их идентичность , которая в большинстве случаев не превышает адрес его памяти. Struct обозначает структуру данных (не является надлежащим объектом, и поэтому они не имеют идентичности в любом случае), и данные могут быть изменены. В других языках запись (вместо struct , как в случае с Pascal) является словом и имеет ту же цель: просто переменная записи данных, предназначенная для чтения из файлов, изменения и выгрузки в файлы (то есть основной используйте и во многих языках вы даже можете определить выравнивание данных в записи, хотя это не всегда верно для правильно названных объектов).
Хотите хороший пример? Структуры используются для чтения файлов легко. У Python есть эта библиотека, потому что, поскольку она является объектно-ориентированной и не поддерживает структуры, она должна была реализовать ее другим способом, что несколько уродливо. Языки, реализующие структуры, имеют эту функцию ... встроенную. Попробуйте прочитать заголовок растрового изображения с соответствующей структурой на языках, таких как Pascal или C. Это будет легко (если структура правильно построена и выровнена; в Pascal вы не будете использовать доступ на основе записей, а функции для чтения произвольных двоичных данных). Таким образом, для файлов и прямого (локального) доступа к памяти структуры лучше, чем объекты. На сегодняшний день мы привыкли к JSON и XML и поэтому забываем об использовании двоичных файлов (и, как побочный эффект, об использовании структур). Но да: они существуют и имеют цель.
Они не злые. Просто используйте их для правильной цели.
Если вы думаете с точки зрения молотков, вам следует относиться к шурупам как к гвоздям, так как шурупы сложнее вонзить в стену, и это будет виной винта, и они будут злыми.
источник
Представьте, что у вас есть массив из 1 000 000 структур. Каждая структура, представляющая капитал с такими вещами, как bid_price, offer_price (возможно, десятичные) и так далее, создается C # / VB.
Представьте, что массив создается в блоке памяти, выделенной в неуправляемой куче, так что какой-то другой нативный кодовый поток может одновременно получить доступ к массиву (возможно, некоторый высокопроизводительный код выполняет математику).
Представьте, что код C # / VB прослушивает рыночный поток ценовых изменений, этот код, возможно, должен получить доступ к некоторому элементу массива (в зависимости от того, какую защиту), а затем изменить некоторые ценовые поля.
Представьте, что это делается десятки или даже сотни тысяч раз в секунду.
Что ж, давайте посмотрим правде в глаза, в этом случае мы действительно хотим, чтобы эти структуры были изменяемыми, они должны быть такими, потому что их разделяет какой-то другой нативный код, поэтому создание копий не поможет; они должны быть такими, потому что создание копии примерно 120-байтовой структуры с такими скоростями - безумие, особенно когда обновление может фактически повлиять только на один или два байта.
Хьюго
источник
Когда что-то может быть видоизменено, оно приобретает чувство идентичности.
Поскольку
Person
это изменчиво, более естественно думать об изменении позиции Эрика, чем клонировать Эрика, перемещать клона и уничтожать оригинал . Обе операции успешно изменят содержимоеeric.position
, но одна из них более понятна, чем другая. Аналогично, более интуитивно понятно передать Эрику (в качестве ссылки) методы его модификации. Давать метод клону Эрика почти всегда будет удивительно. Любой, кто хочет мутировать,Person
должен не забыть попросить ссылку,Person
иначе они поступят неправильно.Если вы сделаете тип неизменным, проблема исчезнет; если я не могу изменить
eric
, мне все равно, получу ли яeric
или клонeric
. В более общем случае тип безопасно передавать по значению, если все его наблюдаемое состояние содержится в членах, которые:Если эти условия выполняются, то изменчивый тип значения ведет себя как ссылочный тип, потому что мелкая копия все еще позволит получателю изменять исходные данные.
Интуитивность неизменного
Person
зависит от того, что вы пытаетесь сделать. ЕслиPerson
просто представляет собой набор данных о человеке, в этом нет ничего не интуитивного;Person
переменные действительно представляют абстрактные значения , а не объекты. (В этом случае, вероятно, было бы более уместно переименовать егоPersonData
.) ЕслиPerson
на самом деле моделируется сам человек, идея постоянного создания и перемещения клонов глупа, даже если вы избежали ловушки мышления, которое вы модифицируете оригинал. В этом случае, вероятно, было бы более естественным просто создатьPerson
ссылочный тип (то есть класс.)Конечно, функциональное программирование научило нас, что делать все неизменяемым есть преимущества (никто не может тайно держаться за ссылку на него
eric
и изменять его), но, поскольку это не является идиоматическим в ООП, оно все равно будет непонятным для всех, кто работает с вашим код.источник
foo
юниверсе содержится единственная ссылка на свою цель, и ничто не захватило значение хеш-идентификатора этого объекта, то изменяющее полеfoo.X
семантически эквивалентно указаниюfoo
на новый объект, который аналогичен тому, на который он ранее ссылался, но сX
удерживая желаемое значение. С типами классов, как правило, трудно понять, существует ли несколько ссылок на что-то, но с структурами это легко: их нет.Thing
является изменяемым типом класса, объектThing[]
будет инкапсулировать объектные идентификаторы - хочет ли он того или нет - если только он не может гарантировать, что ни одинThing
в массиве, на который существуют какие-либо внешние ссылки, не будет мутирован. Если кто-то не хочет, чтобы элементы массива инкапсулировали идентичность, обычно необходимо убедиться, что ни один из элементов, на которые он ссылается, никогда не будет мутирован, или что никакие внешние ссылки никогда не будут существовать для любых элементов, которые он содержит [гибридные подходы также могут работать ]. Ни один подход не очень удобен. ЕслиThing
это структура,Thing[]
инкапсулирует только значения.Он не имеет ничего общего со структурами (и не с C #), но в Java могут возникнуть проблемы с изменяемыми объектами, когда они являются, например, ключами в хэш-карте. Если вы измените их после добавления их на карту, и она изменит свой хэш-код , могут произойти злые вещи.
источник
Лично, когда я смотрю на код, мне кажется, что следующее выглядит довольно неуклюже:
data.value.set (data.value.get () + 1);
а не просто
data.value ++; или data.value = data.value + 1;
Инкапсуляция данных полезна при передаче класса, и вы хотите убедиться, что значение изменено контролируемым образом. Однако, когда у вас есть общедоступные функции set и get, которые делают чуть больше, чем устанавливают значение того, что когда-либо передается, как это по сравнению с простой передачей общедоступной структуры данных?
Когда я создаю частную структуру внутри класса, я создал эту структуру, чтобы организовать набор переменных в одну группу. Я хочу иметь возможность изменять эту структуру в рамках класса, а не получать копии этой структуры и создавать новые экземпляры.
Для меня это предотвращает правильное использование структур, используемых для организации открытых переменных, если бы я хотел контроля доступа, я бы использовал класс.
источник
Есть несколько проблем с примером мистера Эрика Липперта. Он придуман, чтобы проиллюстрировать, как копируются структуры и как это может быть проблемой, если вы не будете осторожны. Глядя на пример, я вижу, что это результат плохой привычки программирования, а не проблемы со структурой или классом.
Предполагается, что структура имеет только открытые члены и не требует инкапсуляции. Если это так, то это действительно должен быть тип / класс. Вам действительно не нужны две конструкции, чтобы сказать то же самое.
Если у вас есть класс, содержащий структуру, вы должны вызвать метод в классе, чтобы изменить структуру члена. Это то, что я бы сделал, как хорошую привычку программирования.
Правильная реализация будет выглядеть следующим образом.
Похоже, что это проблема привычки программирования, а не проблема самой структуры. Структуры должны быть изменчивыми, это идея и намерение.
Результат изменений вуаля ведет себя как положено:
1 2 3 Нажмите любую клавишу для продолжения. , ,
источник
Есть много преимуществ и недостатков изменяемых данных. Недостаток в миллион долларов - это алиасинг. Если одно и то же значение используется в нескольких местах, и одно из них меняет его, то оно будет волшебным образом изменено на другие места, которые его используют. Это связано, но не совпадает с условиями гонки.
Преимущество в миллион долларов иногда - модульность. Изменяемое состояние может позволить вам скрыть изменяющуюся информацию от кода, который не должен знать об этом.
Искусство Интерпретатора подробно описывает эти компромиссы и приводит некоторые примеры.
источник
Я не верю, что они злые, если их правильно использовать. Я бы не включил его в свой производственный код, но я бы использовал что-то вроде структурированных блочных тестов, где продолжительность жизни структуры относительно мала.
Используя пример Эрика, возможно, вы хотите создать второй экземпляр этого Эрика, но внести коррективы, так как это характер вашего теста (т.е. дублирование, а затем изменение). Неважно, что случится с первым экземпляром Эрика, если мы просто используем Eric2 для оставшейся части тестового сценария, если только вы не планируете использовать его в качестве тестового сравнения.
Это было бы в основном полезно для тестирования или изменения унаследованного кода, который мелко определяет конкретный объект (точку структур), но наличие неизменяемой структуры предотвращает его использование.
источник
Range<T>
тип с членамиMinimum
иMaximum
полями типаT
и кодаRange<double> myRange = foo.getRange();
, должны быть получены любые гарантии относительно того, чтоMinimum
и чтоMaximum
содержитсяfoo.GetRange();
. Наличие структурыRange
с открытым полем будет ясно, что она не добавит никакого собственного поведения.