Value type. Reference type. Heap. Stack.
В чем же между ними различия? Для этого надо понять организацию памяти в .NET. Здесь память делится на два типа: стек и куча (heap). Все типы значений являются производными от типа System.ValueType и размещают свое значение в стеке. Стек представляет собой структуру данных, которая растет снизу вверх: каждый новый добавляемый элемент помещаются поверх предыдущего. Время жизни переменных таких типов ограничено их контекстом. Физически стек — это некоторая область памяти в адресном пространстве.
Когда программа только запускается на выполнение, в конце блока памяти, зарезервированного для стека устанавливается указатель стека. При помещении данных в стек указатель переустанавливается таким образом, что снова указывает на новое свободное место.
Ссылочные типы хранятся в куче или хипе, которую можно представить как неупорядоченный набор разнородных объектов. Физически это остальная часть памяти, которая доступна процессу.
При создании объекта ссылочного типа в стеке помещается ссылка на адрес в куче (хипе). Когда объект ссылочного типа перестает использоваться, то ссылка из стека удаляется, и память очищается. После этого в дело вступает автоматический сборщик мусора: он видит, что на объект в хипе нету больше ссылок, и удаляет этот объект и очищает память.
Generic Collections не используют autoboxing. То есть могут работать с value типами. Чтобы работало только с object надо указать, что T is class.
Boxing. Unboxing. Nullable
Упаковка — преобразование переменной типа значения в ссылочный(object). При распаковке наоборот.
Получаемый при распаковке тип переменной должен точно соответствовать тому типу, который был упакован.
Generic Collections не используют autoboxing. То есть могут работать с value типами. Чтобы работало только с object надо указать, что T is class.
Nullable — структура, которая позволяет типам-значениям иметь null.
Несмотря на то, что при распаковке тип переменной должен точно соответствовать тому типу, который был упакован:
Garbage collector
При инициализации объекта в памяти выделяется нужное под объект место. Использование ключевого слова new приводит к добавлению объекта класса в так называемую управляемую кучу, а назад возвращается ссылка на объект.
Объект удаляется, когда на него нет ссылок.
Поколения объектов
При попытке обнаружить недостижимый код объекты CLR-среды не проверяют буквально каждый находящийся в куче объект. Очевидно, что на это уходила бы масса времени, особенно в крупных проектах.
Для оптимизации процесса каждый объект в куче относится к определённому «поколению» Смысл в применении поколений выглядит довольно просто:
Чем дольше объект находится в куче, тем выше вероятность того, что он там будет оставаться.
Например, класс, определённый в главном окне настольного приложения, будет оставаться в памяти вплоть до завершения программы. С другой стороны, объект, попавший в кучу совсем недавно (например те, что находятся в области видимости методов), вероятнее всего будут становиться недостижимыми достаточно быстро. Изводя из этих предположений, каждый объект в куче относится к:
· Поколение 0. Идентифицируется новый только что размещённый объект, который ещё никогда не помечался как надлежащий удалению в процессе сборки мусора
· Поколение 1. Идентифицирует объект, который уже «пережил» один процесс сборки мусора (был помечен, как надлежащий удалению, но не был удалён из-за достаточного свободного места в куче).
· Поколение 2. Идентифицирует объект, который пережил более одного прогона сбора мусора
Dispose/Finalize. Using
Dispose нужен для освобождения ресурсов “здесь и сейчас” (не совсем так на самом деле. Вызов Dispose сигнализирует, что вы хотите освободить ресурс, но не факт, что это обязательно случится вот прямо тут же). Необходимость и преимущество интерфейса IDisposable именно в том, что его реализация позволяет освобождать ресурсы не тогда, когда до них доберется сборщик мусора, а тогда, когда это нужно программисту. Ресурсы могут быть дорогими, и держать их в памяти неопределенно долгое время может быть слишком расточительным. Деструкторов в C# нет вовсе. Есть финализаторы. Разница в том, что время вызова финализатора не определено. Стоит также отметить, что если Dispose предназначен для вызова вручную, то финализатор вручную вызвать нельзя. Это делается автоматически.
Finalize выполняется перед уничтожением объекта. Можно сказать, что это “последний шанс” освободить ресурсы корректно. Можно также считать, что Finalize — это “последняя воля умирающего” объекта. Определять этот метод имеет смысл только в случае, если класс имеет доступ к каким-либо неуправляемым ресурсам.
В C# же деструктор (т.е. метод Finalize) вызывается сборщиком мусора автоматически в неопределенное время, может быть даже не вызван вообще. Поэтому чтобы освобождать ресурс вручную, если он больше не нужен и придумали интерфейс IDisposable с методом Dispose, а также оператор using.
Что такое value type c
Ранее мы рассматривали следующие элементарные типы данных: int, byte, double, string, object и др. Также есть сложные типы: структуры, перечисления, классы. Все эти типы данных можно разделить на типы значений, еще называемые значимыми типами, (value types) и ссылочные типы (reference types). Важно понимать между ними различия.
Целочисленные типы ( byte, sbyte, short, ushort, int, uint, long, ulong )
Типы с плавающей запятой ( float, double )
В чем же между ними различия? Для этого надо понять организацию памяти в .NET. Здесь память делится на два типа: стек и куча (heap). Параметры и переменные метода, которые представляют типы значений, размещают свое значение в стеке. Стек представляет собой структуру данных, которая растет снизу вверх: каждый новый добавляемый элемент помещается поверх предыдущего. Время жизни переменных таких типов ограничено их контекстом. Физически стек — это некоторая область памяти в адресном пространстве.
Когда программа только запускается на выполнение, в конце блока памяти, зарезервированного для стека устанавливается указатель стека. При помещении данных в стек указатель переустанавливается таким образом, что снова указывает на новое свободное место. При вызове каждого отдельного метода в стеке будет выделяться область памяти или фрейм стека, где будут храниться значения его параметров и переменных.
Пи запуске такой программы в стеке будут определяться два фрейма — для метода Main (так как он вызывается при запуске программы) и для метода Calculate:
При вызове этого метода Calculate в его фрейм в стеке будут помещаться значения t, x, y и z. Они определяются в контексте данного метода. Когда метод отработает, область памяти, которая выделялась под стек, впоследствии может быть использована другими методами.
Причем если параметр или переменная метода представляет тип значений, то в стеке будет храниться непосредсвенное значение этого параметра или переменной. Например, в данном случае переменные и параметр метода Calculate представляют значимый тип — тип int, поэтому в стеке будут храниться их числовые значения.
Ссылочные типы хранятся в куче или хипе, которую можно представить как неупорядоченный набор разнородных объектов. Физически это остальная часть памяти, которая доступна процессу.
При создании объекта ссылочного типа в стеке помещается ссылка на адрес в куче (хипе). Когда объект ссылочного типа перестает использоваться, в дело вступает автоматический сборщик мусора: он видит, что на объект в хипе нету больше ссылок, условно удаляет этот объект и очищает память — фактически помечает, что данный сегмент памяти может быть использован для хранения других данных.
Так, в частности, если мы изменим метод Calculate следующим образом:
То теперь значение переменной x будет храниться в куче, так как она представляет ссылочный тип object, а в стеке будет храниться ссылка на объект в куче.

Составные типы
Теперь рассмотим ситуацию, когда тип значений и ссылочный тип представляют составные типы — структуру и класс:
Здесь в методе Main в стеке выделяется память для объекта state1. Далее в стеке создается ссылка для объекта country1 ( Country country1 ), а с помощью вызова конструктора с ключевым словом new выделяется место в хипе ( new Country() ). Ссылка в стеке для объекта country1 будет представлять адрес на место в хипе, по которому размещен данный объект..

Таким образом, в стеке окажутся все поля структуры state1 и ссылка на объект country1 в хипе.
Но, допустим, в структуре State также определена переменная ссылочного типа Country. Где она будет хранить свое значение, если она определена в типе значений?
Значение переменной state1.country также будет храниться в куче, так как эта переменная представляет ссылочный тип:

Копирование значений
Тип данных надо учитывать при копировании значений. При присвоении данных объекту значимого типа он получает копию данных. При присвоении данных объекту ссылочного типа он получает не копию объекта, а ссылку на этот объект в хипе. Например:
Так как state1 — структура, то при присвоении state1 = state2 она получает копию структуры state2. А объект класса country1 при присвоении country1 = country2; получает ссылку на тот же объект, на который указывает country2. Поэтому с изменением country2, так же будет меняться и country1.
Ссылочные типы внутри типов значений
Теперь рассмотрим более изощренный пример, когда внутри структуры у нас может быть переменная ссылочного типа, например, какого-нибудь класса:
Переменные ссылочных типов в структурах также сохраняют в стеке ссылку на объект в хипе. И при присвоении двух структур state1 = state2; структура state1 также получит ссылку на объект country в хипе. Поэтому изменение state2.country повлечет за собой также изменение state1.country.

Объекты классов как параметры методов
Организацию объектов в памяти следует учитывать при передаче параметров по значению и по ссылке. Если параметры методов представляют объекты классов, то использование параметров имеет некоторые особенности. Например, создадим метод, который в качестве параметра принимает объект Person:
При передаче объекта класса по значению в метод передается копия ссылки на объект. Эта копия указывает на тот же объект, что и исходная ссылка, потому мы можем изменить отдельные поля и свойства объекта, но не можем изменить сам объект. Поэтому в примере выше сработает только строка person.name = «Alice» .
А другая строка person = new Person < name = "Bill", age = 45 >создаст новый объект в памяти, и person теперь будет указывать на новый объект в памяти. Даже если после этого мы его изменим, то это никак не повлияет на ссылку p в методе Main, поскольку ссылка p все еще указывает на старый объект в памяти.
Но при передаче параметра по ссылке (с помощью ключевого слова ref ) в метод в качестве аргумента передается сама ссылка на объект в памяти. Поэтому можно изменить как поля и свойства объекта, так и сам объект:
Операция new создаст новый объект в памяти, и теперь ссылка person (она же ссылка p из метода Main) будет указывать уже на новый объект в памяти.
Популярные заблуждения о C#
Эта статья является развёрнутым комментарием к другой статье. Обычно я прохожу мимо, но сейчас меня почему-то задело.
Та статья представляла из себя практически «идеальную подборку заблуждений в вакууме». Причём они (заблуждения) являются довольно популярными и постоянно встречаются в различных блогах и подборках «99 вопросов для собеседования», «как пройти собеседование на джуниора» или в данном случае «шпаргалка по C#».
Почему они такие популярные? Я считаю, что потому, что они дают простые и короткие ответы, которые очень удобны для формата теста или квиза. Думать не надо, можно просто запомнить. Практикующие же разработчики повторяют их потому что эти заблуждения часто описывают наблюдаемое положение дел, но это не значит, что они истинны. Если посмотреть в окно в центре города, то можно сделать вывод что все машины — легковые, и это в большинстве случаев будет верно, но к правде имеет довольно слабое отношение.
Мне хочется, чтобы данная статья была не чисто моим мнением и не очередной компиляцией блогов. Поэтому я проведу аналогию (весьма сомнительную, но мне так захотелось) с юриспруденцией. У нас будет так:
Законопроекты — информация о версиях языка новее, чем в ECMA 334, а также ещё не выпущенных версиях. Эту информацию можно найти на гитхабе dotnet, а так же в статьях-анонсах на MSDN.
Подзаконные акты — документация (не статьи) MSDN.
Опыт законоприменения — статьи MSDN, Wikipedia и на других сайтах, информация не абсолютная, требует проверки.
Прямой опыт — то, что мы можем просто взять и проверить. Тут нам повезло больше, чем юристам, ведь нам не придётся что-то красть, чтобы проверить на сколько лет за это посадят ))
Не долго думая, возьмём план статьи отсюда и будем рассматривать мифы по списку. Если что-то забуду, добавляйте в комментах.
Ссылочные и значимые типы (value vs reference types)
Заблуждение: ссылочные типы (reference type) хранятся в куче (heap), а значимые (value types) — на стэке.
Почему распространено? Это — простое объяснение, и оно часто тиражируется. Более того, если написать простой метод с простыми переменными одного и другого типа, чем обычно его и иллюстрируют, то всё именно так и будет.
Закон: во всём стандарте есть только 2 упоминания слова «куча», и это вполне объяснимо, поскольку стэк и куча являются деталями реализации, а не самого языка. Второе упоминание — про то, что зафиксированные (fixed) объекты могут приводить к фрагментации кучи, это нам пока не интересно. А первое упоминание — в разделе 16.1 Structs/General, то есть общем описании, а не определении:
However, unlike classes, structs are value types and do not require heap allocation
Это означает только то, что структуры не требуют выделения на куче (но не означает что они располагаются на стэке).
Опыт: действительно, значимые типы, являющиеся локальными переменными в текущей реализации скорее всего окажутся на стэке. Но если мы создадим класс, членом которого является структура, то она, как и все остальные данные этого класса окажется в куче.
Для классов (ссылочных типов) на текущий момент действительно верно, что во всех простых случаях они окажутся размещёнными в куче. Но ссылочный тип чисто теоретически можно разместить на стэке. Более того, скоро это изменится вполне официально, так же спасибо @VladD-exrabbit за вот эту ссылку: https://github.com/dotnet/runtime/issues/11192
P.S.: ещё спасибо @PsyHaSTe за ссылки: один, два, три. «Как можно видеть, Липперта эти сравнения бесили ещё в 2009. »
Так чем же отличаются value и reference типы?
Читаем стандарт (в нём всё про значимые типы лежит в разделе Structs и слово struct используется для их описания):
16.4.2 Value semantics
— A variable of a struct type directly contains the data of the struct, whereas a variable of a class type contains a reference to an object that contains the data…
Тут текст про то, что структуры содержат в себе сами данные, а переменная со ссылочным типом — только ссылку. А значит структура не может в себе содержать поля, размер которых ещё не известен (в том числе своего же типа):
With classes, it is possible for two variables to reference the same object…
Переменные ссылочного типа могут (но не обязаны) указывать на один и тот же объект, а каждая значимая переменная содержит свою копию данных.
Это всё, что описано в разделе про семантику, а для языка это самое главное. Дальше идёт описание конкретно структур (16.4.3 Inheritance) а также свойства, вытекающие из семантики (16.4.4 Assignment — копирование данных при присваивании), 16.4.5 Default values — значение по умолчанию, 16.4.6 Boxing and unboxing — если нам надо передать ссылку, то требуется боксинг. А так же конструкции языка 16.4.7 Meaning of this, 16.4.8 Field initializers, 16.4.9 Constructors, 16.4.10 Static constructors, 16.4.11 Automatically implemented properties.
На собеседовании на вопрос о различии этих типов главное ответить, что у них разная семантика. Можно, конечно, погрузиться в дальнейшие различия (по списку из стандарта), но нигде среди них нет упоминания, что одно — это то, что идёт на стэк, а другое — в кучу.
stack vs heap
Заблуждение (1): стэк быстрый, а куча большая.
Почему? Потому что мелкие локальные переменные, такие как числа, обычно располагаются на стэке, а жирные объекты размещают в куче. Очевидно, что с мелкими объектами, которые известно где лежат, работать проще и быстрее. Но это скорее следствие, чем причина.
Закон: слово heap мы уже искали в стандарте и ничего серьёзного не нашли. Слово stack в основном встречается в параграфах про unsafe-блоки и stackalloc, но мы сейчас не про это.
Факт: размещать данные на куче действительно сложнее (нужно пройти через аллокатор памяти), а на стэке — быстрее (делать ничего не надо, уже известно где и сколько памяти мы используем). Но дальнейший доступ будет аналогичен (если и то и другое мы будем трогать одинаковым способом, например по ссылке) и скорость доступа будет зависеть только от эффектов локальности данных в кэше, но это уже не вопрос языка, а физической машины.
Размер стэка можно поменять для всего бинарника с помощью EDITBIN.EXE /STACK:<stacksize> file.exe
А для каждого отдельного потока — через второй аргумент конструктора new Thread().
По умолчанию, куча действительно больше стэка, но стэк можно увеличить, а куча, хоть и растёт по мере надобности, но не безгранична. Есть куча правил, по которым определяется её размер, так же её можно уменьшить в настройках. Иногда доступная куча оказывается меньше физически доступной памяти Тогда приходится расчехлять unsafe и делать offheap-аллокации.
Вывод: размер и того и другого определяются машиной и рантаймом, но не является определяющим признаком.
Передача по значению / указателю
Заблуждение: значимые типы (структуры) передаются по значению а ссылочные (классы) — по ссылке.
Почему оно популярно? Честно — не знаю. Возможно из курсов Си для начинающих.
Закон:
10.2.5 Value parameters
A parameter declared without a ref or out modifier is a value parameter.10.2.6 Reference parameters
A parameter declared with a ref modifier is a reference parameter.10.2.7 Output parameters
A parameter declared with an out modifier is an output parameter.
Думаю, тут всё понятно. То, как передаётся объект, определяется не его типом (ссылочный/значимый) а тем, как объявлен и передан аргумент функции. Добавляется ещё странная вещь под названием Output parameter со своей семантикой, но в реализации это такой же ref-параметр, только требующий инициализации в вызываемом методе. На этом дискуссию можно было бы закончить, но давайте немного займёмся сравнительным языкознанием.
Для сравнения я возьму Java 8-летней давности (в последний раз что-то значимое на джаве я писал примерно тогда, а с тех пор могло что-то поменяться), С++ (а не C, потому что иначе я сам запутаюсь) и C#. Я хотел тут повторить анализ целиком, но просто приведу ссылки: Java, C++, C#.
Краткий пересказ: в Java передача только по значению, но есть ссылочные типы и примитивы (это не совсем значимые типы как в C#, но для сравнения сойдёт) В C++ все типы — значимые, но можно передавать как по значению так и по ссылке (и ещё по указателю/адресу). А в C# сочетаются обе эти семантики: можно взять значимый или ссылочный тип и передать любой из них по ссылке или по значению. Это ортогональные понятия и не надо их смешивать.
P.S.: в новых версиях языка появились in-параметры. С семантической точки зрения они не определяют способ передачи (ведь при запрете изменения объекта нет никакой разницы, как он был передан), но с точки зрения реализации они работают как неизменяемые ref-параметры (readonly ref) и соответственно тоже передаются по ссылке.
P.P.S.: с out-параметрами тоже не всё так просто. Вот в этой статье есть подробный разбор: https://m.habr.com/ru/company/pvs-studio/blog/542210/, рекомендую к прочтению.
string — особенный тип
Заблуждение: ведёт себя как значимый тип, а лежит в куче.
Закон:
9.2.5 The string type
The string type is a sealed class type that inherits directly from object. Instances of the string class represent Unicode character strings.
Values of the string type can be written as string literals (§7.4.5.6).
The keyword string is simply an alias for the predefined class System.String
Как видим, не такой уж он и особенный.
Опыт: ну да, как и все другие классы (ссылочные типы) строки обычно размещаются в куче. Почему говорят, что он ведёт себя как значимый тип? Я много раз такое слышал, но так и не получил чёткого ответа, почему.
Про какие особенности речь?
1. Это неизменяемый (immutable) и запечатанный (sealed) класс. Это значит, что обычными способами нельзя изменить внутри него данные и нельзя от него унаследоваться. Ну и что? Вы можете создавать классы с такими же ограничениями, ничего особенного.
2. Можно сравнивать с помощью оператора==, а обычные структуры нельзя, и для голых классов сравнивается инстанс, но не данные. Ну и что? Для любого своего класса или структуры вы можете написать такой же оператор и они будут вести себя абсолютно так же.
Более того, иммутабельность строк можно нарушить с помощью небольшой щепотки магии. И да, это уже было на хабре.
const vs readonly
Заблуждение:
· const — значение подставляется при компиляции => установить можно только до компиляции
· readonly — установить значение можно только до компиляции или в конструкторе
Закон:
12.20 Constant expressions
A constant expression is an expression that shall be fully evaluated at compile-time
Не «установить значение», а «значение вычисляется» (это тонкое, но важное различие).
15.5.3 Readonly fields
15.5.3.1 General When a field-declaration includes a readonly modifier, the fields introduced by the declaration are readonly fields. Direct assignments to readonly fields can only occur as part of that declaration or in an instance constructor or static constructor in the same class.
Да, вроде бы похоже. Но почему они в разных разделах (12 и 15)? Давайте посмотрим на название: одно — это выражение, а другое — модификатор поля. И это главное их семантическое отличие.
Отсюда вытекает важная деталь реализации:
15.5.3.3 Versioning of constants and static readonly fields
Constants and readonly fields have different binary versioning semantics. When an expression references a constant, the value of the constant is obtained at compile-time, but when an expression references a readonly field, the value of the field is not obtained until run-time.
Значение константы фиксируется на момент компиляции. А статических readonly-полей (которые часто используют как замену) — на этапе выполнения. Если одна сборка зависит от другой, и берёт из неё константы и ридонли-поля, то при их изменении в первой сборке, константы во второй останутся старыми, а readonly-поля подцепятся свежие.
Факт: соберите тестовый проект, и откройте его в декомпиляторе. Вы увидите, что вместо констант встанут их значения (поэтому они и являются выражениями), а для readonly-полей останутся на них ссылки.
Хозяйке на заметку: до .NET Core 3 можно было поменять значение readonly-полей через рефлекшен, начиная же с этой версии такой простой способ больше не работает, но остались другие. Поменять же значения констант, не замаравшись в декомпиляции и рекомпиляции методов, у вас не выйдет.
ref и out
Из статьи-«шпаргалки»: ref и out позволяют внутри метода использовать new и для class и для struct
Факт: см выше (Передача по указателю)
out тоже что ref, только говорит о том что, метод обязательно пересоздаст переменную
Тут, наверное, имелось в виду «переназначит», а не пересоздаст, но сильно придираться не будем.
События, делегаты
Заблуждение:
Закон: 15.8.2 Field-like events
Надеюсь, разницу, объяснять не надо.
P.S.: в новом C# можно не задумываться и писать Evt?.Invoke(«hello»);
Finalizer (destructor)
Заблуждение (1):
Foo() это «деструктор» класса Foo
Почему? Потому что по той или иной причине сами авторы языка так это называли, хотя вовремя одумались.
Закон:
15.13 Finalizers
[Note: In an earlier version of this standard, what is now referred to as a «finalizer» was called a
«destructor». Experience has shown that the term «destructor» caused confusion and often resulted to incorrect expectations, especially to programmers knowing C++. In C++, a destructor is called in a determinate manner, whereas, in C#, a finalizer is not. To get determinate behavior from C#, one should use Dispose. end note]
Причина: деструкторы и финализаторы отличаются семантикой, главное отличие — детерминированность.
P.S.: в комментах появилась хорошая историческая справка, спасибо @rstm-sf.
Заблуждение (2): вызывается, когда garbage collector доберется до объекта
Почему популярно? В большинстве простых случаев это действительно именно так и происходит.
Закон:
An instance becomes eligible for finalization when it is no longer possible for any code to use that instance. Execution of the finalizer for the instance may occur at any time after the instance becomes eligible for finalization (§8.9).
Обращаем внимание, что никаких гарантий нам тут не предоставляют.
Факт: финализатор будет вызван у объекта, если он у него есть и объект не побывал в методе SuppressFinalize до начала маркировки объектов на финализацию. Где-то между моментом определения что объект недоступен до момента фактического освобождения памяти. Но это не точно. Отменить финализацию можно с помощью метода GC.SuppressFinalize, хотя это может и не сработать. Более того, рекомендованный шаблон реализации IDisposable именно так и поступает.
Заблуждение (3): вызывается только автоматически средой .Net, нельзя вызвать самостоятельно
Факт: действительно, специального синтаксиса для этого нет. Но можно, как обычно, залезть туда через рефлекшен:
Конечно, для симуляции корректного поведения следует пройтись по всей цепочке наследования (ведь так написано в стандарте).
Singleton
Заблуждение: не забудьте про потокобезопасность и lock
Почему? Потому что часто синглтон смешивают с ленивой инициализацией, хотя это два разных паттерна, которые могут встретиться независимо друг от друга.
В стандарте очевидно ничего такого нет, так что будем опираться на здравый смысл.
Раньше собеседующий ждал тут что-то типа такого кода:
Это известный паттерн double checked locking, он нужен для ленивой инициализации. А вопрос, напомню, стоял про синглтон.
Более того, этот код тоже имеет проблемы. Дело в том, что модель памяти работает не совсем так, как кажется на первый взгляд (это отдельная большая тема, явно не для собеседований уровня джун/миддл). Чтобы её решить, требуется или вставить volatile в нужном месте или аккуратно использовать MemoryBarrier().
Опыт: ничего из этого на самом деле не требуется для синглтона. Ведь рантайм нам даёт чёткие гарантии о потокобезопасности статических инициализаторов и мы просто можем написать:
Рантайм гарантирует, что это свойство будет потокобезопасно проинициализировано в какой-то момент начиная от запуска и до первого использования. Причём в текущей реализации это делается с достаточной степенью ленивости, так что для 99% случаев такой простой код будет самым безопасным и надёжным. Для любителей чуть большей ленивости есть решение со вложенным классом, но всё же не полной гарантией ленивости.
Для другого паттерна, ленивой инициализации, в дотнете есть готовое решение — Lazy<T>, которое использует правильные для данного рантайма примитивы синхронизации.
И, значит, для оставшегося 1% случаев, когда синглтону нужна гарантированная ленивость, мы можем написать:
И да, это тоже уже было на хабре, с кучей комментов.
P.S.: и так совпало, что сегодня же на хабре выложили вот такую подробную статью (18+)!
Вроде, по статье всё. Я явно что-то забыл, или написал неправильно, но для этого есть комменты, да будет срач!
Name already in use
docs / docs / csharp / language-reference / builtin-types / value-types.md
- Go to file T
- Go to line L
- Copy path
- Copy permalink
- Open with Desktop
- View raw
- Copy raw contents Copy raw contents
Copy raw contents
Copy raw contents
Value types (C# reference)
Value types and reference types are the two main categories of C# types. A variable of a value type contains an instance of the type. This differs from a variable of a reference type, which contains a reference to an instance of the type. By default, on assignment, passing an argument to a method, and returning a method result, variable values are copied. In the case of value-type variables, the corresponding type instances are copied. The following example demonstrates that behavior:
As the preceding example shows, operations on a value-type variable affect only that instance of the value type, stored in the variable.
If a value type contains a data member of a reference type, only the reference to the instance of the reference type is copied when a value-type instance is copied. Both the copy and original value-type instance have access to the same reference-type instance. The following example demonstrates that behavior:
[!NOTE] To make your code less error-prone and more robust, define and use immutable value types. This article uses mutable value types only for demonstration purposes.
Kinds of value types and type constraints
A value type can be one of the two following kinds:
- a structure type, which encapsulates data and related functionality
- an enumeration type, which is defined by a set of named constants and represents a choice or a combination of choices
A nullable value type T? represents all values of its underlying value type T and an additional null value. You cannot assign null to a variable of a value type, unless it’s a nullable value type.
You can use the struct constraint to specify that a type parameter is a non-nullable value type. Both structure and enumeration types satisfy the struct constraint. You can use System.Enum in a base class constraint (that is known as the enum constraint) to specify that a type parameter is an enumeration type.
Built-in value types
C# provides the following built-in value types, also known as simple types:
All simple types are structure types and differ from other structure types in that they permit certain additional operations:
You can use literals to provide a value of a simple type. For example, ‘A’ is a literal of the type char and 2001 is a literal of the type int .
You can declare constants of the simple types with the const keyword. It’s not possible to have constants of other structure types.
Constant expressions, whose operands are all constants of the simple types, are evaluated at compile time.
A value tuple is a value type, but not a simple type.
C# language specification
For more information, see the following sections of the C# language specification: