为简单起见,我将使用 PHP 作为示例语言。让我马上解释这一点。在现代语言中,尤其是解释语言中,传统上存在非严格类型:
function join($a){
return is_array($a) ? implode('', $a) : $a;
}
并且有严格的打字:
function join(array $a) : string{
return implode('', $a);
}
例如,还有 C++ 中的模板类型:
template <class T, class R>
R* join(T * array, int count)
{
R *output = new R(count);
for (int i = 0; i < count; i++)
output += array[i] ;
return output;
}
C++中也有类型重载,但不是很灵活,也很繁琐,像模板类型一样,后者也没有可读性。
但是缺少多重类型(正如我发现的那样-这称为联合类型)-我想要以下内容:
function join(array|Iterator|ICollectable $input) : string|null{
/*какой-то код обработки*/
}
问题是——是否有任何语言支持类似的类型限制语法,如果有,是哪些?如果不是,这个概念不好的原因是什么?
为什么会出现对此缺乏内置支持的问题 - 具有变体类型的语言的程序员通常在方法注释中写这样的东西:
/**
* Some function describe
*
* @param array|ISomeList $list ...
* @return false|string ...
*/
但在 f-ii 声明中,他们被迫不对类型进行显式引用(依赖于注释),从而留下变体类型:
public function someFunction($list) {... return $some;};
这种情况经常发生在不能为了强类型而忽视代码可读性的严肃框架中。
因此,在没有明确指定所需类型的情况下检查代码时:可读性会降低。注释并不能 100% 保证类型限制:程序员和 IDE 一样,无法确切知道输入参数中可以输入什么。当程序运行时——如果输入参数突然出现错误:没有错误/警告发生,这使捕获错误变得复杂,降低了稳定性。
相反,当只使用严格类型时,整个系统变得更加复杂,无可替代:类的数量呈几何级数增长。更糟糕的是,因为类型的非严格性适用于简单的架构决策和可读代码,并且在脚本语言的情况下,它不能被忽视。
UPD:
是的,当然,这样的功能并不完全符合 OOP 的原则。但这将是修改这些原则的理由——因为。他们非常温和地没有考虑到“班级数量的几何增长”。此外,联合类型与鸭子类型结合使用——我不会称之为减号,但这是一个值得争论的大领域。
UPD2:
如果出现多类型有效性的问题:PHP 的一个更生动的例子是声明一个重要的、经常使用的函数,用于从数据库表中选择对象(db-table-mapper)Zend 框架(1.9)类 Zend_Db_Table_Abstract .
/**
* Fetches all rows.
*
* Honors the Zend_Db_Adapter fetch mode.
*
* @param string|array|Zend_Db_Table_Select $where OPTIONAL An SQL WHERE clause or Zend_Db_Table_Select object.
* @param string|array $order OPTIONAL An SQL ORDER clause.
* @param int $count OPTIONAL An SQL LIMIT count.
* @param int $offset OPTIONAL An SQL LIMIT offset.
* @return Zend_Db_Table_Rowset_Abstract The row results per the Zend_Db_Adapter fetch mode.
*/
public function fetchAll($where = null, $order = null, $count = null, $offset = null)
{
if (!($where instanceof Zend_Db_Table_Select)) {
$select = $this->select();
if ($where !== null) {
$this->_where($select, $where);
}
if ($order !== null) {
$this->_order($select, $order);
}
if ($count !== null || $offset !== null) {
$select->limit($count, $offset);
}
} else {
$select = $where;
}
$rows = $this->_fetch($select);
$data = array(
'table' => $this,
'data' => $rows,
'readOnly' => $select->isReadOnly(),
'rowClass' => $this->getRowClass(),
'stored' => true
);
$rowsetClass = $this->getRowsetClass();
if (!class_exists($rowsetClass)) {
Zend_Loader::loadClass($rowsetClass);
}
return new $rowsetClass($data);
}
根据设计,第一个参数可以是几种类型之一。这是必要的,以便该方法的潜在用户(程序员)很容易将映射器融入他们的头脑。毕竟,他可以通过不同的方式从代码中调用此方法,但方法保持不变:
$table->fetchAll('user_id in (1,2,5)');
$table->fetchAll(['user_id < 100', 'gender = "male"']);
$table->fetchAll( $table->select()->where('user_id IN (?)', $userIdsArray) );
一种方法很容易记住,很适合头脑。虽然 fetchAll 方法的程序员不想将一个任务的逻辑(从数据库中选择对象)分散到他自己内部的多个重载方法中,但他希望所有内容都在一个地方,以便打开的代码立即了解什么是有效的,并且在什么顺序。至少这对应于SRP - 该方法有一个职责:从数据库中选择对象。
即提供一整套语言来与数据库表进行通信。它更像是对程序员的一种服务,而不仅仅是一种方法。框架中的此类方法:每秒一次,因为框架首先应该方便程序员。一个程序员不应该钻研“几千个类”——为了使用特性——这是解决代码可读性问题的一个生动例子,其中联合类型会派上用场。
UPD3 :
事实证明,正如评论中所建议的那样,PHP 开发人员也曾考虑在 2015 年引入这个概念,但他们在投票中否决了。
兴趣问。我不知道其他语言的情况如何,但是
scala你想做的是这样解决的:使用
pattern matching也可以定义参数类型应该具有哪些方法。例如。
任何设计都必须有一个理由:为什么需要它,它带来了什么优势,它带来了什么劣势。
从设计理论我们知道,一个软件单元必须执行严格分配给它的任务算法。这种语义的存在意味着代码将被称为“和收割者、读者和烟斗上的玩家”。这完全打破了逻辑并导致了糟糕的编程风格。此外,关于它的使用也有很多疑问。
例如,期望从他那里得到什么回报?然后你打算如何处理你收到的东西?
事实证明,为了确定结果,我必须再检查 20 次,他们是否真的返回了一个字符串或一个数组?或者建议如何在代码内部工作?同样,我不认为存在可以很好地与各种类型协调工作的算法。您仍然需要返回注释并阅读代码的作用和方式。
如果你真的想要,创建自己的类或方法来整理内部类型并抛出 TypeError 比猜测我们的方法实际进入了什么以及如何使用它更容易。而且总是使用统一的东西更容易。
总的来说,我几乎看不到任何优点,你可以放心地没有它。
据我了解,仅在文档级别指定此内容的麻烦在于,在那里编写的内容无法验证或可验证非常困难:它需要额外的解析、静态分析或其他一些额外的步骤。
我有时会在clojure中看到扮演这个角色的协议。
协议是包含多个方法签名的抽象,其文档包含对它们的实现的说明和限制。
到目前为止,它看起来像interfaces。并且协议的声明确实导致了同名接口的创建,它可以通过代码实现,而不是在 Clojure 中,而是在 Java 中。
现在是有趣的部分:任何类型都可以实现一个接口,而且这个事实可以在任何地方声明。在最有趣的情况下,您可以在自己的代码中使用来自另一个库的类型来实现来自一个库的协议支持。不干扰库代码!您甚至可以使用内置类型来实现协议!
事实上,检查协议类型会自动检查对象是否属于实现的类型和(尽管在运行时:您需要编写测试)。但是能够在那里添加其他东西。
在 OOP 中,这类似于一组具有共同祖先或接口的包装器类。在这种情况下,值必须在手动传输到使用地点之前包装在包装器中,因为在那个阶段仍然不知道选择哪个包装器。Clojure 会根据值的类型自行执行此操作。只是语法糖,没有魔法。但是,最后,我们谈论的是语言手段。
当然,还有不同种类的断言。当编译成一个版本时,其中一些可能会被自动排除。它们通常表达与文档中相同的内容,但采用机器可验证的方式。您需要进入文档的源代码这一事实似乎是一个减号,但是......也许文档和代码没有分开的事实甚至是一个加号?
一个平庸的先决条件(也是唯一一个内置在所提出解决方案的语言中的先决条件):
还有
core.typed一个额外的“静态类型”(工作更像测试,不是在编译时,而是仅在显式运行时),只有一个函数U接受一组类型并返回它们的总和类型*。这被放在注释中,紧挨着实现,尽管它可以单独放置。的确,core.typed它仍然是一个小众的“玩具”,您需要用它来注释一般的所有内容,否则检查几乎没有用,并且类型推断效果不佳。计划,其中最受欢迎的选择是
prismatic/schema. 在那里either, , 的直接类似物U已core.typed被弃用,因为它很慢并且违反它会产生奇怪的错误消息。现在它部分地执行了它的功能cond-pre(我们的案例也是如此):这是精简版。可以在
cond-pre某处写下结果并明确地对其进行操作validate。check当然,检查是必要的。例如,在测试中。这是一个单独的运行,也可以解析注释并根据它们进行检查,但这在技术上更加困难。
* sum-type 是对类型的操作,返回一个与所有组成类型的值相匹配的类型。