为什么此Swift代码不能编译?

protocol P { }
struct S: P { }

let arr:[P] = [ S() ]

extension Array where Element : P {
    func test<T>() -> [T] {
        return []
    }
}

let result : [S] = arr.test()


编译器说:“类型P不符合协议P”(或者,在更高版本的Swift中,“不支持将“ P”用作符合协议“ P”的具体类型。”)。

为什么不呢?某种程度上,这感觉就像是语言上的一个漏洞。我意识到问题源于将数组arr声明为协议类型的数组,但这是不合理的事情吗?我认为协议确实可以帮助提供类似类型层次结构的结构吗?

评论

当您在let arr行中删除类型注释时,编译器会将类型推断为[S],然后代码进行编译。看来协议类型不能以与类相同的方式使用-超类关系。

@vadian是的,这就是我说“我意识到问题源于将数组arr声明为协议类型的数组”时的意思。但是,正如我继续在我的问题中要说的那样,协议的全部重点通常是它们可以与类-超类关系相同的方式使用!它们旨在为结构世界提供一种层次结构。他们通常这样做。问题是,为什么不能在这里工作?

在Xcode 7.1中仍然不起作用,但是错误消息现在为“不支持使用'P'作为不符合协议'P'的具体类型”。

@MartinR这是一条更好的错误消息。但是在我看来,这仍然是语言上的一个漏洞。

当然!即使使用协议P:Q {},P也不符合Q。

#1 楼

编辑:使用Swift还要工作18个月,Swift是另一个主要版本(提供新的诊断),并且@AyBayBay的评论使我希望重写此答案。新的诊断程序是:


“不支持将'P'用作符合协议'P'的具体类型。”


实际上使整个事情变得更加清晰。此扩展名:

extension Array where Element : P {


Element == P上不适用,因为P不被视为P的具体一致性。 (下面的“放在盒子里”解决方案仍然是最通用的解决方案。)


旧答案:

这是元类型的另一种情况。斯威夫特(Swift)确实希望您对大多数不重要的事情都变得具体。 [P]不是具体类型(您无法为P分配大小已知的内存块)。 (我不认为这是真的,您绝对可以创建大小为P的东西,因为它是通过间接完成的。)我认为没有证据表明这是“不应该”工作的情况。这看起来非常像他们的“不起作用”案例之一。 (不幸的是,要让Apple确认这两种情况之间的区别几乎是不可能的。)Array<P>可以是变量类型(Array不能为变量类型)这一事实表明,他们已经在这个方向上做了一些工作,但是Swift元类型有很多锋利的边缘和未实现的案例。我认为您不会得到比这更好的“为什么”答案。 “因为编译器不允许。” (不满意,我知道。我的整个Swift生活…)

解决方案几乎总是把东西放在盒子里。我们构建了一个类型擦除器。

protocol P { }
struct S: P { }

struct AnyPArray {
    var array: [P]
    init(_ array:[P]) { self.array = array }
}

extension AnyPArray {
    func test<T>() -> [T] {
        return []
    }
}

let arr = AnyPArray([S()])
let result: [S] = arr.test()


当Swift允许您直接执行此操作(我最终希望这样做)时,很可能只是通过自动为您创建此框即可。递归枚举正是有这个历史的。您必须将它们装箱,这令人难以置信的烦人和限制,然后最终编译器添加了indirect以更自动地执行相同的操作。

评论


这个答案中有很多有用的信息,但是,智博智答案中的实际解决方案要比此处介绍的拳击解决方案好。

– jsadler
16年4月1日在22:18

@jsadler问题不是如何解决限制,而是为什么存在限制。实际上,就解释而言,Tomohiro的解决方法提出的问题多于其答案。如果在我的数组示例中使用==,则会出现错误,同类型要求使通用参数'Element'变为非通用。“为什么Tomohiro不使用==会产生相同的错误?

–马特
16年6月23日在17:15

@Rob Napier我仍然对您的回应感到困惑。与原始版本相比,Swift如何在您的解决方案中看到更多具体性?您似乎只是将东西包装在一个结构中……Idk也许我在努力理解快速类型系统,但这一切似乎都像是魔法伏都教

–AyBayBay
17 Mar 29 '17在5:48



@AyBayBay更新答案。

–罗布·纳皮尔
17 Mar 29 '17 at 14:11

非常感谢@RobNapier我总是对您的回复速度感到惊讶,并且坦率地说,您如何找到时间与您一样多地帮助他人。但是,您的新编辑肯定会使它成为透视图。我还要指出的另一件事是,了解类型擦除也对我有所帮助。尤其是这篇文章做得很棒:krakendev.io/blog/generic-protocols-and-irir的缺点TBH Idk对这些东西的看法。似乎我们正在考虑该语言的漏洞,但Idk苹果公司将如何在其中构建一些漏洞。

–AyBayBay
17 Mar 29 '17 at 16:41

#2 楼

为什么协议不符合自己?

在一般情况下允许协议符合自己是不合理的。问题在于静态协议要求。

包括:



static方法和属性
启动器
相关类型(尽管这些当前阻止将协议用作实际类型)

我们可以在通用占位符T上访问这些要求,其中T : P –但是我们无法在协议类型本身上访问它们,因为没有具体的整合类型。因此,我们不允许TP

如果允许Array扩展名适用于[P],请考虑以下示例中的情况:

我们不可能在appendNew()上调用[P],因为PElement)不是具体类型,因此无法实例化。必须在具有具体类型元素的数组上调用该类型符合P的类型。

类似的故事具有静态方法和属性要求:

protocol P {
  init()
}

struct S  : P {}
struct S1 : P {}

extension Array where Element : P {
  mutating func appendNew() {
    // If Element is P, we cannot possibly construct a new instance of it, as you cannot
    // construct an instance of a protocol.
    append(Element())
  }
}

var arr: [P] = [S(), S1()]

// error: Using 'P' as a concrete type conforming to protocol 'P' is not supported
arr.appendNew()


我们不能谈SomeGeneric<P>。我们需要静态协议要求的具体实现(请注意,在上面的示例中如何定义没有foo()bar的实现)。尽管我们可以在P扩展名中定义这些要求的实现,但这些定义仅针对符合P的具体类型定义-您仍然不能在P本身上调用它们。

因此,Swift完全禁止我们使用协议作为符合自身的类型–因为当该协议具有静态要求时,它就没有。

实例协议要求没有问题,因为您必须在符合协议的实际实例上调用它们(因此必须已实现要求)。因此,当在类型为P的实例上调用需求时,我们可以将该调用转发到该需求的基础具体类型的实现上。

但是在这种情况下对该规则进行特殊例外可能会导致令人惊讶通用代码如何对待协议不一致。话虽这么说,但情况与associatedtype的要求并不太相似-(目前)阻止您将协议用作类型。有一个限制,可以防止您在有静态需求时将协议用作符合其自身类型的协议,这可能是该语言的未来版本的一种选择。

编辑:如下所述,看起来就像Swift团队的目标。



@objc协议

实际上,这正是该语言对待@objc协议的方式。当它们没有静态要求时,它们就会符合自己。

以下编译就很好:

protocol P {
  static func foo()
  static var bar: Int { get }
}

struct SomeGeneric<T : P> {

  func baz() {
    // If T is P, what's the value of bar? There isn't one – because there's no
    // implementation of bar's getter defined on P itself.
    print(T.bar)

    T.foo() // If T is P, what method are we calling here?
  }
}

// error: Using 'P' as a concrete type conforming to protocol 'P' is not supported
SomeGeneric<P>().baz()


baz要求T符合至P;但是我们可以用P代替T,因为P没有静态要求。如果我们向P添加静态要求,则该示例将不再编译:

import Foundation

@objc protocol P {
  func foo()
}

class C : P {
  func foo() {
    print("C's foo called!")
  }
}

func baz<T : P>(_ t: T) {
  t.foo()
}

let c: P = C()
baz(c)


因此,解决此问题的一种方法是使您的协议@objc。当然,这在许多情况下都不是理想的解决方法,因为它会迫使您的符合类型成为类,并且需要Obj-C运行时,因此使其在非Apple平台(例如Linux)上不可行。 >
但是我怀疑这种限制是@objc协议语言已经实现“没有静态要求的协议符合自身的协议”的主要原因之一。编译器可以大大简化围绕它们编写的通用代码。

为什么?因为@objc协议类型的值实际上只是使用objc_msgSend调度其需求的类引用。另一方面,非@objc协议类型的值更为复杂,因为它们携带值和见证表,以便管理其(可能是间接存储的)包装值的内存并确定要调用哪种实现。

由于@objc协议的这种简化表示,这样的协议类型P的值可以与某些通用占位符T : P的“通用值”共享相同的内存表示使Swift团队更容易实现自我整合。对于非@objc协议,情况并非如此,但是由于此类通用值当前不包含值或协议见证表。

但是此功能是有意的,希望可以推广到非@objc协议中。 @objc协议,已由Swift团队成员Slava Pestov在SR-55的评论(针对此问题的提示)中得到证实(此问题提示):


Matt Neuburg添加了评论- 2017年9月7日1:33 PM

可以编译:

import Foundation

@objc protocol P {
  static func bar()
  func foo()
}

class C : P {

  static func bar() {
    print("C's bar called")
  }

  func foo() {
    print("C's foo called!")
  }
}

func baz<T : P>(_ t: T) {
  t.foo()
}

let c: P = C()
baz(c) // error: Cannot invoke 'baz' with an argument list of type '(P)'


添加@objc使其可以编译;删除它会使它无法再次编译。
我们某些处于Stack Overflow上的人感到惊讶,并希望
知道这是故意的还是有问题的边缘情况。

Slava Pestov添加了一条评论-2017年9月7日1:53 PM

这是有意的–解除此限制就是此bug的目的。
就像我说的那样,这很棘手,我们还没有任何具体的计划。


所以希望它也有一天该语言也将支持非@objc协议。

但是对于非==协议,目前有什么解决方案?


使用协议约束实现扩展

在Swift 3.1中,如果要扩展约束条件是给定的通用占位符或关联类型必须是给定的协议类型(而不仅仅是符合该协议的具体类型)–您可以使用P约束简单地定义它。

例如,我们可以将您的数组扩展名写为:

@objc protocol P {}
class C: P {}

func process<T: P>(item: T) -> T { return item }
func f(image: P) { let processed: P = process(item:image) }


当然,现在这可以防止我们在具有符合Element : P的具体类型元素的数组上调用它。我们可以通过为== P定义一个附加扩展名来解决此问题,然后直接转到[P]扩展名:

extension Array where Element == P {
  func test<T>() -> [T] {
    return []
  }
}

let arr: [P] = [S()]
let result: [S] = arr.test()


但是值得注意的是,这将执行O( n)将数组转换为[P],因为每个元素都必须装在存在容器中。如果性能是一个问题,则可以通过重新实现扩展方法来解决。这并不是一个完全令人满意的解决方案–希望该语言的未来版本将包括一种表达“协议类型或符合协议类型”约束的方法。

在Swift 3.1之前,最通用如Rob在他的回答中所示,实现此目的的方法是简单地为p构建包装类型,然后您可以在其中定义扩展方法。


受约束的通用占位符的协议类型实例

请考虑以下情况(人为的,但并不罕见):

我们无法将takesConcreteP(_:)传递给P,因为我们目前无法用T : P代替通用占位符P。让我们看一下解决此问题的几种方法。

1。打开存在性

而不是尝试用T : P代替P,如果我们可以深入研究P键入的值所包装的基础具体类型怎么办呢?不幸的是,这需要一种称为开放存在性的语言功能,当前用户无法直接使用它。

但是,Swift在访问它们上的成员(即它)时会隐式打开存在性(协议类型的值)找出运行时类型,并以通用占位符的形式对其进行访问)。我们可以在Self的协议扩展中利用这一事实:

extension Array where Element : P {
  func test<T>() -> [T] {
    return (self as [P]).test()
  }
}

let arr = [S()]
let result: [S] = arr.test()


请注意扩展方法采用的隐式通用self占位符,该占位符用于键入隐式P参数–这发生在所有协议扩展成员的幕后。在协议类型值Self上调用此类方法时,Swift会挖掘出底层的具体类型,并使用它来满足takesConcreteP(_:)通用占位符。这就是为什么我们可以用self调用T的原因–我们用Self满足takesConcreteP(_:)的要求。

这意味着我们现在可以说:

protocol P {
  var bar: Int { get set }
  func foo(str: String)
}

struct S : P {
  var bar: Int
  func foo(str: String) {/* ... */}
}

func takesConcreteP<T : P>(_ t: T) {/* ... */}

let p: P = S(bar: 5)

// error: Cannot invoke 'takesConcreteP' with an argument list of type '(P)'
takesConcreteP(p)


然后调用T时,其通用占位符S被底层具体类型(在本例中为P)满足。请注意,这不是“符合自身的协议”,因为我们要替换的是具体类型而不是takesConcreteP(_:),请尝试向协议中添加静态要求,并查看从T中调用该协议会发生什么情况。

如果Swift继续禁止协议遵循自己的要求,那么下一个最佳选择就是尝试将它们作为通用类型参数传递给参数时隐式打开存在-有效地完成我们的协议扩展蹦床所做的事情,而没有样板。 />
但是请注意,开放存在不是解决协议不符合自身的一般解决方案。它不处理协议类型值的异构集合,这些协议类型值可能都具有不同的基础具体类型。例如,请考虑:

extension P {
  func callTakesConcreteP/*<Self : P>*/(/*self: Self*/) {
    takesConcreteP(self)
  }
}


出于相同的原因,具有多个P参数的函数也会有问题,因为这些参数必须采用相同类型的参数–但是,如果我们有两个P值,则无法保证在编译时它们都具有相同的基础具体类型。

为了解决此问题,我们可以使用类型擦除器。 />
2。构建类型擦除器

正如Rob所说,类型擦除器是解决协议不符合自身要求的最通用解决方案。通过将实例需求转发到基础实例,它们使我们能够将协议类型的实例包装为符合该协议的具体类型。

因此,让我们构建一个转发P的类型擦除框。实例需求到符合AnyP的基础任意实例上:

p.callTakesConcreteP()


现在我们可以用P来代替P

struct Q : P {
  var bar: Int
  func foo(str: String) {}
}

// The placeholder `T` must be satisfied by a single type
func takesConcreteArrayOfP<T : P>(_ t: [T]) {}

// ...but an array of `P` could have elements of different underlying concrete types.
let array: [P] = [S(bar: 1), Q(bar: 2)]

// So there's no sensible concrete type we can substitute for `T`.
takesConcreteArrayOfP(array) 


现在,考虑一下为什么我们必须建造那个盒子。正如我们前面讨论的,Swift在协议具有静态需求的情况下需要一种具体的类型。考虑一下AnyP是否有静态需求–我们将需要在P中实现它。但是它应该被实现为什么呢?我们正在处理符合AnyP的任意实例–我们不知道其底层具体类型如何实现静态要求,因此我们无法在P中有意义地表达这一点。

因此,解决方案在这种情况下,仅在实例协议要求的情况下才真正有用。在一般情况下,我们仍然不能将P视为符合q4312079q的具体类型。

评论


也许我只是很笨,但是我不明白为什么静态情况很特殊。我们(编译器)在编译时对协议的静态属性的了解与对协议的实例属性的了解或多或少一样,即采用者将实现它。那有什么区别呢?

–马特
17年4月14日14:54



@matt协议类型的实例(即包裹在存在性P中的具体类型的实例)很好,因为我们可以将对实例需求的调用转发到基础实例。但是,对于协议类型本身(即P.Protocol,实际上只是描述协议的类型)–没有采用者,因此没有什么可以调用静态要求的,这就是为什么在上面的示例中我们不能具有SomeGeneric

(对于P.Type(现有元类型)有所不同,它描述了符合P的事物的具体元类型–但这是另一回事了)

– Hamish
17年4月14日在15:07


我在本页顶部询问的问题是,为什么协议类型采用者很好,而协议类型本身却不好。我了解,对于协议类型本身而言,没有采用者。 —我不明白的是,为什么将静态调用转发给采用类型要比将实例调用转发给采用类型更难。您在争辩说,这里存在困难的原因是因为静态需求的本质,但是我看不出静态需求比实例需求更难。

–马特
17年4月14日在15:29

@matt并不是说静态需求比实例需求“更难” –编译器可以通过实例的存在(即实例类型为P)和存在的元类型(即P.Type元类型)来很好地处理。问题在于,对于泛型–我们并没有真正地进行比较。当T为P时,没有可将静态要求转发给(T为P.Protocol,而不是P.Type)的底层混凝土(元)类型。

– Hamish
17年4月14日在15:39

我真的不在乎健全性等,我只想编写应用程序,并且如果感觉应该可行就应该这样做。语言应该只是一种工具,而不是产品本身。如果在某些情况下确实无法使用,则可以在这种情况下禁止使用它,但让其他人使用它适用的情况并让他们继续编写应用程序。

–乔纳森。
19 Mar 22 '19 at 10:52

#3 楼

如果您扩展CollectionType协议而不是Array并按协议约束作为具体类型,则可以按如下方式重写前面的代码。

protocol P { }
struct S: P { }

let arr:[P] = [ S() ]

extension CollectionType where Generator.Element == P {
    func test<T>() -> [T] {
        return []
    }
}

let result : [S] = arr.test()


评论


我认为Collection vs Array在这里不相关,重要的变化是使用== P vs:P。使用==,原始示例也适用。 ==的潜在问题(取决于上下文)是它排除了子协议:如果我创建协议SubP:P,然后将arr定义为[SubP],则arr.test()将不再起作用(错误:SubP和P必须相等)。

– imre
2月18日晚上8:34