ООП. Часть 4: Полиморфизм, перегрузка методов и операторов

C# позволяет использовать один метод для разных типов данных и даже переопределить логику операторов. Разбираемся в перегрузках.

Полиморфизм (от греч. poly — много и morphe — форма) — один из главных столпов объектно-ориентированного программирования. Его суть заключается в том, что один фрагмент кода может работать с разными типами данных.

В C# это реализуется с помощью перегрузок (overloading).

Евгений Кучерявый

Пишет о программировании, в свободное время создает игры. Мечтает открыть свою студию и выпускать ламповые RPG.


Перегрузка методов

C# — строго типизированный язык. Это значит, что вы не можете поместить строку в переменную типа int — сначала нужно провести преобразование. Так же и в метод нельзя передать параметр типа float, если при объявлении метода был указан тип double.

Однако если вы экспериментировали с методом WriteLine() класса Console, то могли заметить, что в него можно передавать аргументы разных типов:

Console.WriteLine("Hello, World!"); //string
 
Console.WriteLine(50f); //float
 
Console.WriteLine(0.23); //double
 
Console.WriteLine(42); //int
 
Console.WriteLine("The numbers are {0} and {1}", 17, 3); //Строка с интерполяцией

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

Так происходит потому, что у метода WriteLine() есть перегрузки — методы с таким же названием, но принимающие другие аргументы:

int Sum(int a, int b)
{
    return a + b; //Сумма двух целых чисел
}
 
double Sum(double a, double b)
{
    return a + b; //Сумма двух чисел с плавающей запятой (double)
}
 
float Sum(float a, float b, float c)
{
    return a + b + c; //Сумма трёх чисел с плавающей запятой (float)
}
 
int Sum(int[] array)
{
    int sum = 0;
 
    foreach (int num in array)
    {
        sum += num;
    }
 
    return sum; //Сумма всех целых чисел в массиве
}

Когда вы вызовете метод Sum(), компилятор по переданным аргументам узнает, какую из его перегрузок вы имели в виду — так же, как это происходит с методом WriteLine().

При этом стоит учитывать, что значение имеют только типы и количество передаваемых аргументов. Например, можно написать такие перегрузки:

int Multiply(int a, int b)
{
    return a * b;
}
 
void Multiply(int a, int b)
{
    Console.WriteLine(a * b);
}

У этих методов одинаковые параметры, но разный возвращаемый тип. Попытка скомпилировать такой код приведёт к ошибке — так же, как и создание перегрузки с такими же аргументами, но с другими названиями:

int Multiply(int a, int b)
{
    return a * b;
}
 
int Multiply(int num1, int num2)
{
    return num1 * num2;
}

Перегрузка конструкторов

То же самое можно сделать и с конструкторами классов:

class Item
{
    public string name;
    public int price;
    public int lvl;
 
    public Item()
    {
        this.name = "Item";
        this.price = 15;
        this.lvl = 1;
    }
 
    public Item(string name)
    {
        this.name = name;
        this.price = 15;
        this.lvl = 1;
    }
 
    public Item(string name, int price, int lvl)
    {
        this.name = name;
        this.price = price;
        this.lvl = lvl;
    }
}

Альтернатива этому решению — указать значения для аргументов по умолчанию:

public Item(string name = "Item", int price = 15, int lvl = 1)
{
    this.name = name;
    this.price = price;
    this.lvl = lvl;
}

Несмотря на, то что здесь меньше кода, на мой взгляд, это может запутать. Потому что придётся каждый раз заполнять все значения, даже если нужен только один аргумент из конца списка. Перегрузка же позволяет определить и порядок параметров (если они разных типов).

Перегрузка операторов

Перегрузить можно даже операторы, то есть:

  • +,
  • ++,
  • -,
  • --,
  • *,
  • /,
  • ==,
  • >,
  • <,
  • >=,
  • <=.

Для этого в определении класса нужно добавить вот такую конструкцию:

public static возвращаемый_тип operator оператор(аргументы)
{
    //Логика
}

Так как использоваться этот оператор должен без объявления экземпляра класса (item1 + item2, а не item1 item1.+ item2), то указываются модификаторы public static.

Например, мы хотим улучшать предметы в играх. Во многих MMO1 популярна механика, когда один предмет улучшается за счёт другого. Мы можем сделать это с помощью перегрузки оператора сложения:

public static Item operator +(Item i1, Item i2)
{
    return new Item(i1.name + "+", i1.price + i2.price / 2, ++i1.lvl);
}

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

//Создаём три предмета
Item i1 = new Item("Sword", 15, 1);
Item i2 = new Item();
Item i3 = new Item();
 
Console.WriteLine($"{i1.name} | Price: {i1.price} | Level: {i1.lvl}"); //Выводим параметры первого предмета
 
i1 += i2; //Улучшаем предмет с помощью другого предмета
 
Console.WriteLine($"{i1.name} | Price: {i1.price} | Level: {i1.lvl}"); //Снова выводим данные
 
i1 += i3; //Повторяем улучшение
 
Console.WriteLine($"{i1.name} | Price: {i1.price} | Level: {i1.lvl}"); //Выводим новые характеристики

В результате в консоль будет выведено следующее:

1) MMO (англ. Massively Multiplayer Online Game, MMO, MMOG)


Массовая многопользовательская онлайн-игра

Перегрузка операторов преобразования типов

Хотя типизация в C# строгая, типы можно преобразовывать. Например, мы можем конвертировать число типа float в число типа int:

float x = 50f;
int y = (int)y;

С помощью перегрузки операторов преобразования типов мы можем прописать любую логику для конвертации объектов. Для наглядности создадим класс Hero:

class Hero
{
    public string name = "";
    public int str = 1;
    public int dex = 1;
    public int intel = 1;
    public int lvl = 1;
}

В этом классе хранятся данные о персонаже. В MMO часто можно увидеть такой параметр, как мощь — это сумма всех характеристик героя или предмета. Например, её можно посчитать по следующей формуле:

Мощь = (сила + ловкость + интеллект) * уровень.

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

public static implicit|explicit operator новый_тип(исходный_тип arg)
{
    // логика
}

Модификатор implicit говорит компилятору, что преобразование может быть неявным. То есть оно сработает, если написать так:

Hero h1 = new Hero();
int power = h1;

Explicit, наоборот, означает, что преобразование должно быть явным:

Hero h1 = new Hero();
int power = (int)h1;

Вот как будет выглядеть перегрузка преобразования объекта класса Hero в int:

public static explicit operator int(Hero hero)
{
    return (hero.str + hero.dex + hero.intel) * hero.lvl;
}

Вот как она будет использоваться:

Hero h = new Hero //Создаём героя
{
    name = "Super Dragon Slayer 2000",
    str = 10,
    dex = 7,
    intel = 25,
    lvl = 5
};
 
int power = (int)h; //Конвертируем его в int
 
Console.WriteLine($"{h.name}'s power: {power}"); //Выводим результат

Вывод в консоль будет следующим:

Проблемы читаемости

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

Или же непонятно, зачем конвертировать Hero в int. Ясность вносит название переменной (power), но этого недостаточно.

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

Вместо сложения объектов можно написать метод Enhance(), который будет принимать другой предмет и прибавлять его характеристики к  текущему.

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

Домашнее задание

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

Заключение

Полиморфизм — очень удобный инструмент. Однако в этой статье была затронута лишь его часть; чтобы начать работать со второй, нужно ознакомиться с принципами наследования и абстракции.

Вы можете изучить ООП гораздо глубже, записавшись на курс «Профессия C#-разработчик». Он раскрывает лучшие практики работы с C# в объектно-ориентированной парадигме программирования.

Курс

Профессия C#-разработчик


130 часов — и вы научитесь писать программы на языке, созданном Microsoft. Вы создадите 5 проектов для портфолио, даже если до этого никогда не программировали. После обучения — гарантированное трудоустройство.

Хочешь получать крутые статьи по программированию?
Подпишись на рассылку Skillbox