Дженерики в PHP

Kate

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

Давайте поговорим о том, что такое дженерики, почему PHP их не поддерживает и что, возможно, нас ждёт в будущем.

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

Системы типов используются по разным причинам, самая очевидная из них — проверка типов.

Представим, что у нас есть функция, которая принимает два числа, два целых числа и выполняет некоторую математическую операцию:

<?php

function add($a, $b)
{
return $a + $b;
}
PHP с радостью позволит вам передавать в эту функцию любые данные: числа, строки, логические значения — неважно. PHP изо всех сил постарается преобразовать переменную, когда в этом есть смысл, например, в случае сложения.

<?php

add('1', '2'); // 3
Но эти преобразования — жонглирование типами — часто приводят к неожиданным результатам, если не сказать, что к ошибкам и сбоям.

<?php

add([], true); // ?
Можно добавить проверку, чтобы функция корректно работала с любыми входными данными:

<?php

function add($a, $b)
{
if (!is_int($a) || !is_int($b)) {
return null;
}

return $a + $b;
}
Или можно использовать встроенные объявления типов PHP:

<?php

function add(int $a, int $b): int
{
return $a + $b;
}
Многие разработчики в сообществе PHP не используют объявления типов, потому что знают, какие входные данные передавать в функцию — в конце концов, они сами её написали.

Однако такие аргументы быстро рассыпаются: зачастую вы не единственный, кто работает с этим кодом, вы также используете код, который написан другими — подумайте о том, сколько composer-пакетов вы используете. Поэтому, хотя этот пример может показаться не таким уж важным, проверка типов действительно пригодится, когда кодовая база начнёт расти.

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

IDE активно помогают в работе: они могут сообщить программисту, какой тип входных данных ожидает функция или какие поля и методы доступны для объекта, который принадлежит к определённому классу. С помощью IDE, написание кода становится более продуктивным, во многом потому, что они могут статически анализировать объявления типов по всей нашей кодовой базе.

С другой стороны, у систем типов свои ограничения. Простой пример — список элементов:

<?php

class Collection extends ArrayObject
{
public function offsetGet(mixed $key): mixed
{ /* … */ }

public function filter(Closure $fn): self
{ /* … */ }

public function map(Closure $fn): self
{ /* … */ }
}
У коллекции множество методов, которые работают с любыми типами входных данных: цикл, фильтрация, сопоставление — что угодно; коллекции должно быть не важно, имеет ли она дело со строками или целыми числами.

Но давайте посмотрим на это со стороны. Что произойдёт, если мы хотим быть уверены, что одна коллекция содержит только строки, а другая — только объекты User. Коллекцию не волнует тип данных при переборе элементов, но нас волнует. Мы хотим знать, является ли данный элемент в цикле пользователем или строкой — это большая разница. Но без надлежащей информации о типах данных, IDE не сможет эффективно подсказывать нам во время работы.

<?php

$users = new Collection();

// …

foreach ($users as $user) {
$user-> // ?
}
Мы могли бы создать отдельные реализации для каждой коллекции: одна работает только со строками, а другая — только с объектами User:

<?php

class StringCollection extends Collection
{
public function offsetGet(mixed $key): string
{ /* … */ }
}

class UserCollection extends Collection
{
public function offsetGet(mixed $key): User
{ /* … */ }
}
Но что, если нам понадобится третья реализация? Четвёртая? Может быть, десятая или двадцатая. Управлять этим кодом станет довольно мучительно.

Вот тут-то и приходят на помощь дженерики.

Чтобы внести ясность: в PHP нет дженериков. То, что я покажу дальше, невозможно в PHP, но это возможно во многих других языках.

Вместо того чтобы создавать отдельную реализацию для каждого возможного типа, многие языки программирования позволяют разработчикам определять "общий" тип классу коллекции:

<?php

class Collection<Type> extends ArrayObject
{
public function offsetGet(mixed $key): Type
{ /* … */ }

// …
}
По сути, мы говорим, что реализация класса коллекции будет работать для любого типа входных данных, но, когда мы создаём экземпляр коллекции, мы должны указать этот тип. Общая реализация уточняется в зависимости от потребностей программиста:

<?php

$users = new Collection<User>();

$slugs = new Collection<string>();
Может показаться мелочью: добавить тип, но это открывает целый мир возможностей.

Теперь IDE знает, какие данные находятся в коллекции, может подсказать нам многое: не добавляем ли мы элемент с неправильным типом; что мы можем делать с элементами при итерации коллекции; передаём ли мы коллекцию в функцию, которая знает, как работать с этими конкретными элементами.

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

 
Сверху