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

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

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

16 ответов

Решение

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ответы:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Хьюго

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

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

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

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

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

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

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

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

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

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

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

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

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

а не просто

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

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

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

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

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

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

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

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