
3.4 利用参数化方法
Julia的类型系统和多重分派功能为编写可扩展性代码提供了强大的基础。事实证明,我们还可以在函数参数中使用参数化类型。我们可以称这些为参数化方法。参数化方法提供了一种有趣的方式来表达在分派过程中可以匹配哪些数据类型。
在以下各节中,我们将介绍如何在游戏中利用参数化方法。
3.4.1 使用类型参数
在定义函数时,我们可以用类型信息注释每个参数。参数的类型可以是常规抽象类型、具体类型或参数化类型。让我们考虑一下这个示例函数来分解一系列游戏零件:

things参数用AbstractVector{Any}注释,这意味着它可以是任何AbstractVector类型,其中包含任何对象,该对象是Any的子类型(实际上就是所有内容)。为了使该方法参数化,我们可以使用T型参数重写它,如下所示:

在这里,explode函数可以接受带有参数T的任何AbstractVector,它可以是Any的任何子类型。因此,如果我们仅传递一个Asteroid对象的向量,即Vector{Asteroid},它应该可以工作。如果我们传递一个符号向量,即Vector{Symbol},它也可以工作。试试看。

请注意,Vector{Asteroid}实际上是AbstractVector{Asteroid}的子类型。通常,只要SomeType是SomeOtherType的子类型,我们就可以说SomeType{T}是SomeOtherType{T}的子类型。但是,如果我们不确定,也很容易检查。

也许我们真的不希望explode函数接受任何东西的向量。由于此函数是为我们的太空战争游戏编写的,因此我们可以将函数限制为接受Thing子类型的任何类型的向量。它可以很容易地实现,如下所示:

使用where符号进一步用超类信息限定参数。每当在函数签名中使用类型参数时,我们都必须在该参数的后面加上where子句以用于相同的参数。
函数参数中的类型参数使我们可以指定适合where子句中指示的约束的数据类型类别。前面的explode函数可以采用包含Thing的任何子类型的向量。这意味着该函数是泛型的,即只要它满足约束条件,就可以使用无限数量的类型进行分派。
接下来,我们将探讨使用抽象类型作为指定函数参数的另一种方法。它看起来与使用参数化类型非常相似。但是,会有细微的差别,我们将在3.4.2节中进行解释。
3.4.2 使用类型参数替换抽象类型
通常,我们可以在函数签名中用类型参数替换任何抽象类型。当我们这样做时,将得到一种与原始语义相同的参数化方法。
这是很重要的一点。让我们看看是否可以通过示例演示此行为。
假设我们正在构建tow函数,以便飞船可以拖曳宇宙中的某些东西,如下所示:

现在tow函数是用具体的Spaceship类型和抽象的Thing类型参数定义的。如果要查看为此函数定义的方法,则可以使用methods函数显示存储在Julia方法表中的内容。

正如预期的那样,相同的方法签名可以完美地返回。
现在,让我们定义一个参数化方法,其中我们将类型参数用作参数B:

现在,我们定义了一种具有不同签名语法的新方法。但这真的是另一种方法吗?让我们检查一下。

我们可以看到方法列表仍然只有一个条目,这意味着新的方法定义已替换了原来的一个。但是,这并不奇怪。尽管新方法签名看起来与以前的签名不同,但其含义与原始签名相同。最终,第二个自变量B仍然接受Thing子类型的任何类型。
那么,为什么我们还要经历所有的麻烦呢?好吧,在这种情况下,没有理由将此方法转换为参数化方法。但是通过3.4.3节,你将了解为什么这样做会很有用。
3.4.3 在使用参数时强制类型一致性
类型参数最有用的功能之一是,它们可用来强制类型的一致性。
假设我们要创建将两个Thing对象组合在一起的新函数。由于我们并不真正在乎传递什么具体类型,因此我们可以只编写一个能够完成工作的函数:

我们还可以快速进行一些小测试,以确保飞船和小行星的所有四个组合都正常工作。

你可能想知道我们如何获得如此出色的有关特定武器的输出。如前面所述,我们可以使用类型从Base包扩展show函数。你可以在本书的GitHub仓库中找到我们对show函数的实现。
看起来一切都正常,但是随后我们意识到需求与我们最初的想法略有不同。该函数不能将任何种类的对象进行分组,该函数应该只能将相同类型的对象进行分组,也就是说,可以将飞船与飞船进行分组,小行星与小行星进行分组,而不能将飞船与小行星进行分组。那么我们在这里可以做什么?一个简单的解决方案是在方法签名中抛出一个类型参数:

在此函数中,我们为两个参数都注释上了类型T,并指定T必须是Thing的子类型。因为两个参数都使用相同的类型,所以现在我们指示系统仅在两个参数具有相同的类型时才分派给该方法。现在,我们可以尝试与之前相同的四个测试用例,如下代码所示。

实际上,我们现在可以确保仅在参数具有相同类型时才分派该方法。这就是将类型参数用作函数参数是一个好主意的原因之一。
接下来,我们将讨论使用类型参数的另一个原因——从方法签名中提取类型信息。
3.4.4 从方法签名中提取类型信息
有时,我们想在方法主体中找出参数类型。这实际上很容易做到。事实证明,所有参数也都绑定为变量,我们可以在方法主体本身中访问该变量。标准eltype函数的实现为此类用法提供了一个很好的例子:

我们可以看到类型参数T在主体中被引用。让我们看看它是如何工作的。

在第一次调用中,由于数组中的所有对象都是Spaceship类型,因此将返回Spaceship类型,同样,对于第二次调用,将返回Asteroid。第三次调用返回Thing,因为我们混合使用了多个Spaceship和Asteroid对象。这些类型可以进一步检查,如下所示。

总而言之,我们可以通过在函数定义中使用类型参数来构建更灵活的函数。从表达的角度来看,每个类型参数都可以覆盖整个数据类型。我们还可以在多个参数中使用相同的类型参数来实现类型一致性。最后,我们可以轻松地从方法签名中直接提取类型信息。
现在,让我们继续讨论本章的最后一个主题——接口。