在Swift中,从技术角度来看,为什么编译器会关心协议是否只能用作通用约束?

时间:2017-09-19 14:01:15

标签: swift

在Swift中,从技术角度来看,为什么编译器会关心协议是否只能用作通用约束?

说我有:

protocol Fooable {
    associated type Bar: Equatable
    func foo(bar: Bar) {
        bar==bar
    }
}

为什么以后我不能声明一个以Fooable对象作为参数的func?

在我看来,编译器应该只关心给定的Fooable可以发送消息" foo"用一个参数" bar"这是一个Equatable,因此响应消息," =="。

我理解Swift是静态类型的,但是为什么Swift甚至真的关心这个上下文中的类型,因为唯一重要的是是否可以将给定的消息有效地发送到对象?

我正在努力理解这背后的为什么,因为我怀疑必须有充分的理由。

1 个答案:

答案 0 :(得分:1)

在上面的示例中,如果您编写的函数采用Fooable参数,例如

func doSomething(with fooable:Fooable) {
    fooable.foo(bar: ???) // what type is allowed to be passed here? It's not _any_ Equatable, it's the associated type Bar, which here would be...???
}

可以将哪种类型传递到fooable.foo(bar:)?它不能是Equatable,它必须是特定的关联类型Bar

最终,它归结为协议引用" Self"或者具有相关类型的最终具有基于具体实现已经符合的的不同接口(即,Self的特定类型或特定的关联类型)。因此,可以将这些协议视为缺少直接解决它们所需的类型和签名的信息,但仍然可以作为符合类型的模板,因此可以用作通用约束。

例如,编译器会接受这样写的函数:

func doSomething<T: Fooable>(with fooable:T, bar: T.Bar) {
    fooable.foo(bar: bar)
}

在这种情况下,我们不会尝试将Fooable协议作为协议来解决。相反,我们接受任何具体类型T,它本身被约束为符合Fooable。但是每次调用函数时,编译器都会知道T的确切具体类型,因此它将知道确切的关联类型Bar,并且确切地知道可以将哪个类型作为参数传递给fooable.foo(bar:)

更多细节

  • 可能有助于考虑&#34;通用协议&#34; - 即具有相关类型的协议,包括可能的类型Self - 与正常协议略有不同。正如您所说,正常协议定义了消息传递要求,并且可用于抽象出特定实现并将任何符合类型的地址作为协议本身。

    通用协议被更好地理解为Swift中泛型系统的一部分,而不是普通协议。您不能转换为类似Equatable(无if let equatable = something as? Equatable)的通用协议,因为作为通用系统的一部分,Equatable必须在编译时专门化并理解。更多关于此的信息。

    您从通用协议获得的与普通协议相同的是符合类型必须遵守的合同的概念。通过说associatedtype Bar: Equatable你得到一个合同,类型Bar将为你提供一种方式来调用`func ==(左:Bar,右:Bar) - &gt;布尔&#39 ;.它需要符合要求的类型来提供特定的接口。

    通用协议和普通协议之间的区别在于,您可以转换为正常协议并将其作为协议类型(不是具体类型)发送消息,但您必须始终按照其具体类型将通信协议解析为通用协议(就像所有泛型一样) )。这意味着普通协议是运行时功能(用于动态转换)以及编译时功能(用于类型检查)。但通用协议编译时功能(无动态转换)。

  • 为什么我们不能说var a:Equatable?好吧,让我们稍微挖掘一下。 Equatable意味着可以将特定类型的一个实例与另一个相同类型的实例进行相等性比较。即func ==(left:A, right:A) -> Bool。如果Equatable是一个正常的协议,你会说更像:func ==(left:Equatable, right:Equatable) -> Bool。但如果你考虑一下,它就没有意义。 String与其他字符串相等,Int与其他Int相同,但这并不意味着字符串与Ints相等。如果Equatable协议只需要为您的类型实现func ==(left:Equatable, right:Equatable) -> Bool,那么您现在和将来如何编写该函数来将您的类型与其他所有可能的Equatable类型进行比较?

    由于那是不可能的,因此Equatable仅要求您为Self类型的两个实例实现==。因此,如果Foo:Equatable,则必须仅为两个Foo实例定义==。

    现在让我们看一下var a:Equatable的问题。这一开始似乎有道理,但事实上,它并没有:

    var a: Equatable = "A String"
    var b: Equatable = 100
    let equal = a == b
    

    由于ab都是Equatable,我们可以比较它们是否相等,对吧?但事实上,a的相等实现仅限于将String与String进行比较,而b的相等实现仅限于将Int与Int进行比较。因此,最好将通用协议与其他泛型一样认为Equatable<String>Equatable<Int>的协议不同,即使它们都被认为只是&#34; Equatable&#34;

  • 至于为什么你可以拥有[AnyHashable: Any]类型的词典,而不是[Hashable: Any],这一点就越来越清晰了。 Hashable协议继承自Equatable,因此它是一个&#34;通用协议&#34;。这意味着对于任何Hashable类型,必须有func ==(left: Self, right:Self) -> Bool。字典使用hashValue和相等比较来存储和检索键。但是,字典如何比较String键和Int键是否相等,即使它们都符合Hashable / Equatable?它不可能。因此,您需要将钥匙包裹在一个特殊的&#34;类型的橡皮擦中。叫AnyHashable。类型擦除器的工作方式对于这个问题的范围来说太详细了,但是足以说像AnyHashable这样的类型擦除器用某种类型T:Hashable实例化,然后将hashValue的请求转发到它的包装类型,并实现{{ 1}}也使用包装类型的相等实现。我认为这个要点应该很好地说明如何实现&#34; AnyEquatable&#34;橡皮擦。

    https://gist.github.com/JadenGeller/f0d05a4699ddd477a2c1

    继续前进,因为AnyHashable是单个具体类型(不是像Hashable协议那样的泛型类型),您可以使用它来定义字典。因为AnyHashable的每个实例都可以包装不同的Hashable类型(String,Int,等等),并且还可以生成hashValue并检查其与任何其他AnyHashable实例是否相等,它正是字典对其键所需要的

    因此,从某种意义上说,像AnyHashable这样的类型擦除器是一种实现技巧,可以将通用协议转换为普通协议。通过擦除/丢弃通用关联类型信息,但保留所需的方法,您可以有效地将Hashable的特定一致性抽象为一般类型&#34; AnyHashable&#34;可以包装任何Hashable,但在非通用情况下使用。

  • 如果您查看用于创建&#34; AnyEquatable&#34;:https://gist.github.com/JadenGeller/f0d05a4699ddd477a2c1的实现的要点,这可能会全部汇集在一起​​,然后返回查看如何将此变为不可能/非 - 编译前面的代码:

    ==(left:AnyHashable, right: AnyHashable) -> Bool

    进入概念上相似但实际上有效的代码:

    var a: Equatable = "A String"
    var b: Equatable = 100
    let equal = a == b