Swift中的Erasure类型
使用Swift,您可以通过关联一个或多个通用类型来定义协议。 这些类型是使用associatedtype关键字定义的。 名称“ 通用类型 ”在这里有点被篡改,我们应该谈论预留类型的占位符。 确实,我们将这种协议视为通用协议时,并没有提供很大的使用灵活性。
如果您需要有关泛型类型的提醒,请参阅以下文章,概述其所有可能性。
随时访问我的博客以阅读原始文章(语法突出显示更好)
在本文的其余部分中,我们将依靠一个简单的案例: 杯子是一种可以容纳任何Liquid的类型。 显然,我们将使用协议来定义这两种类型。
液体是一种协议,它具有三种属性:颜色,粘度和温度(可变的)
public protocol Liquid {
var temperature: Float { get set }
var viscosity: Float { get }
var color: String { get }
}
杯子是使用LiquidType关联类型声明的协议。 此关联类型必须遵守上述Liquid协议。 杯子展示了LiquidType类型的简单属性以及填充它的函数。
public protocol Cup {
associatedtype LiquidType: Liquid
var liquid: LiquidType? { get }
func fill (with liquid: LiquidType)
}
首先,两种液体: 咖啡和牛奶 。
struct Coffee: Liquid {
let viscosity: Float = 3.4
let color = "black"
var temperature: Float
}struct Milk: Liquid {
let viscosity: Float = 2.2
let color = "white"
var temperature: Float
}
然后是两种类型的杯子: CeramicCup和PlasticCup 。 这些类是通用类(能够容纳任何类型的Liquid ),并用类型L替换协议杯的相关类型。 顺便说一句,我们确实有义务强迫L遵守Liquid协议(如Cup协议中所定义)。
class CeramicCup: Cup {
var liquid: L?func fill(with liquid: L) {
self.liquid = liquid
self.liquid!.temperature -= 1
}
}class PlasticCup: Cup {
var liquid: L?func fill(with liquid: L) {
self.liquid = liquid
self.liquid!.temperature -= 10
}
}
我们现在有两种具体类型的杯子 ,可以容纳任何类型的液体 。
现在,我们很想使用这样的实现:
那是一个失败! 我们都已经看到了这些令人毛骨悚然的错误:“ 协议’xxx’不能用作通用约束,因为它具有Self或associatedtype ”
实际上,不可能将Cup用作通用类型。 编译器不容许由与协议关联的类型表示的未知数。 这就像求解一个方程组,其中两个未知数只知道一个方程。
即使我们试图通过显式指定关联类型来帮助编译器,也会由于Cup 表示法而被阻塞。
如果我们参考在Swift Github上发布的Generics Manifesto,则有一天可能会支持Generic协议。 但是与此同时,有一个技巧可以实现我们的目标: Type Erasure 。 顾名思义,它是一种技术,它使我们能够删除与协议关联的类型并使之通用。 最初,这个技巧可能会吓到您,因为它并非微不足道,但只需机械地应用两个众所周知的设计模式即可完成:
- 抽象类:https://en.wikipedia.org/wiki/Template_method_pattern
- 装饰器:https://en.wikipedia.org/wiki/Decorator_pattern
在Swift中,没有我们在Java中所知道的抽象类。 但是,抽象类只是类型的部分且非即时的实现。 因此,很容易编写Cup的实现。 通过声明一个遵循该协议的通用类(就像CeramicCup或PlasticCup一样 )但不允许其使用来完成此操作(指令fatalError禁止我们直接使用AbstractCup )
private class AbstractCup: Cup {
var liquid: L? {
fatalError("Must implement")
}func fill(with liquid: L) {
fatalError("Must Implement")
}
}
现在已达到该技术的第一步,让我们开始装饰。
如果您已经在Java中使用过InputStream ,则可以使用Decorator模式,而不必实现它。 正是这种模式允许FileInputStream成为InputStream,同时向其添加新功能。 FileInputStream将封装经典的InputStream (作为其构造函数的参数提供),同时专门化某些行为。 这种模式的好处在于,您可以无限期嵌套装饰器,而无需冻结继承树。 这就是BufferedInputStream可以修饰FileInputStream和基本InputStream的方式 。
但是回到我们的杯子。 在我们的案例中,我们将构建一个封装Cup的装饰器。 我们已经具有AbstractCup的Cup的基本实现(与Java示例中的InputStream相同),因此我们可以定义一个包装器(或装饰器),该继承器将继承AbstractCup,同时委派对其封装的Cup的属性和函数调用。
final private class CupWrapper: AbstractCup {
var cup: Cpublic init(with cup: C) {
self.cup = cup
}override var liquid: C.LiquidType? {
return self.cup.liquid
}override func fill(with liquid: C.LiquidType) {
self.cup.fill(with: liquid)
}
}
我们可以注意到对Cup和LiquidType类型施加的约束。 我们必须确保装饰的AbstractCup中的液体类型与构造函数中传入参数的杯子之一完全相同。
因此, CupWrapper同时是一个Cup和一个Cup包装器。 从某种意义上讲,它允许将Cup (仅是协议)转换为具体类型。 但是最后, Cup作为构造函数参数传递仍然可以很好地指示包装器的行为。
在此阶段的过程中,我们已经获得了可用的结果,并且已使我们的协议以通用的方式可用:
var cupsOfCoffee = [AbstractCup]()
cupsOfCoffee.append(CupWrapper(with: CeramicCup()))
cupsOfCoffee.append(CupWrapper(with: PlasticCup()))
我们设法申报了杯咖啡。 关联的类型已按预期删除。
如果我们想结束“类型擦除”的概念(并且以与Swift的标准库中实现的方式相同),我们还有最后一步。 我邀请您查看AnyIterator类型的官方文档(标准Swift库),以使您了解我们设定的最终目标。
首先,我想提请您注意AbstractCup和CupWrapper类的声明。 一切都已完成,因此我们的模型用户( final / private )既看不见它们也不可以直接修改它们。 这个想法是尽可能地隐藏我们的擦除类型模式的实现,并且仅将最简单的机制暴露给外界。
因此,我们将提供一个真正通用的AnyCup类,它将是一个简单的Cup包装器。 在某个地方,可以直接在Cup协议上第二次应用装饰器模式(使用我们的CupWrapper进行内部委派工作):
final public class AnyCup: Cup {
private let abstractCup: AbstractCuppublic init(with cup: C) where C.LiquidType == L {
abstractCup = CupWrapper(with: cup)
}public func fill(with liquid: L) {
self.abstractCup.fill(with: liquid)
}public var liquid: L? {
return self.abstractCup.liquid
}
}
等等!
它为我们提供了非常简单直观的使用方法:
var coffeeCups = [AnyCup]()
coffeeCups.append(AnyCup(with: CeramicCup()))
coffeeCups.append(AnyCup(with: PlasticCup()))coffeeCups.forEach { (anyCup) in
anyCup.fill(with: Coffee(temperature: 60.4))
print(anyCup.liquid!.color)
print(anyCup.liquid!.temperature)
}var milkCups = [AnyCup]()
milkCups.append(AnyCup(with: CeramicCup()))
milkCups.append(AnyCup(with: PlasticCup()))milkCups.forEach { (anyCup) in
anyCup.fill(with: Milk(temperature: 30.9))
print(anyCup.liquid!.color)
print(anyCup.liquid!.temperature)
}
导致我们出现问题的代码行:
var cupsOfCoffee = [Cup]()
变成:
var coffeeCups = [AnyCup]()
赌注赢了。
就我个人而言,今天我仍然在努力使用这种机制,因为它确实不是那么琐碎,我必须重新阅读几次以确保对它有所了解。但是如果我们机械地运用我刚才概述的步骤,肯定会达到预期的结果,希望不必写太久。
可在我的Github上访问该代码:Playground Type Erasure
希望对您有所帮助。
我们甚至可以在Cup协议中添加一些辅助功能:
extension Cup {
func toAnyCup () -> AnyCup {
return AnyCup(with: self)
}
}
这是一个很好的快捷方式,可以通过以下方式使用:
var coffeeCups = [AnyCup]()
coffeeCups.append(CeramicCup().toAnyCup())
coffeeCups.append(PlasticCup().toAnyCup())
很好nice
敬请关注。
[更新2019–05–11]
当我需要为我的一个专业项目设置“类型擦除”时,我想起了基于闭包的解决方案。 我想在这里分享它,因为它是一种非常简单而优雅的模式。
快速提醒我们必须“键入擦除”的协议:
public protocol Liquid {
var temperature: Float { get set }
var viscosity: Float { get }
var color: String { get }
}
和
public protocol Cup {
associatedtype LiquidType: Liquid
var liquid: LiquidType? { get }
func fill (with liquid: LiquidType)
}
我们可以直接构建一个通用包装器类,而不是先创建一个抽象类,然后在其上保存符合Cup的类型的引用的包装器,该类将保留一些有关Cup 行为的引用。
您可以将其视为某种委派机制。
由于我们不能根据杯子的相关类型保留引用,因此包装器将保留杯子的功能和属性的引用,并负责其执行。
很简单forward。