编译器错误:比较`某个协议`实例时发生错误。

huangapple go评论60阅读模式
英文:

Compiler Error When Comparing `some Protocol` Instances

问题

我一直在比较Swift的不透明类型与存在/封装类型,并尝试理解它们的一些细微差别,但我遇到了一些问题。

我明白与不透明类型相比,返回一个存在类型允许在运行时使用不同的动态类型,但有以下缺点:i)一些性能开销和ii)丢失了一些特定的类型信息。

Swift Book文档确认了上述情况,并建议存在类型由于这一点失去了比较可比类型的能力:

"> 使用封装协议类型作为函数的返回类型允许您灵活返回符合协议的任何类型。但是,这种灵活性的代价是一些操作在返回值上是不可能的。... ==运算符不可用 - 它依赖于不使用封装协议类型时保留的特定类型信息。

然后它继续说,返回不透明类型会保留类型的标识:

"> 相比之下[存在类型],不透明类型保留了底层类型的标识。

我决定自己实现这个来建立对使用不透明类型与存在类型之间权衡的直觉。在这样做时,我遇到了一个无法解释的问题。我将我的问题缩小到了一个非常简单但刻意制造的示例。

我定义了一个名为ContentFetcher的协议,其中包含一个fetch()函数。我还有一个符合此协议的结构VideoFetcher。以下是这些代码:

protocol ContentFetcher: Equatable {
    func fetch() -> Int
}

struct VideoFetcher: ContentFetcher {
    func fetch() -> Int {
        1
    }
}

然后,我创建了一个名为FetcherFactory的结构,其中有一个静态函数getFetcher(),它返回了some ContentFetcher

struct FetcherFactory {
    static func getFetcher() -> some ContentFetcher {
        return VideoFetcher()
    }
}

然而,当我尝试创建ContentFetcher的实例并进行比较时,我收到了编译错误。以下是代码和错误信息:

struct S {
    let fetcher1: some ContentFetcher = FetcherFactory.getFetcher()
    let fetcher2: some ContentFetcher = FetcherFactory.getFetcher()
    
    func start() {
        let fetcher3 = FetcherFactory.getFetcher()
        let fetcher4 = FetcherFactory.getFetcher()
        
        // Success: prints 'true'
        print(fetcher3 == fetcher4)
        
        // Error: Cannot convert value of type 'some ContentFetcher' (type of 'S.fetcher2')
        // to expected argument type 'some ContentFetcher' (type of 'S.fetcher1')
        print(fetcher1 == fetcher2)
        
        let fetcher5: some ContentFetcher = FetcherFactory.getFetcher()
        let fetcher6: some ContentFetcher = FetcherFactory.getFetcher()
        
        // Error: Cannot convert value of type 'some ContentFetcher' (type of 'fetcher6')
        // to expected argument type 'some ContentFetcher' (type of 'fetcher5')
        print(fetcher5 == fetcher6)
    }
}

S().start()

我理解static func getFetcher() -> some ContentFetcher返回的不透明类型用于隐藏返回值的确切类型。然而,我不能解释为什么fetcher3 == fetcher4可以成功编译,而fetcher1 == fetcher2fetcher5 == fetcher6都生成错误。

看起来,当我让编译器推断fetcher3fetcher4的不透明返回类型时,类型标识得以保留,编译器知道两种类型是相同的。但如果我明确指定不透明类型(例如,let fetcher1: some ContentFetcher ...),那么此标识将丢失。这是为什么,当getFetcher()方法的返回类型仍然是some ContentFetcher

此外,编译器强制我为用作存储属性的不透明类型指定类型;如果我从fetcher1fetcher2的类型注释中删除some ContentFetcher(错误:属性定义具有推断类型'some ContentFetcher',涉及另一个声明的'some'返回类型),则会出现编译器错误。为什么会这样?

英文:

I've been spending some time comparing Swift's opaque types with existential/boxed types and trying to understand some of their nuances and I am running into some issues.

I understand that returning an existential type, compared to an opaque type, allows for the flexibility of using different dynamic types at runtime with the downside of i) some performance overhead and ii) some specific type information is lost.

The Swift Book documentation confirms the above and suggests existential types lose the ability to compare equatable types as a result:

> Using a boxed protocol type as the return type for a function gives you the flexibility to return any type that conforms to the protocol. However, the cost of that flexibility is that some operations aren’t possible on the returned values. ... the == operator isn’t available — it depends on specific type information that isn’t preserved by using a boxed protocol type.

It then goes on to say that returning opaque types preserves type identity:

> In contrast [to existential types], opaque types preserve the identity of the underlying type.

I decided to implement this myself to build an intuition around the tradeoffs between using opaque vs existential types. In doing so, I've encountered an issue I can't explain. I've narrowed down my issue to a very simple, albeit contrived, example.

I've defined a protocol ContentFetcher with a function fetch(). I also have a struct VideoFetcher that conforms to this protocol. Here's the code for these:

protocol ContentFetcher: Equatable {
    func fetch() -> Int
}

struct VideoFetcher: ContentFetcher {
    func fetch() -> Int {
        1
    }
}

I've then created a FetcherFactory struct that has a static function getFetcher() which returns some ContentFetcher:

struct FetcherFactory {
    static func getFetcher() -> some ContentFetcher {
        return VideoFetcher()
    }
}

However, when I try to create instances of ContentFetcher and compare them, I'm receiving compiler errors. Here's the code and the errors:

struct S {
    let fetcher1: some ContentFetcher = FetcherFactory.getFetcher()
    let fetcher2: some ContentFetcher = FetcherFactory.getFetcher()
    
    func start() {
        let fetcher3 = FetcherFactory.getFetcher()
        let fetcher4 = FetcherFactory.getFetcher()
        
        // Success: prints 'true'
        print(fetcher3 == fetcher4)
        
        // Error: Cannot convert value of type 'some ContentFetcher' (type of 'S.fetcher2')
        // to expected argument type 'some ContentFetcher' (type of 'S.fetcher1')
        print(fetcher1 == fetcher2)
        
        let fetcher5: some ContentFetcher = FetcherFactory.getFetcher()
        let fetcher6: some ContentFetcher = FetcherFactory.getFetcher()
        
        // Error: Cannot convert value of type 'some ContentFetcher' (type of 'fetcher6')
        // to expected argument type 'some ContentFetcher' (type of 'fetcher5')
        print(fetcher5 == fetcher6)
    }
}

S().start()

I understand that the opaque type returned by static func getFetcher() -> some ContentFetcher is used to hide the exact type of the return value. However, I cannot explain why fetcher3 == fetcher4 compiles successfully when both fetcher1 == fetcher2 and fetcher5 == fetcher6 generate errors.

It seems that when I let the compiler infer the opaque return type for fetcher3 and fetcher4 that the type identity is preserved and the compiler knows both types are the same. But if I make the opaque types explicit (let fetcher1: some ContentFetcher ...) then this identity is lost. Why is this when the return type of the getFetcher() method is still some ContentFetcher?

Also, the compiler forces me to specify the type for the opaque types used as stored properties; I get a compiler error if I remove some ContentFetcher from the type annotation of fetcher1 and fetcher2 (Error: Property definition has inferred type 'some ContentFetcher', involving the 'some' return type of another declaration). Why is this?

Any help would be much appreciated. Thanks!

答案1

得分: 1

以下是您要翻译的内容:

不透明结果类型在 SE-0244 不透明结果类型 中有解释。您关心的部分是与您的问题相关的部分,即不透明结果类型的唯一性

不透明结果类型是基于函数/属性/下标和任何泛型类型参数的唯一性。

在这种情况下,这意味着fetcher3fetcher4的类型都是(FetcherFactory.getFetcher() 的结果)。这个类型以及它的协议是在编译时可用的全部信息。关于类型的其他一切都是"不透明的"。尽管实际的具体类型是 VideoFetcher,但在编译时它绝对不是 VideoFetcher。它最重要的是与其他some ContentFetcher类型不兼容,即使这些类型碰巧是 VideoFetcher。它(FetcherFactory.getFetcher() 的结果)兼容。

some ContentFetcher在编译时不是一种类型。它被替换为一个唯一的、无名的、基于赋值的编译时类型。这个无名类型在运行时并不存在。此时,只有完整的具体类型存在。不透明类型只是编译时的构造。(参见类型标识。)

这是因为您可以自由地更改getFetcher()的实现以返回其他内容,而这不会影响调用者。如果以某种方式泄漏了实际上是 VideoFetcher,那么更改getFetcher将会破坏调用代码,这正是不透明类型存在的目的。

正如在属性和下标部分所讨论的那样,不透明属性类型是"该属性的类型"。没有更多信息。赋予了一个也是不透明的值并不会改变这一点。fetcher1的编译时类型就是(S.fetcher1的类型),正如您在错误消息中看到的那样。这与任何其他类型都不兼容。重要的是,它与(S.fetcher2的类型)不同。不透明类型是唯一的。

与以前一样,这允许您更改用于初始化fetcher1的具体类型,而不会破坏调用者。这就是不透明类型的用途。

只是为了再次强调根据您的确切问题:

当我让编译器推断fetcher3和fetcher4的不透明返回类型时,似乎类型标识被保留,编译器知道这两个类型是相同的。但如果我明确指定了不透明类型(例如let fetcher1: some ContentFetcher ...),则这种标识就丢失了。为什么当getFetcher()方法的返回类型仍然是some ContentFetcher时会发生这种情况?

编译器分配了以下类型:

  • fetcher1: (S.fetcher1的类型)(显式)
  • fetcher2: (S.fetcher2的类型)(显式)
  • fetcher3: (FetcherFactory.getFetcher()的结果)(推断)
  • fetcher4: (FetcherFactory.getFetcher()的结果)(推断)
  • fetcher5: (fetcher5的类型)(显式)
  • fetcher6: (fetcher6的类型)(显式)

具有相同类型的属性是可比较的。具有不同类型的属性是不可比较的。

可能仍然有一个问题,那就是"我如何显式表示(FetcherFactory.getFetcher()的结果)?"目前这是不可能的。不透明类型是"无名字的"。没有直接引用它们的方式。它们只能通过类型推断来引用。这会带来一些重要的限制。例如,无法表示两个函数返回相同的不透明类型。还无法表示fetcher1fetcher2是相同的类型。(不允许为属性使用不透明类型推断。)参见不透明类型别名以了解可能会改善这一点的未来工作。

英文:

Opaque result types are explained in SE-0244 Opaque Result Types. The part of interest to your question is Uniqueness of opaque result types:

>Opaque result types are uniqued based on the function/property/subscript and any generic type arguments.

In this case, it means that the type of fetcher3 and fetcher4 are both (result of 'FetcherFactory.getFetcher()'). That, and its protocol, is the entirety of the information available at compile-time. Everything else about the type is "opaque." It is absolutely not VideoFetcher at compile-time, even though that is the actual concrete type returned. It is most importantly not compatible with other some ContentFetcher types, even if those types happen to be VideoFetcher. It is only compatible with (result of 'FetcherFactory.getFetcher()').

some ContentFetcher is not a type at compile-time. It is replaced with a unique, nameless, compile-time type based on the assignment. This nameless type does not exist at runtime. At that point, only the full, concrete type exists. Opaque types are only a compile-time construct. (See Type Identity.)

This is because you are free to change the implementation of getFetcher() to return something else, and that will not change anything about the callers. If somehow the fact that it really were VideoFetcher leaked out to the caller, then changing getFetcher would break calling code, and that's exactly what opaque types exist to prevent.

As discussed in the Properties and subscripts section, an opaque property type is "the type of that property." There is no further information. The fact that there is a value assigned that also happens to be opaque doesn't change anything about that. The compile-time type of fetcher1 is literally (type of 'S.fetcher1'), as you see in your error messages. That is not compatible with any other type. Importantly, it is not the same as (type of 'S.fetcher2'). Opaque types are unique.

As before, this allows you to change the specific type that fetcher1 is initialized with, without breaking callers. This is the point of opaque types.

Just to reiterate based on your exact question:

>It seems that when I let the compiler infer the opaque return type for fetcher3 and fetcher4 that the type identity is preserved and the compiler knows both types are the same. But if I make the opaque types explicit (let fetcher1: some ContentFetcher ...) then this identity is lost. Why is this when the return type of the getFetcher() method is still some ContentFetcher?

The compiler assigns the following types:

  • fetcher1: (type of 'S.fetcher1') (explicit)
  • fetcher2: (type of 'S.fetcher2') (explicit)
  • fetcher3: (result of 'FetcherFactory.getFetcher()') (inferred)
  • fetcher4: (result of 'FetcherFactory.getFetcher()') (inferred)
  • fetcher5: (type of 'fetcher5') (explicit)
  • fetcher6: (type of 'fetcher6') (explicit)

Properties with the same type are comparable. Properties with different types are not.

One question that may remain is "how do I express (result of 'FetcherFactory.getFetcher()') explicitly?" That is not currently possible. Opaque types are "nameless." There is no way to directly refer to them. They can only be referenced by type-inference. This creates some significant limitations. It's impossible to express that two functions return the same opaque type, for example. It's also impossible to express that fetcher1 and fetcher2 are the same type. (Opaque type inference is not allowed for properties.) See Opaque type aliases for possible future work that may improve this.

huangapple
  • 本文由 发表于 2023年6月12日 01:49:29
  • 转载请务必保留本文链接:https://go.coder-hub.com/76451752.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定