Привет, Хабр!
Сегодня мы рассмотрим самый — казалось бы — скромный модификатор, который способен сэкономить кучу времени в горячих участках кода. Речь, конечно, про in
‑аргументы. Рассмотрим, чем они отличаются от ref
и out
, где ими действительно стоит пользоваться, а где лучше пройти мимо.
Ещё со времён C# 1.0 мы имели два способа передать значение «по ссылке»:
ref
— вызывающий обязан инициализировать переменную; метод может менять её содержимое.out
— вызывающий может передать мусор; метод гарантированно задаёт значение, иначе компилятор ругнётся.
Обе опции выполняют одно и то же по своей сути: метод получает адрес переменной, а не копию. Отличие лишь в том, кто отвечает за инициализацию и что разрешено менять внутри метода.
Проблема выплыла, когда мы начали перебирать массивы больших значений. Каждое копирование 128-байтного struct
на каждый вызов — это лишние мегабайты памяти, cache miss и потеря драгоценных наносекунд. Поэтому в C# 7.2 подбросили третий вариант — in
— в одном пакете с readonly struct
.
in
in
говорит компилятору: «передай по ссылке, но не дай ничего менять». Синтаксис тривиален:
public static double Length(in BigVector v) =>
Math.Sqrt(v.X * v.X + v.Y * v.Y + v.Z * v.Z);
Под всем этим:
Передача по адресу. IL‑код содержит
ldarga.s
, неldarg.0
, а значит никаких копий.Защита от мутаций. Любая попытка изменить поле внутри метода — ошибка компиляции.
Дефензивная копия при подозрении. Если
BigVector
не помеченreadonly
, компилятор может скопировать значение, если счёт покажется сомнительным.
Чтобы копий не было вообще — делаем структуру readonly
:
public readonly struct BigVector
{
public readonly double X;
public readonly double Y;
public readonly double Z;
public BigVector(double x, double y, double z) => (X, Y, Z) = (x, y, z);
}
Теперь Length(in BigVector)
пройдет без лишних аллокаций и копирований даже в Release‑сборке с агрессивной inlining‑оптимизацией.
Что генерирует компилятор
Возьмём BenchmarkDotNet и проверим скорость трёх вариантов:
[StructLayout(LayoutKind.Sequential)]
public readonly struct Huge
{
public readonly long A, B, C, D, E, F, G, H;
}
public class Bench
{
private readonly Huge _value = new(1,2,3,4,5,6,7,8);
[Benchmark] public long ByValue() => Sum(_value);
[Benchmark] public long ByRef() => SumRef(ref _value);
[Benchmark] public long ByIn() => SumIn(in _value);
static long Sum(Huge h) => h.A + h.B + h.C + h.D + h.E + h.F + h.G + h.H;
static long SumRef(ref Huge h) => h.A + h.B + h.C + h.D + h.E + h.F + h.G + h.H;
static long SumIn(in Huge h) => h.A + h.B + h.C + h.D + h.E + h.F + h.G + h.H;
}
ByValue: компилятор делает копию Huge
(64 байта) на стеке вызова. ByRef: передаём адрес, но открываем ворота для мутаций — JIT не даёт проворачивать такие штуки в многопотоке без блокировок. ByIn: передаём адрес и обещаем неизменность, поэтому JIT смело инлайнит и читает данные напрямую из исходного адреса.
Результаты на.NET 9 Preview (Release, x64):
Method Mean Ratio Gen0 Allocated
ByValue 18.34 ns 2.01 - -
ByRef 9.12 ns 1.00 - -
ByIn 9я.18 ns 1.01 - -
Копия съела ровно половину производительности. in
даёт тот же выигрыш, что ref
, но без риска нечаянно мутировать значение.
Примеры применения
Тайм-слот в календарном сервисе
Задача. Для корпоративного календаря нужно быстро выбирать свободные окна. Интервал описывается TimeSlot
— пара DateTime
плюс флаги (64 байта). Функция Overlaps
вызывается тысячами раз при поиске общего слота для митинга.
public readonly struct TimeSlot
{
public readonly DateTime Start;
public readonly DateTime End;
public readonly byte Flags; // recurrence, PTO и т. д.
public TimeSlot(DateTime start, DateTime end, byte flags = 0) =>
(Start, End, Flags) = (start, end, flags);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Overlaps(in TimeSlot a, in TimeSlot b) =>
a.Start < b.End && b.Start < a.End;
64-байтный объект не копируется при каждом сравнении, а календарный алгоритм оперирует десятками тысяч ячеек за один запрос. Код остаётся thread‑safe — метод не может случайно поменять интервал.
Контекст логирования в микросервисах
Во внутренних API‑гейтвеях мы логируем каждый проход HTTP‑запроса. Контекст включает trace‑id, span‑id, user‑id и восемь флагов: struct LogScope
(48 байт). Его нужно передавать в цепочку LogDebug
, LogInfo
, LogError
.
public readonly struct LogScope
{
public readonly Guid TraceId;
public readonly Guid SpanId;
public readonly int UserId;
public readonly byte Flags;
}
public static void LogInfo(
in LogScope scope,
string message,
[CallerMemberName] string? member = null)
{
// быстрый StringBuilder-роут без аллокаций
}
Контекст создаётся один раз на входе и «едет» по всему графу вызовов без лишних дубликатов. Любая попытка мутировать LogScope
внутри лог‑метода ловится компилятором → нельзя случайно изменить trace‑id. Передача ссылки вместо копии заметно важна под нагрузкой 50–100 k RPS.
ETL-парсер CSV
Задача. Каждый вечер бухгалтерия грузит многогигабайтный CSV с операциями. Строка распарсена в TransactionRow
— 9 decimal
, 2 DateTime
, пара bool (около 104 байт). После парсинга десяток функций вычисляют налоги, валидации и агрегаты.
public readonly struct TransactionRow
{
public readonly decimal Amount;
public readonly decimal Tax;
public readonly decimal Fee;
// …ещё поля
public readonly DateTime Created;
public readonly DateTime Booked;
}
static decimal CalcVat(in TransactionRow row) => row.Amount * 0.20m;
static bool IsSuspicious(in TransactionRow row) =>
row.Fee > 100m && (row.Created - row.Booked).TotalDays > 3;
Парсинг и валидация ходят по тем же данным 3–4 раза; без in
каждая функция копирует 100+ байт. Структура readonly
, значит JIT оптимизирует доступы напрямую.
Сравнение с in, ref и out
Характеристика |
|
|
|
---|---|---|---|
Как передаётся |
По адресу (без копии) |
По адресу |
По адресу |
Можно ли менять значение в теле метода |
Нельзя — компилятор запретит |
Можно |
Обязательно задать перед выходом |
Требуется ли инициализация до вызова |
Да |
Да |
Нет |
Риск защитной копии от компилятора |
Есть, если |
Нет |
Нет |
Подходит для ссылочных типов |
Не даёт плюсов, передавать бессмысленно |
Не даёт плюсов |
Не даёт плюсов |
Типичный кейс |
Чтение «толстых» |
Двусторонний обмен данными, быстрая мутация |
Множественный «выход» из метода (Try‑API) |
Потенциальные ловушки |
Boxing с интерфейсами, async‑замыкания |
Сложнее параллелить из‑за мутабельности |
Лишняя связность, зачастую хуже, чем возвращаемое значение |
in
берите, когда методу нужно только читать крупный readonly struct
(20 + байт) в горячем цикле: адрес передаётся без копии, JIT смело инлайнит, а запрет на мутации не даёт случайно расшатать данные. Для ссылочных типов, мелких структур или ситуаций, где всё равно придётся менять поля, профита нет — оставляйте обычную передачу по значению или переходите на ref
.
ref
— ваш выбор, если нужно передать и тут же изменить объект (будь то struct или класс) без лишних аллокаций, но помните о потокобезопасности: мутабельность усложняет жизнь в параллели. out
остается рабочей лошадкой Try‑паттерна и многовыходных методов: запрашиваете ресурс, получаете bool ok
плюс заполненные параметры. Во всех менее горячих сценариях выгоднее вернуть результат кортежем или record‑типом.
Спасибо, что дочитали. Если есть интересный опыт с in
— делитесь в комментариях!
Если вы до сих пор не понимаете, почему одни алгоритмы работают быстрее других или как не допускать архитектурных ошибок в коде, возможно, вам стоит обратить внимание на эти темы. Не теряйте время на поиски решений на уровне "потому что так работает". Разберитесь, что стоит за оптимизацией и правильно строите приложение с самого начала.
3 июля в 20:00 — Анализ сложности алгоритмов и сортировка на C#
Поговорим о том, что такое алгоритмическая сложность и как она влияет на производительность кода.15 июля в 20:00 — Переиспользуемый код на C#: архитектурный подход
Обсудим принципы архитектуры приложения и применение SOLID, DRY, KISS, YAGNI.
Получить глубокие знания C# и практические навыки с поддержкой преподавателей можно с нуля на специализации "C# Developer".
Комментарии (2)
malstraem
01.07.2025 06:12Разбирать модификатор из версии языка 7.2 в 25 году это сильно.
Там, где нужна передача жирной структуры по ссылке и мутации нет, на замену
in
давно пришелref readonly
- ругается на передачу по значению в компайл тайме.
iamkisly
Больше статей богу статей ?
ref, out, in: как понять, кто из них тебе нужен / Хабр