Уровень материала: 🐣 #junior
Хотел к кейсам по ref из видео оставить ссылок на доп материалы и сделать конспект. Т.к. тема интересная и часто используемая в популярных ECS-фреймворках.
Но увлёкся дополнением контекста — слишком всё разрослось. Попробовал ужать до формата карточек, но туда тоже никак не помещается.
В общем, ни туда, ни сюда. Удалять тоже жалко. Оставлю в формате "шпаргалки". Может окажется полезно. А может кринж-треш-слоп. Как минимум, сам многое с удовольствием перечитал и повторил.
✏️ Стек (stack)
Подробнее: 🔗 wiki.merion.net
Цельная непрерывная область памяти, которая работает по принципу LIFO (Last‑In, First‑Out).
Что хранит: локальные переменные функции, аргументы функции, информация о вызовах функций.
Принцип:
- При вызове функции выделяется фрейм стека (stack frame) для этой функции.
- При завершении функции указатель (stack pointer) смещается на предыдущий фрейм.
Свойства:
- Автоматическая и простая очистка.
- Быстрый доступ и частое попадание в кэш.
- Ограничен, может переполниться (stack overflow).
- Для каждого потока свой стек.
✏️ Куча (heap)
Подробнее: 🔗 wiki.merion.net
Область памяти для динамического выделения ресурсов.
Что хранит: объекты, массивы, структуры нефиксированного размера, долгоживущие данные.
Принцип:
- При создании объекта в куче выделяется блок памяти, на который возвращается ссылка.
- Пока на объект есть ссылки, объект остаётся в памяти.
- Когда ссылок не остаётся, сборщик мусора освобождает занятую память и уплотняет кучу (перемещает данные) для снижения фрагментации.
Свойства:
- Очистка через сборщик мусора.
- Работа сборщика мусора тормозит выполнение.
- Нельзя предсказать, когда объект будет удалён.
- Данные в куче располагаются "беспорядочно".
- Чаще промахи кэша (cache miss).
- Доступ к данным сложнее и дольше.
- Может возникать фрагментация.
- Все потоки работают с одной кучей.
✏️ Указатель (pointer)
Документация: 🔗 learn.microsoft.com
- Доступен только через unsafe { int* ptr = &value; ... }.
- Даёт доступ к адресам памяти.
- Можно смещать указатель: ptr + 1 (но есть ограничения).
- Размер смещения зависит от типа. Для int смещение +1 — это +4 байта.
- Чтобы получить значение, нужно разыменовать (deferencing): int result = *ptr.
- Некорректное разыменование может привести к вылету.
- Может указывать на "чужой" участок памяти.
- Требует ручного контроля за памятью и целостностью данных.
- Есть механизм закрепления (pinning) объектов в памяти через fixed, чтобы сборщик мусора не перемещал объект при уплотнении кучи: fixed (int* p = array).
✏️ Ссылка (reference)
- Абстракция над адресом, т.к. адрес может меняться: new MyClass().
- Привязана к объекту. Нельзя "перемещать" или "смещать".
- Автоматическое разыменование: obj.Method().
- Некорректное обращение приводит к исключениям.
- Гарантия валидного объекта по ссылке или null.
- Сборщик мусора управляет очисткой и обновлением ссылки при перегруппировке данных.
💬 Для простоты восприятия можно считать, что ссылка — это указатель на участок памяти или адрес. Но технически — это не так. Это чуть более сложная конструкция, ограничивающая возможности, но и кратно упрощающая взаимодействие.
✏️ Значимые типы (value types)
Документация: 🔗 learn.microsoft.com
- Переменная хранит сами данные. int value = 67.
- Хранится там, где объявлено. В т.ч. в стеке.
- При присваивании в другую переменную происходит копирование значения.
- При передаче в метод передаётся копия значения.
Когда в стеке:
- Аргументы функции.
- Переменные внутри функции.
Когда в куче:
- Поле класса (класс в куче).
- Элемент массива (массив в куче).
- Статическое поле (долго живёт).
- Упаковывание (boxing).
- Замыкание (closure).
✏️ Ссылочные типы (reference types)
Документация: 🔗 learn.microsoft.com
- Переменная хранит ссылку на данные, которые хранятся в другом месте. MyClass mc = new(67).
- Ссылка хранится там, где объявлена. Значение — всегда в куче.
- При присваивании в другую переменную копируется ссылка, но не значение по ссылке. Обе ссылки будут указывать на одно и то же значение.
- При передаче в метод передаётся копия ссылки. Внутри метода присваивания в эту копию не повиляют на "оригинальную" ссылку.
💬 Именно копия, не оригинальная ссылка. Присваивание в такую переменную внутри метода не повлияет на "оригинальную" ссылку. В то время как изменение значения по этой ссылке отразиться везде.
💬 Для простоты восприятия можно считать, что сама ссылка — это значимый тип (но технически это не тип, а механизм). Ведёт себя она аналогично. Просто при обращении к ссылке автоматически выдаются данные из кучи, на которые она ссылается.
✏️ Упаковывание (boxing)
Документация: 🔗 learn.microsoft.com
Процесс преобразования значимого типа в ссылочный тип, когда значимый тип используется там, где ожидается ссылочный.
Когда происходит:
- Присваивание в переменную ссылочного типа: object obj = 67.
- Передача в аргумент ссылочного типа: MethodForObject(67).
- Вызов общих для всех типов object-методов: (67).GetType().
- Использование через ссылку на интерфейс: IComparable ms = new MyStruct().
- Использование коллекций без дженериков (ArrayList вместо List<T> и т.д.): (new ArrayList()).Add(67).
💬 Вызовы виртуальных методов GetHashCode, Equals и ToString к боксингу тоже могут приводить (но что-то может соптимизировать и компилятор). Явное переопределение этих методов позволяет избежать боксинга.
Какие проблемы:
- Выделение памяти для нового объекта в куче.
- Доп нагрузка на сборщик мусора.
- Время на копирование из стека в кучу.
✏️ Замыкание (closure)
Документация: 🔗 learn.microsoft.com
Функция, которая сохраняет доступ к переменным из своего внешнего контекста, даже после того, как этот контекст завершил выполнение.
Как работает:
Компилятор генерирует специальный класс, где захваченные переменные становятся полями, а сама функция — его методом.
Когда происходит:
- При использовании лямбда-выражений или анонимных методов, которые обращаются к переменным из родительского метода.
- Т.е. работают не с локальными переменными и не с входящими аргументами.
```
int counter = 0;
Action increment = () => counter++;
```
Какие проблемы:
- Выделение памяти для нового объекта в куче.
- Боксинг значимых захваченных типов.
- Захваченные переменные не будут очищены, пока существует замыкание.
- Если была подписка замыканием, то без отписки захваченный контекст не будет очищен сборщиком мусора.
✏️ Parameter Modifiers
Документация: 🔗 learn.microsoft.com
Позволяют определять, каким образом передаются параметры в метод: по значению или по ссылке.
Передача по ссылке позволяет работать в методе с оригинальным значением любого типа.
💬 Это возможно даже для значимых типов в стеке. Т.к. передаём в следующий метод, то фрейм прошлого метода не будет удалён раньше, чем завершится следующий метод. Т.е. ссылка на данные в стеке будет актуальна на протяжении всего метода.
Когда может потребоваться передача по ссылке:
- Необходимость изменить оригинальное значение внутри метода.
- Работа с объёмной структурой, копирование которой затруднено или может привести к переполнению стека.
Для этого есть модификаторы ref и out, которые доступны в C# с самых ранних версий.
ref: передача параметра по ссылке, позволяя работать с оригинальными данными на чтение и запись.
```
int num = 5;
Increment(ref num);
Console.WriteLine(num); // 6
void Increment(ref int value) => value++;
```
- Переменная должна быть уже инициализирована до вызова метода (иначе работать будет не с чем).
- Если передать значимый тип, то передастся ссылка на оригинал значения в стеке или куче.
- Если передать ссылочный тип, то передастся оригинал ссылки, которую можно поменять или об'null'ить.
out: возврат дополнительного значения (помимо return) из метода через параметр по ссылке. Т.е. ref - про вход. А out - про выход.
```
int a;
GetValue(out int a);
Console.WriteLine($"{a}"); // 10
void GetValue(out a) => a = 10;
```
- Метод обязан присвоить значение out-параметру до завершения.
- Но передаваемый out-параметр не должен быть инициализирован до вызова метода.
💬 Так мы размечаем место в памяти под данные, но наполняем их во вложенном контексте. Значение попадёт в нужный фрейм стека вызывающего метода, и "не потеряется", когда вызванный метод закончит работу и очистит свой стек выполнения.
in: передача входного параметра для чтения. Как ref, но запрещает изменение внутри метода.
- Если передаётся ссылочный тип, то запрещается модификация его ссылки, но не его содержимого.
- Производительнее, чем ref.
```
void PrintList(in List<int> numbers)
{
// numbers = new List<int>() нельзя
numbers.Add(67); // можно
}
```
💬 В основном нужен для эффективной передачи больших структур с защитой от изменения.
💬 Появился только в C# 7.0.
✏️ ref struct
Документация: 🔗 learn.microsoft.com
Особый вид структуры, который всегда размещается только на стеке и не может быть перемещён в кучу.
💬 Доступно с C# 7.2
Зачем: строгий контроль памяти и высокая производительность.
Особенности:
- Нельзя использовать в асинхронных методах (частично).
- Недоступно для боксинга и замыкания.
- Не может быть элементом массива.
- Не может быть полем класса.
- Можно использовать интерфейсы, но нельзя к ним приводить.
- Можно использовать как дженерик: where T : allows ref struct.
- Можно использовать с IDisposable и using.
Примеры: Span<T> и ReadOnlySpan<T>.
```
public ref struct BufferReader
{
public ReadOnlySpan<byte> Buffer
public int Position;
}
```
Есть поддержка readonly ref struct.
✏️ ref return
Документация: 🔗 learn.microsoft.com
Механизм, позволяющий возвращать из метода ссылку на переменную (а не её значение).
💬 Доступно с C# 7.0
Зачем: прямой производительный доступ к данным без копирования по стеку. Аналогично обычному ref.
Особенности:
- Только для value type и ref struct.
- Нет боксинга (исключены риски возникновения).
- Нельзя использовать для асинхронных методов.
- Нельзя возвращать ссылку на локальную переменную метода (уничтожится при выходе).
- Подходит только для переменных, время жизни которых гарантированно выходит за пределы выполнения метода.
Что можно вернуть:
- Элемент массива: ref array[index] (самый популярный кейс).
- Поле объекта: ref obj.Field.
- Переменную, переданную по ref/out в метод.
- Статическое поле.
- ref struct.
```
int[] arr = { 1, 3, 4, 7, 8 };
ref int evenRef = ref FindFirstEven(arr);
evenRef = 67; // 1, 3, 67. 7. 8
public ref int FindFirstEven(int[] numbers)
{
for (int i = 0; i < numbers.Length; i++)
if (numbers[i] % 2 == 0)
return ref numbers[i];
throw new Exception();
}
```
С C# 7.2 доступен ref readonly для возврата ссылки, которая доступна только для чтения.
✏️ ref local
Документация: 🔗 learn.microsoft.com
Переменная, которая хранит не само значение, а ссылку на другую переменную. Преимущественно для работы с ref return.
Особенности:
- ref local должна быть обязательно инициализирована в момент объявления.
- ref local не живёт дольше, чем переменная, на которую она ссылается.