Найти в Дзене
Unity и геймдев | aks2dio

Шпаргалка по базовым концепциям C#

Уровень материала: 🐣 #junior Хотел к кейсам по ref из видео оставить ссылок на доп материалы и сделать конспект. Т.к. тема интересная и часто используемая в популярных ECS-фреймворках. Но увлёкся дополнением контекста — слишком всё разрослось. Попробовал ужать до формата карточек, но туда тоже никак не помещается. В общем, ни туда, ни сюда. Удалять тоже жалко. Оставлю в формате "шпаргалки". Может окажется полезно. А может кринж-треш-слоп. Как минимум, сам многое с удовольствием перечитал и повторил. Подробнее: 🔗 wiki.merion.net Цельная непрерывная область памяти, которая работает по принципу LIFO (Last‑In, First‑Out). Что хранит: локальные переменные функции, аргументы функции, информация о вызовах функций. Принцип: Свойства: Подробнее: 🔗 wiki.merion.net Область памяти для динамического выделения ресурсов. Что хранит: объекты, массивы, структуры нефиксированного размера, долгоживущие данные. Принцип: Свойства: Документация: 🔗 learn.microsoft.com 💬 Для простоты восприятия можно счи
Оглавление

Уровень материала: 🐣 #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 не живёт дольше, чем переменная, на которую она ссылается.