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


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

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

16 443

16 ответов:

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

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

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

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

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

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

это еще одна причина, почему mutable типы ценностей-это зло. Старайтесь всегда сделайте типы значений неизменяемыми.

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

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С,string, a DateTime и bool, вы можете очень быстро прожечь много памяти. С помощью класса несколько вызывающих объектов могут совместно использовать ссылку на один и тот же экземпляр (ссылки невелики).

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

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

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

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

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

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

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

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

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

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

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

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

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", исчезнут. Method3, в отличие от Method2, получает беспорядочно разделяемую ссылку на foo, и никто не знает, что он может с ней сделать. Он может вообще не изменить foo, он может изменить foo, а затем вернуться, или он может дать ссылку на foo на другой поток, который может мутировать его каким-то произвольным образом в какое-то произвольное будущее время. Единственный способ ограничьте то, что Method3 может сделать с изменяемым объектом класса, переданным в него, было бы инкапсулировать изменяемый объект в оболочку только для чтения, которая является уродливой и громоздкой.

массивы структур предлагают замечательную семантику. Учитывая RectArray [500] типа Rectangle, ясно и очевидно, как, например, Скопировать элемент 123 в элемент 456, а затем через некоторое время установить ширину элемента 123 в 555, не нарушая элемент 456. "RectArray[432] = Ректаррей[321];...; RectArray[123].Ширина= 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] = Новый Класс RectClass(RectClassArray[321]); RectClassArray[321].X = 555; " чтобы знать, что нужно делать, нужно было бы знать намного больше как о RectClass (например, поддерживает ли он конструктор копирования, метод копирования из, так далее.) и предполагаемое использование массива. Нигде не так чисто, как с помощью структуры.

чтобы быть уверенным, к сожалению, нет хорошего способа для любого класса контейнера, кроме массива, чтобы предложить чистую семантику массива struct. Лучшее, что можно сделать, если вы хотите, чтобы коллекция индексировалась, например, строкой, вероятно, будет предлагать общий метод "ActOnItem", который будет принимать строку для индекса, общий параметр и делегат, который будет передаваться по ссылке как общий параметр, так и элемент коллекции. Это позволило бы почти такую же семантику, как структурные массивы, но если vb.net и людей C# можно преследовать, чтобы предложить хороший синтаксис, код будет неуклюжим, даже если это разумная производительность (передача общего параметра позволит использовать статический делегат и избежать необходимости создавать какие-либо временные экземпляры класса).

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

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

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

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

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

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

// 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);

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

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

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

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

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

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

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

Угу

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

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

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

point.x = point.x + 1

против:

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

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

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

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

Они не злые. Просто используйте их по назначению.

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

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

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 и мутировать его), но поскольку это не идиоматично в ООП, это все равно будет неинтуитивно для всех, кто работает с вашим кодом.

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

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

данные.значение.set (данные.значение.get () + 1);

а не просто

данные.значение++; или данные.стоимостные данные.значение + 1 ;

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

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

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

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

  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 Два Три Нажмите любую клавишу, чтобы продолжить . . .

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

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

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

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

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

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