Почему изменчивые структуры «злые»? - PullRequest
463 голосов
/ 14 января 2009

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

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

Ответы [ 16 ]

278 голосов
/ 14 января 2009

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

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

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

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

164 голосов
/ 14 января 2009

С чего начать; -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;

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

68 голосов
/ 14 января 2009

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

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

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

41 голосов
/ 04 июня 2013

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

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

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

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

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

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

36 голосов
/ 29 сентября 2011

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

Методы структуры (в отличие от установщиков свойств), которые видоизменяют «это», являются несколько злыми, только потому, что .net не предоставляет средства отличить их от методов, которые этого не делают. Методы структуры, которые не изменяют «это», должны вызываться даже в структурах только для чтения без необходимости защитного копирования. Методы, которые изменяют "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', может ли существовать внешняя ссылка после?

Ответы:

Method1 не может изменить foo и никогда не получает ссылку. Method2 получает кратковременную ссылку на foo, которую он может использовать для изменения полей foo любое количество раз в любом порядке, пока не вернется, но он не может сохранить эту ссылку. Перед возвратом Method2, если он не использует небезопасный код, все копии, которые могли быть сделаны из его ссылки 'foo', исчезнут. Метод 3, в отличие от метода 2, получает беспорядочную ссылку на foo, и никто не знает, что он может с этим делать. Это может вообще не изменить foo, это может изменить foo и затем вернуться, или это может дать ссылку на foo другому потоку, который может каким-то образом изменить его в некоторый произвольный момент времени в будущем. Единственный способ ограничить то, что Method3 может делать с передаваемым в него объектом изменяемого класса, - это заключить изменяемый объект в оболочку, доступную только для чтения, что выглядит уродливо и громоздко.

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

22 голосов
/ 14 января 2009

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

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

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

17 голосов
/ 14 июля 2011

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

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

</p> <pre><code>// 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); } }

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

Предположим, у нас есть массив нашей изменяемой структуры, и мы вызываем метод 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);

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

16 голосов
/ 18 октября 2013

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

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

11 голосов
/ 02 декабря 2015

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

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

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

point.x = point.x + 1

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

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

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

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

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

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

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

11 голосов
/ 22 марта 2010

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

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

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

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

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

Hugo

...