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

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

Я часто читаю, что struct должен быть неизменным - не по определению?

Вы считаете, что 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);. Но остается вопрос - не являются ли значения значения неизменяемыми по определению?

UPDATE

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

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);
4b9b3361

Ответ 1

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

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


Длинный ответ: во-первых, вопрос о неизменяемости применяется только к структурам или классам с полями или свойствами. Самые основные типы (числа, строки и нуль) по своей сути неизменяемы, потому что нет ничего (поля/свойства), чтобы изменить их. A 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 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(). Или представьте себе сортировку по значению, которое может впоследствии измениться. Тогда ваша отсортированная коллекция больше не будет сортироваться так, как ожидалось. То же самое касается словарей и хешей. Fabulous Eric Lippert (блог) написал целую серию о неизменности и почему он считает это будущим С#. Вот один из его примеров, который позволяет вам "изменять" переменную только для чтения.


UPDATE: ваш пример:

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. Все, что было изменено в 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.

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


UPDATE: исправлена опечатка в первом примере кода

Ответ 2

Типы взаимозаменяемости и значения - две разные вещи.

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

Ответ 3

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

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

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

Ответ 4

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

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

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

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

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

Ответ 5

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

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

Ответ 6

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

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

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

Ответ 7

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

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

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

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

Ответ 8

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

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

Это пример того, как все может пойти ужасно неправильно:

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

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

Ответ 9

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

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

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

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

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

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

Ответ 10

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

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

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

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

Ответ 11

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

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

Я думаю, что более полезно думать о том, что тип данных "экземпляр" является полями, а не значениями, которые они хранят. По этому определению любой тип значения, хранящийся в изменяемом хранилище, и для которого существует любое значение, отличное от значения по умолчанию, будет always изменяться, независимо от того, как он объявлен. Оператор 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.

Ответ 12

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

String s = "abc";

s.toLower();

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