Почему пара и кортеж — это чаще всего плохо
Многим программистам знакомы концепции пар и кортежей (pair и tuple) — их реализации есть в STL, Boost (и может быть где-нибудь еще). Для тех, кто не знает, что это такое, я коротко поясню — это шаблоны, позволяющие сгруппировать несколько значений (пара — только 2, tuple — много) с целью хранить\передавать\принимать их вместе. Пример из MSDN:
- Вместо передачи в функцию нескольких векторов одинаковой размерости можно передать только один вектор пар\кортежей, не заботясь о проверке их соответствия.
- Можно легко вернуть из функции набор значений, не мороча голову с указателями или ссылками в out-параметрах (для многих это сложно)
- Можно избежать создания кучи мелких структур из 2-3 полей (меньше кода — лучше).
Давайте посмотрим на объявление вот такой функции:
Что бы Вы думали она возвращает? Имя и фамилию? А с чего Вы взяли? Может быть — номер паспорта и код в налоговой? А может быть полное имя и номер телефона. Или реальное имя и никнейм. Мысль в том, что нигде в объявлении функции не описывается, что будет содержаться в возвращаемой паре. Вы, конечно, помните это сейчас. А вспомните через год? Кроме того, другому программисту, который решит воспользоваться этой функцией, придётся лезть в её код и выискивать, что же она возвращает — а уж это и вовсе ставит крест на всём ООП-подходе, модульности, инкапсуляции и прочих важных вещах. Сравните вышеуказанный код со следующим:
Всё кристально ясно, читать код функции для понимания возвращаемого значения нет нужды.
Порядок данных в паре или кортеже — загадкаОк, мы хорошо подумали над прошлым примером и переписали нашу функцию вот так:
Теперь чётко ясно, что возвращает она имя и фамилию человека. Но вот в чём вопрос — в каком порядке? Вам кажется вполне очевидным какой-то определённый порядок этих строк в паре, но у меня для Вас плохая новость. Вы живёте в мире, где люди пользуются разным порядком слов в именах, разными форматами дат и времён, пишут как слева направо, так и наоборот, ездят по дорогам с разносторонним движением и т.д. Тот факт, что Вам кажется единственно возможным только этот вариант порядка значений в паре не доказывает ровным счётом ничего. Как говорит один из законов Мёрфи — "Если что-нибудь может быть истолковано несколькими способами, оно будет истолковано именно самым неверным из них". В случае использования отдельной структуры (класса) для возвращаемого значения мы всегда имеем однозначную трактовку кода.
Плохая расширяемостьИдём дальше — что будет, если со временем в нашу функцию мы захотим добавить еще данных? Да, мы можем заменить пару на кортеж и наращивать его до абсудрного размера:
Но какой же это кошмар для всех, кто этой функцией пользуется! Получается, что после каждого её изменения нужно пересматривать все вызовы функции, проверяя, к правильным ли полям мы обращаемся. Ужас.
Невозможность показать отсутствие одного из значенийИногда в наборе значений одно или несколько полей могут быть не установленными. Это очень легко отобразить в структуре или классе (завести переменную isSet или написать метод проверки поля), но совершенно невозможно отобразить в паре или кортеже, где предполагается, что набор содержит все значения и они валидны. В итоге приходится изгаляться с соглашениями в духе «если второй параметр равен -1, значит на самом деле информации нет», которые не очевидны, забываемы и неудобны.
Некуда вставить проверку валидностиДавайте посмотрим на вот такую функцию, возвращающую диапазон рабочих температур некоторого устройства:
В следствии опечатки в 1 символ функция (не сильно напрягаясь) расширила границы физической реальности, заявив что устройство может работать при -300 по Цельсию. Никакой проверки на валидность такой температуры ни в момент создания объекта пары, ни в момент возврата этого значения из функции попросту нет. И написать его вообще некуда. То ли дело, если бы возвращался объект диапазона температур, при создании которого можно было бы как-то поймать невалидное значение и отреагировать на него (ассерт, лог, исключение, замена на валидное значение и т.д.)
КонтрпримерЧто бы означало "чаще всего плохо" в названии статьи? Нужно признать, что иногда пары использовать можно и нужно. Например, у нас есть игра, в которой по ходу игровой механики для двух игроков нужно выбросить некоторые случайные значения (числа в диапазоне int). Это вполне может сделать функция вида:
- Чётко понятно, что находится в паре. Нет никаких способов двусмысленной трактовки.
- Порядок не имеет значения. Что сначала число для первого игрока, потом для второго, что наоборот — по барабану.
- Наша игра только на двух игроков и никогда (by design) не будет возможна для большего количества — беспокоиться о расширяемости не нужно
- Оба значения точно должны быть. Отсутствие одного из них невозможно.
- Проверка валидности не нужна — по определению весь диапазон int нам подходит.
Более того, в этом примере пара лучше отдельного класса (меньше кода), лучше out-параметров в виде указателей (не нужно проверять их на валидность) и лучше массива или вектора (те могут быть любого размера, что путает). В общем, пример имеет право на жизнь.