不透明类型和协议类型之间的区别 (下)
in 技术 with 0 comment

不透明类型和协议类型之间的区别 (下)

in 技术 with 0 comment

返回不透明类型看起来与使用协议类型作为函数的返回类型非常相似,但这两种返回类型在是否保留类型标识方面有所不同。不透明类型是指一种特定类型,尽管函数的调用者无法看到是哪种类型;协议类型可以指任何符合协议的类型。一般来说,协议类型在它们存储的值的底层类型方面为您提供了更大的灵活性,而不透明类型让您对这些底层类型做出更强有力的保证。

不透明类型解决的问题(上)
返回不透明类型(中)

区别

例如,这是一个 flip(_:) 的版本,使用协议类型作为其返回类型,而不是不透明返回类型:

func protoFlip<T: Shape>(_ shape: T) -> Shape {
    return FlippedShape(shape: shape)
}

这个版本的 protoFlip(_:)flip(_:) 具有相同的主体,并且始终返回相同类型的值。与 flip(_:) 不同的是,protoFlip(_:) 返回的值不需要总是具有相同的类型——它只需要符合 Shape 协议即可。换句话说,protoFlip(_:) 与调用者的 API 约束比 flip(_:) 宽松得多。它保留了返回多种类型值的灵活性:

func protoFlip<T: Shape>(_ shape: T) -> Shape {
    if shape is Square {
        return shape
    }

    return FlippedShape(shape: shape)
}

上述代码的版本返回的 Square 实例或 FlippedShape 的实例,具体取决于传入的 shape 。此函数返回的两个翻转后的 Shape 可能具有完全不同的类型。

我的注解:这其实也是协议和泛型的区别,协议的返回类型完全擦除了泛型 T 的类型信息。

当翻转相同 shape 类型的多个实例时,此函数的其它有效版本可能会返回不同类型的值。

我的注解:暂时没有理解这句话的意思。

不太具体的返回类型信息 protoFlip(_:) 意味着许多依赖于类型信息的操作在返回值上不可用。例如,不可能编写一个 == 运算符来比较此函数返回的结果。

let protoFlippedTriangle = protoFlip(smallTriangle)
let sameThing = protoFlip(smallTriangle)
protoFlippedTriangle == sameThing  // Error

示例最后一行出现的错误有几个原因。直接的问题是它 Shape 不包括 == 操作符作为其协议要求的一部分。

我的注解:下面是 Equatable 协议里的 == 函数。

public protocol Equatable {
    static func == (lhs: Self, rhs: Self) -> Bool
}

如果您尝试添加一个,您将遇到的下一个问题是 == 运算符需要知道其左侧和右侧参数的类型。这种运算符通常采用 Self 作为参数类型,与继承该协议的任何具体类型匹配,但是向协议添加 Self 要求不允许在将协议用作类型时发生类型擦除。

使用协议类型作为函数的返回类型使您可以灵活地返回任何符合协议的类型。但是,这种灵活性的代价是无法对返回的值执行某些操作。

这个例子展示了 == 操作符是如何不可用的——它依赖于特定的类型信息,而这些信息没有通过使用协议类型来保存。

此方法的另一个问题是形状转换不嵌套。翻转三角形的返回值是 Shape 类型的值,并且 protoFlip(_:) 函数采用符合 Shape 协议的泛型参数。但是,它的返回值 Shape 协议不符合该泛型协议。这意味着像 protoFlip(protoFlip(smallTriange)) 这样应用多个转换的代码是无效的,因为翻转的结果不是 protoFlip(_:) 的有效参数。

我的注释:返回值 Shape 只是一个协议类型,而参数 T 期待更加具体的类型,仅仅通过协议类型是无法满足泛型要求。

相反,不透明类型保留了底层类型的标识。Swift 可以推断关联类型,这使您可以在协议类型不能用作返回值的地方使用不透明的返回值。例如,下面是泛型协议 Container 的一个版本:

protocol Container {
    associatedtype Item
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}
extension Array: Container { }

您不能将 Container 其用作函数的返回类型,因为该协议具有关联的类型。您也不能将它用作泛型返回类型中的约束,因为在函数体之外没有足够的信息来推断泛型类型需要是什么。

// Error: Protocol with associated types can't be used as a return type.
func makeProtocolContainer<T>(item: T) -> Container {
    return [item]
}

// Error: Not enough information to infer C.
func makeProtocolContainer<T, C: Container>(item: T) -> C {
    return [item]
}

使用 opaque 类型作为返回类型表达了所需的 API 协定——该函数返回一个容器,但拒绝指定容器的类型:some Container

func makeOpaqueContainer<T>(item: T) -> some Container {
    return [item]
}
let opaqueContainer = makeOpaqueContainer(item: 12)
let twelve = opaqueContainer[0]
print(type(of: twelve))
// Prints "Int"

返回值 twelve 的类型被推断为 Int,这说明了类型推断适用于不透明类型的事实。在 makeOpaqueContainer(item:) 的实现中,不透明容器的底层类型是 [T]。 在这个例子中,TInt,因此返回值是一个整数数组,并且 Item 关联的类型被推断为 IntContainer 的下标返回 Item,这意味着类型 twelve 也被推断为是 Int

我的注释

不透明类型是运行时决定的类型,并不是编译时。如果你执行 twelve + 2 表达式会报错。因为在编译期间,twelve 类型仍然是 (some Container).Item,它的类型是由函数 makeOpaqueContainer 内部决定的。

// Error: Cannot convert value of type '(some Container).Item' to expected argument type 'Int'
twelve + 2

// Success
if let _twelve = twelve as? Int {
    _twelve + 2
}

参考:https://docs.swift.org/swift-book/LanguageGuide/OpaqueTypes.html

Responses