Методы и параметры: ref, out, in, params, именованные аргументы

↑ 1.3 C♯: основы языка · ← Предыдущая · → Следующая

Обязательно~5 мин чтенияУровень: junior

Зачем это на собесе

модификаторы параметров ref, out, in и передача ссылочных типов по значению — классическая ловушка на собесе: «изменится ли объект или ссылка вызывающего».

Объяснение

Передача по значению (по умолчанию)

В метод передаётся копия переменной:

  • value type — копия данных;
  • reference type — копия ссылки (метод может изменить объект, но присвоение параметру новой ссылки не влияет на вызывающего).
void Rename(Patient p)   { p.Name = "Новое"; }         // вызывающий УВИДИТ изменение объекта
void Replace(Patient p)  { p = new Patient(); }         // вызывающий НЕ увидит: заменили копию ссылки
void Inc(int x)          { x++; }                       // вызывающий НЕ увидит

ref — передача по ссылке

Метод работает с самой переменной вызывающего. Переменная должна быть инициализирована.

void Replace(ref Patient p) { p = new Patient { Name = "Новый" }; }  // вызывающий увидит новый объект
void Inc(ref int x) { x++; }
int n = 1; Inc(ref n);  // n == 2

out — выходной параметр

Метод обязан присвоить значение. Переменная может быть не инициализирована. Паттерн Try*:

if (int.TryParse(text, out var number)) Use(number);
if (_cache.TryGetValue(id, out var patient)) return patient;
 
bool TryFindPatient(Guid id, [NotNullWhen(true)] out Patient? patient) { ... }

in — только для чтения по ссылке

Передача без копирования, но изменять нельзя. Полезно для больших struct:

double Distance(in Vector3 a, in Vector3 b) => (a - b).Length();

Для readonly struct защитных копий не будет. Для обычной struct вызов метода внутри in может создать скрытую копию.

ref readonly и ref-возвраты (продвинуто)

ref int FindSlot(int[] arr, int value) { ... return ref arr[i]; }   // вернуть ссылку на элемент
ref var slot = ref FindSlot(data, 5); slot = 10;                     // изменили элемент массива

params

Переменное число аргументов:

void Log(string template, params object[] args) { ... }
Log("{0} {1}", a, b);
// C# 13: params с коллекциями и Span без аллокации массива
void Sum(params ReadOnlySpan<int> values) { ... }

Именованные и необязательные аргументы

Task<Report> BuildAsync(Guid patientId, string format = "pdf", bool includeHistory = false, CancellationToken ct = default);
 
await BuildAsync(id, includeHistory: true, ct: token);   // пропускаем format, читаемо

Значения по умолчанию вшиваются в вызывающий код (как const) — меняя их в публичной библиотеке, пересобирайте потребителей.

Локальные функции и expression-bodied

decimal Total(IEnumerable<Item> items)
{
    return items.Sum(Price);
    static decimal Price(Item i) => i.Price * i.Qty;   // static — не захватывает внешние переменные
}

Нюансы и подводные камни

  • ref, out и in нельзя использовать в async-методах и итераторах (yield).
  • Злоупотребление out усложняет API: для нескольких результатов лучше кортеж или record.
  • Много необязательных bool-параметров — признак, что нужен объект настроек (options-объект).
  • Изменение объекта, переданного в метод, — побочный эффект, неочевидный для вызывающего. Документируйте или избегайте.

Тестирование

[Fact]
public void Reassigning_parameter_does_not_affect_caller()
{
    var p = new Patient { Name = "A" };
    Replace(p);                    // без ref
    p.Name.Should().Be("A");
}

Вопросы с ответами

Связанные темы