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
}

然后是两种类型的杯子: CeramicCupPlasticCup 。 这些类是通用类(能够容纳任何类型的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的实现。 通过声明一个遵循该协议的通用类(就像CeramicCupPlasticCup一样 )但不允许其使用来完成此操作(指令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的装饰器。 我们已经具有AbstractCupCup的基本实现(与Java示例中的InputStream相同),因此我们可以定义一个包装器(或装饰器),该继承器将继承AbstractCup,同时委派对其封装的Cup的属性和函数调用。

 final private class CupWrapper: AbstractCup { 
var cup: C
public 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)
}
}

我们可以注意到对CupLiquidType类型施加的约束。 我们必须确保装饰的AbstractCup中的液体类型与构造函数中传入参数的杯子之一完全相同。

因此, CupWrapper同时是一个Cup和一个Cup包装器。 从某种意义上讲,它允许将Cup (仅是协议)转换为具体类型。 但是最后, Cup作为构造函数参数传递仍然可以很好地指示包装器的行为。

在此阶段的过程中,我们已经获得了可用的结果,并且已使我们的协议以通用的方式可用:

 var cupsOfCoffee = [AbstractCup]() 
cupsOfCoffee.append(CupWrapper(with: CeramicCup()))
cupsOfCoffee.append(CupWrapper(with: PlasticCup()))

我们设法申报了杯咖啡。 关联的类型已按预期删除。

如果我们想结束“类型擦除”的概念(并且以与Swift的标准库中实现的方式相同),我们还有最后一步。 我邀请您查看AnyIterator类型的官方文档(标准Swift库),以使您了解我们设定的最终目标。

首先,我想提请您注意AbstractCupCupWrapper类的声明。 一切都已完成,因此我们的模型用户( final / private )既看不见它们也不可以直接修改它们。 这个想法是尽可能地隐藏我们的擦除类型模式的实现,并且仅将最简单的机制暴露给外界。

因此,我们将提供一个真正通用的AnyCup类,它将是一个简单的Cup包装器。 在某个地方,可以直接在Cup协议上第二次应用装饰器模式(使用我们的CupWrapper进行内部委派工作):

 final public class AnyCup: Cup { 
private let abstractCup: AbstractCup
public 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。