Набор типов в C# широк и многообразен. Типы могут быть стандартными: int, byte, double, string, object, short и т. д. Также могут быть и более сложными: интерфейсы, классы, структуры, перечисления и т. д. Но все существующие типы можно разделить на две группы: значимые типы (value type) и ссылочные типы (reference type).
Значимые типы (value type) | Ссылочные типы (reference type) |
Целочисленные типы | Тип object |
Типы с плавающей точкой | Тип string |
Decimal | Класс (class) |
Логический тип (bool) | Интерфейсы (interface) |
Перечисления (enum) | Делегаты (delegate) |
Структуры |
В чем же принципиальное отличие работы с данными группами типов? Для начала нужно понять организацию памяти в .NET:
Рис. 1.1. Организация памяти
На самом нижнем слое находится блок «исполняемого кода». Чуть выше располагается блок «данных», где хранятся глобальные переменные, относящиеся ко всему проекту. На третьем уровне располагается блок «Куча», а на самом верху блок «Стек».
Значимые типы
Объявляя и инициализируя новую переменную со значимым типом, мы помещаем ее на Стек. К примеру, создаем переменную типа int со значением 10:
int i = 10;
На стеке выделяется ячейка, в которую помещается значение 10:
Рис. 1.2. Стек. Ячейка со значением 10
Ведя работу со значимыми переменными, мы не используем кучу, она остаётся пустой. Далее в процессе разработки возникла необходимость объявить еще одну переменную со значением 20:
int j = 20;
Она также становится в стек:
Рис. 1.3. Стек. Две ячейки со значениями 10 и 20.
Дальше в процессе разработки мы приходим к необходимости присвоить значению переменной j значение переменной i, после чего увеличиваем j в 3 раза:
j = i;
j *= 3;
Какими же получатся значения переменных i и j после произведенных действий? Т. к. мы никак не изменяли переменную i, ее значение останется неизменным, т. е. на стеке в нижней ячейке по-прежнему наблюдаем 10. Что же касается j, то, присвоив этой переменной значение i, мы поменяли значение ее ячейки на стеке на 10, а увеличив в три раза, поменяли еще раз, и теперь оно составляет значение 30:
Рис. 1.4. Стек. Изменение ячейки, хранящей значение j
Таким образом, можно заключить, что при работе со значимыми типами их значения хранятся на стеке. Под каждую конкретную переменную выделяется своя ячейка памяти. Операции присвоения меняют значения этих ячеек, но не сами ячейки. Что же происходит при работе со ссылочными типами?
Ссылочные типы
Рассмотрим аналогичный пример c классами. Вначале создаем простенький класс Example с одним полем x типа int:
class Example
{
public int x;
}
Далее, в головной части программы в методе main, создаем и инициализируем два объекта o1 и o2:
Example o1 = new Example();
Example o2 = new Example();
o1.x = 5;
o2.x = 7;
Что происходит с памятью? В куче выделяются две ячейки памяти, куда помещаются значения 5 и 7, а на стек встают две ссылки o1 и o2, указывающие на ячейки в куче:
Рис. 1.5. Стек и куча. Расположение значений 7 и 5 и ссылок
Далее, аналогично примеру со значимыми типами, присваиваем объекту o1 объект o2 и меняем после этого значение поля x у объекта o2 на 12:
o2 = o1;
o2.x = 12;
Чему будут равны значения o1.x и o2.x? Казалось бы, по аналогии с предыдущим примером, мы никак не меняли объект o1 и его значение должно сохраниться, т. е. o1.x = 5, а вот значение x у объекта o2 должно быть равным 12, т. е. o2.x == 12. На деле же все выглядит иначе. Когда мы присвоили объекту o2 объект o1, ссылка o1 стала указывать не на ячейку c цифрой 7, а на ячейку с цифрой 5. То есть мы скопировали не сам объект со значением, а только ссылку на значение 5 в куче:
Рис. 1.6. Стек и куча. Копирование ссылки o1 в o2.
Далее, меняя значение o2.x на 12, мы по ссылке o2 меняем значение 5 в куче на значение 12. Т. к. ссылка o1 тоже указывает на эту ячейку, то и ее значение x становится равным 12, т. е. o1.x == 12:
Рис. 1.7. Стек и куча. Две ссылки указывают на одну ячейку в куче.
Вывод:
Все типы в .NET можно разделить на две группы: ссылочные и значимые. Ключевым отличием этих групп является работа с памятью. Значимые типы работают со стеком, копирование одной переменной значимого типа в другую происходят классическим образом. В ячейках стека меняются значения, а при объявлении новой переменной и копировании в нее старой путем присваивания, копируется ячейка стека. В случае ссылочным типов работа ведется и со стеком, и с кучей. В куче хранятся значения, а на стеке – ссылки, указывающие на соответствующие им значения в куче. Копирование путем присваивания одной переменной ссылочного типа другой представляет собой опасность, т. к. копируется не объект со значением (ячейка в куче), а ссылка на ячейку в стеке. Таким образом, меняя значение по одной ссылке, автоматически меняется значение и по другой.
Хорошо разбираясь в определениях ссылочных и значимых типов, особенностях работы с ними, разработчик избегает простых, но, тем не менее, приводящих к непредсказуемым результатам ошибок.
Более глубоко и подробно тема изучается в курсе «Язык программирования Visual C#. Создание .Net Framework приложений».