공변성과 반공변성
공변성과 반공변성 (Covariance and Contravariance)
PHP의 타입 시스템에서 상속 관계에 있는 메서드의 반환 타입(공변)과 파라미터 타입(반공변)이 어떻게 유연해졌는지를 다룬다. PHP 7.4 이후 본격 지원된 이 개념과 프로퍼티 타입의 변화 규칙을 예제로 설명한다.
본문
PHP 7.2.0 에서 하위(자식) 메서드의 파라미터에 대한 타입 제한을 제거함으로써 부분적인 반공변(partial contravariance)이 도입되었습니다. PHP 7.4.0 부터는 완전한 공변성과 반공변성을 지원합니다.
공변성(covariance) 은 자식 메서드가 부모 메서드의 반환 타입보다 더 구체적인(specific) 타입을 반환할 수 있게 해 줍니다. 반공변성(contravariance) 은 자식 메서드의 파라미터 타입이 부모의 그것보다 덜 구체적일 수 있게 해 줍니다.
타입 선언이 더 구체적인 것으로 간주되는 경우는 다음과 같습니다.
- 유니언 타입(union type)에서 타입 하나가 제거된 경우
- 인터섹션 타입(intersection type)에 타입 하나가 추가된 경우
- 클래스 타입이 자식 클래스 타입으로 바뀐 경우
iterable이array또는Traversable로 바뀐 경우
반대의 경우 타입 선언은 덜 구체적인 것으로 간주됩니다.
공변성 (Covariance)
공변성이 어떻게 동작하는지 보여 주기 위해, 간단한 추상 부모 클래스 Animal 을 만듭니다. Animal 은 자식 클래스인 Cat 과 Dog 에 의해 확장됩니다.
<?php
abstract class Animal
{
protected string $name;
public function __construct(string $name)
{
$this->name = $name;
}
abstract public function speak();
}
class Dog extends Animal
{
public function speak()
{
echo $this->name . " barks";
}
}
class Cat extends Animal
{
public function speak()
{
echo $this->name . " meows";
}
}
이 예제에는 값을 반환하는 메서드가 없다는 점을 주목하세요. 이제 Animal, Cat, Dog 클래스 타입의 새 객체를 반환하는 팩토리(factory) 몇 개를 추가하겠습니다.
<?php
interface AnimalShelter
{
public function adopt(string $name): Animal;
}
class CatShelter implements AnimalShelter
{
public function adopt(string $name): Cat // instead of returning class type Animal, it can return class type Cat
{
return new Cat($name);
}
}
class DogShelter implements AnimalShelter
{
public function adopt(string $name): Dog // instead of returning class type Animal, it can return class type Dog
{
return new Dog($name);
}
}
$kitty = (new CatShelter)->adopt("Ricky");
$kitty->speak();
echo "\n";
$doggy = (new DogShelter)->adopt("Mavrick");
$doggy->speak();
위 예제의 출력은 다음과 같습니다.
Ricky meows
Mavrick barks
Note: PHP 8.5.0 부터 부모 메서드가 반환 타입으로
static을 선언한 경우, final 클래스의 오버라이딩 메서드는 대신self또는 클래스 자신의 타입을 반환 타입으로 선언할 수 있습니다. 더 이상 확장될 수 없는 클래스에서는 이것들이static과 동일하기 때문입니다. 자세한 내용은 마이그레이션 가이드를 참고하세요.
반공변성 (Contravariance)
앞선 예제의 Animal, Cat, Dog 클래스에 이어서 Food 와 AnimalFood 클래스를 추가하고, Animal 추상 클래스에 eat(AnimalFood $food) 메서드를 추가하겠습니다.
<?php
class Food {}
class AnimalFood extends Food {}
abstract class Animal
{
protected string $name;
public function __construct(string $name)
{
$this->name = $name;
}
public function eat(AnimalFood $food)
{
echo $this->name . " eats " . get_class($food);
}
}
반공변성의 동작을 확인하기 위해, Dog 클래스에서 eat 메서드를 오버라이드해 어떤 Food 타입 객체든 허용하도록 만듭니다. Cat 클래스는 그대로 둡니다.
<?php
class Dog extends Animal
{
public function eat(Food $food) {
echo $this->name . " eats " . get_class($food);
}
}
다음 예제는 반공변성의 동작을 보여 줍니다.
<?php
$kitty = (new CatShelter)->adopt("Ricky");
$catFood = new AnimalFood();
$kitty->eat($catFood);
echo "\n";
$doggy = (new DogShelter)->adopt("Mavrick");
$banana = new Food();
$doggy->eat($banana);
위 예제의 출력은 다음과 같습니다.
Ricky eats AnimalFood
Mavrick eats Food
그렇다면 $kitty 가 $banana 를 eat() 하려 하면 어떻게 될까요?
$kitty->eat($banana);
위 예제의 출력은 다음과 같습니다.
Fatal error: Uncaught TypeError: Argument 1 passed to Animal::eat() must be an instance of AnimalFood, instance of Food given
프로퍼티의 공변성 (Property variance)
기본적으로 프로퍼티는 공변적이지도 반공변적이지도 않으며, 따라서 불변(invariant) 입니다. 즉 자식 클래스에서 그 타입이 전혀 바뀌지 않을 수 있습니다. 그 이유는 "get" 연산은 공변적이어야 하고, "set" 연산은 반공변적이어야 하기 때문입니다. 프로퍼티가 두 요구 조건을 모두 만족하는 유일한 방법은 불변인 것입니다.
PHP 8.4.0 부터 추상 프로퍼티(인터페이스나 추상 클래스에서)와 가상 프로퍼티(virtual properties)가 추가되면서, get 또는 set 연산만 갖는 프로퍼티를 선언할 수 있게 되었습니다. 그 결과 get 연산만 요구되는 추상 프로퍼티나 가상 프로퍼티는 공변적일 수 있습니다. 마찬가지로 set 연산만 요구되는 추상 프로퍼티나 가상 프로퍼티는 반공변적일 수 있습니다.
그러나 프로퍼티가 get 과 set 연산을 모두 갖게 되면, 추가 확장에 대해 더 이상 공변적이거나 반공변적이지 않게 됩니다. 즉 이제 불변(invariant)이 됩니다.
예제 #1 프로퍼티 타입의 공변성
<?php
class Animal {}
class Dog extends Animal {}
class Poodle extends Dog {}
interface PetOwner
{
// Only a get operation is required, so this may be covariant.
public Animal $pet { get; }
}
class DogOwner implements PetOwner
{
// This may be a more restrictive type since the "get" side
// still returns an Animal. However, as a native property
// children of this class may not change the type anymore.
public Dog $pet;
}
class PoodleOwner extends DogOwner
{
// This is NOT ALLOWED, because DogOwner::$pet has both
// get and set operations defined and required.
public Poodle $pet;
}
?>
더 알아보기
- 타입 선언(유니언 타입, 인터섹션 타입) 관련 문서
- 추상 프로퍼티와 가상 프로퍼티 (PHP 8.4)
- PHP 8.5 마이그레이션 가이드 —
static반환 타입 관련 변경 사항