web-dev-qa-db-ja.com

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

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

108
matt

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

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

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

extension Array where Element : P {

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


古い答え:

メタタイプのさらに別のケースです。 Swiftreallyは、ほとんどの非自明なことに対して具体的な型を取得することを望んでいます。 [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を追加して同じことをより自動的に行いました。

58
Rob Napier

プロトコルがそれ自体に適合しないのはなぜですか?

一般的な場合にプロトコルを自分自身に適合させることは不適切です。問題は静的プロトコルの要件にあります。

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

  • staticのメソッドとプロパティ
  • 初期化子
  • 関連するタイプ(これらは現在、実際のタイプとしてのプロトコルの使用を妨げていますが)

これらの要件には、汎用プレースホルダーT where T : Pでアクセスできますが、転送する具体的な準拠タイプがないため、プロトコルタイプ自体で cannot アクセスできません。したがって、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()

PElement)は具象型ではないため、インスタンス化できないため、[P]appendNew()を呼び出すことはできません。 /は、具象型の要素を持つ配列で呼び出される必要があり、その型は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の実装が no あることに注意してください)。これらの要件の実装をP拡張で定義できますが、これらはPに準拠する具象型に対してのみ定義されます。P自体でそれらを呼び出すことはできません。

このため、Swiftは、プロトコルをそれ自体に準拠する型として使用することを完全に禁止しています。なぜなら、そのプロトコルに静的な要件がある場合、プロトコルを使用できないためです。

インスタンスプロトコルの要件は、プロトコルに準拠する実際のインスタンスで/must を呼び出す必要があるため、問題はありません(したがって、要件を実装する必要があります)。したがって、Pと入力されたインスタンスの要件を呼び出すとき、その要件の基になる具象型の実装にその呼び出しを転送できます。

ただし、この場合、ルールに対して特別な例外を作成すると、プロトコルが汎用コードによってどのように処理されるかについて驚くべき矛盾が生じる可能性があります。それは言われていますが、状況はassociatedtypeの要件にあまり似ていません-これは(現在)型としてプロトコルを使用することを妨げています。静的な要件がある場合にプロトコルを自分自身に準拠する型として使用できないようにする制限があることは、言語の将来のバージョンのオプションになる可能性があります

編集:そして、以下で説明するように、これはSwiftチームが目指しているもののように見えます。


@objcプロトコル

実際、それは exactly であり、言語が@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では、TPに準拠している必要があります。 Pには静的な要件がないため、TPに置き換えることができます。 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以外のプラットフォームで実行できないためです。

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

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

@objcプロトコルのこの簡略化された表現により、そのようなプロトコルタイプPの値は、何らかの汎用プレースホルダーT : Pおそらく/のタイプの 'generic value'と同じメモリ表現を共有できますSwiftチームが自己適合性を許可するのは簡単です。非@objcプロトコルの場合は同じではありませんが、このような汎用値は現在値またはプロトコル監視テーブルを保持していません。

ただし、この機能は /であり、SwiftチームメンバーSlava Pestovによって確認されたように、_@objcプロトコル以外に展開されることが望まれます SR-55のコメントで それについてのあなたの質問に答えて( この質問 によって促されます):

Matt Neuburgがコメントを追加しました-7 Sep 2017 1:33 PM

これはコンパイルします:

@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で私たちの何人かはこれを驚くべきことに気づき、それが意図的かバグのあるEdgeケースかを知りたいと思っています。

Slava Pestovがコメントを追加しました-7 Sep 2017 1: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に準拠する具象型要素を持つ配列で呼び出すことができなくなります。 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)変換が実行されることに注意してください。パフォーマンスが問題の場合は、拡張メソッドを再実装することでこれを簡単に解決できますが、これは全体満足のいく解決策ではありません–将来のバージョンの言語に 'プロトコルタイプ or は、プロトコルタイプの制約に従います。

Swift 3.1、これを達成するための最も一般的な方法、 ロブが彼の答えで示しているように の前に、単に[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)

現在、ptakesConcreteP(_:)に渡すことはできません。汎用のプレースホルダーT : Pを現在Pに置き換えることができないためです。この問題を解決できるいくつかの方法を見てみましょう。

1.存在を開く

T : PPに置き換えようとするのではなく、P型付きの値がラッピングしている具体的な型を調べて、代わりにそれを置き換えたらどうでしょうか。残念ながら、これには opening existentials と呼ばれる言語機能が必要です。これは現在ユーザーが直接利用することはできません。

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

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

拡張メソッドが使用する暗黙の汎用Selfプレースホルダーに注意してください。これは、暗黙のselfパラメーターの入力に使用されます。これは、すべてのプロトコル拡張メンバーの背後で発生します。プロトコル型の値Pでそのようなメソッドを呼び出す場合、Swift=基になる具象型を掘り下げ、これを使用してSelfジェネリックプレースホルダーを満たします。これがtakesConcreteP(_:) with selfTSelfで満たしています。

これは、次のことができるようになったことを意味します。

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でこれを意味的に表現できません。

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

81
Hamish

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