1. Вступ
У реальному житті багато дій — наче багатофункціональні швейцарські ножі: одна й та сама команда може працювати з різними наборами інструментів. Наприклад, уявіть собі банкомат: якщо ви вставили картку — банкомат запитує PIN‑код; якщо ввели номер телефону — очікує код підтвердження з 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) вважаються різними перевантаженнями. Якщо не враховувати це, легко заплутатися й викликати неправильну версію методу.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ