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
函数。 myTiger
和myTiger
都是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中分解类型擦除