プロトコルが自分自身に準拠しないのはなぜですか?
一般的なケースでプロトコルが自分自身に準拠することを許可することは適切ではありません。問題は、静的プロトコルの要件にあります。
これらには以下が含まれます:
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]
ので、P
(Element
)は、具体的なタイプではないので、インスタンス化することはできません。これは、必要があり、具体的な型付けされた要素、そのタイプ準拠へとアレイ上に呼び出されます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)'
したがって、この問題に対する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(_:)
、に渡すことはできません。この問題を解決する方法をいくつか見てみましょう。P
T : 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
私たちは満足している- T
とSelf
。
つまり、次のように言うことができます。
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
。
let arr
行の型注釈を削除すると、コンパイラーが型を推測し[S]
、コードがコンパイルされます。プロトコルタイプは、クラス-スーパークラスの関係と同じ方法では使用できないようです。