Являются ли типы значений неизменяемыми по определению? - PullRequest
36 голосов
/ 15 мая 2009

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

Считаете ли вы 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);

Ответы [ 12 ]

43 голосов
/ 15 мая 2009

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

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


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

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


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

9 голосов
/ 15 мая 2009

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

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

6 голосов
/ 15 мая 2009

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

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

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

5 голосов
/ 15 мая 2009

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

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

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

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

В чем-то вроде функционального языка с неизменяемыми переменными это было бы недопустимо ++ я не был бы возможен. Как только переменная была объявлена, она имеет фиксированное значение.

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

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

4 голосов
/ 15 мая 2009

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

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

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

4 голосов
/ 15 мая 2009

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

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

2 голосов
/ 02 июня 2009

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

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

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

//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 (), что, конечно, не повлияет на оригинальные элементы списка.

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

2 голосов
/ 15 мая 2009

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

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 является ярким примером такого поведения.

1 голос
/ 10 июня 2012

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

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

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

1 голос
/ 18 мая 2009

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

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

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

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

...