Почему изменчивые структуры «злые»?

485

После обсуждений здесь, посвященных SO, я уже несколько раз читал замечание о том, что изменяемые структуры являются «злыми» (как в ответе на этот вопрос ).

Какова реальная проблема с изменчивостью и структурами в C #?

Дирк Воллмар
источник
12
Утверждать, что изменяемые структуры являются злом, все равно что утверждать, что изменяемые ints, bools и все другие типы значений являются злом. Есть случаи изменчивости и неизменности. Эти случаи зависят от роли, которую играют данные, а не от типа распределения / распределения памяти.
Слипп Д. Томпсон
41
@slipp intи неbool являются изменяемыми ..
Blorgbeard отсутствует
2
.-Синтаксис, заставляющий операции с данными с типами ref и данными с типами значений выглядеть одинаково, даже если они явно отличаются. Это ошибка свойств C #, а не структур - некоторые языки предлагают альтернативный a[V][X] = 3.14синтаксис для мутирования на месте. В C # лучше предлагать методы-мутаторы struct-member, такие как MutateV (Action <ref Vector2> mutator), и использовать их как a.MutateV((v) => { v.X = 3; }) (пример слишком упрощен из-за ограничений, которые C # имеет в отношении refключевого слова, но с некоторыми обходные пути должны быть возможными) .
Слипп Д. Томпсон
2
@Slipp Ну, я думаю, что прямо противоположно такого рода структурам. Почему вы думаете, что структуры, которые уже реализованы в библиотеке .NET, такие как DateTime или TimeSpan (такие похожие), являются неизменяемыми? Может быть, было бы полезно изменить только один член такой структуры var, но это слишком неудобно, приводит к слишком большому количеству проблем. На самом деле вы не правы относительно того, что вычисляет процессор, поскольку C # не компилируется в ассемблер, он компилируется в IL. В IL (при условии, что у нас уже есть именованная переменная x) эта единственная операция состоит из 4 инструкций: ldloc.0(загружает переменную с 0 индексами в ...
Sushi271
1
... тип. Tэто тип. Ref - это просто ключевое слово, которое заставляет переменную передаваться самому методу, а не его копии. Это также имеет смысл для ссылочных типов, поскольку мы можем изменить переменную , т.е. ссылка вне метода будет указывать на другой объект после изменения в методе. Поскольку ref Tэто не тип, а способ передачи параметра метода, вы не можете поместить его в него <>, поскольку в него можно помещать только типы. Так что это просто неверно. Может быть, это было бы удобно, может быть, команда C # могла бы сделать это для какой-то новой версии, но сейчас они работают над некоторыми ...
Sushi271

Ответы:

290

Структуры являются типами значений, что означает, что они копируются при передаче.

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

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

Если вы хотите изменить его, вы должны сделать это сознательно, создав новый экземпляр структуры с измененными данными. (не копия)

trampster
источник
82
«Если ваша структура неизменна, то все копии будут одинаковыми». Нет, это означает, что вы должны сознательно сделать копию, если хотите другое значение. Это означает, что вы не будете замечены при изменении копии, думая, что вы изменяете оригинал.
Лукас
19
@ Лукас Я думаю, что вы говорите о другом виде копии. Я говорю об автоматических копиях, сделанных в результате передачи по значению. Ваша «сознательно сделанная копия» отличается тем, что вы не сделали ее по ошибке, и ее на самом деле не копия это преднамеренные новые моменты, содержащие разные данные.
трампстер
3
Ваше редактирование (16 месяцев спустя) делает это немного яснее. Я по-прежнему придерживаюсь "(неизменяемая структура) означает, что вы не будете замечены при изменении копии, думая, что вы изменяете оригинал", однако.
Лукас
6
@Lucas: Опасность сделать копию структуры, модифицировать ее и каким-то образом думать, что кто-то модифицирует оригинал (когда сам факт написания поля структуры делает самоочевидным тот факт, что кто-то только пишет свою копию) кажется довольно мала по сравнению с опасностью того, что тот, кто держит объект класса в качестве средства хранения содержащейся в нем информации, будет мутировать объект для обновления своей собственной информации и в процессе повредить информацию, находящуюся в каком-либо другом объекте.
суперкат
2
Третий абзац звучит в лучшем случае неправильно или неясно. Если ваша структура неизменна, вы просто не сможете изменять ее поля или поля любых копий. «Если вы хотите изменить это, вы должны ...», что также вводит в заблуждение, вы не можете изменить это никогда , ни сознательно, ни бессознательно. Создание нового экземпляра, для которого данные, которые вы хотите, не имеет ничего общего с исходной копией, кроме наличия той же структуры данных.
Саиб Амини
167

С чего начать ;-p

Блог Эрика Липперта всегда хорош для цитаты:

Это еще одна причина, по которой изменчивые типы значений являются злом. Старайтесь всегда делать типы значений неизменяемыми.

Во-первых, вы, как правило, легко теряете изменения ... например, вынимаете вещи из списка:

Foo foo = list[0];
foo.Name = "abc";

что это изменило? Ничего полезного ...

То же самое со свойствами:

myObj.SomeProperty.Size = 22; // the compiler spots this one

заставляя вас делать:

Bar bar = myObj.SomeProperty;
bar.Size = 22;
myObj.SomeProperty = bar;

менее критично, есть проблема размера; изменчивые объекты обычно имеют несколько свойств; тем не менее, если у вас есть структура с двумя ints, a string, a DateTimeи a bool, вы можете очень быстро прожечь много памяти. С классом несколько абонентов могут совместно использовать ссылку на один и тот же экземпляр (ссылки маленькие).

Марк Гравелл
источник
5
Ну да, но компилятор просто глуп. Не разрешать присваивание членам структуры свойств было ИМХО глупым дизайнерским решением, потому что это разрешено для ++оператора. В этом случае компилятор просто пишет явное присваивание вместо того, чтобы толкать программиста.
Конрад Рудольф
12
@Konrad: myObj.SomeProperty.Size = 22 изменит КОПИЮ myObj.SomeProperty. Компилятор избавляет вас от очевидной ошибки. И это НЕ допускается для ++.
Лукас
@Lucas: Ну, компилятор Mono C #, безусловно, позволяет это - поскольку у меня нет Windows, я не могу проверить компилятор Microsoft.
Конрад Рудольф
6
@ Конрад - с одним меньшим косвенным воздействием он должен работать; это «изменение значения чего-то, что существует только как временное значение в стеке и которое собирается испариться в ничто», что является случаем, который блокируется.
Марк Гравелл
2
@Marc Gravell: В первом фрагменте кода вы получили «Foo» с именем «abc», а другие атрибуты принадлежат List [0], не нарушая List [0]. Если бы Foo был классом, необходимо было бы его клонировать, а затем изменить копию. На мой взгляд, большая проблема, связанная с различием типа значения и класса, заключается в использовании «.» оператор для двух целей. Если бы у меня были мои барабанщики, классы могли бы поддерживать оба "." и "->" для методов и свойств, но нормальная семантика для "." Свойства будут создавать новый экземпляр с измененным соответствующим полем.
суперкат
71

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

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

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

Конрад Рудольф
источник
1
Эрик Липперт говорит, что они ... см. Мой ответ.
Марк Гравелл
42
Как бы я ни уважал Эрика Липперта, он не Бог (или, по крайней мере, пока). Пост в блоге, на который вы ссылаетесь, и ваш пост выше, являются разумными аргументами для того, чтобы сделать структуры неизменяемыми, как само собой разумеющееся, но на самом деле они очень слабы в качестве аргументов для того, чтобы никогда не использовать изменяемые структуры. Этот пост, однако, +1.
Стивен Мартин
2
При разработке на C # время от времени вам обычно требуется изменчивость, особенно в вашей бизнес-модели, где вы хотите, чтобы потоковая передача и т. Д. Работала гладко с существующими решениями. Я написал статью о том, как работать с изменчивыми и неизменяемыми данными, решая большинство проблем, связанных с изменчивостью (я надеюсь): rickyhelgesson.wordpress.com/2012/07/17/…
Рики Хельгессон,
3
@StephenMartin: Структуры, которые инкапсулируют одно значение, часто должны быть неизменяемыми, но структуры, безусловно, являются лучшей средой для инкапсуляции фиксированных наборов независимых, но связанных переменных (таких как координаты X и Y точки), которые не имеют «идентичности» как группа. Структуры, которые используются для этой цели, обычно должны представлять свои переменные как открытые поля. Я бы посчитал, что для таких целей более целесообразно использовать класс, чем структуру, а не просто структуру. Неизменяемые классы часто менее эффективны, а изменяемые классы часто имеют ужасную семантику.
суперкат
2
@StephenMartin: рассмотрим, например, метод или свойство, которое должно возвращать шесть floatкомпонентов графического преобразования. Если такой метод возвращает структуру открытого поля с шестью компонентами, очевидно, что изменение полей структуры не изменит графический объект, от которого она была получена. Если такой метод возвращает объект изменяемого класса, возможно, изменение его свойств изменит базовый графический объект, а может и нет - никто не знает.
суперкат
48

Изменчивые структуры не являются злом.

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

Я бы не назвал использование неизменяемой структуры в этих совершенно правильных сценариях использования «злом».

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

Однако вместо простой маркировки неизменяемых структур как «злых» я бы посоветовал принять язык и отстаивать более полезное и конструктивное правило.

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

JE42
источник
8
Я также хотел бы сказать, что если кто-то хочет закрепить фиксированный набор переменных вместе с клейкой лентой, чтобы их значения могли обрабатываться или храниться отдельно или как единое целое, имеет больше смысла просить компилятор закрепить фиксированный набор переменные вместе (т.е. объявлять structс открытыми полями), чем определять класс, который можно использовать, неуклюже, для достижения тех же целей, или добавить кучу мусора в структуру, чтобы он эмулировал такой класс (вместо того, чтобы иметь его) вести себя как набор переменных, склеенных клейкой лентой, что и
нужно
41

Структуры с открытыми изменяемыми полями или свойствами не являются злом.

Методы структуры (в отличие от установщиков свойств), которые изменяют «это», являются чем-то злым, только потому, что .net не предоставляет средства отличить их от методов, которые этого не делают. Методы структуры, которые не изменяют «this», должны вызываться даже для структур только для чтения без необходимости защитного копирования. Методы, которые изменяют "this", вообще не должны вызываться в структурах только для чтения. Поскольку .net не хочет запрещать использование структурных методов, которые не изменяют «this», для структур, доступных только для чтения, но не хочет разрешать изменение структур, доступных только для чтения, он защищенно копирует структуры в режиме чтения. только контексты, возможно получая худшее из обоих миров.

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

struct PointyStruct {public int x, y, z;};
class PointyClass {public int x, y, z;};

void Method1 (PointyStruct foo);
void Method2 (ref PointyStruct foo);
void Method3 (PointyClass foo);

Для каждого метода ответьте на следующие вопросы:

  1. Предполагая, что метод не использует «небезопасный» код, может ли он изменить foo?
  2. Если до вызова метода не существует внешних ссылок на 'foo', может ли существовать внешняя ссылка после?

ответы:

Вопрос 1::
Method1()нет (ясное намерение)
Method2() : да (ясное намерение)
Method3() : да (неопределенное намерение)
Вопрос 2
Method1():: нет
Method2(): нет (если небезопасно)
Method3() : да

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

Supercat
источник
1
@ Рон Уорхолик: не очевидно, что SomeRect представляет собой прямоугольник. Это может быть какой-то другой тип, который может быть неявным образом приведен к типу из Rectangle. Хотя единственный системный тип, который может быть неявным образом приведен к типу из Rectangle, - это RectangleF, и компилятор закричит, если попытаться передать поля RectangleF конструктору Rectangle (поскольку первые - Single, а последние - Integer) , могут быть определенные пользователем структуры, которые допускают такие неявные типы типов. Кстати, первое утверждение будет одинаково хорошо работать независимо от того, был ли SomeRect прямоугольником или RectangleF.
суперкат
Все, что вы показали, это то, что в надуманном примере вы считаете, что один метод более понятен. Если мы возьмем ваш пример с собой, Rectangleя легко смогу придумать общую ситуацию, когда вы будете вести себя крайне неясно . Учтите, что WinForms реализует изменяемый Rectangleтип, используемый в Boundsсвойстве формы . Если я хочу изменить границы, я бы хотел использовать ваш красивый синтаксис: form.Bounds.X = 10;однако это точно ничего не меняет в форме (и выдает прекрасную ошибку, сообщающую вам о таком). Несоответствие - проклятие программирования и именно поэтому нужна неизменность.
Рон Уорхолик
3
@ Рон Уорхолик: Кстати, я бы хотел сказать "form.Bounds.X = 10;" и это просто работает, но система не предоставляет никакого чистого способа сделать это. Соглашение о представлении свойств типа значения в качестве методов, принимающих обратные вызовы, может предложить гораздо более чистый, эффективный и достоверно корректный код, чем любой подход, использующий классы.
суперкат
4
Этот ответ гораздо более проницательный, чем несколько ответов с наибольшим количеством голосов. Это своего рода абсурдно, что аргумент против изменяемых типов значений опирается на понятие «того, что вы ожидаете», когда вы смешиваете псевдонимы и мутации. В любом случае , это ужасная вещь !
Имон Нербонн
2
@supercat: Кто знает, может быть, эта функция ref-return, о которой они говорят для C # 7, могла бы охватить эту базу (на самом деле я не рассматривал ее подробно, но внешне это звучит похоже).
Имон Нербонн
24

Типы значений в основном представляют собой неизменные понятия. Fx, не имеет смысла иметь математическое значение, такое как целое число, вектор и т. Д., А затем иметь возможность изменять его. Это было бы как переопределение значения стоимости. Вместо изменения типа значения имеет смысл назначить другое уникальное значение. Подумайте о том, что типы значений сравниваются путем сравнения всех значений его свойств. Дело в том, что если свойства одинаковы, то это одно и то же универсальное представление этого значения.

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

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

Мортен Кристиансен
источник
Ну, они должны представлять собой непреложные понятия, по крайней мере ;-p
Марк
3
Ну, я полагаю, что они могли бы сделать System.Drawing.Point неизменным, но это было бы серьезной ошибкой дизайна IMHO. Я думаю, что точки на самом деле являются типичными типами значений, и они изменчивы. И они не доставляют никаких проблем никому, кроме начинающих программистов.
Стивен Мартин
3
В принципе, я думаю, что пункты также должны быть неизменными, но если это делает тип более сложным или менее элегантным в использовании, то, конечно, это тоже нужно учитывать. Нет смысла иметь кодовые конструкции, которые поддерживают лучшие принципы, если никто не хочет их использовать;)
Мортен Кристиансен
3
Типы значений полезны для представления простых неизменяемых концепций, но структуры с открытыми полями являются наилучшими типами для хранения или передачи небольших фиксированных наборов связанных, но независимых значений (таких как координаты точки). Место хранения такого типа значения инкапсулирует значения его полей и ничего больше. В отличие от этого, место хранения изменяемого ссылочного типа может использоваться с целью хранения состояния изменяемого объекта, но также инкапсулирует идентичность всех других ссылок во всем юниверсе, которые существуют для того же объекта.
суперкат
4
«Типы значений в основном представляют собой неизменные понятия». Нет, они не Одним из самых старых и наиболее полезных применений переменной со значением типа является intитератор, который был бы совершенно бесполезным, если бы он был неизменным. Я думаю, что вы объединяете «реализации типов значений компилятора / среды выполнения» с «переменными, типизированными для типа значения» - последнее, безусловно, изменяемо для любого из возможных значений.
Слипп Д. Томпсон
19

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

Типы неизменяемых значений и поля только для чтения

    // Simple mutable structure. 
    // Method IncrementI mutates current state.
    struct Mutable
    {
        public Mutable(int i) : this() 
        {
            I = i;
        }

        public void IncrementI() { I++; }

        public int I { get; private set; }
    }

    // Simple class that contains Mutable structure
    // as readonly field
    class SomeClass 
    {
        public readonly Mutable mutable = new Mutable(5);
    }

    // Simple class that contains Mutable structure
    // as ordinary (non-readonly) field
    class AnotherClass 
    {
        public Mutable mutable = new Mutable(5);
    }

    class Program
    {
        void Main()
        {
            // Case 1. Mutable readonly field
            var someClass = new SomeClass();
            someClass.mutable.IncrementI();
            // still 5, not 6, because SomeClass.mutable field is readonly
            // and compiler creates temporary copy every time when you trying to
            // access this field
            Console.WriteLine(someClass.mutable.I);

            // Case 2. Mutable ordinary field
            var anotherClass = new AnotherClass();
            anotherClass.mutable.IncrementI();

            // Prints 6, because AnotherClass.mutable field is not readonly
            Console.WriteLine(anotherClass.mutable.I);
        }
    }

Типы изменяемых значений и массив

Предположим, у нас есть массив нашей Mutableструктуры, и мы вызываем IncrementIметод для первого элемента этого массива. Какое поведение вы ожидаете от этого звонка? Должно ли это изменить значение массива или только копию?

    Mutable[] arrayOfMutables = new Mutable[1];
    arrayOfMutables[0] = new Mutable(5);

    // Now we actually accessing reference to the first element
    // without making any additional copy
    arrayOfMutables[0].IncrementI();

    // Prints 6!!
    Console.WriteLine(arrayOfMutables[0].I);

    // Every array implements IList<T> interface
    IList<Mutable> listOfMutables = arrayOfMutables;

    // But accessing values through this interface lead
    // to different behavior: IList indexer returns a copy
    // instead of an managed reference
    listOfMutables[0].IncrementI(); // Should change I to 7

    // Nope! we still have 6, because previous line of code
    // mutate a copy instead of a list value
    Console.WriteLine(listOfMutables[0].I);

Таким образом, изменчивые структуры не являются злом, если вы и остальная часть команды четко понимаете, что вы делаете. Но есть слишком много угловых случаев, когда поведение программы будет отличаться от ожидаемого, что может привести к едва заметным трудным для понимания и трудным для понимания ошибкам.

Сергей Тепляков
источник
5
Что должно произойти, если бы языки .net имели немного лучшую поддержку типов значений, было бы запрещено использовать методы struct для изменения этого выражения, если только они явно не объявлены как таковые, а методы, объявленные таким образом, должны быть запрещены только для чтения. контексты. Массивы изменяемых структур предлагают полезную семантику, которая не может быть эффективно достигнута другими способами.
суперкат
2
Это хорошие примеры очень тонкой проблемы, возникающей из-за изменчивых структур. Я бы не ожидал такого поведения. Почему массив дает вам ссылку, а интерфейс дает вам значение? Я бы подумал, кроме значений «все время» (что я действительно и ожидал), что это будет, по крайней мере, наоборот: интерфейс, дающий ссылки; массивы, дающие значения ...
Dave Cousineau
@Sahuagin: К сожалению, не существует стандартного механизма, с помощью которого интерфейс может предоставлять ссылку. Существуют способы, с помощью которых .net может позволить, чтобы такие вещи выполнялись безопасно и с пользой (например, путем определения специальной структуры «ArrayRef <T>», содержащей a T[]и целочисленный индекс, и обеспечения того, что доступ к свойству типа ArrayRef<T>будет интерпретироваться как доступ к соответствующему элементу массива) [если класс хочет предоставить объект ArrayRef<T>для какой-либо другой цели, он может предоставить метод - в отличие от свойства - для его извлечения]. К сожалению, таких положений не существует.
суперкат
2
О боже ... это делает изменчивые структуры чертовски злыми!
Nawfal
1
Мне нравится этот ответ, потому что он содержит очень ценную информацию, которая не очевидна. Но на самом деле это не аргумент против изменчивых структур, как утверждают некоторые. Да, то, что мы видим здесь, это «яма отчаяния», как сказал бы Эрик, но источником этого отчаяния является не изменчивость. Источником отчаяния являются методы самовосстановления структур . (Что касается того, почему массивы и списки ведут себя по-разному, то это потому, что один в основном является оператором, который вычисляет адрес памяти, а другой является свойством. В общем, все становится ясно, когда вы понимаете, что «ссылка» является значением адреса .)
AnorZaken
18

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

Таким образом, изменчивые структуры не являются злом, C # сделал их злыми. Я все время использую изменяемые структуры в C ++, и они очень удобны и интуитивно понятны. Напротив, C # заставил меня полностью отказаться от структур как членов классов из-за того, как они обрабатывают объекты. Их удобство стоило нам нашего.

ThunderGr
источник
Наличие полей классов структурных типов часто может быть очень полезным шаблоном, хотя по общему признанию существуют некоторые ограничения. Производительность будет ухудшаться, если кто-то использует свойства, а не поля или использует readonly, но если кто-то избегает делать это, классовые поля структурных типов просто хороши. Единственное действительно фундаментальное ограничение структур состоит в том, что поле структуры типа изменяемого класса, такого как, int[]может инкапсулировать идентичность или неизменный набор значений, но не может использоваться для инкапсуляции изменяемых значений без инкапсуляции нежелательной идентичности.
суперкат
13

Если вы будете придерживаться того, для чего предназначены структуры (в C #, Visual Basic 6, Pascal / Delphi, типе структуры C ++ (или классах), когда они не используются в качестве указателей), вы обнаружите, что структура не больше, чем составная переменная , Это означает: вы будете рассматривать их как упакованный набор переменных под общим именем (переменная записи, на которую вы ссылаетесь, члены).

Я знаю, что это сбило бы с толку многих людей, глубоко привыкших к ООП, но этого недостаточно для того, чтобы говорить, что такие вещи по своей сути злые, если их правильно использовать. Некоторые структуры являются неизменяемыми по своему назначению (это в случае с Python namedtuple), но это еще одна парадигма, которую следует рассмотреть.

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

point.x = point.x + 1

по сравнению с:

point = Point(point.x + 1, point.y)

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

Но, наконец, структуры - это структуры , а не объекты. В POO основным свойством объекта является их идентичность , которая в большинстве случаев не превышает адрес его памяти. Struct обозначает структуру данных (не является надлежащим объектом, и поэтому они не имеют идентичности в любом случае), и данные могут быть изменены. В других языках запись (вместо struct , как в случае с Pascal) является словом и имеет ту же цель: просто переменная записи данных, предназначенная для чтения из файлов, изменения и выгрузки в файлы (то есть основной используйте и во многих языках вы даже можете определить выравнивание данных в записи, хотя это не всегда верно для правильно названных объектов).

Хотите хороший пример? Структуры используются для чтения файлов легко. У Python есть эта библиотека, потому что, поскольку она является объектно-ориентированной и не поддерживает структуры, она должна была реализовать ее другим способом, что несколько уродливо. Языки, реализующие структуры, имеют эту функцию ... встроенную. Попробуйте прочитать заголовок растрового изображения с соответствующей структурой на языках, таких как Pascal или C. Это будет легко (если структура правильно построена и выровнена; в Pascal вы не будете использовать доступ на основе записей, а функции для чтения произвольных двоичных данных). Таким образом, для файлов и прямого (локального) доступа к памяти структуры лучше, чем объекты. На сегодняшний день мы привыкли к JSON и XML и поэтому забываем об использовании двоичных файлов (и, как побочный эффект, об использовании структур). Но да: они существуют и имеют цель.

Они не злые. Просто используйте их для правильной цели.

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

Луис Масуэлли
источник
12

Представьте, что у вас есть массив из 1 000 000 структур. Каждая структура, представляющая капитал с такими вещами, как bid_price, offer_price (возможно, десятичные) и так далее, создается C # / VB.

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

Представьте, что код C # / VB прослушивает рыночный поток ценовых изменений, этот код, возможно, должен получить доступ к некоторому элементу массива (в зависимости от того, какую защиту), а затем изменить некоторые ценовые поля.

Представьте, что это делается десятки или даже сотни тысяч раз в секунду.

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

Хьюго

Хьюго
источник
3
Правда, но в этом случае причина использования структуры заключается в том, что это накладывается на дизайн приложения внешними ограничениями (такими, как использование нативного кода). Все остальное, что вы описываете об этих объектах, предполагает, что они должны быть классами в C # или VB.NET.
Джон Ханна
6
Я не уверен, почему некоторые люди думают, что вещи должны быть объектами класса. Если все слоты массива заполнены ссылками на отдельные экземпляры, использование типа класса добавит дополнительные двенадцать или двадцать четыре байта к требованию памяти, и последовательный доступ к массиву ссылок на объекты класса может быть намного медленнее, чем последовательный доступ на массив структур.
суперкат
9

Когда что-то может быть видоизменено, оно приобретает чувство идентичности.

struct Person {
    public string name; // mutable
    public Point position = new Point(0, 0); // mutable

    public Person(string name, Point position) { ... }
}

Person eric = new Person("Eric Lippert", new Point(4, 2));

Поскольку Personэто изменчиво, более естественно думать об изменении позиции Эрика, чем клонировать Эрика, перемещать клона и уничтожать оригинал . Обе операции успешно изменят содержимое eric.position, но одна из них более понятна, чем другая. Аналогично, более интуитивно понятно передать Эрику (в качестве ссылки) методы его модификации. Давать метод клону Эрика почти всегда будет удивительно. Любой, кто хочет мутировать, Personдолжен не забыть попросить ссылку, Personиначе они поступят неправильно.

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

  • неизменный
  • ссылочные типы
  • безопасно передавать по значению

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

Интуитивность неизменного Personзависит от того, что вы пытаетесь сделать. Если Personпросто представляет собой набор данных о человеке, в этом нет ничего не интуитивного; Personпеременные действительно представляют абстрактные значения , а не объекты. (В этом случае, вероятно, было бы более уместно переименовать его PersonData.) Если Personна самом деле моделируется сам человек, идея постоянного создания и перемещения клонов глупа, даже если вы избежали ловушки мышления, которое вы модифицируете оригинал. В этом случае, вероятно, было бы более естественным просто создать Personссылочный тип (то есть класс.)

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

Doval
источник
3
Ваша точка зрения на личность хорошая; возможно, стоит отметить, что идентичность актуальна только тогда, когда существует несколько ссылок на что-то. Если где-то в fooюниверсе содержится единственная ссылка на свою цель, и ничто не захватило значение хеш-идентификатора этого объекта, то изменяющее поле foo.Xсемантически эквивалентно указанию fooна новый объект, который аналогичен тому, на который он ранее ссылался, но с Xудерживая желаемое значение. С типами классов, как правило, трудно понять, существует ли несколько ссылок на что-то, но с структурами это легко: их нет.
суперкат
1
Если Thingявляется изменяемым типом класса, объект Thing[] будет инкапсулировать объектные идентификаторы - хочет ли он того или нет - если только он не может гарантировать, что ни один Thingв массиве, на который существуют какие-либо внешние ссылки, не будет мутирован. Если кто-то не хочет, чтобы элементы массива инкапсулировали идентичность, обычно необходимо убедиться, что ни один из элементов, на которые он ссылается, никогда не будет мутирован, или что никакие внешние ссылки никогда не будут существовать для любых элементов, которые он содержит [гибридные подходы также могут работать ]. Ни один подход не очень удобен. Если Thingэто структура, Thing[]инкапсулирует только значения.
суперкат
Для объектов их идентичность происходит от их местоположения. Экземпляры ссылочных типов имеют свою идентичность благодаря их расположению в памяти, и вы передаете только их идентичность (ссылку), а не свои данные, в то время как типы значений имеют свою идентичность во внешнем месте, где они хранятся. Идентичность вашего типа значения Eric исходит только из переменной, в которой он хранится. Если вы передадите его, он потеряет свою личность.
IllidanS4 хочет вернуть Монику
6

Он не имеет ничего общего со структурами (и не с C #), но в Java могут возникнуть проблемы с изменяемыми объектами, когда они являются, например, ключами в хэш-карте. Если вы измените их после добавления их на карту, и она изменит свой хэш-код , могут произойти злые вещи.

Бомбы
источник
3
Это верно, если вы используете класс в качестве ключа на карте тоже.
Марк Гравелл
6

Лично, когда я смотрю на код, мне кажется, что следующее выглядит довольно неуклюже:

data.value.set (data.value.get () + 1);

а не просто

data.value ++; или data.value = data.value + 1;

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

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

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

Майк
источник
1
Прямо в точку! Структуры являются организационными единицами без ограничений контроля доступа! К сожалению, C # сделал их бесполезными для этой цели!
ThunderGr
это полностью упускает суть, поскольку оба ваших примера показывают изменчивые структуры.
Виджет
C # сделал их бесполезными для этой цели, потому что это не цель структур
Луис Фелипе
6

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

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

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

Правильная реализация будет выглядеть следующим образом.

struct Mutable {
public int x;
}

class Test {
    private Mutable m = new Mutable();
    public int mutate()
    { 
        m.x = m.x + 1;
        return m.x;
    }
  }
  static void Main(string[] args) {
        Test t = new Test();
        System.Console.WriteLine(t.mutate());
        System.Console.WriteLine(t.mutate());
        System.Console.WriteLine(t.mutate());
    }

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

Результат изменений вуаля ведет себя как положено:

1 2 3 Нажмите любую клавишу для продолжения. , ,

Рамеш Кадамби
источник
4
Нет ничего плохого в проектировании небольших непрозрачных структур, которые ведут себя как неизменяемые объекты класса; руководящие принципы MSDN разумны, когда кто-то пытается создать нечто, похожее на объект . Структуры уместны в некоторых случаях, когда нужны легкие вещи, которые ведут себя как объекты, и в случаях, когда требуется куча переменных, склеенных клейкой лентой. По некоторым причинам, однако, многие люди не понимают, что структуры имеют два различных использования, и что руководящие принципы, подходящие для одного, не подходят для другого.
суперкат
5

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

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

Искусство Интерпретатора подробно описывает эти компромиссы и приводит некоторые примеры.

Андру Лувизи
источник
структуры не должны быть псевдонимами в C #. Каждое присвоение структуры является копией.
рекурсивный
@recursive: В некоторых случаях это главное преимущество изменяемых структур, и это заставляет меня усомниться в том, что структуры не должны быть изменяемыми. Тот факт, что компиляторы иногда неявно копируют структуры, не уменьшает полезности изменяемых структур.
суперкат
1

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

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

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

ShaunnyBwoy
источник
На мой взгляд, структура в своей основе - это набор переменных, склеенных клейкой лентой. В .NET структура может претендовать на то, чтобы быть чем-то иным, чем набором переменных, склеенных вместе с клейкой лентой, и я хотел бы предложить, чтобы, когда это возможно, тип, который будет представлять собой нечто иное, чем набор переменных, слипшихся вместе с клейкой лентой должен вести себя как единый объект (что для структуры означало бы неизменность), но иногда полезно соединить кучу переменных вместе с клейкой лентой. Даже в
рабочем
... который явно не имеет семантики за исключением того, что «каждое поле содержит последнюю записанную вещь», что толкает всю семантику в код, который использует структуру, чем пытаться заставить структуру делать больше. Например, учитывая Range<T>тип с членами Minimumи Maximumполями типа Tи кода Range<double> myRange = foo.getRange();, должны быть получены любые гарантии относительно того, что Minimumи что Maximumсодержится foo.GetRange();. Наличие структуры Rangeс открытым полем будет ясно, что она не добавит никакого собственного поведения.
суперкат