Программа курса← C#: Базовый курс
1Знакомство с C# и .NET
2Данные и типы
3Ветвления и циклы
4Методы и ошибки
5Коллекции и строки
6Классы и объекты

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

Одно и то же действие часто нужно вызывать по-разному: с полным набором данных и с сокращённым. В C# для этого два механизма, и они решают почти одну задачу.

Перегрузка

Несколько методов с одним именем, но разным набором параметров:

static void Log(string message)
{
    Console.WriteLine(message);
}

static void Log(string message, string prefix)
{
    Console.WriteLine($"[{prefix}] {message}");
}

static void Log(int code)
{
    Console.WriteLine($"Код: {code}");
}

Компилятор выбирает подходящий вариант по аргументам вызова:

Log("Запуск");              // первый
Log("Сбой", "ОШИБКА");      // второй
Log(404);                   // третий

Выбор происходит при компиляции, а не во время работы, – поэтому ошибиться в нём невозможно: если ни один вариант не подходит, программа просто не соберётся.

Вы уже пользовались перегрузкой, не зная об этом. У Console.WriteLine около двух десятков вариантов: для строки, для целого, для дробного, для объекта. Поэтому метод и принимает что угодно, оставаясь строго типизированным.

Тип возврата в различении не участвует. Такая пара не соберётся:

static int Parse(string text) { ... }
static double Parse(string text) { ... }    // ошибка: уже есть такой метод

Причина понятна, если посмотреть на вызов Parse("5"); без присваивания: выбрать один из двух компилятору не по чему.

Необязательные параметры

Часто перегрузки отличаются одним хвостовым аргументом. Тогда проще задать значение по умолчанию:

static void Log(string message, string prefix = "ИНФО")
{
    Console.WriteLine($"[{prefix}] {message}");
}

Теперь один метод обслуживает оба случая:

Log("Запуск");                 // [ИНФО] Запуск
Log("Сбой", "ОШИБКА");         // [ОШИБКА] Сбой

Правило одно: необязательные параметры идут последними. Иначе при вызове Log("Сбой") было бы непонятно, какому параметру предназначен единственный аргумент.

static void Log(string prefix = "ИНФО", string message)    // ошибка компиляции

Значением по умолчанию может быть только константа, известная при компиляции: число, строка, true, null. Написать DateTime moment = DateTime.Now нельзя – «сейчас» у каждого вызова своё. Обычный обходной приём: поставить null и подставить настоящее значение внутри метода.

Именованные аргументы

Когда параметров много, на месте вызова непонятно, что означает каждый:

CreateUser("Аня", 19, true, false);    // что true, а что false?

Аргумент можно передать по имени параметра – и тогда пояснения не нужны:

CreateUser(name: "Аня", age: 19, isAdmin: true, isBlocked: false);

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

static void Connect(string host, int port = 80, bool secure = false, int timeout = 30) { }

Connect("example.com", timeout: 5);    // порт и secure остались по умолчанию

Логический аргумент на месте вызова почти всегда стоит называть по имени. Save(data, true) заставляет открывать объявление метода, Save(data, overwrite: true) – нет.

Перегрузка или значение по умолчанию

Выбор между ними не только вкусовой.

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

Последняя строка требует пояснения. Значение по умолчанию подставляется на месте вызова, а не внутри метода: компилятор буквально дописывает его в вызывающий код. Если библиотека поменяет умолчание, а вы не пересоберёте свой проект, у вас останется старое значение. Внутри одного проекта, который всегда собирается целиком, эта тонкость не проявляется.

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

Когда заранее неизвестно, сколько значений передадут, помогает params:

static int Sum(params int[] numbers)
{
    int total = 0;

    foreach (var number in numbers)
    {
        total += number;
    }

    return total;
}

Вызывать можно как угодно – с любым количеством аргументов или сразу массивом:

Sum(1, 2, 3);              // 6
Sum(1, 2, 3, 4, 5);        // 15
Sum();                     // 0
Sum(new[] { 1, 2, 3 });    // тоже работает

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