Ковариантность, контравариантность в PHP
Ковариантность и контравариантность — это концепции, связанные с типизацией данных и описывают совместимость типов по отношению друг к другу. Это механизм типо-безопасности при полиморфизме (разных реализации одного контракта/интерфейса).
-
Ковариантность — "сужает" тип - позволяет использовать более конкретный тип, чем тот, который определен в родительском классе - уточняет тип возвращаемого значения.
- Контравариантность — "расширять" тип - наоборот, позволяет использовать более общий тип. Снижает требования для входных параметров.
Пример ковариантности:
class Parent { public function method(): int|float {} } // Правильно: Ковариантность соблюдена class Child extends Parent { public function method(): int {} } // Неправильно: Ковариантность нарушена class Child extends Parent { public function method(): string {} // FATAL ERROR !!! }
Пример контравариантности:
class Parent { public function method( int|float $param ) {} } // Правильно: Контравариантность соблюдена class Child extends Parent { public function method( int|float|string $param ) {} } // Неправильно: Контравариантность нарушена class Child extends Parent { public function method( int $param ) {} // FATAL ERROR !!! }
Правила ковариантности и контравариантности в PHP
Ковариантность и контравариантность предоставляются в PHP на уровне языка. В PHP 7.2 была добавлена частичная поддержка, а в PHP 7.4 полная. Подробнее.
Правила ковариантности и контравариантности в PHP накладывают ограничения на использования в коде одного типа данных вместо другого. Т.е. эти правила определяют, как типы данных возвращаемых значений и принимаемых параметров методов могут меняться в подклассах.
Когда класс использует метод интерфейса или переопределяет метод, который уже был определен в родительском классе, переопределяемый метод должен соответствовать правилам вариантности, иначе код выдаст фатальную ошибку.
Другими словами, переопределяемый метод должен быть совместим с методом, который был определен ранее.
Эти понятия напрямую связаны с одним из принципов SOLID: LSP (принцип подстановки Барбары Лисков). Согласно этому принципу мы должны использовать дочерние классы вместо базового, так чтобы дочерний класс умел делать все тоже что и базовый. Например, при перегрузке метода, новый метод должен уметь все тоже самое что и исходный метод (получать такие же параметры, возвращать совместимые данные/типы).
В ООП ковариантность и контравариантность напрямую связаны с концепцией "полиморфизма", так как позволяет использовать объекты разных подтипов вместо объектов базового типа, что является одним из принципов полиморфизма - разные реализации одного контракта.
Для чего это нужно?
Хорошо типизированная программа делает код более надежным и предотвращая ошибки связанные с неверными типами данных.
Ограничения ковариантности нужны, чтобы мы могли использовать только дочерние классы вместо базового. Если нарушить это правило, то метод который раньше возвращал один тип данных, может вернуть то, с чем код работать не умеет. В результате может произойти что угодно. Когда мы не типизируем данные, это хорошо, если дальше по коду программа "упадет" с ошибкой. Но программа может продолжить работу, в результате код может работать неправильно и может привести к печальным последствиям.
Ограничения контравариантности нужны, чтобы мы обязательно получили типы данных с которыми умеет работать базовый метод - это гарантирует обратную совместимость. Если это правило будет нарушено, и где-то в коде вместо дочернего метода будет использоваться базовый метод, то в метод может прийти тип, с которым базовый метод работать не умеет и работа программы будет нарушена.
Ковариантность
Позволяет дочернему методу возвращать указанный или более конкретный тип, чем тип определенный в родительском методе.
Пример:
class Parent { public function method(): int|float|string { ... } } class Child extends Parent { public function method(): int { ... } }
Пример:
class Parent { public function method(): Base { ... } } class Child extends Parent { public function method(): Child { ... } }
Пример:
class Parent { public function method(): iterable { ... } } class Child extends Parent { public function method(): array { ... } }
Типы считаются более конкретными в следующих случаях:
// Если удалено объединение типов : int|float|string >> : int // Если добавлено пересечение типов : Iterator >> : Iterator & Countable // Когда происходит изменение типа на под тип : Base >> : Child : iterable >> : array : iterable >> : Traversable
Если мы попытаемся расширить возвращаемый тип:
: int|float >> : int|float|string|bool : int|float >> : mixed : Iterator >> : iterable
То мы получим предупреждение в IDE и фатальную ошибку в рантайме (при запуске кода).
class Father { public function method(): int { } } class Child extends Father { public function method(): int|string { } } // Compile Error: // Declaration of Child::method(): string|int // must be compatible with Father::method(): int
Контравариантность
Снижает требования для входных данных, позволяя расширять тип параметров метода. Контравариантность разрешает параметру метода быть более общим чем указано в родительском классе.
Пример:
class Parent { public function method( int|float $param ): void {} } class Child extends Parent { public function method( int|float|string $param ): void {} }
Пример:
class Parent { public function method( Child $param ): void {} } class Child extends Parent { public function method( Base $param ): void {} }
Пример:
class Parent { public function method( Traversable $param ): void {} } class Child extends Parent { public function method( iterable $param ): void {} }
Дочерний метод должен принимать все те типы с которыми умеет работать родительский метод, а также может работать с дополнительными типами, если ему это необходимо.
Таким образом, дочерний метод может работать с тем, с чем не умеет работать родительский метод (контравариантность), но при этом возвращать он должен, обязательно то, что ожидается от родительского метода (ковариантность). Т.е. "наружу" метод ограничен родительским типом, а "внутрь" неограничен (расширяем).
Инвариантность
Инвариантность — ситуация, когда любое наследование запрещено и нужно использовать именно тот тип, который был указан. Т.е. нет возможности использовать другие варианты типа - только указанный.
Другими словами — это "константность", неизменность, постоянство, независимость от каких-либо условий. Например, скорость света инвариантна.
Инвариантность в PHP
PHP не предоставляет встроенной поддержки инвариантности на уровне языка.
Однако можно использовать различные техники и практики для обеспечения инвариантности в коде. Например, можно использовать проверки условий, чтобы обеспечить соблюдение инвариантности.