139

この 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の配列として宣言することに起因していることに気付きましたが、それは不合理なことでしょうか? プロトコルは、構造体に型階層のようなものを提供するのに役立つと思っていましたか?

4

3 に答える 3

133

プロトコルが自分自身に準拠しないのはなぜですか?

一般的なケースで、プロトコルがそれ自体に準拠することを許可することは健全ではありません。問題は、静的プロトコル要件にあります。

これらには以下が含まれます:

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

(the ) は具象型ではないため、インスタンス化できないため、a をappendNew()呼び出すことはできません。これは、具体的な型の要素を持つ配列で呼び出す必要があり、その型は に準拠しています。[P]PElementP

これは、静的メソッドとプロパティの要件に関する同様の話です。

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

このため、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)

bazTに準拠する必要があり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)'

したがって、この問題の回避策の 1 つは、プロトコルを作成することです@objc。確かに、これは多くの場合理想的な回避策ではありません。準拠する型を強制的にクラスにし、Obj-C ランタイムを必要とするため、Linux などの Apple 以外のプラットフォームでは実行できないからです。

しかし、この制限は、言語がプロトコルに対して「静的要件のないプロトコルがそれ自体に準拠する」を既に実装している主な理由 (の 1 つ) であると思われ@objcます。それらの周りに書かれた汎用コードは、コンパイラーによって大幅に簡素化できます。

なんで?@objcプロトコル型の値は事実上、要件が を使用してディスパッチされる単なるクラス参照であるためですobjc_msgSend。反対に、非@objcプロトコル型の値はより複雑です。なぜなら、(潜在的に間接的に格納された) ラップされた値のメモリを管理し、異なる値に対してどの実装を呼び出すかを決定するために、値テーブルと監視テーブルの両方を持ち歩くからです。要件、それぞれ。

プロトコルのこの簡略化された表現により@objc、そのようなプロトコル型のP値は、型 some generic placeholder の「ジェネリック値」と同じメモリ表現を共有できT : PおそらくSwift チームが自己適合を許可することを容易にします。非プロトコルには同じことは当てはまりません@objcが、そのような一般的な値は現在、値またはプロトコル監視テーブルを保持していません。

ただし、この機能は意図的なものであり、Swift チーム メンバーの Slava Pestovが SR-55 のコメントで@objc確認したように、意図的なものであり、非プロトコルに展開されることを願っています (この質問によって促されます)。

Matt Neuburg がコメントを追加しました - 2017 年 9 月 7 日 13: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 が彼の回答で示しているように、単純に a のラッパー タイプを構築することであり[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(_:)PT : P

1. 存在論を開く

を代入しようとするのではなく、型付きの値がラップされていた基礎となる具体的な型を掘り下げ、代わりにそれを代用できたらどうPでしょうか? 残念ながら、これには、opening existentialsと呼ばれる言語機能が必要ですが、現在、ユーザーが直接利用することはできません。T : PP

ただし、Swift存在​​要素 (プロトコル型の値) のメンバーにアクセスするときに暗黙的にそれらを開きます (つまり、実行時の型を掘り出して、一般的なプレースホルダーの形式でアクセスできるようにします)。上のプロトコル拡張でこの事実を利用できますP

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

Self拡張メソッドが取る暗黙的なジェネリック プレースホルダーに注意してください。これは、暗黙的なselfパラメーターを型指定するために使用されます。これは、すべてのプロトコル拡張メンバーで舞台裏で行われます。プロトコル型の value でそのようなメソッドを呼び出すとP、Swift は基礎となる具象型を掘り出し、これを使用してSelfジェネリック プレースホルダーを満たします。これが、 で呼び出すことができる理由です– で満足takesConcreteP(_:)しています。selfTSelf

これは、次のように言えることを意味します。

p.callTakesConcreteP()

そして、基になる具象型 (この場合は ) によって満たされるtakesConcreteP(_:)一般的なプレースホルダーで呼び出されます。これは「それ自体に準拠するプロトコル」ではないことに注意してください。具体的な型を代入しているためです。TSPtakesConcreteP(_:)

Swift がプロトコルがそれ自体に準拠することを引き続き禁止する場合、次善の代替策は、ジェネリック型のパラメーターに引数としてそれらを渡そうとするときに、暗黙的に存在をオープンすることです。定型文なしで、プロトコル拡張トランポリンが行ったこととまったく同じことを効果的に行います。

ただし、existentials を開くことは、プロトコルがそれ自体に準拠していないという問題に対する一般的な解決策ではないことに注意してください。プロトコル型の値の異種コレクションは扱いません。これらはすべて、異なる基になる具象型を持つ可能性があります。たとえば、次のことを考慮してください。

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. 消しゴムを作る

ロブが言うように、型消しゴムは、プロトコルがそれ自体に準拠していないという問題に対する最も一般的な解決策です。インスタンスの要件を基礎となるインスタンスに転送することにより、プロトコル型のインスタンスをそのプロトコルに準拠する具象型にラップできます。

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

したがって、この場合のソリューションは、インスタンスプロトコル要件の場合にのみ実際に役立ちます。P一般的なケースでは、まだに準拠する具象型として扱うことはできませんP

于 2017-04-14T08:45:42.860 に答える
67

編集:Swift、別のメジャーリリース(新しい診断を提供する)、および@AyBayBayからのコメントを使用してさらに18か月間、この回答を書き直したくなりました。新しい診断は次のとおりです。

「プロトコル 'P' に準拠する具象型として 'P' を使用することはサポートされていません。」

これにより、この全体がより明確になります。この拡張機能:

extension Array where Element : P {

の具体的な適合と見なされないElement == Pため、適用されません。(以下の「ボックスに入れる」ソリューションは、依然として最も一般的なソリューションです。)PP


古い答え:

これは、メタタイプのもう 1 つのケースです。Swiftは、ほとんどの重要なことに対して具体的な型を取得することを本当に望んでいます。[P]は具象型ではありません (既知のサイズのメモリ ブロックを に割り当てることはできませんP)。(実際にはそうではないと思います。 indirection を介して行われるPため、サイズのあるものを絶対に作成できます。)これが「すべきではない」というケースであるという証拠はないと思います。これは、「まだ機能しない」ケースの 1 つに非常によく似ています。(残念ながら、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が同じことをより自動的に行うように追加しました.

于 2015-11-04T14:54:04.013 に答える