Являются ли типы значений неизменяемыми по определению?

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

Вопрос

Я часто это читаю structs должны быть неизменяемыми — не так ли по определению?

Рассматриваете ли вы int быть неизменным?

int i = 0;
i = i + 123;

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

i++;

Хорошо, мы можем думать об этом как о ярлыке.

i = i + 1;

Что насчет struct Point?

Point p = new Point(1, 2);
p.Offset(3, 4);

Это действительно меняет суть? (1, 2)?Разве мы не должны думать об этом как о ярлыке для следующего с помощью Point.Offset() вернуть новую точку?

p = p.Offset(3, 4);

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

Я не хочу усложнять рассуждения по этому поводу, рассматривая ref параметры и бокс.Я также знаю, что p = p.Offset(3, 4); выражает неизменность гораздо лучше, чем p.Offset(3, 4); делает.Но остается вопрос: не являются ли типы значений неизменяемыми по определению?

ОБНОВЛЯТЬ

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

public class Foo
{
    private Point point;
    private readonly Point readOnlyPoint;

    public Foo()
    {
        this.point = new Point(1, 2);
        this.readOnlyPoint = new Point(1, 2);
    }

    public void Bar()
    {
        this.point = new Point(1, 2);
        this.readOnlyPoint = new Point(1, 2); // Does not compile.

        this.point.Offset(3, 4); // Is now (4, 6).
        this.readOnlyPoint.Offset(3, 4); // Is still (1, 2).
    }
}

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

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


Кажется, ответ не так прост, поэтому я перефразирую вопрос.

Учитывая следующее.

public struct Foo
{
    public void DoStuff(whatEverArgumentsYouLike)
    {
        // Do what ever you like to do.
    }

    // Put in everything you like - fields, constants, methods, properties ...
}

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

foo.DoStuff(whatEverArgumentsYouLike);

с

foo = foo.DoStuff(whatEverArgumentsYouLike);
Это было полезно?

Решение

Объект неизменен, если его состояние не изменяется после создания объекта.

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


Длинный ответ:Прежде всего, вопрос неизменности применим только к структурам или классам с полями или свойствами.Самые базовые типы (числа, строки и null) по своей сути неизменяемы, поскольку в них нет ничего (поля/свойства), которое можно было бы изменить.5 это 5 это 5.Любая операция с числом 5 возвращает только другое неизменяемое значение.

Вы можете создавать изменяемые структуры, такие как System.Drawing.Point.Оба X и Y имеют сеттеры, которые изменяют поля структуры:

Point p = new Point(0, 0);
p.X = 5;
// we modify the struct through property setter X
// still the same Point instance, but its state has changed
// it's property X is now 5

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

void Main()
{
    Point p1 = new Point(0, 0);
    SetX(p1, 5);
    Console.WriteLine(p1.ToString());
}

void SetX(Point p2, int value)
{
    p2.X = value;
}

В этом случае Console.WriteLine() пишет "{X=0,Y=0}".Здесь p1 не был изменен, потому что SetX() модифицированный p2 который представляет собой копировать из p1.Это происходит потому, что p1 это тип значения, не потому что это неизменный (это не так).

Почему должен типы значений быть неизменяемыми?Много причин...Видеть этот вопрос.В основном это связано с тем, что изменяемые типы значений приводят к разного рода неочевидным ошибкам.В приведенном выше примере программист мог ожидать p1 быть (5, 0) после звонка SetX().Или представьте себе сортировку по значению, которое позже может измениться.Тогда ваша отсортированная коллекция больше не будет отсортирована должным образом.То же самое касается словарей и хешей.В Потрясающий Эрик Липперт (блог) написал целая серия о неизменности и почему он считает, что это будущее C#. Вот один из его примеров это позволяет вам «изменить» переменную, доступную только для чтения.


ОБНОВЛЯТЬ:ваш пример с:

this.readOnlyPoint.Offset(3, 4); // Is still (1, 2).

это именно то, о чем говорил Липперт в своем сообщении об изменении переменных, доступных только для чтения. Offset(3,4) фактически изменил Point, но это был копировать из readOnlyPoint, и он никогда ничему не был назначен, поэтому он потерян.

И что вот почему изменяемые типы значений являются злом:Они позволяют вам думать вы что-то изменяете, хотя иногда вы фактически изменяете копию, что приводит к неожиданным ошибкам.Если Point был неизменным, Offset() придется вернуть новый Point, и вы не смогли бы присвоить его readOnlyPoint.И тогда ты уходишь «Ах да, по какой-то причине он доступен только для чтения.Почему я пытался это изменить?Хорошо, что компилятор остановил меня сейчас».


ОБНОВЛЯТЬ:По поводу вашего перефразированного запроса...Думаю, я знаю, к чему вы клоните.В некотором смысле, вы можете «думать» о структурах как о внутренне неизменяемый, что изменение структуры равносильно замене ее измененной копией.Насколько я знаю, возможно, это даже то, что CLR делает внутри памяти.(Вот как работает флэш-память.Вы не можете редактировать всего несколько байтов, вам нужно прочитать в память целый блок килобайт, изменить те немногие, которые вы хотите, и записать весь блок обратно.) Однако даже если они были «внутренне неизменяемыми», это деталь реализации. а для нас, разработчиков, как пользователей структур (их интерфейса или API, если хотите), они может быть изменен.Мы не можем игнорировать этот факт и «думать о них как о неизменных».

В комментарии вы сказали: «Вы не можете иметь ссылку на значение поля или переменной».Вы предполагаете, что каждая структурная переменная имеет свою копию, поэтому изменение одной копии не влияет на другие.Это не совсем верно.Строки, отмеченные ниже, не подлежат замене, если...

interface IFoo { DoStuff(); }
struct Foo : IFoo { /* ... */ }

IFoo otherFoo = new Foo();
IFoo foo = otherFoo;
foo.DoStuff(whatEverArgumentsYouLike); // line #1
foo = foo.DoStuff(whatEverArgumentsYouLike); // line #2

Строки №1 и №2 дают разные результаты...Почему?Потому что foo и otherFoo обратитесь к тот же коробочный экземпляр из Фу.Что бы ни изменилось в foo в строке №1 отражается в otherFoo.Строка №2 заменяет foo с новым значением и ничего не делает otherFoo (при условии, что DoStuff() возвращает новый IFoo экземпляр и не изменяет foo сам).

Foo foo1 = new Foo(); // creates first instance
Foo foo2 = foo1; // create a copy (2nd instance)
IFoo foo3 = foo2; // no copy here! foo2 and foo3 refer to same instance

Модификация foo1 не повлияет foo2 или foo3.Модификация foo2 отразится в foo3, но не в foo1.Модификация foo3 отразится в foo2 но не в foo1.

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


ОБНОВЛЯТЬ:исправлена ​​опечатка в первом примере кода

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

Изменяемость и типы значений — это две разные вещи.

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

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

Например, DateTime всегда создает новые экземпляры при выполнении любой операции.Точка изменчива и может быть изменена.

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

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

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

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

И это упускает суть.Согласно спецификации .NET, типы значений являются изменяемыми.Вы можете изменить его.

int i = 0;
Console.WriteLine(i); // will print 0, so here, i is 0
++i;
Console.WriteLine(i); // will print 1, so here, i is 1

но это все то же самое.Переменная i объявляется только один раз.Все, что происходит с ним после этого объявления, является модификацией.

В функциональном языке с неизменяемыми переменными это было бы незаконно.++i было бы невозможно.После объявления переменной она имеет фиксированное значение.

В .NET это не так, ничто не мешает мне изменить i после того, как это было объявлено.

Подумав еще немного, вот еще один пример, который может быть лучше:

struct S {
  public S(int i) { this.i = i == 43 ? 0 : i; }
  private int i;
  public void set(int i) { 
    Console.WriteLine("Hello World");
    this.i = i;
  }
}

void Foo {
  var s = new S(42); // Create an instance of S, internally storing the value 42
  s.set(43); // What happens here?
}

В последней строке, согласно вашей логике, мы могли бы сказать, что фактически создаем новый объект и перезаписываем старый объект этим значением.Но это невозможно!Чтобы создать новый объект, компилятор должен установить i переменная 42.Но это личное!Доступ к нему возможен только через пользовательский конструктор, который явно запрещает значение 43 (вместо этого устанавливая его в 0), а затем через наш set метод, который имеет неприятный побочный эффект.Компилятор не имеет возможности только создание нового объекта с нужными ему значениями.Единственный способ, которым s.i можно установить на 43 модификация текущий объект, вызвав set().Компилятор не может просто так сделать, потому что это изменит поведение программы (она выведет на консоль)

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

Я не хочу усложнять рассуждения об этом, учитывая refпараметры и бокс.Я также знаю, что p = p.Offset(3, 4); выражает неизменность намного лучше, чем p.Offset(3, 4); делает.Но остается вопрос - разве типы ценных типов неизбежны по определению?

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

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

Нет, они не:если вы посмотрите на System.Drawing.Point например, у нее есть как сеттер, так и геттер. X свойство.

Однако можно сказать, что все типы значений должен определяться с помощью неизменяемых API.

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

public class foo
{
    public int x;
}

public struct bar
{
    public int x;
}


public class MyClass
{
    public static void Main()
    {
        foo a = new foo();
        bar b = new bar();

        a.x = 1;
        b.x = 1;

        foo a2 = a;
        bar b2 = b;

        a.x = 2;
        b.x = 2;

        Console.WriteLine( "a2.x == {0}", a2.x);
        Console.WriteLine( "b2.x == {0}", b2.x);
    }
}

Производит:

a2.x == 2
b2.x == 1

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

Конечно, класс System.String является ярким примером такого поведения.

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

Полный пост можно прочитать здесь

Вот пример того, как все может пойти не так:

//Struct declaration:

struct MyStruct
{
  public int Value = 0;

  public void Update(int i) { Value = i; }
}

Пример кода:

MyStruct[] list = new MyStruct[5];

for (int i=0;i<5;i++)
  Console.Write(list[i].Value + " ");
Console.WriteLine();

for (int i=0;i<5;i++)
  list[i].Update(i+1);

for (int i=0;i<5;i++)
  Console.Write(list[i].Value + " ");
Console.WriteLine();

Вывод этого кода:

0 0 0 0 0
1 2 3 4 5

Теперь сделаем то же самое, но заменим массив на общий List<>:

List<MyStruct> list = new List<MyStruct>(new MyStruct[5]); 

for (int i=0;i<5;i++)
  Console.Write(list[i].Value + " ");
Console.WriteLine();

for (int i=0;i<5;i++)
  list[i].Update(i+1);

for (int i=0;i<5;i++)
  Console.Write(list[i].Value + " ");
Console.WriteLine();

Результат:

0 0 0 0 0
0 0 0 0 0

Объяснение очень простое.Нет, это не бокс/распаковка...

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

Во втором примере мы использовали общий List<>.Что происходит, когда мы обращаемся к определенному элементу?Ну, вызывается свойство indexer, которое является методом.Типы значений всегда копируются при возврате методом, поэтому происходит именно это:метод индексатора списка извлекает структуру из внутреннего массива и возвращает ее вызывающему объекту.Поскольку это касается типа значения, будет создана копия и для копии будет вызван метод Update(), что, конечно, не влияет на исходные элементы списка.

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

Нет, они не.Пример:

Point p = new Point (3,4);
Point p2 = p;
p.moveTo (5,7);

В этом примере moveTo() является на месте операция.Он меняет структуру, которая скрывается за ссылкой. p.Вы можете увидеть это, посмотрев на p2:Его положение также изменится.С неизменяемыми структурами, moveTo() придется вернуть новую структуру:

p = p.moveTo (5,7);

Сейчас, Point является неизменяемым, и когда вы создадите ссылку на него в любом месте вашего кода, вы не получите никаких сюрпризов.Давайте посмотрим на i:

int i = 5;
int j = i;
i = 1;

Это другое. i не является неизменным, 5 является.И второе присвоение не копирует ссылку на структуру, содержащую i но он копирует содержимое i.Итак, за кулисами происходит нечто совершенно иное:Вы получаете полную копию переменной, а не только копию адреса в памяти (ссылку).

Эквивалентом объектов будет конструктор копирования:

Point p = new Point (3,4);
Point p2 = new Point (p);

Здесь внутренняя структура p копируется в новый объект/структуру и p2 будет содержать ссылку на него.Но это довольно дорогостоящая операция (в отличие от целочисленного присваивания, описанного выше), поэтому большинство языков программирования проводят это различие.

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

Нет, типы значений нет неизменяемый по определению.

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

struct MutableStruct
{
    private int state;

    public MutableStruct(int state) { this.state = state; }

    public void ChangeState() { this.state++; }
}

struct ImmutableStruct
{
    private readonly int state;

    public MutableStruct(int state) { this.state = state; }

    public ImmutableStruct ChangeState()
    {
        return new ImmutableStruct(this.state + 1);
    }
}

[Продолжение следует...]

Чтобы определить, является ли тип изменяемым или неизменяемым, необходимо определить, на что ссылается этот «тип».Когда объявляется место хранения ссылочного типа, объявление просто выделяет пространство для хранения ссылки на объект, хранящийся в другом месте;объявление не создает рассматриваемый объект.Тем не менее, в большинстве контекстов, когда речь идет об определенных типах ссылок, речь не идет о место хранения, содержащее ссылку, скорее объект, идентифицированный этой ссылкой.Тот факт, что можно выполнить запись в место хранения, содержащее ссылку на объект, никоим образом не подразумевает, что сам объект является изменяемым.

Напротив, когда объявлено место хранения типа значения, система будет выделять в этом месте хранения вложенные места хранения для каждого общедоступного или частного поля, содержащегося в этом типе значения.Все, что касается типа значения, хранится в этом месте хранения.Если определить переменную foo типа Point и два его поля, X и Y, удерживайте 3 и 6 соответственно.Если определить «экземпляр» Point в foo как пара поля, этот экземпляр будет изменчивым тогда и только тогда, когда foo является изменчивым.Если кто-то определяет экземпляр Point как являющийся ценности проводимые в этих областях (например,"3,6"), то такой экземпляр по определению является неизменяемым, поскольку изменение одного из этих полей приведет к Point для хранения другого экземпляра.

Я думаю, что более полезно думать о типе значения «экземпляр» как о полях, а не о значениях, которые они содержат.Согласно этому определению, любой тип значения, хранящийся в изменяемом месте хранения и для которого существует любое значение, отличное от значения по умолчанию, будет всегда быть изменчивым, независимо от того, как оно объявлено.Заявление MyPoint = new Point(5,8) создает новый экземпляр Point, с полями X=5 и Y=8, а затем мутирует MyPoint заменив значения в его полях на значения вновь созданного Point.Даже если структура не предоставляет возможности изменить какое-либо из своих полей за пределами своего конструктора, тип структуры не может защитить экземпляр от перезаписи всех его полей содержимым другого экземпляра.

Кстати, простой пример, когда изменяемая структура может достичь семантики, недостижимой другими способами:Предполагая myPoints[] представляет собой одноэлементный массив, доступный нескольким потокам, двадцать потоков одновременно выполняют код:

Threading.Interlocked.Increment(myPoints[0].X);

Если myPoints[0].X начинается с нуля, и двадцать потоков выполняют приведенный выше код одновременно или нет, myPoints[0].X будет равно двадцати.Если бы кто-то попытался имитировать приведенный выше код с помощью:

myPoints[0] = new Point(myPoints[0].X + 1, myPoints[0].Y);

тогда если какую нить прочитать myPoints[0].X между моментом, когда другой поток прочитает его и запишет исправленное значение, результаты приращения будут потеряны (в результате чего myPoints[0].X может произвольно получить любое значение от 1 до 20.

Объекты/структуры являются неизменяемыми, когда они передаются в функцию таким образом, что данные не могут быть изменены, а возвращаемая структура является new структура.Классический пример:

String s = "abc";

s.toLower();

если toLower Функция написана так, что возвращается новая строка, заменяющая "s", она неизменяемая, но если функция выполняет буквенную замену буквы внутри "s" и никогда не объявляет "новую строку", она изменяема.

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