簡體   English   中英

協議不符合自身?

[英]Protocol doesn't conform to itself?

為什么這個 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聲明為協議類型的數組,但這是不合理的做法嗎? 我認為協議確實可以幫助提供具有類型層次結構之類的結構嗎?

為什么協議不符合自身?

在一般情況下允許協議符合自身是不合理的。 問題在於靜態協議要求。

這些包括:

  • static方法和屬性
  • 初始化程序
  • 關聯類型(盡管這些當前阻止將協議用作實際類型)

我們可以在通用占位符T上訪問這些要求,其中T : P - 但是我們不能在協議類型本身上訪問它們,因為沒有具體的符合類型要轉發。 因此我們不能允許TP

如果我們允許Array擴展適用於[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()

我們不可能在[P]上調用appendNew() ,因為PElement )不是具體類型,因此無法實例化。 必須在具有具體類型元素的數組上調用,其中該類型符合P

這是一個與靜態方法和屬性要求類似的故事:

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()

我們不能談論SomeGeneric<P> 我們需要靜態協議要求的具體實現(注意上面例子中沒有定義foo()bar實現)。 盡管我們可以在P擴展中定義這些要求的實現,但這些實現僅針對符合P的具體類型定義——您仍然不能在P本身上調用它們。

正因為如此,Swift 完全不允許我們將協議用作符合自身的類型——因為當該協議有靜態要求時,它沒有。

實例協議要求沒有問題,因為您必須在符合協議的實際實例上調用它們(因此必須已經實現了要求)。 因此,當在類型為P的實例上調用需求時,我們可以將該調用轉發到該需求的底層具體類型的實現上。

然而,在這種情況下為規則設置特殊例外可能會導致通用代碼如何處理協議的驚人不一致。 盡管如此,這種情況與associatedtype要求並沒有太大不同——這(當前)阻止您將協議用作類型。 有一個限制,阻止你使用協議作為一種在它有靜態要求時符合自身的類型,這可能是該語言未來版本的一個選項

編輯:正如下面所探討的,這確實看起來像 Swift 團隊的目標。


@objc協議

事實上,這正是語言對待@objc協議的方式。 當他們沒有靜態需求時,他們就順應自己。

以下編譯就好了:

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)

baz要求T符合P 但我們可以用P代替T因為P沒有靜態要求。 如果我們向P添加靜態要求,則示例不再編譯:

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 誠然,這在很多情況下都不是理想的解決方法,因為它強制您的符合類型成為類,並且需要 Obj-C 運行時,因此在非 Apple 平台(如 Linux)上不可行。

但我懷疑這種限制是該語言已經為@objc協議實現“沒有靜態要求的協議符合自身”的主要原因(之一)。 圍繞它們編寫的通用代碼可以被編譯器顯着簡化。

為什么? 因為@objc協議類型的值實際上只是類引用,其需求是使用objc_msgSend分派的。 另一方面,非@objc協議類型的值更復雜,因為它們攜帶值表和見證表,以便管理其(可能間接存儲的)包裝值的內存並確定要調用的實現不同的要求,分別。

由於@objc協議的這種簡化表示,這樣的協議類型P的值可以與某個泛型占位符T : P類型的“泛型值”共享相同的內存表示,大概使 Swift 團隊可以輕松地允許自我一致性。 對於非@objc協議,情況並非如此,但是因為此類通用值當前不攜帶值或協議見證表。

然而,此功能有意為之,並有望推廣到非@objc協議,正如 Swift 團隊成員 Slava Pestov 在 SR-55 的評論中所確認的那樣以回應您的查詢(由這個問題提示):

Matt Neuburg 添加了評論 - 2017 年 9 月 7 日下午 1:33

這確實編譯:

 @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) }

添加@objc使其編譯; 刪除它使其不再編譯。 我們中的一些人在 Stack Overflow 上發現這令人驚訝,並想知道這是故意還是錯誤的邊緣情況。

Slava Pestov 添加了評論 - 2017 年 9 月 7 日下午 1:53

這是故意的——解除這個限制就是這個錯誤的意義所在。 就像我說的這很棘手,我們還沒有任何具體的計划。

因此,希望有一天語言也能支持非@objc協議。

但是對於非@objc協議,目前有哪些解決方案?


使用協議約束實現擴展

在 Swift 3.1 中,如果你想要一個帶有約束的擴展,即給定的泛型占位符或關聯類型必須是給定的協議類型(不僅僅是符合該協議的具體類型)——你可以簡單地用==約束來定義它。

例如,我們可以將您的數組擴展寫為:

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

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

當然,這現在阻止我們在具有符合P具體類型元素的數組上調用它。 我們可以通過為 when Element : P定義一個額外的擴展來解決這個問題,然后轉發到== P擴展上:

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

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

然而值得注意的是,這將執行數組到[P]的 O(n) 轉換,因為每個元素都必須裝箱在一個存在容器中。 如果性能是一個問題,您可以通過重新實現擴展方法來簡單地解決這個問題。 這不是一個完全令人滿意的解決方案——希望該語言的未來版本將包括一種表達“協議類型符合協議類型”約束的方法。

在 Swift 3.1 之前,實現這一點的最一般方法,正如 Rob 在他的回答中所示,是簡單地為[P]構建一個包裝器類型,然后您可以在其上定義您的擴展方法。


將協議類型的實例傳遞給受約束的通用占位符

考慮以下(人為的,但並不少見)的情況:

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)

我們不能將p傳遞給takesConcreteP(_:) ,因為我們目前不能用P代替通用占位符T : P 讓我們來看看解決這個問題的幾種方法。

1. 開放存在主義

與其嘗試用P代替T : P ,如果我們可以深入研究P類型值所包裝的底層具體類型並用它代替呢? 不幸的是,這需要一種稱為openexistentials的語言功能,目前用戶無法直接使用該功能。

然而,Swift 在訪問它們的成員時隱式地打開存在項(協議類型的值)(即它挖掘出運行時類型並使其以通用占位符的形式訪問)。 我們可以在P的協議擴展中利用這一事實:

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

請注意擴展方法采用的隱式通用Self占位符,用於鍵入隱式self參數 - 這發生在所有協議擴展成員的幕后。 當在協議類型值P上調用這樣的方法時,Swift 會挖掘出底層的具體類型,並使用它來滿足Self泛型占位符。 這就是為什么我們可以用self調用takesConcreteP(_:) ——我們用Self滿足了T

這意味着我們現在可以說:

p.callTakesConcreteP()

並且takesConcreteP(_:)被調用,其通用占位符T由底層具體類型(在本例中為S )滿足。 請注意,這不是“符合自身的協議”,因為我們正在替換一個具體的類型而不是P - 嘗試向協議添加一個靜態要求,看看當你在takesConcreteP(_:)調用它時會發生什么。

如果 Swift 繼續禁止協議符合自身,那么下一個最好的選擇是在嘗試將它們作為參數傳遞給泛型類型的參數時隱式打開存在項——有效地完成我們的協議擴展蹦床所做的事情,只是沒有樣板。

但是請注意,打開存在性並不是解決協議不符合自身的問題的通用解決方案。 它不處理協議類型值的異構集合,這些集合可能都有不同的底層具體類型。 例如,考慮:

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) 

出於同樣的原因,具有多個T參數的函數也會有問題,因為參數必須采用相同類型的參數——但是如果我們有兩個P值,我們無法在編譯時保證它們都具有相同的參數底層具體類型。

為了解決這個問題,我們可以使用類型橡皮擦。

2. 建立一個類型橡皮擦

正如Rob 所說類型擦除器是解決協議不符合自身問題的最通用的解決方案。 它們允許我們通過將實例需求轉發給底層實例,將協議類型的實例包裝在符合該協議的具體類型中。

因此,讓我們構建一個類型擦除框,將P的實例要求轉發到符合P的底層任意實例:

struct AnyP : P {

  private var base: P

  init(_ base: P) {
    self.base = base
  }

  var bar: Int {
    get { return base.bar }
    set { base.bar = newValue }
  }

  func foo(str: String) { base.foo(str: str) }
}

現在我們可以談論AnyP而不是P

let p = AnyP(S(bar: 5))
takesConcreteP(p)

// example from #1...
let array = [AnyP(S(bar: 1)), AnyP(Q(bar: 2))]
takesConcreteArrayOfP(array)

現在,考慮一下為什么我們必須構建那個盒子。 正如我們之前討論的,Swift 需要一個具體的類型來處理協議有靜態要求的情況。 考慮一下P是否有靜態需求——我們需要在AnyP實現它。 但是它應該作為什么來實施? 我們在這里處理符合P任意實例——我們不知道它們的底層具體類型如何實現靜態需求,因此我們無法在AnyP有意義地表達這AnyP

因此,這種情況下的解決方案僅在實例協議要求的情況下才真正有用。 在一般情況下,我們仍然不能將P視為符合P的具體類型。

編輯:與 Swift 一起工作 18 個月,另一個主要版本(提供新的診斷),以及來自 @AyBayBay 的評論讓我想重寫這個答案。 新的診斷是:

“不支持使用‘P’作為符合協議‘P’的具體類型。”

這實際上使整個事情變得更加清晰。 這個擴展:

extension Array where Element : P {

當不適Element == P ,因為P是沒有考慮具體的一致性P (下面的“把它放在一個盒子里”的解決方案仍然是最通用的解決方案。)


舊答案:

這是元類型的另一種情況。 Swift 真的希望你為大多數非平凡的事情找到一個具體的類型。 [P]不是具體類型(您不能為 P分配已知大小的內存塊)。 (我認為這不是真的;你絕對可以創建大小為P東西,因為它是通過間接完成的。)我認為沒有任何證據表明這是“不應該”工作的情況。 這看起來很像他們的一個“還不行”的案例。 (不幸的是,幾乎不可能讓 Apple 確認這些情況之間的區別。) Array<P>可以是變量類型(而Array不能)這一事實表明他們已經在這個方向上做了一些工作,但是 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來更自動indirect做同樣的事情。

如果您擴展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()

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM