Почему числовые типы С# неизменяемы?

Почему ints и doubles неизменяемы? Какова цель возврата нового объекта каждый раз, когда вы хотите изменить значение?

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


person Nobody    schedule 20.10.2010    source источник
comment
Этими вопросами вы открываете огромные банки с червями. Я рекомендую дважды подумать, прежде чем пытаться внедрить проверку данных в сами данные. Проверка имеет больше смысла на более высоком уровне абстракции, чем базовые типы данных. Например, вам может понадобиться, чтобы одна часть диапазона данных была связана значением другой части данных.   -  person Merlyn Morgan-Graham    schedule 21.10.2010
comment
@Merlyn: Как насчет данных с неявными границами, таких как длина [0, ∞), угол [0, 360) или результат броска костей [1,6]? Я, конечно, не хочу быть ростом -180 см.   -  person Seth    schedule 21.10.2010
comment
@Seth: это особые случаи, которые не определяют нашу архитектуру. Является ли значение 361 или -1 недопустимым, полностью зависит от того, как они интерпретируются, а не от значений самих по себе.   -  person Steven Sudit    schedule 21.10.2010
comment
@Seth: ограничение по умолчанию, привязанное к типу, может быть полезным (например, 6-гранный кубик), но очень часто вам нужно настроить это ограничение. Единицы - связанная проблема. Лучшим подходом является использование системы решения/отображения ограничений или среды программирования на основе контрактов (например, spec#).   -  person Merlyn Morgan-Graham    schedule 21.10.2010
comment
вам нужно послушать дискуссию дяди Боба на DotNetRocks о «будущем ООП»   -  person lawphotog    schedule 15.04.2016


Ответы (6)


Во-первых:

Какова цель возврата нового объекта каждый раз, когда вы хотите изменить значение?

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

Во-вторых: вот очень простой пример того, почему числа неизменны:

5.Increase(1);
Console.WriteLine(5); // What should happen here?

Правда, это надуманный пример. Итак, давайте рассмотрим еще пару интересных идей.

Изменяемый ссылочный тип

Во-первых, вот что: что, если бы Integer был изменяемым ссылочным типом?

class Integer
{
    public int Value;
}

Тогда у нас может быть такой код:

class Something
{
    public Integer Integer { get; set; }
}

А также:

Integer x = new Integer { Value = 10 };

Something t1 = new Something();
t1.Integer = x;

Something t2 = new Something();
t2.Integer = t1.Integer;

t1.Integer.Value += 1;

Console.WriteLine(t2.Integer.Value); // Would output 11

Кажется, это противоречит интуиции: строка t2.Integer = t1.Integer просто скопирует значение (на самом деле так и есть, но это «значение» на самом деле является ссылкой) и, таким образом, t2.Integer останется независимым от t1.Integer.

Изменяемый тип значения

Конечно, к этому можно подойти и по-другому, сохранив Integer в качестве типа значения, но сохранив его изменчивость:

struct Integer
{
    public int Value;

    // just for kicks
    public static implicit operator Integer(int value)
    {
        return new Integer { Value = value };
    }
}

Но теперь допустим, что мы делаем это:

Integer x = 10;

Something t = new Something();
t.Integer = x;

t.Integer.Value += 1; // This actually won't compile; but if it did,
                      // it would be modifying a copy of t.Integer, leaving
                      // the actual value at t.Integer unchanged.

Console.WriteLine(t.Integer.Value); // would still output 10

По сути, неизменяемость значений — это очень интуитивно понятная вещь. Обратное очень неинтуитивно.

Я думаю, что это субъективно, хотя, если честно ;)

person Dan Tao    schedule 20.10.2010
comment
Хорошие улучшения в вашем последнем редактировании. Если бы я мог проголосовать во второй раз (в так называемом чикагском стиле), я бы это сделал. - person Steven Sudit; 21.10.2010
comment
Мне приходит в голову, что в Java есть примитив int, который не является объектом, а также класс-оболочка примитива Integer. Это означает, что ваши надуманные примеры на самом деле не так уж надуманы. :-) - person Steven Sudit; 21.10.2010
comment
Рассмотрим реальный тип структуры, такой как Rectangle. Что понятнее: someRect.Width *= 2; или someRect = someRect.withNewWidth(someRect.Width*2); Жаль, что .net предоставляет такую ​​слабую поддержку изменяемых свойств типа значения, но часто семантика изменяемого типа значения намного чище, чем что-либо еще. - person supercat; 21.10.2011
comment
@supercat: я на полпути к этому. Я, конечно, согласен с тем, что изменяемые типы значений не так уж плохи сами по себе. На самом деле я нахожу им применение довольно часто. Прискорбный факт, однако, заключается в том, что они могут быть сложными даже для умных разработчиков. Возьмем ваш пример: someRect.Width *= 2; действительно кажется красивым и ясным; но затем сделайте это свойством, и вдруг код перестанет работать: someObject.Rectangle.Width *= 2; Конечно, последний не скомпилируется; но, к сожалению, это было бы (при условии, что метод существует): someObject.Rectangle.Scale(2); Как и во всем, есть аргументы с обеих сторон. - person Dan Tao; 21.10.2011
comment
@Dan Tao: я считаю, что .net имеет несколько недостатков в обработке типов значений: (1) нет параметров const ref или средств определения того, какие методы структуры изменяют это; (2) отсутствие поддержки каких-либо свойств, кроме доступа чтения, записи или пары (чтение-доступ/запись-доступ). Я думаю, что все проблемы с изменяемыми структурами сводятся к этим двум вещам. Учитывая № 1, кстати, я бы согласился с Эриком Липпертом, что структуры почти никогда не должны изменять это в своих методах. - person supercat; 21.10.2011
comment
@Dan Tao: если бы метод масштабирования был статическим, Rectangle.Scale(ref theRectangle, double Amount); Я не думаю, что что-то скомпилируется, чего не должно. Если бы метод расширения мог иметь такую ​​сигнатуру, его можно было бы использовать как обычный метод, когда это необходимо, но отказываться от компиляции, когда это уместно. Мое мнение о том, что изменяемые типы значений являются злом, заключается в том, что самоизменяющиеся типы являются злом в контексте .net, учитывая, что такие вещи, как упаковка, которую структура не может предотвратить, не предполагают самоизменения, но в остальном они re не зло - просто ограничено в некоторых контекстах. - person supercat; 21.10.2011
comment
@Dan Tao: Если бы у меня были свои барабанщики, свойство типа значения типа T могло бы быть методом по крайней мере с одним открытым универсальным типом U [V, W...], который принимал бы метод, имеющий ref параметры типа T и U [V, W...] и будет вызывать его с элементом типа T вместе с переданным в U [V, W...]. someObject.theRect.x=someInteger преобразуется в someObject.theRect__invoker‹Integer›(ref someInteger, (ref theRect, ref param1)=›{theRect.x = param1;}. Передача типов значений по ссылке очень эффективна. Одна проблема заключается в том, что количество необходимых параметров ref будет варьироваться. - person supercat; 21.10.2011

Целочисленные переменные являются изменяемыми. Однако целочисленные литералы являются константами, а значит, неизменяемыми.

int i = 0;

// Mutation coming!
i += 3;

// The following line will not compile.
3 += 7;

Целочисленное поле можно сделать неизменяемым, используя readonly. Точно так же целочисленное свойство может быть доступно только для чтения.

person Steven Sudit    schedule 20.10.2010
comment
Спасибо за минус. Теперь я был бы признателен, если бы вы предложили объяснение, чтобы я мог учиться на своей ошибке. - person Steven Sudit; 21.10.2010
comment
@Robert: Не сказал, что это так. :-) - person Steven Sudit; 21.10.2010
comment
Было бы здорово, если бы кто-нибудь указал на ошибку. В противном случае я мог бы провести остаток своей жизни, воображая, что здесь нет ошибки. - person Steven Sudit; 21.10.2010
comment
Учитывая молчание и положительные голоса, я подозреваю, что первоначальный отрицательный голос мог быть ошибочным. Если это не так, я открыт для исправления. - person Steven Sudit; 21.10.2010
comment
Аргумент не является 3 неизменяемым... это неизменное целое число. Огромная разница. i += 3 - это то же самое, что и i = i + 3... оно по-прежнему неизменно. неизменяемый объект — это объект, состояние которого нельзя изменить после его создания. Вы не редактируете i НА МЕСТЕ. Вы редактируете его и возвращаете обратно в новый i. Так же, как привет + мир! НИЧЕГО не означает без s = ... с левой стороны. - person WernerCD; 21.10.2010
comment
@WernerCD: Спасибо за ваш ответ. Чтобы что-то было неизменным, оно должно быть инициализировано в каком-то состоянии, которое никогда не может измениться. Это явно относится к неизменяемым классам, таким как System.String, поскольку существует различие между экземпляром и его ссылками. Для такой структуры, как System.Windows.Point, существует различие между заменой всего объекта и изменением только его части, поэтому предотвращение последнего является типом неизменяемости. Однако System.Int32 — это значение без частей, поэтому невозможно провести различие между полной заменой и модификацией. - person Steven Sudit; 21.10.2010
comment
Если вы объявляете поле int в классе, оно полностью изменчиво, поскольку его можно изменить по желанию. Вы можете использовать не только семантику замены, например _x = 7;, но и семантику модификации, например _x += 7;. С другой стороны, если вы сделаете это readonly, то никакое изменение невозможно. Сравните это, скажем, с System.Text.StringBuilder, для которого Append возможно, даже если поле равно readonly, а замена - нет. - person Steven Sudit; 21.10.2010
comment
Когда есть различие между экземпляром и его ссылками или целым и его частями, тогда мы можем говорить о неизменности. Но различие нарушается для типов значений с самозначением, таких как примитивные числовые значения. Невозможно, даже в принципе, ответить на вопрос, изменяем ли мы целое число или заменяем его. Следовательно, вопреки тому, что вы сказали, целые числа не являются неизменными в каком-либо значимом смысле, хотя их можно сделать неизменными с помощью readonly и const. - person Steven Sudit; 21.10.2010
comment
@StevenSudit Я думаю, что Immutable не означает, что переменная может быть изменена или нет. Это означает, создаете ли вы новую копию в памяти или просто изменяете значение исходного адреса. А когда вы вызываете int a=5; a=6, в стеке создается новое значение 6 вместо изменения 5 на 6. См. ImmutableList, int делает то же самое. - person joe; 13.12.2018
comment
Я знаю, что это своего рода семантика и может иметь разное значение в разных обстоятельствах. Но согласно документу Microsoft Int32 — это неизменяемый тип значения, который ...... и члены, которые изменяют состояние экземпляра, фактически возвращают новый экземпляр, инициализированный новым значением - person joe; 13.12.2018
comment
@joe То, что я сделал почти десять лет назад, остается в силе: нет принципиальной основы для различия между созданием новой копии int и ее изменением. У него нет другой идентичности, кроме его значения, поэтому изменение его значения обязательно меняет его идентичность. - person Steven Sudit; 20.05.2019

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

Почему? Допустим, вы увеличивали int, например:

myInt++

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

Что, если другой поток изменит это число, пока оно находится в процессе увеличения? Ваш номер будет поврежден.

Однако, если вы сделаете потокобезопасную копию вашего объекта перед его увеличением, будете работать с вашей потокобезопасной копией и вернете новый объект после завершения инкремента, вы гарантируете, что ваше приращение будет потокобезопасным; на него не могут повлиять никакие операции с исходным объектом, выполняемые в других потоках, поскольку вы больше не работаете с исходным объектом. По сути, вы сделали свой объект неизменным.

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

person Robert Harvey    schedule 20.10.2010
comment
Я не уверен, что это следует. На самом деле код, который вы показываете, не ориентирован на многопотоковое исполнение. Вам нужно будет использовать Interlocked.Increment, чтобы убедиться в этом. - person Steven Sudit; 21.10.2010
comment
Я также должен упомянуть, что невозможно заблокировать int, так как это не ссылочный тип. - person Steven Sudit; 21.10.2010
comment
Спасибо, что изменили свой ответ, но мне жаль говорить, что он все еще неверен. Переменная int является изменяемой, о чем свидетельствует тот факт, что вы можете увеличивать ее. А операция инкремента, как уже упоминалось, не является атомарной. Несмотря на то, что обычно он реализуется как код операции INC, он не является атомарным с префиксом LOCK. Это может быть один код операции, но он преобразуется в выборку и сохранение, а это означает, что другое ядро ​​может выиграть гонку, сохраняя между этими двумя шагами. - person Steven Sudit; 21.10.2010
comment
@Steven: На самом деле я никогда не говорю в своем ответе, что int является неизменным, и я не говорю, что приращение является атомарным, а только то, что оно могло бы быть атомарным, если бы оно было выполнено за один шаг. Инкремент, вероятно, не лучший пример, так как он в любом случае многошаговый (он возвращает значение, а затем увеличивает). Я хочу сказать, что вы можете вернуть новый объект и обеспечить потокобезопасность при условии, что вы можете гарантировать, что исходный объект не будет видоизменяться во время вашей операции. Вы можете обеспечить эту гарантию, создав поточно-безопасную копию исходного объекта перед выполнением операции. - person Robert Harvey; 21.10.2010
comment
Я понимаю, что вы используете многочисленные противоречия как часть своего объяснения, но они не кажутся однозначно идентифицированными как ложные. - person Steven Sudit; 21.10.2010
comment
@Стивен: я не знаю, что это значит. Ответ говорит то, что говорит, не больше и не меньше. Я думаю, вы пытаетесь прочитать больше, чем есть на самом деле. - person Robert Harvey; 21.10.2010
comment
Противоречие — это условное выражение, о котором известно, что оно ложно. Если бы у свиней были крылья, то бекон был бы кошерным. В своем ответе вы говорите что-то вроде: «Если бы int были изменяемыми, нам пришлось бы заблокировать их, прежде чем изменять их. Проблема в том, что это неверно: переменные int являются изменяемыми, тогда как константы int (как и все константы) неизменяемы. Кроме того, мы должны использовать блокировку (или эквивалент), чтобы сделать изменения атомарными. Это помогает объяснить мою озабоченность? - person Steven Sudit; 21.10.2010
comment
Роберт, я просто хочу извиниться, если я показался вам недостаточно ясным или слишком придирчивым. Моя мотивация состоит в том, чтобы сделать ваш ответ настолько хорошим, насколько это возможно, а не быть критичным ради самого ответа. - person Steven Sudit; 21.10.2010

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

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

person Mark Byers    schedule 20.10.2010

Все, что имеет семантику значений, должно быть неизменным в C#.

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

MyClass o1=new MyClass();
MyClass o2=o1;
o1.Mutate();
//o2 got mutated too
//=> no value but reference semantics

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

MyStruct S1;
MyStruct S2{get;set;}

S1.Mutate(); //Changes S1
S2.Mutate();//Doesn't change S2

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

person CodesInChaos    schedule 20.10.2010

Я работаю над академическим проектом с нейронными сетями. Эти сети выполняют тяжелые вычисления с двойниками. Я запускаю его в облаке Amazon в течение нескольких дней на 32 основных серверах. При профилировании приложения главной проблемой производительности является выделение двойного!! Было бы справедливо иметь выделенное пространство имен с изменяемыми типами. В качестве дополнительной меры предосторожности можно использовать «небезопасные» ключевые слова.

person random    schedule 17.08.2014