Menu

Параметры ref, out и in в C#: передача по ссылке

Как работают параметры ref, out и in в C#: передача самой переменной вместо копии, out для дополнительных результатов и шаблон TryParse, out var, параметры in только для чтения, правила определённого присваивания и что меняется, когда аргумент ссылочного типа.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

По умолчанию 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);.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ