Почему изменяемые структуры являются “злыми”?

StackOverflow https://stackoverflow.com/questions/441309

Вопрос

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

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

Это было полезно?

Решение

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

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

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

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

Другие советы

С чего начать ;-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 # не помогает различить доступ к элементу типа значения или ссылочного типа, поэтому я полностью за предпочитаю неизменяемые структуры, которые обеспечивают неизменность , над изменчивыми структурами.

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

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

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

Структурные методы (в отличие от установщиков свойств), которые изменяют " this " являются чем-то злым, только потому, что .net не предоставляет средства, чтобы отличить их от методов, которые этого не делают. Структурировать методы, которые не видоизменяют «это» должен вызываться даже на структурах только для чтения без необходимости защитного копирования. Методы, которые изменяют "это" вообще не должен вызываться в структурах только для чтения. Поскольку .net не хочет запрещать методы struct, которые не изменяют " 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:
     & # 8195; Method1 () : нет (чистое намерение)
     & # 8195; Method2 () : да (чистое намерение)
     & # 8195; Method3 () : да (неопределенное намерение)
     Вопрос 2:
     & # 8195; Method1 () : нет
     & # 8195; Method2 () : нет (если не опасно)
     & # 8195; 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] = Новый RectClass (RectClassArray [321]); RectClassArray [321] .X = 555; " Чтобы знать, что нужно делать, нужно знать гораздо больше о RectClass (например,

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

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

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

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

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

    <код>
    // 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);
        }
    }
    

    <код>

    <Ол>
  • Типы изменяемых значений и массив
  • Предположим, у нас есть массив нашей изменяемой структуры, и мы вызываем метод 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 ++, структуры можно использовать как изменяемые. Просто передайте их с реф, вокруг, и нет ничего, что может пойти не так. Единственная проблема, которую я нахожу, - это ограничения компилятора C # и то, что в некоторых случаях я не могу заставить глупую вещь использовать ссылку на структуру вместо копии (например, когда структура является частью класса C #) ).

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

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

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

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

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

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

    Хьюго

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

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

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

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

    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, но одно из них более интуитивно понятно, чем другое.Аналогично, более интуитивно понятно передавать Eric (в качестве ссылки) для поиска методов его модификации.Предоставление методу клона Эрика почти всегда будет неожиданным.Кто-нибудь хочет мутировать Person необходимо не забыть попросить ссылку на Person или они будут поступать неправильно.

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

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

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

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

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

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

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

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

    а не просто

    data.value ++; или data.value = data.value + 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 2 3 Нажмите любую клавишу, чтобы продолжить...

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

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

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

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

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

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

    Лицензировано под: CC-BY-SA с атрибуция
    Не связан с StackOverflow
    scroll top