Ковариантность, контравариантность в 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 не предоставляет встроенной поддержки инвариантности на уровне языка.
Однако можно использовать различные техники и практики для обеспечения инвариантности в коде. Например, можно использовать проверки условий, чтобы обеспечить соблюдение инвариантности.
