По умолчанию C# передаёт каждый аргумент по значению: метод получает копию, и присваивание параметру не затрагивает вызывающий код. Модификаторы ref, out и in вместо этого передают саму переменную вызывающего кода.
Вывод:
by value: 50
by ref: 60
Ключевое слово встречается дважды: в списке параметров (ref int score) и в месте вызова (ref score). Писать его в вызове обязательно, и это сделано намеренно: любой, кто читает AddPoints(ref score), знает, что score может измениться. Забудьте его, и компилятор сообщит CS1620, Argument 1 must be passed with the 'ref' keyword.
ref: читать и писать переменную вызывающего кода
Параметр ref это псевдоним переменной вызывающего кода. Метод может прочитать текущее значение и заменить его. Классический пример это обмен значениями, невозможный с обычными параметрами:
Вывод:
Linus Ada
volume 100
Правила для ref:
- Аргумент должен быть переменной (локальной переменной, параметром, полем или элементом массива), а не литералом или выражением. Свойство тоже не подходит:
Clamp(ref player.Volume, 0, 100)это ошибка CS0206,A non ref-returning property or indexer may not be used as an out or ref value. Скопируйте свойство в локальную переменную, передайте её и присвойте значение обратно. - Переменная должна быть определённо присвоена до вызова.
int n; Clamp(ref n, 0, 10);это CS0165,Use of unassigned local variable 'n'.
out: дополнительные результаты
out предназначен для значений, которые метод производит. Вызывающему коду не нужно инициализировать переменную, а метод обязан присвоить её на каждом пути до возврата:
static void Split(string fullName, out string first, out string last)
{
// error CS0177: The out parameter 'last' must be assigned to before control leaves the current method
first = fullName.Split(' ')[0];
}
Внутри метода параметр out считается неприсвоенным, пока вы в него не запишете, поэтому прочитать старое значение вызывающего кода нельзя. В этом отличие от ref: ref работает в обе стороны, out в одну сторону, от метода к вызывающему коду.
Шаблон TryParse
Самое известное применение out это шаблон Try в базовой библиотеке: вернуть bool для признака успеха и положить результат в параметр out, чтобы плохой ввод никогда не выбрасывал исключение:
Вывод:
"vip25" is a coupon: 25% off
"42" is a quantity: 42
"BOGUS" is not recognized
"" is not recognized
out var quantity (C# 7.0) объявляет переменную прямо в вызове; её тип берётся из параметра. При неудаче TryParse присваивает ей 0 и возвращает false. Когда важно только, удался ли разбор, отбросьте значение через out _: bool isNumber = int.TryParse(input, out _);.
Обратите внимание, как TryGetDiscount присваивает rate на обоих путях: один раз явно и один раз передавая его в Dictionary.TryGetValue, который сам является вызовом с out.
out или возврат кортежа
До C# 7 out был основным способом вернуть несколько значений. Теперь кортежи делают это прямее:
// with out parameters
static void MinMax(int[] values, out int min, out int max) { ... }
MinMax(temps, out int lo, out int hi);
// with a tuple
static (int Min, int Max) MinMax(int[] values) { ... }
var (lo, hi) = MinMax(temps);
Используйте кортеж (или небольшой класс) для обычных множественных результатов. Оставьте out для шаблона Try, где возвращаемый bool позволяет поместить вызов в условие if.
Ссылочные типы: изменить объект или заменить его
Передача экземпляра класса по значению уже позволяет методу менять объект, потому что копируемое значение это ссылка на тот же объект. ref добавляет ещё одну возможность: заменить объект, на который указывает переменная вызывающего кода.
Вывод:
after AddItem: 2 items
after ResetByValue: 2 items
after ResetByRef: 0 items
ResetByValue направил свою копию ссылки на новый список; вызывающий код этого не увидел. ResetByRef изменил саму переменную вызывающего кода. В повседневном коде ref для параметра-класса нужен редко: возвращайте новый объект.
Для структур разница больше, поскольку аргумент-структура копируется поле за полем. Методу, который должен изменить структуру вызывающего кода (Point, большую структуру настроек), нужен ref.
in: по ссылке только для чтения
C# 7.2 добавил in, который передаёт по ссылке, но запрещает присваивание. Его цель производительность: большая структура не копируется при каждом вызове, а вызывающему коду гарантируется, что она не изменится.
// C# 7.2 and later
struct Matrix4
{
public double M11, M12, M13, M14, M21, M22, M23, M24,
M31, M32, M33, M34, M41, M42, M43, M44;
}
static double Trace(in Matrix4 m) => m.M11 + m.M22 + m.M33 + m.M44;
static void Reset(in Matrix4 m)
{
m.M11 = 0;
// error CS8332: Cannot assign to a member of variable 'm' or use it as the right hand side of a ref assignment because it is a readonly variable
}
В месте вызова in необязателен: работают и Trace(matrix), и Trace(in matrix). Для int, double, DateTime и других маленьких типов in ничего не даёт и может быть немного медленнее; используйте его для структур из нескольких полей, которые вы часто передаёте.
Ограничения
- Параметры
refиoutне могут быть необязательными (CS1741). Параметрinможет:static void Connect(in int retries = 3)допустимо. - Перегрузки не могут различаться только
refиout(илиin), потому что на уровне среды выполнения это одно и то же.F(int x)иF(ref int x)могут сосуществовать. - Методы
asyncи итераторы (методы сyield return) не могут иметь параметровref,outилиin. Возвращайте из них кортеж или объект результата. - Лямбда или локальная функция не может захватить параметр
ref,outилиinобъемлющего метода; сначала скопируйте его в локальную переменную.
Сравнение ref, out и in
| Вызывающий код обязан присвоить заранее | Метод обязан присвоить | Метод может читать | Ключевое слово в вызове | |
|---|---|---|---|---|
| (нет) | да | нет | да (копию) | нет |
ref | да | нет | да | обязательно |
out | нет | да, на каждом пути | только после присваивания | обязательно |
in | да | запрещено | да | необязательно |
Часто задаваемые вопросы
Чем ref отличается от out в C#?
Оба передают саму переменную вызывающего кода, а не копию. С ref переменная должна быть присвоена до вызова, а метод может её читать и менять. С out переменную не обязательно присваивать заранее, метод не может читать её до записи и обязан присвоить её перед возвратом (иначе CS0177). out означает «это дополнительный результат».
Что означает out var в C#?
Начиная с C# 7.0 out-переменную можно объявить прямо в вызове: if (int.TryParse(text, out var number)). Тип переменной выводится из параметра, и она видна и после оператора. out _ отбрасывает ненужный результат.
Передаются ли объекты по ссылке в C#?
Нет, каждый аргумент передаётся по значению, если вы не пишете ref, out или in. Для класса копируемое значение это ссылка, поэтому метод может изменить поля объекта, и вызывающий код это увидит. Чего он не может без ref, так это заставить переменную вызывающего кода указывать на другой объект.
Что такое модификатор параметра in в C#?
in (C# 7.2 и новее) передаёт аргумент по ссылке, но только для чтения: метод не может ему присваивать. Он нужен, чтобы не копировать большие структуры при каждом вызове. Для маленьких типов вроде int или DateTime он не даёт выгоды.
Почему возникает ошибка «Argument 1 must be passed with the 'ref' keyword»?
Метод объявляет параметр ref, а в вызове это ключевое слово пропущено. C# требует ref (и out) и в месте вызова, чтобы читатели видели, что переменная может измениться: Increment(ref count);, а не Increment(count);.