У необязательного параметра есть значение по умолчанию в объявлении метода, поэтому вызывающий код может его опустить. Именованный аргумент передаёт значение по имени параметра, а не по позиции. Вместе они позволяют одному методу обслуживать много вариантов вызова без груды перегрузок.
Вывод:
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для ссылочного типа или типа, допускающего nulldefault(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.