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

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

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

Ответы

Ответ 1

Во-первых:

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

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

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

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

В принципе, неизменность значений - это то, что очень интуитивно понятное. Противоположность очень неинтуитивная.

Я предполагаю, что это субъективно, однако, со всей справедливостью;)

Ответ 2

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

Почему? Скажем, вы увеличивали int, например:

myInt++

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

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

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

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

Ответ 3

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

int i = 0;

// Mutation coming!
i += 3;

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

Можно сделать неизменяемым целое поле с помощью readonly. Аналогично, целочисленное свойство может быть только для получения.

Ответ 4

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

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

Ответ 5

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

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

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

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

MyStruct S1;
MyStruct S2{get;set;}

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

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

Ответ 6

Я работаю над научным проектом с Neural Networks. Эти сети выполняют тяжелые вычисления с удвоениями. Я запускаю его на облаке amazon в течение нескольких дней на 32-х основных серверах. При профилировании приложения главная проблема с производительностью - это выделение double! Было бы справедливо иметь специальное пространство имен с изменяемыми типами. "Небезопасные" ключевые слова могут быть применены для дополнительной меры предосторожности.