Типы значений и ссылочные типы
C# разделяет все типы данных на две разновидности: типы значений и ссылочные типы. Это различие не имеет эквивалента в C++, где переменные всегда неявно содержат значения, если только переменная специально не объявлена как ссылка на другую переменную.
В C# тип значения действительно содержит свое значение. Все предопределенные типы данных в C# являются типами значений, за исключением object
и string
. Если определить свои собственные структуры и перечисления, они также будут типами значений. Это означает, что простые типы данных в C# обычно действуют точно таким же образом как в C++, когда им присваивают значения.
int I = 10;
long J = I; // создаёт копию значения 10
I = 15; //не влияет на J
Ссылочный тип, как предполагает его имя, содержит только ссылку на то место в памяти, где хранятся данные. Синтаксически он действует таким же образом как ссылки в C++, но в терминах того, что происходит реально, ссылки C# ближе к указателям C++. В C# object
и string
являются ссылочными типами, как и любые определенные самостоятельно классы. Ссылки C# могут быть переназначены для указания на другие элементы данных, по большей части таким же образом, как можно переназначить указатели C++. Также ссылкам C# можно присваивать значение null
для указания, что они ни на что не ссылаются. Например, возьмем класс с именем MyClass
, который имеет открытое свойство Width
.
MyClass My1 = new MyClass(); // в C# new просто вызывает конструктор
My1.Width = 20;
MyClass My2 = My1; // My2 указывает теперь на то же место
// в памяти, что и My1
Му2.Width = 30; // Теперь My1.Width = 30, так как My1
// и Му2 указывают на одно место в памяти
My2 = null; // Теперь My2 не ссылается ни на что,
// My1 по прежнему ссылается на тот же объект
В C# невозможно программным путем объявить определенную переменную как тип значения или как ссылочный тип, это определяется исключительно типом данных переменной.
Тип значения и ссылочный тип данных имеют особенности в управлении памятью, так как ссылочные типы всегда хранятся в куче, в то время как типы значений обычно хранятся в стеке. Это рассматривается более подробно в следующем разделе об управлении памятью.
Инициализация переменных
В C++ переменные никогда не инициализируются, если их явно не инициализировать (или в случае классов предоставить конструкторы). Если этого не сделать, переменные будут содержать какие-то случайные данные, оказавшиеся в памяти, это отражает особое внимание в C++ к производительности. C# уделяет больше внимания исключению ошибок во время выполнения и поэтому строже относится к инициализации переменных. В C# существуют следующие правила:
□ Переменные, которые являются полями-членами, по умолчанию инициализируются с помощью нулевых значений, если они не инициализируются явно. Это означает, что числовые типы данных будут содержать нули, bool
будут содержать false
, а все ссылочные типы (включая строки и объекты) будут содержать ссылку null
. Структуры инициализируют нулем каждый свой член.
□ Локальные переменные методов не инициализируются по умолчанию. Однако компилятор будет давать ошибку, если локальная переменная используется до инициализации. Можно при желании инициализировать переменную, вызывая ее конструктор по умолчанию (тот, который обнуляет память).
// локальные переменные метода
int X1; //в этом месте X1 содержит случайные данные
// int Y = X1; // эта закомментированная строка будет создавать ошибку
// компиляции, т.к. X1 используется до инициализации
X1 = new int(); // теперь X1 будет содержать ноль.
Упаковка
В некоторых случаях необходимо использовать тип значения, как если бы он был ссылочным типом. Это можно сделать с помощью процесса, называемого упаковкой (boxing). Синтаксически упаковка означает просто преобразование переменной в объект.
int J = 10;
object BoxedJ = (object)J;
Упаковка действует как любое другое преобразование типов, но надо знать, что содержимое переменной скопируется в кучу и будет создана ссылка (так как объект BoxedJ
является ссылочным типом).
Обычная причина для использования упаковки значения состоит в передаче его в метод, который ожидает в качестве параметра ссылочный тип. Можно также распаковать упакованное значение, преобразуя его просто назад в первоначальный тип данных.
int J = 10;
object BoxedJ = (object)J;
int K = (int)BoxedJ;
Отметим, что процесс распаковки будет инициировать исключение, если попытаться преобразовать значение к неправильному типу данных.
Управление памятью
В C++ переменные (включая экземпляры классов или структур) могут храниться в стеке или в куче. Обычно переменная хранится в куче, если она или некоторый содержащий ее класс был распределен с помощью оператора new
, или в противном случае помещается в стек. Это означает, что возможность выделить динамически память для переменной с помощью оператора new позволяет выбирать, будет ли переменная храниться в стеке либо в куче. (Хотя очевидно в связи со способом работы стека, что хранящиеся в стеке данные будут существовать до тех пор, пока соответствующая переменная находится в области действия.)
C# работает совершенно по-другому. Чтобы понять как именно, рассмотрим два обычных сценария в C++. Возьмем следующее объявление двух переменных в C++:
int j = 30;
CMyClass *pMine = new CMyClass;
Здесь содержимое j
хранится в стеке. Это в точности та ситуация, которая