понедельник, 18 июля 2011 г.

О вреде изменяемых значимых типов

Большинство программистов, которых нелегкая судьба свела с платформной.Net знают о существовании значимых типов (value types) и ссылочных типов (reference types). И довольно многие из них прекрасно знают, что помимо названия, эти типы имеют и другие различия, такие как расположение объектов оных типов в памяти, а также в семантике.

Что касается первого различия (о котором стоит упомянуть как минимум ради полноты изложения), то оно заключается в том, что экземпляры ссылочных типов всегда располагаются в управляемой куче, в то время как экземпляры значимых типов по умолчанию располагаются в стеке, но могут мигрировать в управляемую кучу вследствие упаковки, будучи членами ссылочных типов, а также при использовании их в хитрых экзотических конструкциях языка C#, типа замыканий (*).

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

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

Давайте рассмотрим некоторые из таких примеров.

1. Изменяемый значимый тип в виде свойства объекта

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

struct Mutable
{
    public Mutable(int x, int y)
        : this()
    {
        X = x;
        Y = y;
    }
    public void IncrementX() { X++; }
    public int X { get; private set; }
    public int Y { get; set; }
}
class A
{
    public A() { Mutable = new Mutable(x: 5, y: 5); }
    public Mutable Mutable { get; private set; }
}

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

A a = new A(); 
a.Mutable.Y++;

Самое интересное, что этот код вообще не скомпилируется, поскольку вторая строка (a.Mutable.Y++;) является некорректной с точки зрения языка C#. Поскольку значение структуры Mutable копируется при возвращении из одноименного свойства, то компилятор уже на этапе компиляции понимает, что ничего хорошего от изменения временного объекта не будет, о чем и говорит красноречиво в сообщении об ошибке: “error CS1612: Cannot modify the return value of 'System.Collections.Generic.IList<MutableValueTypes.Mutable>.this[int]' because it is not a variable”. Всем, кто более или менее знаком с языком С++, такое поведение будет достаточно понятным, поскольку в этой строке кода мы пытаемся сделать не что иное, как изменить значение, не являющееся l-value.

Хотя компилятор понимает семантику оператора ++, в общем случае он понятия не имеет о том, что делает конкретная функция с текущим объектом, в частности, изменяет ли она его или нет. И хотя мы не можем вызвать оператор ++ свойства Y в предыдущем фрагменте кода, мы спокойно сможем вызвать метод IncrementX свойства X:

Console.WriteLine("Исходное значение Mutable.X: {0}", a.Mutable.X); 
a.Mutable.IncrementX();
Console.WriteLine("Mutable.X после вызова IncrementX(): {0}", a.Mutable.X);

Хотя предыдущий код ведет себя некорректно, заметить ошибку невооруженным взглядом не всегда просто. Каждый раз при обращении к свойству Mutable класса создается новая копия, для которой и вызывается метод IncrementX, но поскольку изменение копии никакого отношения к изменению исходного объекта не имеет, то и вывод на консоль, при выполнении предыдущего фрагмента кода будет соответствующий:

Исходное значение Mutable.X: 5
Mutable.X после вызова IncrementX(): 5

«Хм… ничего сверхъестественного», скажите вы и будете правы… до тех пор, пока мы не рассмотрим другие, более интересные случаи.

2. Изменяемые значимые типы и модификатор readonly

Давайте рассмотрим класс B, который в качестве readonly поля содержит нашу изменяемую структуру Mutable:

class B
{
    public readonly Mutable M = new Mutable(x: 5, y: 5);
}

Опять-таки, это не rocket science, а самый простой класс, единственным недостатком которого является использование открытого поля. Но поскольку открытость этого поля обусловлена простой примера и удобством, а не ошибками дизайна, то обращать внимание на эту мелочь не стоит. Вместо этого, стоит обратить внимание на простой пример использования этого класса и на получаемые результаты.

B b = new B(); 
Console.WriteLine("Исходное значение M.X: {0}", b.M.X);
b.M.IncrementX();
b.M.IncrementX();
b.M.IncrementX();
Console.WriteLine("M.X после трех вызовов IncrementX: {0}", b.M.X);

Итак, что будет выведено в результате? 8? (Напомню, что исходное значение свойства X равно 5, а 5 + 3, как известно, равно 8; 7 возможно, было бы лучше, но, увы, получается аж 8) Или, может быть -8? Шутка.

Вроде бы M – это не свойство, которое будет копироваться каждый раз при его возвращении, так что ответ 8 кажется вполне логичным. Однако, компилятор (и спецификация языка C#, кстати, тоже) с нами не согласятся и, в результате выполнения этого кода, M.X все еще будет равен 5:

Исходное значение M.X: 5
M.X после трех вызовов IncrementX(): 5

Все дело здесь в том, что согласно спецификации, при обращении к полю только для чтения вне конструктора, генерируется временная переменная, для которой и вызывается метод IncrementX. По сути, предыдущий фрагмент кода компилятором переписывается таким образом:

Console.WriteLine("Исходное значение M.X: {0}", b.M.X); 
Mutable tmp1 = b.M;
tmp1.IncrementX();
Mutable tmp2 = b.M;
tmp2.IncrementX();
Mutable tmp3 = b.M;
tmp3.IncrementX();
Console.WriteLine("M.X после трех вызовов IncrementX: {0}", b.M.X);

(Да, если вы уберете модификатор readonly, то вы получите ожидаемый результат; после трех вызовов метода IncrementX значение свойства X переменной M будет равно 8.)

3. Массивы и списки

Очередным, но явно не последним, моментом неочевидного поведения изменяемых значимых типов является их использование в массивах и списках. Итак, давайте поместим один элемент изменяемого значимого типа в коллекцию, например в список List<T>.

List<Mutable> lm = new List<Mutable> { new Mutable(x: 5, y: 5) };

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

lm[0].Y++; // Ошибка компиляции
lm[0].IncrementX(); // ведет к изменению временной переменной

Теперь давайте попробуем проделать ту же самую операцию с массивом:

Mutable[] am = new Mutable[] { new Mutable(x: 5, y: 5) }; 
Console.WriteLine("Исходные значения X: {0}, Y: {1}", am[0].X, am[0].Y);
am[0].Y++;
am[0].IncrementX();
Console.WriteLine("Новые значения X: {0}, Y: {1}", am[0].X, am[0].Y);

В этом случае большинство разработчиков будут предполагать, что индексатор массива ведет себя аналогичным образом, возвращая копию элемента, который затем и изменяется в нашем коде. И поскольку язык C# не поддерживает такую возможность, как возвращение «управляемых указателей» (managed pointers) из функции, то других вариантов, вроде бы и нет. Ведь все, что мы можем, так это создавать синонимы нашей переменной (alias) и передать ее в другую функцию с помощью ключевых слов ref или out, но мы не можем написать функцию, возвращающую ссылку на одно из полей объекта.

Но хотя язык C# и не поддерживает возвращение управляемых ссылок в общем случае, существует особая оптимизация в виде специальной инструкции IL-кода, которая позволяет получить не просто копию элемента массива, а ссылку на него (для любознательных, эта инструкция называется ldelema). Благодаря этой возможности, предыдущий фрагмент не только полностью корректен (включая строку am[0].Y++;), но и позволяет изменить непосредственно элементы массива, а не их копии. И если вы запустите предыдущий фрагмент кода, то увидите, что он компилируется, запускается, и напрямую изменяет нулевой объект массива.

Исходные значения X: 5, Y: 5
Новые значения X:6, Y:6

Однако если рассматриваемый выше массив привести к одному из его интерфейсов, такому как IList<T>, то вся уличная магия в виде генерации особых IL-инструкций останутся за бортом, и мы получим поведение, описанное в начале этого раздела.

Mutable[] am = new Mutable[] { new Mutable(x: 5, y: 5) }; 
IList<Mutable> lst = am;
lst[0].Y++; // Ошибка компиляции
lst[0].IncrementX(); // изменение временной переменной

4. И зачем мне все это?

Вопрос резонный, особенно если вспомнить, насколько часто вы создаете свои собственные значимые типы и уж тем более, насколько часто вы их делаете изменяемыми. Но польза от этих знаний есть. Во-первых, мы с вами не единственные программисты на свете, как не сложно догадаться, существует много других «гавриков», которые клепают код со страшной силой и создают свои собственные изменяемые структуры. И даже если лично в вашей команде таких «гавриков» нет, то они есть в других командах, например в команде разработчиков .Net Framework. Да, в составе .Net Framework есть достаточное количество изменяемых значимых типов, неосмотрительное использование которых может привести к дорогостоящим сюрпризам (**).

Классическим примером изменяемого значимого типа является структура Point, а также енумераторы, например ListEnumerator. И если в первом случае отпилить себе ногу весьма сложно, то во втором случае – будь здоров:

var x = new { Items = new List<int> { 1, 2, 3 }.GetEnumerator() };
while (x.Items.MoveNext())
{
    Console.WriteLine(x.Items.Current);
}

(Скопируйте этот код в LINQPad или в метод Main и запустите.)

Заключение

Говорить категорично о том, что изменяемые значимые типы являются полным злом точно также неверно, как и говорить о всеобъемлющем зле оператора goto. Известно, что использование оператора goto программистом напрямую в крупной промышленной системе может привести к сложному для понимания и сопровождения коду, к скрытым ошибкам и головной боли при поиске ошибок. По этой же причине нужно остерегаться и изменяемых значимых типов: если вы умеете их готовить, то аккуратное их применение может быть неплохой оптимизацией производительности. Но эта эффективность вполне может вам аукнуться позднее, когда за дело возьмется ваш сосед, который еще не выучил спецификацию языка C# на зубок и все еще не знает, что использование конструкции using со значимыми типами приводит к очистке копии (***).

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

-----------------------------

(*) Замыкание – это не такой уж страшный зверь, как может показаться из замысловатого названия. И если вдруг, по какой-то причине вы не уверены в своих знаниях по этому поводу, то этот как раз отличный повод это исправить: “Замыкания в языке C#”.

(**) Что самое интересное, изменяемые значимые типы – это далеко не единственное сомнительное решение, проявление которого легко можно найти в составе .Net Framework. Другим, не менее сомнительным дизайнерским решением является поведение виртуальных событий (о которых я писал ранее), и при всем своем неоднозначном поведении, они также присутствуют в .Net Framework (например, события PropertyChanged и CollectionChanged класса ObservableCollection являются виртуальными)

(***) Это тонкий намек на одну из статей Эрика Липперта (который считает изменяемые значимые типы самым большим вселенским злом), в которой он показывает «не совсем очевидное» поведение при использовании изменяемых значимых типов, реализующих интерфейс IDisposable: To box or not to box, that is a question.

32 комментария:

  1. А зачем обобщать на все языки?

    ОтветитьУдалить
  2. Ну, там тег стоит: C# ;)

    А где вы еще знаете такое понятие как "значимый тип"?

    Я, если честно, только в .Net-е такое понятие знаю, а там можно обобщить на все языки, поскольку основная идя этого самого дот нета в том, чтобы можно было написать наодном языке, а использовать на другом. А поскольку C# - это самый популярный язык на этой платформе, то сложности возникнут практически гарантированно.

    ОтветитьУдалить
  3. Лучше было бы, если б C# находился не только в тегах, но и в теме. Собственно, вы сами сослались в твиттере на этот пост, используя только тему без всяких тегов. Я решил прочитать, а тут оказывается про .Net.

    ОтветитьУдалить
  4. Смело можно на ты:)

    Но вопрос все же открытый, а где еще есть термин "значимые типы"?

    Мне тогда уж и тег "программирование" ставить нужно:))

    ОтветитьУдалить
  5. Например, Java. Ну а так, я, видимо, прочитал что-то вроде "о вреде изменямых значений типов", или просто не обратил внимания :)

    ОтветитьУдалить
  6. Java. Есть возможность создавать объекты на стеке (по-моему с версии 1.6). Хотя названия в смысле "значимые типы" - не встречал.

    ОтветитьУдалить
  7. А мне казалось, что в Java только примитивные типы располагаются в стеке, а такого понятия, как значимые типы - нет.

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

    Или я чего-то путаю?

    ОтветитьУдалить
  8. @eugene: см. пред. коммент :))

    Ты меня на секунду опередил:)

    ОтветитьУдалить
  9. Сергей, если честно не очень понятно поведение последнего кода в статье. Не мог бы ты пояснить с чем связано это поведение?

    ОтветитьУдалить
  10. Бред сивой кобылы. Достаточно хоть раз прочитать ECMA, все вопросы отпадут сами собой.

    ОтветитьУдалить
  11. @AlexIgnatkin
    этот пример аналогичен первому примеру в статье:
    у нас в экземпляре х есть свойство Items значимого типа.
    Каждый раз при обращении к свойству Items класса создается новая копия, для которой и вызывается метод MoveNext()

    ОтветитьУдалить
  12. Хороший пост, интересный, я даже про поведение из второго пункта не знал :)

    ОтветитьУдалить
  13. @AlexIgnatkin: Да, Vitalya ответил совершенно верно.

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

    ОтветитьУдалить
  14. @xna: чего заходил-то? Может сказать чего хотел? А то я, по простоте своей даже и не понял к чему был этот коммент...

    Если к тому, что все люди разные и то, что для одного баян, то для другого рокет сайенс, то это ж и так понятно:)))

    ОтветитьУдалить
  15. читать до конца нет сил..
    вот тут УЖЕ ВСЕ сказано:

    struct Mutable
    {
    public Mutable(int x, int y)
    : this()
    {
    X = x;
    Y = y;
    }
    public void IncrementX() { X++; }
    public int X { get; private set; }
    public int Y { get; set; }
    }

    X и Y это СВОЙСТВА объявленные в так называемом ленивом синтаксисе, их get и set это по сути методы возвращающие значения неких членов структуры, которые компилер генерит скрыто. проверьте с помощью reflector и наверняка найдете что в духе _x и _y... (вот именно к ним применение ++ было бы абсолютно корректным)..
    и по такому случаю методы get этих свойств ВСЕГДА возвращают КОПИИ скрытых полей. оператор ++ выполняется в полном согласии семантикой языка на значением КОПИИ. но ни в попытке Y++ ни в IncrementX() { X++; }, господин исследователь языка C# не затруднил себя сохранять результат. вот и все.
    но это ни коим образом не говорит об ущербности языка, платформы и семантики. именно БЛАГОДАРЯ ТАКОЙ семантике возможны конструкции типа

    foreach (var link in Links)
    {
    string linkValue = link.Attributes["href"].Value;
    // все времянки и парсер регэкспов и признак успеха
    if ((new Regex("???????")).Match(linkValue).Success)
    continue;
    x++;
    // а тут как раз с сохранением результата перед проверкой условия
    if (success = (new Regex(site)).Match(linkValue).Success)
    break;
    // такой код НЕ экономит на работе с одним экземпляром парсера
    // регэкспа, это не притензия на образец стиля..
    // но он читаем и переносим.. и отлично работает
    }


    язык просто надо знать..

    для приведенного выше примера будет корректно работать IncrementX() в такой реализации:

    // вариант 1
    struct Mutable
    {
    public Mutable(int x, int y)
    : this()
    {
    X = x;
    Y = y;
    }
    private int x;
    public void IncrementX() { x++; }
    public int X { get { return x}; }
    public int Y { get; set; }
    }

    // вариант 2 для приверженцев ленивого синтаксиса
    struct Mutable
    {
    public Mutable(int x, int y)
    : this()
    {
    X = x;
    Y = y;
    }
    public void IncrementX() { X = X++; } // или { X += 1; }
    public int X { get; private set; }
    public int Y { get; set; }
    }

    ОтветитьУдалить
  16. Я думал, что обострения в основном происходят весной, а не летом...

    @Sky Herder: жаль, что вы не дочитали статью до конца, поскольку смысл ее был не в этом примере, а в следующем.

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


    using System;

    namespace TestMutable
    {

    struct Mutable
    {
    public Mutable(int x, int y)
    : this()
    {
    X = x;
    Y = y;
    }

    public void IncrementX() { X++; }

    public int X { get; private set; }
    public int Y { get; set; }
    }


    class Program
    {
    static void Main(string[] args)
    {
    var m = new Mutable(1, 1);
    Console.WriteLine("X: {0}, Y: {1}", m.X, m.Y);
    m.IncrementX();
    m.Y++;
    Console.WriteLine("X: {0}, Y: {1}", m.X, m.Y);
    Console.ReadLine();
    }
    }
    }

    ОтветитьУдалить
  17. Почитай, как можно использовать структуры в C#. При аккуратном подходе, мой код может быть использован, так же, как и структуры. Моё мнение, таково:

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

    Мой пост на stackoverflow.com:

    http://stackoverflow.com/questions/4720475/why-can-struct-change-their-own-fields/4720966#4720966

    ОтветитьУдалить
  18. На похожую тему была очень хорошая заметка в блоге Эрика Липперта. Она даже доступна на русском языке в блоге-зеркале на русском MSDN`е: http://blogs.msdn.com/b/ruericlippert/archive/2011/03/24/to-box-or-not-to-box-that-is-the-question.aspx

    ОтветитьУдалить
  19. @hack2root: предпочитаю переходить на "ты" при более адекватном тоне общения ;)

    О каком "вашем" коде идет речь?

    З.Ы. Если вы внимательно читали сей пост, а также выводы, то в них (в этих самых выводах) была аналогия изменяемых значимых типов с пресловутым оператором goto. Ведь вреден не сам оператор goto, а та простота, с которой можно допустить ошибки при разработке, и особенно, при сопровождении кода.
    Здесь ситуация аналогична. Вы (да, лично Вы) отлично знаете все тонкости языка C# и подводные камни изменяемых значимых типов, но это не значит, что все ваши коллеги это точно также прекрасно знают.
    З.Ы.Ы. Неизменямость вообще - это здорово, т.к. во многих случаях упрощяет код, так что эта заметка - лишь дополнительные доводы тем, кто хочет использовать неизменяемые типы.

    ОтветитьУдалить
  20. @Алексей. Да, я вкурсе об этой заметке Эрика, поскольку именно я ее и переводил:)

    ОтветитьУдалить
  21. спасибо за статью. Мне казалось, что тема просто замалчивается. Или никто не пишет так, как я и никто не наступает на эти грабли

    ОтветитьУдалить
  22. @Anton Zubarev: да тема не то, чтобы замалчивалась; об этой проблеме очень часто пишет тот же Эрик Липперт, да и на stackoverflow это тоже периодически обсуждается.

    А вот русскоязычных постов, да, я чего-то не припомню.

    ОтветитьУдалить
  23. Для меня "значимые типы" - это типы которые представляют какие-то семейства значений, а значения менять нельзя - они существуют в другой неизменной "вселенной", где "живут" все возможные значения. И в наших приложениях могут быть, лишь, ссылки на них (хороший пример - строки в C#), какие-то идентификаторы, которые мы распознаем и сопоставляем со значением (десятичная запись цифр, например).

    C# же позволяет вводить методы, которые вроде как применяются к "значимым типам", но при этом имеют возможность пытаться "подменять оригинальное значение".

    Для меня все struct типы должны позволять "модификацию", лишь при конструировании. Любые изменения: это методы возвращающие новое значение ("производные" оригинального значения) или же внешние объекты модифицирующие значение (всякие "комбинаторы")

    Я столкнулся с этим в первые несколько дней знакомства, когда я пытался построить гистограмму и использовать List кластеров, которые были struct'ами. Соответственно метод добавления реализации в кластер не приводил к "изменению" кластера в коллекции. Следовательно для корректной работы: конструктор принимающий реализацию величины и статический метод комбинирования двух кластеров.

    P.S. А вот если бы C# не смешивал всё в кучу или точнее не вводил что-либо, лишь, на половину (значимый тип от класса отличается лишь ключевым словом, особенностями передачи и как результат расположением в памяти - да, это, просто, способ предоставить разработчику возможность оптимизировать такие классы с коротким временем жизни как у Point). Так выходит, что он, вроде, и пытается ввести что-то более высокоуровневое, но при этом оставляет всякие гадости как, например, индексация по массиву и свобода действий для методов struct'а.

    Ну как резюме - в C# нет значимых типов - есть, только, хитрые классы объектов, которые называются "значимыми типами".

    ОтветитьУдалить
  24. @ony: отличный комментарий. По поводу семантики - полностью согласен.

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

    Аналогичную картину мы видим и в Java, со своими примитивными типами, часть значений которых еще и кэшируются компилятором, что приводит к нарушению правил транизитивности в некоторых случаях.

    В общем, стараемся как лучше, а получаем, что думать все равно нужно, иначе отпил ноги:)

    ОтветитьУдалить
  25. Ну вообще-то структуры были введены в C# для P/Invoke в основном.

    ОтветитьУдалить
  26. @geegrshsrthsth: речь не о том, для чего ее изобретали разработчики языка и платформы разработки (кстати, я далеко не так уверен, что основная цель значимых типов - это P/Invoke).

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

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

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

    ОтветитьУдалить
  28. Я бы добавил менее известные и (имхо) более интересные corner cases:

    1) http://www.bluebytesoftware.com/blog/2010/07/01/WhenIsAReadonlyFieldNotReadonly.aspx
    2) http://blogs.msdn.com/b/ricom/archive/2006/08/31/733887.aspx

    ОтветитьУдалить
  29. Этот комментарий был удален автором.

    ОтветитьУдалить
  30. @Ivan: поделитесь, в чем заключается большая интересность корнер кейсов по приведенным ссылкам?

    По первой ссылке есть пример модификации readonly поля, который к самой проблеме неизменяемых значимых типов не относится.

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

    З.Ы. Если не сложно, добавляйте ссылки с помощью тегов < a >, так удобнее переходить по ссылкам с мобильных устройств.

    ОтветитьУдалить
  31. На до же, прошло всего лишь 2 года, а всё поменялось. WinRT, Windows 8, HTML5 + JavaScript...

    ОтветитьУдалить
  32. Ваша фраза о том что value типы - оптимизация, а изменяемость value типов - оптимизация в квадрате - очень точно и емко описывает самую их суть. Ну и вообще статья великолепная.
    Очень понравился коммент ony, к нему добавлю: хочется иметь возможность на уровне синтаксиса задавать настоящую неизменяемость для структур, чтобы они вели себя как объекты класса string. Что-то вроде public readonly struct

    ОтветитьУдалить