1. Введение
В реальной жизни многие действия — словно многофункциональные швейцарские ножи: одна и та же команда может работать с разными наборами инструментов. Например, представьте себе банкомат: если вы вставили карту — банкомат спрашивает пин-код; если ввели номер телефона — банкомат ожидает код подтверждения из SMS. Действие одно — "проверить пользователя", но способы разные.
В программировании мы часто сталкиваемся с похожей ситуацией: нужно выполнить, казалось бы, одну операцию, но данные могут быть разных типов или с разным количеством параметров. Например, наш метод должен выводить приветствие как для человека, так и для животного, или суммировать два, три или даже десять целых чисел.
Конечно, можно назвать методы как-нибудь по-разному: SumTwo, SumThree, SumArray. Но программисты ленивы (не зря говорят, что лень — двигатель прогресса). К тому же, так код станет менее читаемым.
Перегрузка метода
Перегрузка методов — это способ "заставить" один и тот же метод работать с разными наборами параметров, при этом имя метода не меняется. Это одна из форм полиморфизма, но не связанная с наследованием.
Перегрузка метода — это возможность создавать в одном классе (или структуре) несколько методов с одинаковым именем, но с разными списками параметров (по типу, количеству и/или порядку).
Сигнатура метода
Сигнатура метода в C# — это его имя плюс тип(ы) и порядок параметров. Возвращаемый тип метода не входит в сигнатуру! Это часто приводит к неожиданным ошибкам (о них расскажу чуть позже).
2. Перегрузка в действии: простые примеры
Давайте создадим класс Greeter, который будет приветствовать пользователей по-разному: просто по имени, по имени и возрасту, или вообще без параметров.
public class Greeter
{
// Приветствие без параметров
public void Greet()
{
Console.WriteLine("Привет, мир!");
}
// Приветствие с именем
public void Greet(string name)
{
Console.WriteLine($"Привет, {name}!");
}
// Приветствие с именем и возрастом
public void Greet(string name, int age)
{
Console.WriteLine($"Привет, {name}! Тебе уже {age} лет? Неплохо!");
}
}
Теперь можно вызвать любой из этих методов, и компилятор C# сам выберет нужный вариант — ориентируясь на количество и типы переданных параметров.
var greeter = new Greeter();
greeter.Greet(); // Привет, мир!
greeter.Greet("Аня"); // Привет, Аня!
greeter.Greet("Пётр", 23); // Привет, Пётр! Тебе уже 23 лет? Неплохо!
3. Различие по типу и количеству параметров
Перегрузка работает, если методы различаются:
- количеством параметров,
- типом хотя бы одного параметра,
- порядком типов параметров (но тут надо быть осторожнее).
Попробуем добавить ещё одну перегрузку, принимающую только возраст:
public void Greet(int age)
{
Console.WriteLine($"Столько лет — это круто! ({age} лет)");
}
Теперь вызовы:
greeter.Greet(10); // Столько лет — это круто! (10 лет)
Важно помнить: если методы различаются только возвращаемым типом, перегрузить их нельзя. Например, такой код вызовет ошибку:
// Ошибка компиляции!
public int Foo(string s) { ... }
public double Foo(string s) { ... }
Компилятор будет ругаться: "Уже определён метод Foo(string), давай что-нибудь посложнее!"
4. Перегрузка и стандартная библиотека C#
Перегрузка — это не только наш выдуманный Greet. Откроем документацию .NET для Console.WriteLine:
| Сигнатура | Назначение |
|---|---|
|
Печатает пустую строку |
|
Печатает строку |
|
Печатает целое число |
|
Печатает дробное число |
|
Форматирует строку с одним аргументом |
|
Форматирует с несколькими аргументами |
Это всё перегрузки одного и того же метода — WriteLine. Теперь вы понимаете, почему вы всегда можете делать:
Console.WriteLine("Просто строка");
Console.WriteLine(123);
Console.WriteLine(2.5);
Console.WriteLine("Сумма: {0}", 42);
И компилятор всегда трактует ваш вызов корректно!
5. Как компилятор выбирает, какую перегрузку вызвать?
Здесь всё строго: он смотрит на типы и количество фактически переданных аргументов. Небольшая табличка для наглядности:
| Вызов | Какая версия сработает? |
|---|---|
|
|
|
|
Что будет при неоднозначности?
Иногда ситуация выходит из-под контроля. Пример неоднозначной перегрузки — компилятор не сможет выбрать нужную версию
public void Print(int a, double b) { ... }
public void Print(double a, int b) { ... }
printer.Print(5, 10);
// Ошибка: двусмысленность — какое Print вызывать? (оба вроде бы подходят)
Компилятор выдаст ошибку двусмысленности. В таких случаях лучше избегать перегрузок с одинаковым количеством параметров и близкими типами, когда это может спутать компилятор.
6. params — переменное количество параметров
Допустим, вы хотите реализовать метод, который принимает неопределённое количество чисел. Вам на помощь придёт ключевое слово params.
public void SumAll(params int[] numbers)
{
int sum = 0;
foreach (int n in numbers)
sum += n;
Console.WriteLine($"Сумма: {sum}");
}
Теперь вы можете вызывать:
SumAll(1, 2, 3); // Сумма: 6
SumAll(10, 20); // Сумма: 30
SumAll(); // Сумма: 0
Методы с params можно комбинировать с перегрузкой, но главное не делать таких перегрузок, которые будут мешать компилятору однозначно понять, какую версию метода вы имели в виду.
7. Перегрузка и модификаторы параметров (ref, out, in)
C# различает методы по модификаторам параметров (то есть, сигнатура void Foo(int a) отличается от void Foo(ref int a), и оба могут существовать в одном классе):
public void SetValue(int a)
{
a = 42;
}
public void SetValue(ref int a)
{
a = 100;
}
Вызов без ref попадёт в первую версию, с ref — во вторую:
int n = 5;
SetValue(n); // n остается 5 (копируется значение)
SetValue(ref n); // n становится 100
8. Схема: что такое перегрузка
+----------+
| MyClass |
+----------+
|
| (фрагмент методов)
+-----------------------+
| void Foo() |
| void Foo(int a) |
| void Foo(string s) |
| void Foo(int a, int b) |
+-----------------------+
А если представить в коде:
// Вызываем перегруженные версии метода Foo():
var mc = new MyClass();
mc.Foo(); // void Foo()
mc.Foo(5); // void Foo(int)
mc.Foo("Hello"); // void Foo(string)
mc.Foo(2, 3); // void Foo(int, int)
9. Пример: перегрузим методы в нашем приложении
Давайте продолжим развивать наше учебное приложение, добавив перегрузку метода в иерархию животных.
public class Animal
{
public string Name { get; set; }
// Метод для издания звука
public virtual void MakeSound()
{
Console.WriteLine("Некий непонятный звук...");
}
// Перегруженный метод: звук с указанной громкостью
public void MakeSound(int volume)
{
Console.WriteLine($"Животное издаёт звук громкостью {volume} дБ.");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Гав!");
}
// Перегруженный метод: гавканье с громкостью
public void MakeSound(int volume)
{
Console.WriteLine($"Гав! (громкость: {volume} дБ)");
}
}
Попробуйте такие вызовы:
Dog rex = new Dog();
rex.MakeSound(); // Гав!
rex.MakeSound(75); // Гав! (громкость: 75 дБ)
Обратите внимание: в классе-наследнике (Dog) мы перегрузили метод MakeSound(int volume), и теперь у него есть обе версии: с параметром и без.
10. Типичные ошибки при перегрузке методов
Ошибка №1: попытка перегрузить метод только по возвращаемому типу.
Это невозможно — возвращаемый тип не входит в сигнатуру метода. Перегрузка должна отличаться по количеству или типам входных параметров, а не по void или int.
Ошибка №2: неоднозначные перегрузки, сбивающие компилятор.
Перегрузки с одинаковым числом параметров и близкими типами (например, int и double) могут запутать компилятор. Пример: Print(int a, double b) и Print(double a, int b) — вызов Print(1, 1) вызовет ошибку неоднозначности.
Ошибка №3: конфликт params с другими перегрузками.
Метод с params может перехватить вызов, предназначенный для другой перегрузки. Если типы совпадают, компилятор может выбрать не тот метод, что вы ожидали.
Ошибка №4: забыли, что ref и out входят в сигнатуру.
Методы Do(ref int x) и Do(out int x) считаются разными перегрузками. Если не учитывать это, легко запутаться и вызвать неправильную версию метода.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ