Menu

Необязательные параметры и именованные аргументы в C#: значения по умолчанию и правила

Как работают необязательные параметры и именованные аргументы в C#: значения по умолчанию и правило константы времени компиляции, порядок параметров, пропуск аргументов по имени, необязательные параметры против перегрузок, атрибуты сведений о вызывающем коде и ловушка версий, когда значения по умолчанию вшиты в вызывающий код.

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

У необязательного параметра есть значение по умолчанию в объявлении метода, поэтому вызывающий код может его опустить. Именованный аргумент передаёт значение по имени параметра, а не по позиции. Вместе они позволяют одному методу обслуживать много вариантов вызова без груды перегрузок.

Вывод:

to ana@mail.com: (no subject), retries 3
to ben@mail.com: Invoice #1042, retries 3
to cy@mail.com: (no subject) [URGENT], retries 3
to dev@mail.com: Build failed, retries 0

Третий вызов пропускает subject и задаёт urgent по имени; без именованных аргументов пришлось бы снова передать "(no subject)", только чтобы добраться до третьей позиции. Последний вызов передаёт все аргументы по имени в порядке, отличном от объявления, и это допустимо.

Правила для значений по умолчанию

Значение по умолчанию должно быть чем-то, что может вычислить компилятор:

  • константа (3, "INFO", true, 1.5m, поле const, член перечисления)
  • null для ссылочного типа или типа, допускающего null
  • default(T) или new T() для типа значения T

Всё, что вычисляется во время выполнения, отвергается. Классический случай это дата:

static void Schedule(string task, DateTime at = DateTime.Now) { }
// error CS1736: Default parameter value for 'at' must be a compile-time constant

Стандартный обходной путь это параметр, допускающий null, со значением null по умолчанию, которое разрешается в теле:

Вывод:

backup at 2026-01-01 09:00, tags: 0
report at 2026-03-15 18:30, tags: 0
deploy at 2026-01-01 09:00, tags: 2

Тот же приём подходит для коллекций: значение по умолчанию new List<string>() не является константой, поэтому по умолчанию используйте null и создавайте список внутри. Это заодно исключает ошибку с общим изменяемым значением по умолчанию, знакомую по Python, которую C# предотвращает, разрешая только константы.

Правила порядка

Сначала идут обязательные параметры, затем необязательные, а массив params (если есть) последним:

static void Log(string message, string level = "INFO", params string[] tags) { }   // OK

static void Log(string level = "INFO", string message) { }
// error CS1737: Optional parameters must appear after all required parameters

Параметры ref и out не могут быть необязательными.

При вызове позиционные аргументы заполняют параметры с начала. Именованные аргументы могут следовать за ними в любом порядке. Начиная с C# 7.2 именованный аргумент может стоять и перед позиционным, но только на своей позиции (SendEmail("a@b.c", subject: "Hi", true)); в более старых версиях все именованные аргументы должны идти в конце. Пропуск обязательного параметра, даже при указании других по имени, это ошибка компиляции.

Именованные аргументы для читаемости

Именованные аргументы полезны, даже когда ничего не является необязательным. Литералы true, false, null и голые числа ничего не говорят в месте вызова:

ResizeImage(photo, 800, 600, true, false);                                   // which is which?
ResizeImage(photo, width: 800, height: 600, keepAspect: true, upscale: false);

Переименование параметра становится несовместимым изменением для кода, который использует его имя, и об этом стоит помнить в открытой библиотеке.

Необязательные параметры и перегрузки

До C# 4 для такой же гибкости требовалась отдельная перегрузка на каждую комбинацию. Необязательные параметры сворачивают их в один метод:

// overloads
static void Connect(string host) => Connect(host, 443);
static void Connect(string host, int port) => Connect(host, port, 30);
static void Connect(string host, int port, int timeoutSeconds) { /* ... */ }

// one method with optional parameters
static void Connect(string host, int port = 443, int timeoutSeconds = 30) { /* ... */ }

Когда есть и то и другое, разрешение перегрузок предпочитает кандидата, которому не нужно подставлять значения по умолчанию:

Вывод:

Greet()
Greet(string) with Lena

Greet() подходит под оба метода, и компилятор выбирает тот, в котором не пропущен необязательный параметр. Смешивание двух приёмов под одним именем в основном даёт вызовы, цель которых трудно предсказать, поэтому выбирайте один приём на метод.

Выбирайте перегрузки, когда вариантам нужен разный код или разные типы параметров, и необязательные параметры, когда они различаются только значениями по умолчанию.

Значения по умолчанию вшиваются в вызывающий код

Значение по умолчанию не ищется во время выполнения. Компилятор копирует его в каждое место вызова при компиляции вызывающего кода. Connect("api.shop.com") компилируется в Connect("api.shop.com", 443, 30).

Для библиотек у этого есть последствие. Допустим, версия 1 пакета поставляет Connect(string host, int timeoutSeconds = 30), а версия 2 меняет значение по умолчанию на 10. Приложение, скомпилированное с версией 1, продолжает передавать 30 после замены DLL на версию 2, пока само приложение не перекомпилируют. Добавление нового необязательного параметра в существующий открытый метод тоже ломает уже скомпилированный вызывающий код, потому что сигнатура метода изменилась, а он всё ещё ищет старую (MissingMethodException во время выполнения).

В пределах одного приложения, которое компилируется целиком, это не имеет значения. Для открытых API в пакетах NuGet проблемы избегают перегрузки (которые держат значения по умолчанию внутри библиотеки) или значение null по умолчанию, разрешаемое в теле.

Значения по умолчанию и объявленный тип

Из того же правила времени компиляции следует, что когда и интерфейс, и класс объявляют значения по умолчанию, значение берётся из типа переменной, через которую идёт вызов, а не из объекта:

Вывод:

printing "report" x5
printing "report" x1

Один и тот же объект, два разных значения по умолчанию. Держите значения по умолчанию одинаковыми в интерфейсе и его реализациях или объявляйте их только в одном месте.

Атрибуты сведений о вызывающем коде

На необязательных параметрах основаны и атрибуты сведений о вызывающем коде из System.Runtime.CompilerServices. Компилятор заполняет их данными о месте вызова:

Вывод:

[Main:18] starting
[SaveOrder:13] order saved

Так библиотеки логирования записывают, откуда пришло сообщение, без того чтобы вызывающий код это набирал, а реализации INotifyPropertyChanged получают имя свойства. [CallerFilePath] так же добавляет путь к исходному файлу.

Часто задаваемые вопросы

Как сделать параметр необязательным в C#?

Задайте ему значение по умолчанию в объявлении: static void Log(string message, string level = "INFO"). Тогда вызывающий код может написать Log("started") или Log("failed", "ERROR"). Необязательные параметры должны идти после всех обязательных, а значение по умолчанию должно быть константой времени компиляции.

Что такое именованные аргументы в C#?

Аргументы, переданные с именем параметра: SendEmail(to: "ana@mail.com", urgent: true). Они позволяют пропускать необязательные параметры в середине, передавать аргументы в любом порядке и делают вызовы с литералами true/false или числами понятными с первого взгляда.

Почему нельзя использовать DateTime.Now как значение параметра по умолчанию?

Значения по умолчанию должны быть константами времени компиляции, а DateTime.Now вычисляется во время выполнения, поэтому компилятор сообщает CS1736, Default parameter value for 'at' must be a compile-time constant. Используйте параметр, допускающий null: DateTime? at = null, а в теле DateTime time = at ?? DateTime.Now;.

Что использовать в C#, необязательные параметры или перегрузки методов?

Необязательные параметры проще, когда варианты различаются только значениями по умолчанию. Перегрузки лучше, когда вариантам нужна разная логика или разные типы, а также в открытых библиотеках, потому что значение по умолчанию компилируется в каждый вызывающий код, и его последующее изменение не доходит до уже скомпилированного кода.

Что означает «Optional parameters must appear after all required parameters»?

Ошибка CS1737: за параметром со значением по умолчанию следует параметр без него. Поставьте обязательные параметры первыми: (string to, bool urgent = false), а не (bool urgent = false, string to). После необязательного параметра могут идти только другие необязательные параметры или массив params.

Coddy programming languages illustration

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

НАЧАТЬ