Привет, Хабр!
Сегодня мы рассмотрим самый — казалось бы — скромный модификатор, который способен сэкономить кучу времени в горячих участках кода. Речь, конечно, про 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".
Комментарии (5)
 - malstraem01.07.2025 06:12- Разбирать модификатор из версии языка 7.2 в 25 году это сильно. - Там, где нужна передача жирной структуры по ссылке и мутации нет, на замену - inдавно пришел- ref readonly- ругается на передачу по значению в компайл тайме. - Proscrito01.07.2025 06:12- И в отличие от in гарантированно избегает defensive copy. А еще сейчас появился дополнительный scoped. Как-то все это выкинуть из контекста совсем не круто для статьи про современный сишарп. 
 
 - S-type01.07.2025 06:12- Защита от мутаций. Любая попытка изменить поле внутри метода — ошибка компиляции. 
 - Враньё. Наружу, конечно, изменения не уйдут. Но, если по in передать экземпляр класса, то внутри метода меняй сколько угодно - ни чего тебе компилятор не скажет. 
 
           
 
iamkisly
Больше статей богу статей ?
ref, out, in: как понять, кто из них тебе нужен / Хабр