Swift:在PAT上实现动态调度(具有关联类型的协议)

面向协议的编程功能强大而强大,我们对此毫无疑问! 但是,Swift仍然还不够完善(至少在撰写本文时),我今天要谈论的Swift的缺点之一是在使用PAT(关联类型的协议)时无法执行动态调度以及如何解决此缺陷并使PAT支持动态调度。

什么是PAT?

关联类型是未知类型的占位符,而PAT是具有一个或多个关联类型要求的协议。

如下所示的Animal协议是PAT的示例。

如果您想了解有关PAT的更多信息,建议您阅读NatashaTheRobot的这篇很棒的文章。

什么是动态调度?

动态调度是选择在运行时调用哪种多态操作(方法或函数)的实现的过程。 —维基百科

下面的示例代码演示了如何使用协议(没有关联的类型)实现动态调度。

上面的代码将生成两行输出:

 我的老虎在丛林中行走。 
我的牛在农场里散步。

这表明walk()函数调用已动态分配给所需的具体类型实现。

PAT的缺点

为了演示PAT的缺点,让我们在Animal协议中添加一个相关的类型要求FoodType 。 我们还将在Animal协议中引入eat(food: FoodType)功能要求。

如果将上面的示例代码粘贴到Xcode游乐场,则会出现如下所示的错误。

  错误:协议“动物”只能用作一般约束,因为它具有“自我”或相关类型要求 

这是什么意思? 基本上,这意味着如果我们的Animal协议包含一个或多个相关的类型要求,就不能再将Animal协议用作类型。

但为什么??? 😢

让我们看一下下面的代码片段。

从逻辑上讲,我们可以说上面的代码是正确的,因为我们将Meat喂给Tiger ,将Grass类喂给Cow 。 但是,编译器无法知道我们是否将正确的FoodType类型传递给eat函数。 myTigermyTiger都是Animal类型,因此编译器不知道哪个是Cow类型,哪个是Tiger类型。 这种歧义解释了为什么编译器阻止我们使用Animal作为类型。 因此,在这种情况下无法进行动态调度。

解决方法💡

要解决此问题,我们将使用一种称为类型擦除的技术来隐藏类型内的动态调度。 要了解有关类型擦除的更多信息,这是gwendolyn weston的精彩演讲,您不可错过!

回到我们的解决方法,我们将要做的是停止使用Animal协议作为类型,并引入另一个新的具体类型来帮助我们执行动态调度。 在这里,我们将命名新的具体类型AnyAnimal ,它是一个符合Animal协议的枚举。

上面的代码是很容易解释的,您可以想象AnyAnimal枚举是一个调度程序,负责调度对所需Animal具体类型的函数调用。

请注意,此处没有强制使AnyAnimal符合Animal协议,但是这样做可以降低代码的健壮性。 想象一下将来将新功能需求添加到Animal协议的情况,如果我们忘记更新AnyAnimal协议,则会触发编译错误。 如果我们不遵守Animal协议,则不会触发任何错误。

说够了! 让我们看一下正在运行的AnyAnimal枚举…

在上面的代码片段中,我们使用AnyAnimal代替Animal作为我们的animalArray元素类型。 这样我们就可以避免使用Animal作为类型,从而避免我们先前遇到的错误。

接下来,让我们看一下通过枚举animalArray每个元素得到的输出。

 我的老虎在丛林中行走。 
我的老虎吃肉
我的牛在农场里散步。
我的牛吃草

我们可以看到, AnyAnimal枚举正在正确调度对期望的Animal具体类型的walk()eat(food: FoodType)函数调用。 🎉

如果您想在Xcode游乐场中试用代码,则可以在此处获取完整的示例代码。

结论

本文中的解决方法可能并不理想,因为它包含许多样板代码,并且我们将不得不手动处理所有调度逻辑。 但是,直到有Swift支持存在的一天,枚举和类型擦除可能是我们可以用来在PAT上完成动态分配的最佳工具。

[更新]:2017年10月24日

感谢Hacker News用户的建议,我们实际上可以通过将AnyAnimal枚举更改为通用结构来对其进行改进。

使用上述通用结构, 当我们添加符合 Animal 协议 的新类时, 无需 每次都手动 更新 AnyAnimal 。 👏👏👏

进一步阅读

  • Alexis Gallagher-关联类型的协议
  • 使用Swift探索存在类型
  • 在Swift中分解类型擦除