Я хотел бы, чтобы они появились и знаю многих разработчиков, которые согласны со мной. С другой стороны, есть группа PHP-программистов, возможно, даже большая, которые не знают, что такое дженерики и для чего они нужны.
Давайте поговорим о том, что такое дженерики, почему PHP не поддерживает их и что, возможно, нас ждёт в будущем.
У каждого языка программирования есть определённая система типов. У некоторых языков очень строгая реализация, в то время как у других — PHP относится к этой категории — более слабая.
Системы типов используются по разным причинам, самая очевидная из них — проверка типов.
Представим, что у нас есть функция, которая принимает два числа, два целых числа и выполняет некоторую математическую операцию:
1<?php2 3function add($a, $b)4{5 return $a + $b;6}
PHP с радостью позволит вам передавать в эту функцию любые данные: числа, строки, логические значения — неважно. PHP изо всех сил постарается преобразовать переменную, когда в этом есть смысл, например, в случае сложения.
1<?php2 3add('1', '2'); // 3
Но эти преобразования — жонглирование типами — часто приводят к неожиданным результатам, если не сказать, что к ошибкам и сбоям.
1<?php2 3add([], true); // ?
Можно добавить проверку, чтобы функция работала с любыми входными данными:
1<?php 2 3function add($a, $b) 4{ 5 if (!is_int($a) || !is_int($b)) { 6 return null; 7 } 8 9 return $a + $b;10}
Или можно использовать встроенные объявления типов PHP:
1<?php2 3function add(int $a, int $b): int4{5 return $a + $b;6}
Многие разработчики в сообществе PHP не используют объявления типов, потому что знают, какие входные данные передавать в функцию — в конце концов, они сами её написали.
Однако такие рассуждения быстро рассыпаются: зачастую вы не единственный, кто работает с этим кодом, вы также используете код, который написан другими — подумайте о том, сколько composer-пакетов вы используете. Поэтому, хотя этот пример в отдельности может показаться не таким уж важным, проверка типов действительно пригодится, когда кодовая база начнёт расти.
Кроме того, добавление объявления типов не только защищает от недопустимого состояния, но и разъясняет, какие входные данные от нас, программистов, ожидаются. Часто с типами данных вам не нужно читать внешнюю документацию, потому что многое из того, что делает функция, уже заключено в объявлении типов.
IDE активно помогают в работе: они могут сообщить программисту, какой тип входных данных ожидает функция или какие поля и методы доступны для объекта, который принадлежит к определённому классу. С помощью IDE, написание кода становится более продуктивным, во многом потому, что они могут статически анализировать объявления типов по всей нашей кодовой базе.
С другой стороны, у систем типов свои ограничения. Простой пример — список элементов:
1<?php 2 3class Collection extends ArrayObject 4{ 5 public function offsetGet(mixed $key): mixed 6 { /* … */ } 7 8 public function filter(Closure $fn): self 9 { /* … */ }10 11 public function map(Closure $fn): self12 { /* … */ }13}
У коллекции множество методов, которые работают с любыми типами входных данных: цикл, фильтрация, сопоставление — что угодно; коллекции должно быть не важно, имеет ли она дело со строками или целыми числами.
Но давайте посмотрим на это с точки зрения стороннего наблюдателя. Что произойдёт, если мы хотим быть уверены, что одна коллекция содержит только строки, а другая — только объекты User
. Коллекцию не волнует тип данных при переборе элементов, но нас волнует. Мы хотим знать, является ли данный элемент в цикле пользователем или строкой — это большая разница. Но без надлежащей информации о типах данных, IDE не сможет эффективно подсказывать нам во время работы.
1<?php2 3$users = new Collection();4 5// …6 7foreach ($users as $user) {8 $user-> // ?9}
Мы могли бы создать отдельные реализации для каждой коллекции: одна работает только со строками, а другая — только с объектами User
:
1<?php 2 3class StringCollection extends Collection 4{ 5 public function offsetGet(mixed $key): string 6 { /* … */ } 7} 8 9class UserCollection extends Collection10{11 public function offsetGet(mixed $key): User12 { /* … */ }13}
Но что, если нам понадобится третья реализация? Четвёртая? Может быть, десятая или двадцатая. Управлять этим кодом станет довольно мучительно.
Вот тут-то и приходят на помощь дженерики.
Чтобы внести ясность: в PHP нет дженериков. То, что я покажу дальше, невозможно в PHP, но это возможно во многих других языках.
Вместо того чтобы создавать отдельную реализацию для каждого возможного типа, многие языки программирования позволяют разработчикам определять "общий" тип классу коллекции:
1<?php2 3class Collection<Type> extends ArrayObject4{5 public function offsetGet(mixed $key): Type6 { /* … */ }7 8 // …9}
По сути, мы говорим, что реализация класса коллекции будет работать для любого типа входных данных, но когда мы создаём экземпляр коллекции, мы должны указать этот тип. Общая реализация уточняется в зависимости от потребностей программиста:
1<?php2 3$users = new Collection<User>();4 5$slugs = new Collection<string>();
Может показаться мелочью: добавить тип, но это открывает целый мир возможностей.
Теперь IDE знает, какие данные находятся в коллекции, может подсказать нам многое: не добавляем ли мы элемент с неправильным типом; что мы можем делать с элементами при итерации коллекции; передаём ли мы коллекцию в функцию, которая знает, как работать с этими конкретными элементами.
И хотя технически мы могли бы добиться того же самого, вручную реализуя коллекцию каждого необходимого нам типа, общая реализация значительно упростит работу для нас, разработчиков, которые пишут и поддерживают код.