web-dev-qa-db-ja.com

関数とメソッドから制約付きジェネリックを返す

プロトコルに準拠したオブジェクトを返す関数を作成したいのですが、プロトコルはtypealiasを使用します。次のおもちゃの例を考えてみましょう:

_protocol HasAwesomeness {
    typealias ReturnType
    func hasAwesomeness() -> ReturnType
}

extension String: HasAwesomeness {
    func hasAwesomeness() -> String {
        return "Sure Does!"
    }
}

extension Int: HasAwesomeness {
    func hasAwesomeness() -> Bool {
        return false
    }
}
_

StringIntHasAwesomenessに準拠するように拡張されており、それぞれがhasAwesomeness()メソッドを実装して異なるタイプを返します。

ここで、HasAwesomenessプロトコルに準拠するオブジェクトを返すクラスを作成します。クラスが何であるかは気にせず、メッセージhasAwesomenss()を送信できます。以下を実行すると、コンパイルエラーが発生します。

_class AmazingClass: NSObject {
    func returnsSomethingWithAwesomeness(key: String) -> HasAwesomeness {
        ...
    }
}
_

エラー:プロトコル 'HasAwesomeness'は、Selfまたは関連する型の要件があるため、一般的な制約としてのみ使用できます

ご想像のとおり、returnsSomethingWithAwesomenessの目的は、Stringパラメータに基づいてIntまたはkeyを返すことです。コンパイラーがスローするエラーは、なぜそれが許可されないのか理解できますが、構文を修正するための洞察を提供します。

_func returnsSomethingWithAwesomeness<T: HasAwesomeness>(key: String) -> T
{
    ...
}
_

よし、私の読みはメソッドreturnsSomethingWithAwesomenessは、サブタイプTを持つすべての型HasAwesomnessを返す汎用メソッドです。ただし、次の実装では、より多くのコンパイル時の型エラーがスローされます。

_func returnsSomethingWithAwesomeness<T: HasAwesomeness>(key: String) -> T
{
    if key == "foo" {
        return "Amazing Foo"
    }
    else {
        return 42
    }
}
_

エラー:タイプ「T」はプロトコル「StringLiteralConvertible」に準拠していません

エラー:タイプ「T」はプロトコル「IntegerLiteralConvertible」に準拠していません

よし、今は行き詰まっている。誰かが型とジェネリックについての私の理解のギャップを埋めるのを手伝ってくれませんか?

29
edelaney05

ここで何が起こっているのかを理解するための鍵は、実行時に動的に決定されるものとコンパイル時に静的に決定されるものを区別することだと思います。 Javaのようなほとんどの言語では、プロトコル(またはインターフェイス)はすべて実行時でポリモーフィックな動作を取得することを目的としていますが、Swiftでは、関連する型を持つプロトコルを使用してポリモーフィックな動作を取得します。 at コンパイル時

例のTのような一般的なプレースホルダーが表示される場合は常に、このTに入力される型はコンパイル時に決定されます。したがって、あなたの例では:

func returnsSomethingWithAwesomeness<T: HasAwesomeness>(key: String) -> T

returnsSomethingWithAwesomenessは、TTに準拠している限り、あらゆるタイプのHasAwesomenessを操作できる関数です。

ただし、Tに入力する内容は、returnsSomethingWithAwesomenessが呼び出された時点で決定されます– Swiftは、呼び出しサイトのすべての情報を調べて、タイプを決定しますT is is、and replace all T placeholders that that type。*

したがって、呼び出しサイトでの選択がTStringであると仮定すると、returnsSomethingWithAwesomenessは、プレースホルダーTのすべての出現で書き換えられると考えることができます。 Stringに置き換えられました:

// giving the type of s here fixes T as a String
let s: String = returnsSomethingWithAwesomeness("bar")

func returnsSomethingWithAwesomeness(key: String) -> String {
    if key == "foo" {
        return "Amazing Foo"
    }
    else {
        return 42
    }
}

TStringに置き換えられ、notHasAwesomenessのタイプに置き換えられます。 HasAwesomenessは、制約としてのみ使用されています。つまり、Tが取り得る型を制限しています。

このように見ると、return 42elseは意味がありません。文字列を返す関数から42を返すにはどうすればよいでしょうか。

returnsSomethingWithAwesomenessTが最終的に何であっても機能することを確認するには、Swiftは、指定された制約から利用できることが保証されている関数のみを使用するように制限します。この場合、Tについて知っているのは、それがHasAwesomenessに準拠しているということだけです。これは、任意のreturnsSomethingWithAwesomenessTメソッドを呼び出すことができること、または型をHasAwesomenessに制約する別の関数で使用するか、型Tの変数を別の変数に割り当てます(すべての型が割り当てをサポートします)、そしてつまりそれ

他のTと比較することはできません(サポートされる保証はありません==)。あなたは新しいものを構築することはできません(Tが適切な初期化メソッドを持っているかどうか誰が知っていますか?)。また、文字列または整数リテラルから作成することはできません(TStringLiteralConvertibleまたはIntegerLiteralConvertibleに準拠する必要があるため、必ずしもそうではありません–したがってこれらの種類のリテラルのいずれかを使用して型を作成しようとすると、これらの2つのエラーが発生します)。

すべてプロトコルに準拠するジェネリック型を返すジェネリック関数を作成することができます。ただし、返されるのはプロトコルではなく特定のタイプなので、どのタイプが動的に決定されることはありません。例えば:

func returnCollectionContainingOne<C: ExtensibleCollectionType where C.Generator.Element == Int>() -> C {

    // this is allowed because the ExtensibleCollectionType procol 
    // requires the type implement an init() that takes no parameters
    var result = C()

    // and it also defines an `append` function that allows you to do this:
    result.append(1)

    // note, the reason it was possible to give a "1" as the argument to
    // append was because of the "where C.Generator.Element == Int" part
    // of the generic placeholder constraint 

    return result
}

// now you can use returnCollectionContainingOne with arrays:
let a: [Int] = returnCollectionContainingOne()

// or with ContiguousArrays:
let b: ContiguousArray = returnCollectionContainingOne()

このコードのreturnCollectionContainingOneは、実際には2つの関数であり、1つはContiguousArray用に実装され、もう1つはArray用であり、呼び出したときにコンパイラによって自動的に書き込まれます(そしてしたがって、Cを特定のタイプに修正できます)。プロトコルを返す1つの関数ではなく、2つの異なる型を返す2つの関数。したがって、returnsSomethingWithAwesomenessが実行時に動的引数に基づいてStringまたはIntを返すことができないのと同じように、returnCollectionContainingOneは、配列または連続した配列を返しました。それが返すことができるのはTだけであり、コンパイル時にTが実際に何であれ、コンパイラーによって埋めることができます。

*これは、コンパイラが実際に行うことを少し単純化しすぎていますが、この説明のために行います。

60