泛型
泛型(Generics) 是一种重要的编程范式,它允许程序员在编写代码时使用类型参数,这些类型参数在编译时或运行时可以被具体的类型所替代。泛型的使用能够增加代码的复用性、灵活性和可维护性。使得这种数据类型能够适用于不同的数据类型,从而实现代码的复用和高效。
PHP是一种动态类型语言,不像C 、Java等语言有强类型机制,因此在PHP中实现泛型编程不是一件容易的事情。
PHP中的泛型
在PHP官方文档中,并没有直接提及泛型
这个概念。这并不意味着PHP不支持泛型,而是说PHP没有像Java或C#那样显式地提供泛型的语法支持。实际上这并不意味着PHP无法实现泛型的功能。
在PHP中,可以通过一些技巧和手段来模拟泛型的行为。例如可以使用接口(Interface)和类型提示(Type Hinting)来实现类似于泛型的功能。通过定义一个接口作为类型参数,我们可以实现类似泛型的类型检查和类型约束。虽然这种方法与Java或C#中的泛型有所不同,但它确实提供了一种在PHP中实现泛型功能的方式。
一个简单的例子
代码语言:javascript复制<?php
/**
* @desc CollectionInterface
* @author Tinywan(ShaoBo Wan)
*/
declare(strict_types=1);
interface CollectionInterface
{
public function add($element);
public function remove($element);
public function contains($element): bool;
public function size(): int;
}
class ArrayCollection implements CollectionInterface
{
private array $elements = [];
public function add($element)
{
$this->elements[] = $element;
}
public function remove($element)
{
if (($key = array_search($element, $this->elements, true)) !== false) {
unset($this->elements[$key]);
}
}
public function contains($element): bool
{
return in_array($element, $this->elements, true);
}
public function size(): int
{
return count($this->elements);
}
}
在上面例子中,使用了一个CollectionInterface
接口来定义了一个通用的集合接口,然后实现了一个ArrayCollection
类来实现CollectionInterface
接口,这个类就可以用于操作任何类型的数据。
PHP为什么不支持泛型?
这可能与PHP的设计理念和历史背景有关。PHP是一种弱类型语言,它允许变量在运行时动态地改变类型。这种灵活性使得PHP在Web开发等领域具有广泛的应用。实际上这种灵活性也带来了一些问题,比如类型安全问题。泛型作为一种强类型特性,可以在一定程度上提高代码的类型安全性。 但是在PHP这种弱类型语言中引入泛型可能会与其设计理念产生冲突。
PHP最初是为了简化Web开发而设计的,它的语法和功能都比较简单和直接。随着PHP的发展,虽然不断有新的特性和语法被加入到PHP中,但PHP始终保持着一种简洁和易用的风格。在这种背景下引入复杂的泛型语法可能会增加PHP的学习成本和开发难度。
完全具体化泛型
使用泛型,您可以使用占位符定义类的属性和方法类型。然后可以在创建类的实例时指定这些。这使代码可重用性和类型安全跨不同的数据类型。具体化
的泛型是定义泛型类型信息并将其延续到运行时的实现,允许在运行时强制执行泛型需求。
作为PHP语法,这可能看起来像这样
代码语言:javascript复制class Entry<KeyType, ValueType>
{
public function __construct(protected KeyType $key, protected ValueType $value)
{
}
public function getKey(): KeyType
{
return $this->key;
}
public function getValue(): ValueType
{
return $this->value;
}
}
new Entry<int, BlogPost>("123", new BlogPost());
在实例化的类中,泛型类型KeyType
将被替换为int
,ValueType
的每个实例将被替换为BlogPost
,从而导致对象的行为类似于以下类定义:
class IntBlogPostEntry
{
public function __construct(protected int $key, protected BlogPost $value)
{
}
public function getKey(): int
{
return $this->key;
}
public function getValue(): BlogPost
{
return $this->value;
}
}
泛型的使用往往会增加代码的冗长性,因为它要求每次引用泛型类型时都指定类型参数。这在下面的PHP代码片段中得到了演示:
代码语言:javascript复制function f(List<Entry<int,BlogPost>> $entries): Map<int, BlogPost>
{
return new Map<int, BlogPost>($entries);
}
function g(List<BlogPostId> $ids): List<BlogPost>
{
return map<int, BlogPostId, BlogPost>($ids, $repository->find(...));
}
类型推断可以通过让编译器自动为我们推断适当的类型来减少这种冗长。例如,在上面的示例中,编译器可能会自动确定new Map()
和map()
的正确类型。但是,这在PHP中很难实现。引用Nikita
的话:主要是由于PHP编译器对代码库的视图非常有限(它一次只能看到一个文件)
。
请看下面的例子
代码语言:javascript复制class Box<T>
{
public function __construct(public T $value) {}
}
new Box(getValue());
在这种情况下,getValue()
表达式的类型是未知的,直到函数在运行时加载,使得无法推断new Box(.)
中的T
。在编译期间。
我们可以在运行时根据函数的返回值分配T,但这会导致类型不稳定。在前面的例子中,new Box()
的类型将取决于getValue()
的返回值的实现,这可能太具体了:联合收割机结合Box
是不变的这一事实,当试图对Box实例做任何有用的事情时,这段代码将很快中断:
interface ValueInterface {}
class A implements ValueInterface {}
class B implements ValueInterface {}
function getValue(): ValueInterface
{
return new A();
}
function doSomething(Box<ValueInterface> $box)
{
}
$box = new Box(getValue()) // runtime: Box<A>, statically: Box<ValueInterface>
doSomething($box); // accepts Box<ValueInterface>, not Box<A>
当类型基于不依赖于实现的编译时/静态信息时,类型是最有用的。
注意:在这个例子中,
Box
是不变的,因为它通常是泛型类的情况。这意味着无论X和Y之间的关系如何,Box<X>
都不是Box<Y>
的子类型或超类型,因此Box<A>
不是Box<ValueInterface>
的子类型,并且doSomething()
不能接受Box<A>
。
集合
泛型的一个主要用例是需要类型化数组。在PHP中,瑞士军刀数组类型的使用(和滥用)有很多原因。但是你目前不能强制将类型用作键或值。
在一个并行项目中,我们一直在研究一种专用的Collections语法,作为完整泛型的一种挑战性较小的替代方案。
集合有三种形式:集合、序列和字典。集合和序列只定义一个值类型,而字典有键和值类型。
其语法可以如下所示:
代码语言:javascript复制class Article
{
public function __construct(public string $subject) {}
}
collection(Seq) Articles<Article>
{
}
collection(Dict) YearBooks<int => Book>
{
}
然后你可以像对普通类一样实例化序列和集合:
代码语言:javascript复制$a1 = new Articles();
$b1 = new YearBooks();
Sequences和Dictionaries将自动定义许多方法,提供类似PHP已经拥有的大量array_*
函数的基本功能。如果使用定义的方法来添加或更新集合中的元素,则键和值的类型必须与集合中定义的类型相匹配。
在上面的例子中,YearBooks字典的add()
方法要求使用int作为键,Book作为值。对于主要的操作方法(add、get、unset和isset),ArrayAccess风格的重载操作也可以工作,以及潜在的操作符重载。
集合的一个缺点是你需要声明它们。按照已采用的做法,这意味着每个集合在单独的文件中有一行声明。
另一个问题是潜在的更高的内存使用,因为对于每个类,PHP必须保留一个相应的类条目,包括所有相关方法的列表。
第三个问题是兼容类型的集合之间没有instanceof/is-a关系,例如:
代码语言:javascript复制class A {}
class B extends A {}
seq As<A> {}
seq Bs<B> {}
new B() instanceof A // true
new Bs() instanceof As // false
集合虽然功能不太强大,但在许多用例中可以替代泛型,但没有太多的复杂性。上面概述的实现也明显更容易。还提供了一个实验性分支。然而,如果发现完整的泛型是可行的并且得到支持,那么直接在标准泛型上实现Seq、Set和Dict将是非常可取的。