Іменовані параметри C++. Не знадобилися

Час від часу раптом починає хотітися іменованих параметрів в C++. Не так давно була стаття, та й сам якийсь час назад писав на цю тему. І ось що дивно — з часів тієї своїй статті я беру участь у новому проекті без необхідності тягти за собою старий код, і якось дивним чином всього цього описаного собою ж не використовую. Тобто в питанні розібрався, захопився перспективами… і продовжив працювати по-старому! Як же так? Лінь? Інерція? Постараюся дати відповідь під катом.

Для початку розглянемо приклад — оголошення функції, що повертає об'єкт дати для заданих дня, місяця і року.

Date createDate(int day, int year, int month);

Проблема очевидна — який порядок параметрів не вибери, через місяць, побачивши подібне

Date theDate = createDate(2, 3, 4);

будеш думати: «Що це? Друге березня 2004-го року, або четверте 2002-го?». Якщо ж особливо пощастило, і команда інтернаціональна, трактування функції розробниками може докорінно відрізнятися. Однакові типи йдуть поспіль у списку параметрів… Ось в таких випадках зазвичай і хочеться іменованих параметрів, яких в С++, на жаль, немає.

Багатьом програмістам доводиться переключатися з однієї мови програмування на іншу. При цьому що-то в новому мовою подобається, щось ні… Погане з часом забувається, хороше ж хочеться неодмінно перенести в те середовище, де зараз працюєш. Он, у тому ж Objective C іменовані параметри є!

+ (UIColor *)colorWithRed:(CGFloat)red
green:(CGFloat)green
blue:(CGFloat)blue
alpha:(CGFloat)alpha

Так, перший параметр йде без імені, але його назва часто включають в ім'я методу. Рішення, звичайно, не ідеальний (наприклад, методу colorWithBlue не існує, така ось колірна несправедливість), зате іменовані параметри підтримуються на рівні мови. У VBA все ще краще — імена можна дати всім параметрам, завдяки їм можна не плодити безліч схожих методів, а обійтися одним. Подивіться, наприклад, на Document.PrintOut

А в C++ нічого такого немає! Відразу хочеться виправляти ситуацію, шукати бібліотеки, придумувати милиці і велосипеди. І навіть щось знайдеться і вийде. Але замість цього можна задуматися, якщо все так чудово, чому іменовані параметри не додали. Стільки парадигм підтримується, а тут таке…

Чи, може, додали? Просто назвали інакше. Наприклад, користувацькими типами. Саме час привести основну думку статті. Примітивним стандартним типам не місце в інтерфейсах реальної системи. Такі типи — просто будівельні блоки, з яких потрібно саме будувати, а не намагатися жити всередині.

Наприклад, об'єкт типу int — знакове ціле, що лежить в певному діапазоні. Що описує цей тип? Тільки свою реалізацію. Це не може бути, наприклад, кількість яблук. Бо яблук не може бути мінус 10. Все ще гірше: unsigned int також непридатний для цієї задачі. Тому що яблука взагалі не мають ніякого відношення до розмірності типу даних на вашій платформі. Прив'язуючи примітивні типи мови до параметрів відкритих методів своїх моделей, ми робимо помилку, яку потім намагаємося «зам'яти» за допомогою різних милиць. Причому у своєму прагненні приховати помилку ми часто ігноруємо той простий факт, що мова намагається нам допомогти, кажучи: «Я не підтримую цього напряму, і неспроста...».

Але головний недолік примітивних типів — компілятор позбавляється шансу виявити логічну помилку. Наприклад, є у нас метод, що приймає два параметри — ім'я та прізвище. Якщо звести їх до стандартних рядковим типом, то компілятор побачить тільки два шматки тексту, від перестановки яких сенс не зміниться. В результаті один розробник передасть першим параметром ім'я, а інший — прізвище. І обидва технічно будуть праві. Помилка знищується в зародку, якщо для імені та прізвища існують спеціальні типи. У реальній системі, де ім'я та прізвище є до того значущими сутностями, що входять в інтерфейс окремо, зводити їх просто до рядків — помилка. Ім'я — це зовсім не довільна рядок. Хоча б тому, що вибирається з заздалегідь відомого множини. Ще воно, скажімо, не містить цифр (хоча тут я не впевнений).

Але повернемося до дат. День — це ні в якому разі не unsigned int, unsigned char не std::string. День — це… день! Якщо ми будуємо модель, в якій присутні дати, то для подання днів має сенс створити спеціальний тип. Користувальницькі типи даних — це як раз те, що додає C++ всю його міць і виразність. Тоді і милиці стають не потрібні. Вводимо клас для представлення днів

class Day
{
explicit Day (unsigned char day);
//...
private:
unsigned char mValue;
};

Як-то ось так. Природно для фізичної подання значення в пам'яті ми все одно використовуємо примітивний тип. Але більше це не є частиною інтерфейсу. Відразу ж ми отримуємо повний контроль над вмістом цього поля, виключаючи ряд можливих помилок. Так, не знаючи повної дати, точні обмеження встановити не вийде, але принаймні перевірку на попадання в інтервал 1..31 вже можна організувати. Найголовніше ж: реалізувавши спеціальні типи даних для місяця і року з явними (explicit) конструкторами для ініціалізації примітивними типами, ми отримаємо іменовані параметри, підтримувані безпосередньо мовою. Функцію тепер можна викликати наступним чином

Date theDate = createDate(Day(2), Month(3), Year(4));

Жодних обхідних шляхів, ніяких додаткових бібліотек. Так, міняти місцями параметри такий підхід не дозволить, але це не так і важливо. Головна місія іменованих параметрів — виключити помилки при виклику функцій і підвищити читабельність коду — здійснюється.

В якості бонусу отримуємо підвищену гнучкість, якщо, скажімо, захотілося задавати місяць ще й у строковому вигляді. Тепер можна не робити перевантажується варіант createDate (це функція створення об'єкта дати, яке їй взагалі справа до формату місяця). Замість цього просто додається ще один явний конструктор для типу «місяць»

class Month
{
explicit Month(unsigned char month);
explicit Month(std::string month);
//...
private:
unsigned char mValue;
};

Кожен тепер займається своєю справою — createDate створює дату, а клас Month інтерпретує і контролює коректність значення місяця.

Date theDate = createDate(Day(2), Month("Jan"), Year(4));

Відразу хочеться заперечити — а чи не забагато зайвих типів буде, якщо для кожного примітивного типу робити свій тип-обгортку? Тут як подивитися. Якщо ви студент, якому потрібно швидше написати лабу, здати і забути, то так багато зайвого коду і втрачений час. Але якщо мова йде про реальній системі, в довгого і щасливого життя якій ви зацікавлені, то називати користувальницькі типи сутностей, що використовуються в інтерфейсі, зайвими я б не став.

Але як бути з користувацькими типами? Що, наприклад, робити, якщо якийсь метод приймає кілька об'єктів однакового типу

User user1, user2;
//...
someMethod(user1, user2);

Тут все залежить від контексту. Якщо всі об'єкти рівнозначні, то і проблеми немає — від порядку їх передачі нічого не змінюється. Щоб підкреслити це, можна хіба що передавати об'єкти, упакованими в масив або інший контейнер. Якщо ж об'єкти нерівнозначні, наприклад, метод ставить у підпорядкування user2 об'єкту user1, то зовсім незайвими будуть спеціальні типи, що відображають ролі об'єктів. Повинні бути обгортки навколо об'єктів користувачів (як у випадку з примітивними типами) або простіше створити спеціальні класи-спадкоємці User, залежить від реалізованої системи. Важливо якимось чином виразити засобами мови різні ролі user1 і user2, дозволяючи компілятору відловлювати помилки, пов'язані з їх можливою плутаниною.

Який можна зробити висновок. Не потрібно прагнути вихопити все найкраще з усіх мов і засунути це в один, і без того багатостраждальний C++. Важливо вміти подолати інерцію при зміні мови програмування. Скажімо, так, в Луа можна присвоювати значення відразу декільком змінним

x, y = getPosition()

Сама ідея прекрасна, але чи потрібна вона в C++. Взагалі не потрібна. Тут простіше створити тип Position і присвоювати значення його об'єкту. Мова — це інструмент, не більше того. І з того, що інструменти іноді схожі, зовсім не випливає, що користуватися ними потрібно однаково аж до дрібниць.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.