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

Posted

技术标签:

【中文标题】在 Swift 中,从技术角度来看,为啥编译器会关心协议是不是只能用作通用约束?【英文标题】:In Swift, from a technical standpoint, why does the compiler care if a protocol can only be used as a generic constraint?在 Swift 中,从技术角度来看,为什么编译器会关心协议是否只能用作通用约束? 【发布时间】:2017-09-19 14:01:15 【问题描述】:

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

说我有:

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

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

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

我知道 Swift 是静态类型的,但是为什么 Swift 还要真正关心这种情况下的类型,因为唯一重要的是给定的 message 是否可以有效地发送到对象?

我正试图了解这背后的原因,因为我怀疑一定有充分的理由。

【问题讨论】:

比较***.com/q/41695792/2976878;简而言之,具有关联类型/Self 要求的协议不能用作实际类型是没有真正原因的。 【参考方案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:)

更多详情

将“通用协议”(即具有关联类型的协议,可能包括 Self 类型)视为与普通协议略有不同的东西可能会有所帮助。正如您所说,普通协议定义了消息传递要求,并且可用于抽象出特定实现并将任何符合类型的协议本身处理。

泛型协议在 Swift 中被更好地理解为泛型系统的一部分,而不是普通协议。您不能转换为像 Equatable 这样的通用协议(没有if let equatable = something as? Equatable),因为作为通用系统的一部分,Equatable 必须在编译时进行专门化和理解。更多关于这下面。

您从与普通协议相同的通用协议中得到的是符合类型必须遵守的契约的概念。通过说associatedtype Bar: Equatable,您将获得一个合同,类型 Bar 将为您提供一种调用 `func ==(left:Bar, right: Bar) -> Bool' 的方法。它需要符合的类型才能提供一定的接口。

泛型协议和普通协议之间的区别在于,您可以将普通协议转换为协议类型(不是具体类型)并发送消息,但您必须始终通过其具体类型来处理泛型协议的符合者(就像所有泛型一样)。这意味着普通协议是运行时特性(用于动态转换)以及编译时特性(用于类型检查)。但是通用协议只是一个编译时特性(没有动态转换)。

为什么我们不能说var a:Equatable?好吧,让我们深入研究一下。 Equatable 意味着可以将特定类型的一个实例与同一类型的另一个实例进行比较是否相等。 IE。 func ==(left:A, right:A) -&gt; Bool。如果 Equatable 是一个普通的协议,你会说更像:func ==(left:Equatable, right:Equatable) -&gt; Bool。但如果你这么想,那就没有意义了。 String 与其他 String 是 Equatable 的,Int 与其他 Int 是 Equatable 的,但这并不意味着 Strings 与 Ints 是 Equatable 的。如果 Equatable 协议只需要为您的类型实现 func ==(left:Equatable, right:Equatable) -&gt; Bool,那么您如何编写该函数来将您的类型与现在和将来的所有其他可能的 Equatable 类型进行比较?

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

现在让我们看看var a:Equatable 的问题。乍一看,这似乎是有道理的,但实际上却没有:

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

由于ab 都是相等的,我们可以比较它们是否相等,对吧?但实际上,a 的相等实现仅限于比较 String 和 String,b 的相等实现仅限于比较 Int 和 Int。因此,最好将泛型协议视为更像其他泛型,以意识到 Equatable&lt;String&gt;Equatable&lt;Int&gt; 不是同一个协议,即使它们都被认为只是“平等的”。

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

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

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

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

如果您查看创建“AnyEquatable”实现的要点:https://gist.github.com/JadenGeller/f0d05a4699ddd477a2c1,然后回头看看您现在如何转换之前的不可能/非编译代码:

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

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

var a: AnyEquatable = AnyEquatable("A String")
var b: AnyEquatable = AnyEquatable(100)
let equal = a == b  

【讨论】:

what type is allowed to be passed here? It's not _any_ Equatable, it's the associated type Bar, which here would be...??? 好吧,我如何声明它使得类型 is Equatable,这样任何响应消息的类型 == 都可以传入?编译器不允许我只有一个函数func foo(bar: Equatable)。我认为协议的主要目的是定义消息传递要求,所以我可以保证 Fooable 的东西总是可以响应foo(bar: SomeEquatable())。为什么编译器会关心 Equatable 类型的任何具体内容? 为什么我可以拥有[AnyHashable:Any] 类型的字典但不能拥有[Hashable:Any] 类型的字典,这非常令人困惑。为什么编译器对一个比另一个更满意?我只是想明白。 @CommaToast 用更多细节扩展了答案,希望在 cmets 中解决您的问题 我仍然认为 Swift 在这些情况下不会自动进行类型擦除,而是强迫用户跳出疯狂的圈子只是为了让协议像协议一样工作,这真是太愚蠢了。 几乎和他们一年专注于字符串一样愚蠢,但仍然无法设计出一种方法让我们在字符串上使用标准数字下标。啊哈哈哈哈

以上是关于在 Swift 中,从技术角度来看,为啥编译器会关心协议是不是只能用作通用约束?的主要内容,如果未能解决你的问题,请参考以下文章

Swift 1D 字典到 2D

从技术角度来看,Selenium 如何点击网页上的元素?

编译器可以根据命名空间生成调试信息吗?

Swift结构体(Struct)

在 Windows 和 iOS 之间共享文件时,从技术角度来看,iCloud 驱动器是如何工作的?

从成本角度来看,在 MySQL 上使用 MSSQL 的理由 [关闭]