プロトコルはそれ自体に準拠していませんか?


125

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


1
let arr行の型注釈を削除すると、コンパイラーが型を推測し[S]、コードがコンパイルされます。プロトコルタイプは、クラス-スーパークラスの関係と同じ方法では使用できないようです。
バディアン

1
@vadian正解です。「問題は、配列arrをプロトコルタイプの配列として宣言することから生じていることに気づきました」と私が言ったときに、私の質問で言及していました。しかし、私の質問で続けて言うように、プロトコルの全体のポイントは、通常、クラスとスーパークラスの関係と同じように使用できるということです。これらは、構造体の世界に一種の階層構造を提供すること目的としています。そして、彼らは通常そうします。問題は、なぜそれがここで機能しないのかということです
2015年

1
Xcode 7.1ではまだ機能しませんが、エラーメッセージは「プロトコル 'P'に準拠する具象型として 'P'を使用することはサポートされていません」になりました
マーティンR

1
@MartinRそれはより良いエラーメッセージです。しかし、それでも言語の穴のように感じます。
マット2015年

承知しました!でもでprotocol P : Q { }、PはQに準拠していない
マーティンR

回答:


66

編集:Swift、新しい診断を提供する別のメジャーリリース、さらに18か月の作業、および@AyBayBayからのコメントにより、この答えを書き直したいと思います。新しい診断は次のとおりです。

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

これにより、実際にこのことがすべて明らかになります。この拡張:

extension Array where Element : P {

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


古い答え:

これは、メタタイプのもう1つのケースです。Swift 、ほとんどの重要なことについて、具体的な型に到達することを本当に望んでいます。[P]は具象型ではありません(に既知のサイズのメモリブロックを割り当てることはできませんP)。(私はそれが実際に本当だとは思いません; 間接的に行われる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は同じことをより自動的に行うように追加しました。


この回答には役立つ情報がたくさんありますが、Tomohiroの回答の実際の解決策は、ここに提示されたボクシングソリューションよりも優れています。
jsadler 2016

@jsadler問題は、制限を回避する方法ではなく、制限が存在する理由です。実際、説明に関する限り、Tomohiroの回避策は、答えよりも多くの質問を投げかけます。私たちが使用している場合は==、私のアレイの例では、我々はエラーを取得し、同じタイプの要件は、一般的なパラメータ「要素」非ジェネリックます「なぜの智弘さんは使用しません。==同じエラーが発生?
マット

@ロブネイピア私はまだあなたの応答に困惑しています。Swiftは、元のソリューションと比べて、ソリューションの具体性をどのように見ていますか?構造体で物事を包み込んだようです... Idk多分私は迅速な型システムを理解するのに苦労していますが、これはすべて魔法のブードゥーのようです
AyBayBay

@AyBayBay回答が更新されました。
Rob Napier

@RobNapierどうもありがとうございました。返信の速さにいつも驚いています。率直に言って、あなたと同じくらい多くの人を助けるための時間をどうやって見つけているかを率直に言っています。それにもかかわらず、あなたの新しい編集は間違いなくそれを展望に入れます。もう1つ指摘したいのは、型の消去を理解することも役に立ちました。特に、この記事では素晴らしい仕事をした:krakendev.io/blog/generic-protocols-and-their-shortcomingsは、 TBH、私はこのようなもののいくつかについてどのように感じているかIDK。Appleが、この中のいくつかを構築する方法を我々は言語で穴を占めているように思えるが、IDK。
AyBayBay

109

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

一般的なケースでプロトコルが自分自身に準拠することを許可することは適切ではありません。問題は、静的プロトコルの要件にあります。

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

  • static メソッドとプロパティ
  • イニシャライザ
  • 関連付けられたタイプ(ただし、これらは現在、実際のタイプとしてのプロトコルの使用を妨げています)

一般的なプレースホルダーTでこれらの要件にアクセスできます。T : Pただし、転送する具体的な準拠タイプがないため、プロトコルタイプ自体では要件にアクセスできません。したがって、私たちはすることを許可TすることはできませんP

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

私たちは、おそらく呼び出すことはできませんappendNew()[P]ので、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拡張機能でこれらの要件の実装を定義できますが、これらは準拠する具象型に対してのみ定義さPPます。それ自体を呼び出すことはできません。

このため、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いくつかの一般的なプレースホルダーのタイプの「一般的な値」と同じメモリ表現を共有できT : Pおそらく Swiftチームが自己適合を容易にするのを可能にします。同じことは非@objcプロトコルにも当てはまりませんが、そのような一般的な値は現在値またはプロトコル監視テーブルを保持していません。

ただし、この機能意図的なものであり、できれば非@objcプロトコルに展開することが望まれます。SR-55に関するコメントで、 SwiftチームメンバーのSlava Pestov それについてのクエリに応じて確認しました(この質問によって促されます)。

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がコメントを追加しました-7 Sep 2017 13:53 PM

これは意図的なものです。この制限を解除することがこのバグの目的です。私が言ったようにそれはトリッキーであり、具体的な計画はまだありません。

うまくいけば、それは言語がいつの日か非@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()

ただし、これは配列のへのO(n)変換を実行することに注意する価値があります。[P]各要素は存在するコンテナーにボックス化される必要があるためです。パフォーマンスに問題がある場合は、拡張メソッドを再実装することで簡単に解決できます。これは完全に満足のいく解決策ではありません。言語の将来のバージョンには、「プロトコルタイプまたはプロトコルタイプに準拠する」制約を表現する方法が含まれることが期待されます。

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(_:)、に渡すことはできません。この問題を解決する方法をいくつか見てみましょう。PT : P

1.存在を開く

むしろ代用しようとするよりも、PためにT : P、我々は、基礎となるコンクリートの型に何を掘ることができればというP型付き値は、代わりにその折り返しと代替でしたか?残念ながら、これはと呼ばれる言語機能が必要ですオープニングexistentials現在のユーザーに直接利用できません。

ただし、Swift それらのメンバーにアクセスするときに、暗黙的に存在(プロトコル型の値)を開きます(つまり、ランタイム型を掘り下げて、汎用のプレースホルダーの形式でアクセス可能にします)。次のプロトコル拡張でこの事実を利用できますP

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

Self拡張メソッドが取る暗黙的な汎用プレースホルダーに注意してください。これは、暗黙的なselfパラメーターを入力するために使用されます。これは、すべてのプロトコル拡張メンバーの背後で発生します。プロトコルタイプの値Pでこのようなメソッドを呼び出すと、Swiftは基になる具象タイプを掘り下げ、これを使用してSelf一般的なプレースホルダーを満たします。我々は呼んことができるしている理由はここにあるtakesConcreteP(_:)self私たちは満足している- TSelf

つまり、次のように言うことができます。

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パラメーターを持つ関数も問題になります。パラメーターは同じ型の引数を取る必要があるためです。ただし、2つの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


2
多分私はただ密集しているかもしれませんが、なぜ静的なケースが特別なのか理解できません。私たち(コンパイラー)は、プロトコルのインスタンスプロパティ、つまりアダプターがそれを実装することを知っているのと同じくらい、コンパイル時にプロトコルの静的プロパティを知っています。違いは何ですか?
マット

1
@mattプロトコル型のインスタンス(つまり、existentialでラップされた具象型のインスタンスP)は、インスタンス要件への呼び出しを基になるインスタンスに転送するだけなので問題ありません。ただし、プロトコルタイプ自体(つまりP.Protocol、文字通りプロトコルを説明するタイプ)の場合、アダプターはありません。そのため、静的要件を呼び出す必要はありません。そのため、上記の例ではできませんSomeGeneric<P>(それはP.Type(存在するメタタイプ)とは異なります。これは、準拠するものの具体的なメタタイプを記述します(ただし、これはP別の話です)
Hamish

このページの上部で私が尋ねる質問は、プロトコルタイプのアダプターはなぜうまく、プロトコルタイプ自体はそうでないのかということです。プロトコルタイプ自体にはアダプターがないことを理解しています。—私が理解していないのは、静的呼び出しを採用タイプに転送するのは、インスタンスコールを採用タイプに転送するよりも難しい理由です。ここで問題がある理由は、特に静的要件の性質によるものだとあなたは主張していますが、静的要件がインスタンス要件よりいかに難しいかはわかりません。
マット

@matt静的な要件がインスタンスの要件よりも「難しい」というわけではありません。コンパイラは、インスタンスの実在性(つまり、として型付けされたインスタンスP)と実在性のメタタイプ(つまり、P.Typeメタタイプ)の両方を適切に処理できます。問題は、ジェネリックスの場合です。私たちは、実際に「好きなもの」を比較していません。ときTP、何underyling具体的な(メタ)はありません、前方の静的要件のタイプ(にTあるP.Protocol、ないP.Type....)
ハミッシュ

1
私は本当に健全性などを気にしません、私はアプリを書きたいだけです。言語は単なるツールであるべきで、製品そのものではありません。それが本当にうまくいかないケースがある場合は、それらのケースでそれをうまく禁止しますが、他のすべての人にそれが機能するケースを使用させ、アプリの作成を続けさせます。
ジョナサン。

17

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は関係ないと思います。重要な変更は== Pvsの使用: Pです。==を使用すると、元の例も機能します。私が作成した場合:そして、==と(コンテキストに応じて)潜在的な問題は、それがサブプロトコルを除外することでprotocol SubP: P、その後、定義arrとして[SubP]、その後arr.test()はもう(エラー:SUBPとPは同等でなければなりませんが)動作しません。
イムレ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.