out в C# — это модификатор параметра, который передает аргумент по ссылке и обязывает метод записать в него значение до выхода. Так метод возвращает дополнительный результат через параметр: классический пример — int.TryParse(text, out int n), где bool сообщает об успехе, а само число приходит через out.
Содержание
- Термины: что именно называют «модификаторами»
- Сводная таблица отличий
- Минимальный пример: все модификаторы в одной программе
- out и TryParse: главный практический сценарий
- ref: когда метод меняет переменную вызывающего
- in и ref readonly: передача без копирования и без записи
- params: переменное число аргументов
- Ошибки компиляции: неверно -> сообщение -> исправление
- out или кортеж
- Выводы
- Где применяется / связь с практикой
- FAQ
У out есть родственники: ref (передать по ссылке с правом читать и менять), in и ref readonly (передать по ссылке только для чтения) и params (передать переменное число аргументов). Ниже — таблица отличий, рабочие примеры и ошибки компиляции, на которых спотыкаются чаще всего. Все примеры проверены на .NET SDK 10.0.401 (C# 14).
Термины: что именно называют «модификаторами»
В заголовке старой версии статьи стояло «модификаторы методов», но точнее — модификаторы параметров: они ставятся перед отдельным параметром, а не перед методом. Модификаторы доступа (public, private и т.д.) — другая тема, здесь их не разбираем.
- Передача по значению — метод получает копию значения аргумента. Для ссылочного типа копируется сама ссылка, а не объект.
- Передача по ссылке — параметр становится псевдонимом переменной вызывающего кода: запись в параметр меняет эту переменную.
- Модификатор на стороне вызова — слово
ref/out/inперед аргументом при вызове. Дляrefиoutоно обязательно, дляin— нет.
Сводная таблица отличий
| Модификатор | Нужно ли инициализировать аргумент до вызова | Метод может читать | Метод обязан/может записать | Слово при вызове | Типичный сценарий |
|---|---|---|---|---|---|
| нет (по значению) | да | да | меняет только свою копию | не пишется | обычный случай |
out |
нет | только после своей записи | обязан записать на каждом пути выхода | out обязательно |
вернуть второй результат, Try-методы |
ref |
да | да | может | ref обязательно |
изменить переменную вызывающего |
in |
да (или выражение — тогда будет временная копия) | да | запрещено | in необязательно |
большая структура без копирования |
ref readonly (C# 12+) |
да, ожидается переменная | да | запрещено | ref или in, иначе предупреждение |
API, где важно, что передается именно переменная |
params |
— | да | — | не пишется, аргументы через запятую | переменное число аргументов |
Правило выбора коротко. Если метод должен вернуть еще одно значение — out (или кортеж, см. ниже). Если должен поменять существующее значение — ref. Если хочет только прочитать большую структуру — in. Если число аргументов заранее неизвестно — params.
Минимальный пример: все модификаторы в одной программе
int a = 5;
ByValue(a);
Console.WriteLine($"по значению: {a}"); // 5
int b = 5;
ByRef(ref b);
Console.WriteLine($"ref: {b}"); // 6
int q, r; // присваивать не нужно
DivRem(17, 5, out q, out r);
Console.WriteLine($"out: {q} и {r}"); // 3 и 2
var big = new BigStruct(1, 2, 3, 4);
Console.WriteLine($"in: {Sum(in big)}"); // 10
Console.WriteLine($"params: {Total(1, 2, 3)} {Total()}"); // 6 0
static void ByValue(int n) => n++;
static void ByRef(ref int n) => n++;
static void DivRem(int x, int y, out int quotient, out int remainder)
{
quotient = x / y;
remainder = x % y;
}
static long Sum(in BigStruct s) => s.A + s.B + s.C + s.D;
static int Total(params int[] items) => items.Sum();
readonly record struct BigStruct(long A, long B, long C, long D);
Вывод в консоли:
по значению: 5
ref: 6
out: 3 и 2
in: 10
params: 6 0
Разбор по строкам. ByValue увеличил свою копию, поэтому a осталось 5. ByRef работал с самой переменной b — она стала 6. В DivRem переменные q и r объявлены без значения: для out это нормально, метод обязан их заполнить. Sum читает структуру через in без копирования ее четырех полей. Total() без аргументов получил пустой массив и вернул 0.
out и TryParse: главный практический сценарий
Методы вида TryXxx — основное место, где новичок встречает out. Они не бросают исключение при плохом вводе, а возвращают false.
using System.Globalization;
string[] inputs = { "42", " 7 ", "12abc", "", "99999999999" };
foreach (var s in inputs)
{
if (int.TryParse(s, out int n))
Console.WriteLine($"'{s}' -> {n}");
else
Console.WriteLine($"'{s}' -> не число");
}
// Если нужен только факт успеха - отбрасываем результат
bool ok = int.TryParse("15", out _);
Console.WriteLine(ok); // True
// Дробные числа: культура решает, что считать разделителем
bool parsed = double.TryParse("3.14", NumberStyles.Float,
CultureInfo.InvariantCulture, out double pi);
Console.WriteLine($"{parsed} {pi.ToString(CultureInfo.InvariantCulture)}"); // True 3.14
// Свой Try-метод по тому же образцу
if (TryParsePoint("10;-3", out var x, out var y))
Console.WriteLine($"x={x}, y={y}"); // x=10, y=-3
static bool TryParsePoint(string text, out int x, out int y)
{
x = 0; y = 0; // out обязан получить значение на любом пути
var parts = text.Split(';');
return parts.Length == 2
&& int.TryParse(parts[0], out x)
&& int.TryParse(parts[1], out y);
}
'42' -> 42
' 7 ' -> 7
'12abc' -> не число
'' -> не число
'99999999999' -> не число
True
True 3.14
x=10, y=-3
Что здесь важно. Пробелы по краям int.TryParse по умолчанию допускает, а число вне диапазона int дает false, а не исключение. Запись out int n объявляет переменную прямо в вызове (с C# 7.0), out _ — отбрасывание, когда значение не нужно. При неудаче int.TryParse записывает в out-параметр 0, поэтому смотреть на число без проверки bool бессмысленно.
Для дробных чисел культура важна: без CultureInfo.InvariantCulture на машине с русской локалью, где десятичный разделитель — запятая, double.TryParse("3.14", out var d) вернет false, а d будет 0. Если входные данные машинные (файлы, API) — передавайте культуру явно.
У своего TryParsePoint есть граница: при частичной неудаче ("10;abc") в x останется 10, а в y — 0. Вызывающий код должен доверять значениям только при true, как и у встроенных Try-методов.
ref: когда метод меняет переменную вызывающего
С типами-значениями (int, структуры) эффект ref очевиден. Со ссылочными типами путаница частая: массив и так «передается по ссылке»? Нет — по значению передается ссылка. Разница видна на примере:
int[] data = { 1, 2, 3 };
Replace(data);
Console.WriteLine(string.Join(",", data)); // 1,2,3 - замена копии ссылки не видна
ReplaceRef(ref data);
Console.WriteLine(string.Join(",", data)); // 9,9 - переменная вызывающего перенаправлена
Mutate(data);
Console.WriteLine(string.Join(",", data)); // 0,9 - содержимое массива меняется и без ref
static void Replace(int[] arr) => arr = new[] { 9, 9 };
static void ReplaceRef(ref int[] arr) => arr = new[] { 9, 9 };
static void Mutate(int[] arr) => arr[0] = 0;
Вывод: 1,2,3, затем 9,9, затем 0,9. Итог для выбора: менять содержимое объекта можно и без ref; ref нужен, только если метод должен заменить сам объект в переменной вызывающего (как Array.Resize(ref arr, n)).
in и ref readonly: передача без копирования и без записи
in полезен для крупных структур (условно, десятки байт и больше): метод получает ссылку и не может записать в параметр. Для int или небольших структур выигрыша обычно нет — ссылка стоит столько же, сколько копия. Оценка «крупная» — ориентир, реальную пользу подтверждает только замер.
Граница, о которой стоит знать: если структура не readonly, вызов ее метода через in-параметр может заставить компилятор сделать защитную копию, и экономия пропадет. Поэтому in лучше сочетать с readonly struct, как BigStruct в первом примере.
ref readonly (C# 12) похож на in, но сигнализирует, что ожидается именно переменная. Компилятор проверяет вызов строже:
int v = 3;
M.Peek(v); // предупреждение CS9192
M.Peek(5); // предупреждение CS9193
M.Peek(in v); // ок
M.Peek(ref v); // ок
static class M
{
public static void Peek(ref readonly int n) => Console.WriteLine(n);
}
Program.cs(2,8): warning CS9192: Argument 1 should be passed with 'ref' or 'in' keyword
Program.cs(3,8): warning CS9193: Argument 1 should be a variable because it is passed to a 'ref readonly' parameter
В прикладном коде ref readonly встречается редко — его чаще видишь в библиотечных API, которые раньше принимали ref, а теперь хотят запретить запись, не ломая существующие вызовы.
params: переменное число аргументов
params позволяет вызывать метод как Max(3, 8, 1) вместо Max(new[] { 3, 8, 1 }). Параметр с params должен быть последним в списке, и такой параметр у метода может быть только один. До C# 13 он мог быть только одномерным массивом; с C# 13 допускаются и коллекции, например ReadOnlySpan<T> или List<T>.
Console.WriteLine(Max(3, 8, 1)); // 8
int[] arr = { 4, 5 };
Console.WriteLine(Max(arr)); // 5 - массив передается как есть
Console.WriteLine(Log("id={0}, user={1}", 7, "ann")); // [INFO] id=7, user=ann
static int Max(params ReadOnlySpan<int> items) // C# 13+
{
if (items.IsEmpty) throw new ArgumentException("нужен хотя бы один аргумент");
int m = items[0];
foreach (var i in items) if (i > m) m = i;
return m;
}
static string Log(string format, params object[] args) => "[INFO] " + string.Format(format, args);
Вызов без аргументов у params законен, поэтому пустой набор нужно обрабатывать явно, как в Max. params — не передача по ссылке, а синтаксическое удобство: он не сочетается с ref/out/in на том же параметре.
Ошибки компиляции: неверно -> сообщение -> исправление
Самая частая ошибка с out — забыть присвоить значение на одном из путей выхода:
static bool TryGet(string s, out int value)
{
if (s.Length > 0) { value = s.Length; return true; }
return false; // value не присвоено
}
error CS0177: The out parameter 'value' must be assigned to before control leaves the current method
Исправление: присвоить значение по умолчанию в начале метода (value = 0;) или в каждой ветке перед return. Остальные типовые ошибки — таблицей; тексты сообщений взяты из прогона на .NET SDK 10.0.401.
| Что написано | Ошибка компилятора | Как исправить |
|---|---|---|
int x; Inc(ref x); |
CS0165: Use of unassigned local variable ‘x’ | для ref инициализировать до вызова или заменить на out |
Inc(y); при Inc(ref int n) |
CS1620: Argument 1 must be passed with the ‘ref’ keyword | дописать ref при вызове |
| чтение out-параметра до записи | CS0269: Use of unassigned out parameter ‘n’ | сначала записать, потом читать |
void Show(in int n) { n = 10; } |
CS8331: Cannot assign to variable ‘n’ … because it is a readonly variable | убрать запись или сменить модификатор на ref |
async Task LoadAsync(out int n) |
CS1988: Async methods cannot have ref, in or out parameters | вернуть Task<(bool, int)> или объект результата |
| лямбда внутри метода использует ref-параметр | CS1628: Cannot use ref, out, or in parameter ‘n’ inside an anonymous method, lambda expression… | скопировать значение в локальную переменную |
перегрузки A(ref int) и A(out int) (так же для пар ref/in и in/out) |
CS0663: cannot define an overloaded method that differs only on parameter modifiers ‘out’ and ‘ref’ | переименовать один из методов |
P(params int[] a, string s) |
CS0231: A params parameter must be the last parameter in a parameter list | перенести params в конец |
Перегрузка «по значению» и «с ref» (A(int) и A(ref int)) при этом допустима — запрещено различать методы только модификаторами ref, out и in между собой.
out или кортеж
Для нового кода, где метод просто возвращает несколько значений, часто читаемее кортеж: (int q, int r) DivRem(int x, int y). Он работает и в async-методах, где out запрещен. out остается естественным выбором для паттерна Try (результат bool удобно ставить прямо в if) и там, где этого требует существующий API.
Выводы
- out передает аргумент по ссылке и обязывает метод записать значение на каждом пути выхода; инициализировать переменную до вызова не нужно.
- ref требует инициализированную переменную и дает методу право читать и менять ее; для ссылочных типов он нужен, только чтобы заменить сам объект.
- in и ref readonly передают по ссылке только для чтения; выигрыш заметен на крупных readonly-структурах, а ref readonly строже проверяет, что передана переменная.
- params — синтаксис для переменного числа аргументов, с C# 13 не только массивы; параметр должен быть последним.
- Типовые ошибки ловит компилятор: CS0177, CS0165, CS1620, CS8331, CS1988.
Где применяется / связь с практикой
Освойте тему на практике
Try-методы, работа с числами и структурами, API без лишних копий — повседневная часть кода на C#: от разбора пользовательского ввода в консольной утилите до игровой логики в Unity и сервисов на ASP.NET Core. Разобраться с передачей параметров, типами-значениями и ссылочными типами последовательно, с практикой и ревью кода, можно на курсе «C#-разработчик. Базовый уровень». Чтобы посмотреть формат обучения, подойдут бесплатные открытые уроки Otus.
FAQ
Можно ли использовать out в свойствах и индексаторах?
Нет. Модификаторы ref и out применимы к параметрам методов, конструкторов, делегатов и локальных функций; у параметров индексатора они запрещены (CS0631), допустим только in. Свойство нельзя передать как ref- или out-аргумент (CS0206), его значение сначала копируют в локальную переменную.
Нужно ли писать out при вызове, если метод объявлен с out?
Да, для out и ref слово при вызове обязательно — это видимый сигнал, что переменная может измениться. Для in его можно опустить.
Что быстрее — out или возврат кортежа?
Для нескольких int или ссылок разница обычно несущественна; выбирать стоит по читаемости и ограничениям (async, лямбды). Если производительность критична — решает замер на своем коде.



