【问题标题】:Differences generic protocol type parameter vs direct protocol type通用协议类型参数与直接协议类型的区别
【发布时间】:2019-11-20 20:03:08
【问题描述】:

这是我的游乐场代码:

protocol A {
    init(someInt: Int)
}

func direct(a: A) {
    // Doesn't work
   let _ = A.init(someInt: 1)
}

func indirect<T: A>(a: T) {
    // Works
    let _ = T.init(someInt: 1)
}

struct B: A {
    init(someInt: Int) {

    }
}

let a: A = B(someInt: 0)

// Works
direct(a: a)

// Doesn't work
indirect(a: a)

使用参数a 调用方法indirect 时会出现编译时错误。所以我理解&lt;T: A&gt; 的意思是某种符合A 的类型。我的变量a 的类型是A 和协议不符合他们自己所以好的,我理解编译时错误。

这同样适用于方法direct 中的编译时错误。我明白了,需要插入一个具体的符合类型。

尝试访问direct 中的static 属性时也会出现编译时间。

我想知道。 定义的 2 种方法是否存在更多差异?我知道我可以从indirect 调用初始化程序和静态属性,我可以直接在direct 中插入类型A,我不能做其他人可以做的事情。但是有什么我错过的吗?

【问题讨论】:

    标签: swift protocols


    【解决方案1】:

    关键的混淆在于 Swift 有两个拼写相同的概念,因此通常是模棱两可的。其中一个是struct T: A {},意思是“T符合协议 A”,另一个是var a: A,意思是“变量a的类型是存在的 的 A。”

    遵守协议不会改变类型。 T 仍然是 T。它只是碰巧符合一些规则。

    “存在”是编译器生成的包装协议的盒子。这是必要的,因为符合协议的类型可能是不同的大小和不同的内存布局。存在是一个盒子,它为任何符合协议的东西在内存中提供一致的布局。存在和协议是相关的,但不是一回事。

    因为existential 是一个可以容纳任何类型的运行时框,所以涉及到一些间接性,这可能会影响性能并阻止某些优化。

    另一个常见的混淆是理解类型参数的含义。在函数定义中:

    func f<T>(param: T) { ... }
    

    这定义了一系列函数f&lt;T&gt;(),它们是在编译时根据您作为类型参数传递的内容创建的。例如,当您以这种方式调用此函数时:

    f(param: 1)
    

    在编译时会创建一个名为f&lt;Int&gt;() 的新函数。这是与f&lt;String&gt;()f&lt;[Double]&gt;() 完全不同的功能。每个都有自己的功能,原则上是f()中所有代码的完整副本。 (实际上,优化器非常聪明,可能会消除一些复制。还有一些与跨越模块边界的事物相关的其他细微之处。但这是考虑正在发生的事情的一种相当不错的方式。)

    由于为每个传递的类型创建了通用函数的专用版本,因此理论上它们可以更加优化,因为函数的每个版本将只处理一种类型。权衡是他们可以添加代码膨胀。不要假设“泛型比协议快”。泛型可能比协议更快是有原因的,但您必须实际查看代码生成和配置文件才能在任何特定情况下了解。

    所以,看看你的例子:

    func direct(a: A) {
        // Doesn't work
       let _ = A.init(someInt: 1)
    }
    

    协议 (A) 只是类型必须遵守的一组规则。你不能构造“一些符合这些规则的未知事物”。将分配多少字节的内存?它将为规则提供哪些实现?

    func indirect<T: A>(a: T) {
        // Works
        let _ = T.init(someInt: 1)
    }
    

    为了调用这个函数,你必须传递一个类型参数,T,并且那个类型必须符合A。当你用一个特定的类型调用它时,编译器会创建一个indirect的新副本,它具体是旨在与您通过的 T 一起使用。由于我们知道 T 有一个适当的 init,我们知道编译器将能够在需要时编写此代码。但indirect 只是编写函数的模式。它本身不是一个函数。直到你给它一个 T 才能使用。

    let a: A = B(someInt: 0)
    
    // Works
    direct(a: a)
    

    a 是 B 的存在包装器。direct() 需要一个存在包装器,因此您可以传递它。

    // Doesn't work
    indirect(a: a)
    

    a 是 B 的存在包装器。存在包装器不符合协议。它们需要符合协议的东西才能创建它们(这就是为什么它们被称为“existentials”;您创建了一个事实证明这样的值确实存在)。但它们本身并不符合协议。如果他们这样做了,那么您可以执行您在direct() 中尝试执行的操作,并说“创建一个存在包装器的新实例,而无需确切知道其中的内容。”没有办法做到这一点。现有包装器没有自己的方法实现。

    在某些情况下,存在的可以符合它自己的协议。只要没有initstatic的要求,原则上其实是没有问题的。但 Swift 目前无法处理这个问题。因为它不能用于 init/static,所以 Swift 目前在所有情况下都禁止它。

    【讨论】:

    • 好的,一个编译成多个专用函数,另一个没有,一个使用包装器类型。我知道一些边缘案例为什么协议可以符合自己,但我想了解更多关于那些存在包装器的信息。引用答案'你不能构造......',为什么?编译器需要知道它需要多少字节,为什么不问包装器呢?使用的类型是/必须在包装器中(否则它不是真正的包装器)?当我传入符合A 的东西时,它是某种符合类型,所以我认为我什至应该能够在该类型上调用init(在我的情况下)。
    • 我还阅读了一些案例,我们不能在这个问题的类型上使用init 方法是合乎逻辑的:stackoverflow.com/questions/33112559 来自答案stackoverflow.com/a/43408193/7715250。我不明白为什么它不应该在我的情况下工作,但也许这是编译器的限制。我想知道,当使用存在包装器时,类型是否会以某种方式丢失(我认为它发生在 Rust 中)?否则我不会看到我的代码没有编译的原因,抛开编译器限制。保证在这两种方法中都传递了符合类型 A 的实例。
    • 我不确定您所说的“询问包装器”以构造对象是什么意思。包装器围绕 其他对象。它没有自己的任何方法实现;如果协议需要var peripheral: CBPeripheral { get set },它将返回什么。 (我选择 CBPeripheral 作为示例,因为它没有公共 init。)您可以通过将构造的现有对象传递给它来创建存在。
    • 虽然它传递了一个一致的实例,但它也传递了一个类型,您可以像在示例中所做的那样忽略该实例并在该类型上调用A.init。正如我所指出的,在某些情况下是可能的(如果您在协议上不需要init),并且已经讨论过允许这种情况。主要的问题是它使带有 init/static 的协议与其他协议非常不同,就像关联类型使协议非常不同一样。有人担心创建具有不同功能的第三版协议。
    • 如果所有需求都有默认实现,或者可能指定一些具体类型作为协议的默认实现,也有关于允许直接构造存在的讨论。这可能是可能的,但同样,它在一个已经非常复杂的系统周围创造了更多的复杂性,所以我不知道这是否会发生(我倾向于怀疑它)。
    猜你喜欢
    • 2021-05-24
    • 1970-01-01
    • 2012-05-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-08-06
    • 1970-01-01
    相关资源
    最近更新 更多