Подтвердить что ты не робот

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

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

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

4b9b3361

Ответ 1

Структуры - это типы значений, которые означают, что они копируются, когда они передаются.

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

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

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

Ответ 2

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

Блог Eric Lippert всегда хорош для цитаты:

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

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

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

Ответ 3

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

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

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

Ответ 4

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

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

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

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

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

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

Ответ 5

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

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

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

    Метод1 не может изменять foo и никогда не получает ссылку. Метод2 получает короткоживущую ссылку на 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 (например, поддерживает ли он конструктор копирования, метод копирования и т.д.) И предполагаемое использование массива. Нигде не так просто, как использование структуры.

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

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

Ответ 6

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

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

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

Ответ 7

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

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

// 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 struct, и мы вызываем метод 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);

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

Ответ 8

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

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

Ответ 9

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

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

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

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

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

Уго

Ответ 10

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

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

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

point.x = point.x + 1

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

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

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

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

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

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

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

Ответ 11

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

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

Ответ 12

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

Ответ 13

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

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

а не просто

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

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

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

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

Ответ 14

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

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

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

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

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

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

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

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

Ответ 15

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

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

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

Ответ 16

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

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

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

Ответ 17

Struct это тип значения, как вы делаете его изменчивым? Тип значения копируется, когда вы передаете var в функцию или переназначаете ей новое значение. Вы подвергаете сомнению конфликт с определением типа значения

Ответ 18

В соответствии с Поваренной книжкой С# (Jay Hilyard, et al.) мы получаем предупреждение об использовании структур, поскольку они часто приводят к созданию IL-бокса и команд без бокса. Возможно, это было исправлено в более поздних версиях поколения С# IL, но стигма может сохраняться. Как бы то ни было, в классах С#, когда у вас есть классы, нет необходимости в структурах. И для тех, кто может спорить с этим, я бы сказал... пусть компилятор это выяснит.

Следующие действия могут помочь предотвратить или устранить бокс:

  • Используйте классы вместо структур... Это изменение может значительно повысить производительность.