В статье про С++ мы упоминали перегрузку операторов. Это мощный и гибкий инструмент, который может оказаться опасным и непредсказуемым в неумелых руках. Настало время разобраться.
👉 Опытным программистам: мы намеренно упростим детали для понимания сути. Ну сорян.
На примере сложения
Во всех языках есть оператор «плюс» — обычно он умеет складывать числа и соединять строки:
2 + 2 = 4
2.2 + 1.8 = 4.0
−2 + 2 = 0
−2 + 0 = –2
−2 + −2 = −4
‘2’ + ‘2’ = ‘22’
‘два’ + ‘два’ = ‘двадва’
‘четы’ + ‘ре’ = ‘четыре’
Допустим, мы пишем софт для интернет-магазина, и у нас есть там класс объектов «заказ». Напомним, что класс — это как бы чертёж, по которому создаются объекты. А объект — это такая коробка с данными и функциями, которыми мы можем управлять как единым целым. Подробнее об этом — в статьях про объекты и классы.
В объекте типа «заказ» лежит куча всего:
- массив с содержимым корзины,
- дата и время, когда сформирован заказ,
- метод «очистить корзину»,
- место для промокода,
- метод «применить промокод»,
- метод «проверить наличие товаров по складу»
- идентификатор пользователя,
- что-нибудь ещё интересное.
Допустим, наша система устроена так, что у любого заказа может быть два идентификатора пользователя: постоянный или временный.
- Если идентификатор постоянный, значит, мы однозначно пользователя узнали (его адрес, номер кредитки и т. д.).
- Если идентификатор временный, значит, мы не знаем, что за пользователь — просто храним его корзину, пока он не оформит заказ. Это может быть новый человек или старый, но ещё не залогинившийся. В любом случае мы должны хранить его данные.
В какой-то момент пользователь с временным идентификатором логинится в систему, и нам хочется сделать следующую операцию:
zakaz_5058303 + zakaz_user121239
Обе части выражения — это объекты класса «Заказ». А наш язык программирования не знает, что значит «сложить два объекта класса „Заказ“». Он не знает:
- Что с чем складывать? Число товаров? Суммы? Номера товаров? Номера телефонов? Ведь язык не понимает, что за объект перед ним. Для него это просто коробка с данными, он может с ними делать что хочешь.
- Что возвращать? Число? Строку? Объект? Список заказов?
- Может быть, нужно сравнить два заказа и по каким-то критериям определить самый актуальный?
- Или нужно объединить две корзины в одну?
- А что тогда делать с повторяющимися товарами? Заменить? Добавить в количество? Проигнорировать?
Вроде бы простая операция — а столько вопросов. Вот этому всему мы можем обучить оператор «+», и это будет перегрузка оператора.
👉 Короче
Перегрузка оператора — это когда мы обучаем язык программирования, как оператору типа плюс, минус, умножить и т. д. вести себя с определённым типом вводных — например, с объектами, матрицами или картинками.
В случае с нашим примером мы можем сказать, что если складываются два заказа, делай следующее:
- Найди, какой из этих заказов постоянный.
- Переложи из временного в постоянный все уникальные товары.
- Если есть неуникальные товары (например, и в том, и в другом заказе была одна и та же позиция), склей их и поставь максимальное количество. Например, если в постоянном заказе стояло 3 штуки одного артикула, а во временном этого же артикула 9 штук, то поставь в постоянный 9 штук.
- Временному заказу поставь статус «Склеено».
- Залогируй время склейки заказов.
- Верни постоянный заказ с обновлёнными данными.
Довольно много действий для одного плюса, не находите?
Что хорошего в перегрузке
Перегруженные операторы позволяют совершать привычные операции над необычными объектами. Если на интуитивном уровне логично, что можно складывать некоторые вещи между собой, то первое, что приходит в голову — использовать для этого стандартный плюс. Единственное, что нужно сделать — перегрузить его новыми обязанностями, а потом можно дальше им пользоваться как привычным сложением, даже с новыми объектами.
В результате программист экономит много кода, не пишет специальный отдельный обработчик такого сложения и не держит в голове параметры его вызова.
Чем опасна перегрузка операторов
Когда вы используете в коде перегруженный оператор, он выглядит как самый обычный оператор. Ну складывает и складывает. Ну умножает и умножает, чего такого-то?
Это, с одной стороны, элегантно. А с другой, создаёт проблемы в отладке.
- Представьте, что после вас какой-то программист переделал структуру класса «Заказ», и теперь там по-другому работает массив с товарными позициями. Раньше у товаров были числовые идентификаторы типа integer (целые числа), а новый программист переделал их на строки.
- Ваш язык в неявном виде поддерживает сравнение чисел со строками и наоборот. Он производит какие-то свои преобразования и позволяет сравнить число со строкой. В 99,9% случаев это не сломает вашу программу, и даже перегруженный оператор будет работать.
- Но в 0,1% случаев сравнение случится некорректно, и никто не будет понимать, в чём дело. Где-то под капотом перегруженный оператор некорректно склеивает списки покупок, у пользователя вываливаются какие-то «левые» товары, которых он не заказывал. Он не глядя их оплачивает и потом катает жалобу на ваш магазин.
А ещё, в особо экзотических случаях и больших проектах, программист шутки ради может перегрузить оператор сложения так, что он будет не складывать, а вычитать. И заметить, в чём тут ошибка, в таких случаях бывает очень сложно.
И что?
Перегрузка операторов — это полезно, но сложно.
Если программист не понимает полностью механизма работы перегрузок, лучше не перегружать.
Если понимает — он молодец и может учить стандартные инструменты нестандартному поведению.
Не перегружайтесь, берегите себя.