Типы значений и ссылочные типы
Разбираем вопрос, который звучит почти на каждом собеседовании: что случится с переменной, если изменить её внутри метода.
Тип значения (value type) хранит данные прямо в переменной. Ссылочный тип (reference type) хранит только адрес — сами данные лежат отдельно. При передаче в метод C# всегда копирует содержимое переменной: у одних это само значение, у других — всего лишь ссылка на него.
Вопрос: что выведет этот код?
Прочитайте и ответьте про себя, прежде чем нажимать «Запустить». Именно так вас и попросят на интервью — с листком бумаги, без IDE.
using System;
class Box
{
public int Value;
}
class Program
{
static void ChangeInt(int n)
{
n = 100;
}
static void ChangeBox(Box b)
{
b.Value = 100;
}
static void ReplaceBox(Box b)
{
b = new Box { Value = 999 };
}
static void Main()
{
int number = 1;
ChangeInt(number);
Console.WriteLine(number);
Box box = new Box { Value = 1 };
ChangeBox(box);
Console.WriteLine(box.Value);
ReplaceBox(box);
Console.WriteLine(box.Value);
}
}
Результат:
1
100
100
Почему так
1. int не изменился — потому что уехала копия
int — тип значения. В переменной number лежит само число 1. Когда мы зовём ChangeInt(number), в параметр n копируется четвёрка байт. Метод меняет свою копию, копия умирает вместе с кадром метода, оригинал не в курсе. Вывод — 1.
2. Поле объекта изменилось — потому что копия ссылки указывает туда же
Box — класс, то есть ссылочный тип. В переменной box лежит не объект, а адрес объекта в куче. При вызове ChangeBox(box) копируется адрес — маленькое число (8 байт на 64-битной платформе). Но указывает эта копия на тот же самый объект. Строчка b.Value = 100 идёт по адресу и правит поле оригинального объекта. Вывод — 100.
3. Замена объекта не видна снаружи — вот где ломаются кандидаты
ReplaceBox делает b = new Box { Value = 999 }. Здесь мы меняем не объект, а переменную b — то есть локальную копию адреса. Она начинает указывать на новый объект, а box в Main по-прежнему указывает на старый. Вывод — снова 100, а не 999.
Отсюда правильная формулировка, которую ждёт интервьюер: в C# по умолчанию всё передаётся по значению; для ссылочного типа «значением» является ссылка. Фраза «классы передаются по ссылке» — популярная неточность, и третья строка вывода — её опровержение.
А как передать по-настоящему по ссылке
Для этого есть модификаторы. С ref в метод уезжает не копия переменной, а сама переменная (её адрес):
using System;
class Box
{
public int Value;
}
class Program
{
static void ReplaceBox(ref Box b)
{
b = new Box { Value = 999 };
}
static void Bump(ref int n)
{
n++;
}
static void Main()
{
Box box = new Box { Value = 1 };
ReplaceBox(ref box);
Console.WriteLine(box.Value);
int number = 41;
Bump(ref number);
Console.WriteLine(number);
}
}
Результат:
999
42
| Модификатор | Смысл |
ref | переменная передаётся по ссылке, до вызова должна быть инициализирована |
out | то же, но инициализировать обязан сам метод (классика — int.TryParse) |
in | по ссылке, но только для чтения — чтобы не копировать большую структуру |
Структуры: копируется всё содержимое
Со struct та же история, что с int, — при присваивании копируются все поля:
using System;
struct Point
{
public int X;
public int Y;
}
class Program
{
static void Main()
{
Point a = new Point();
a.X = 1;
a.Y = 2;
Point b = a;
b.X = 42;
Console.WriteLine($"a.X = {a.X}, b.X = {b.X}");
}
}
Результат:
a.X = 1, b.X = 42
Будь Point классом, обе переменные смотрели бы на один объект и вывелось бы a.X = 42. Одна буква в объявлении типа — и поведение программы противоположное.
Ловушка со string
Любимый добивающий вопрос: «А string — это value или reference?»
using System;
class Program
{
static void Shout(string s)
{
s = s.ToUpper();
}
static void Main()
{
string name = "codechick";
Shout(name);
Console.WriteLine(name);
}
}
Результат:
codechick
string — ссылочный тип, но неизменяемый: ToUpper() не правит строку на месте, а создаёт новую. Метод переприсвоил свою копию ссылки — снаружи ничего не поменялось. Строка ведёт себя «как значение» не потому, что она value type, а потому что immutable.
Как это работает под капотом
Учебная формула «значимые типы — в стеке, ссылочные — в куче» верна лишь наполовину, и хороший интервьюер это проверит. Точнее так:
- Объект класса всегда живёт в куче (managed heap), а переменная-ссылка — там, где объявлена.
- Значимый тип живёт там, где живёт его владелец. Локальная переменная
int— да, обычно в стеке (или прямо в регистре процессора). Но полеintвнутри класса лежит в куче, внутри объекта. Элементыint[]— тоже в куче. Структура, захваченная лямбдой или живущая вasync-методе, уезжает в объект-замыкание или в машину состояний — то есть снова в кучу. - Стек и куча — это деталь реализации JIT и рантайма, а не буква спецификации C#. Спецификация говорит про семантику копирования и время жизни, а не про регион памяти.
Правильный ответ на собеседовании звучит так: «Разница не в стеке и куче, а в том, что копируется при присваивании: у значимых типов — данные, у ссылочных — адрес». Это сразу отличает зубрилу от человека, который понимает модель памяти.
Частые ошибки на собеседовании
- «Классы передаются по ссылке». Нет: по значению передаётся копия ссылки. Разницу видно, только когда метод присваивает параметру новый объект.
- «Значимые типы всегда в стеке». Нет: поля, элементы массивов, замыкания и
asyncутаскивают их в кучу. - Путать
refиout.outобязывает метод присвоить значение и не требует инициализации до вызова. - Считать
stringзначимым типом. Он ссылочный, просто неизменяемый. - Забывать про
null. Ссылочная переменная может не указывать никуда; уintтакого состояния нет — нуженNullable<int>(он жеint?).
Итоги-шпаргалка
- Значимые типы (
int,double,bool,char,enum, любойstruct, включаяDateTime,Guid,decimal) хранят данные; ссылочные (class,interface,delegate, массивы,string,object) хранят адрес. - В C# всё передаётся по значению — вопрос лишь в том, что является значением: данные или ссылка.
- Изменение полей объекта видно вызывающему коду; переприсваивание параметра — нет.
- Настоящая передача по ссылке — только через
ref/out/in. - Присваивание структуры копирует все её поля; присваивание объекта копирует один адрес.
string— ссылочный и неизменяемый: любая «модификация» рождает новую строку.