ジェネリックプロトコルを変数タイプとして使用する方法


89

私がプロトコルを持っているとしましょう:

public protocol Printable {
    typealias T
    func Print(val:T)
}

そしてここに実装があります

class Printer<T> : Printable {

    func Print(val: T) {
        println(val)
    }
}

私の期待は、Printable変数を使用して次のような値を出力できる必要があるということでした。

let p:Printable = Printer<Int>()
p.Print(67)

コンパイラはこのエラーで文句を言っています:

「プロトコル 'Printable'は、Selfまたは関連する型の要件があるため、ジェネリック制約としてのみ使用できます」

私は何か間違ったことをしていますか?とにかくこれを修正するには?

**EDIT :** Adding similar code that works in C#

public interface IPrintable<T> 
{
    void Print(T val);
}

public class Printer<T> : IPrintable<T>
{
   public void Print(T val)
   {
      Console.WriteLine(val);
   }
}


//.... inside Main
.....
IPrintable<int> p = new Printer<int>();
p.Print(67)

編集2:私が欲しいものの実世界の例。これはコンパイルされませんが、私が達成したいことを示していることに注意してください。

protocol Printable 
{
   func Print()
}

protocol CollectionType<T where T:Printable> : SequenceType 
{
   .....
   /// here goes implementation
   ..... 
}

public class Collection<T where T:Printable> : CollectionType<T>
{
    ......
}

let col:CollectionType<Int> = SomeFunctiionThatReturnsIntCollection()
for item in col {
   item.Print()
}

1
これは、2014年のApple Developer Forumsの関連スレッドで、AppleのSwift開発者がこの質問に(ある程度)対処しています:devforums.apple.com/thread/230611(注:これを表示するには、AppleDeveloperアカウントが必要ですページ)。
titaniumdecoy

回答:


88

Thomasが指摘しているように、型をまったく指定しないことで変数を宣言できます(または、明示的に型として指定できますPrinter<Int>。ただし、Printableプロトコルの型を使用できない理由については、次の説明を参照してください。

通常のプロトコルのように関連するタイプのプロトコルを扱い、スタンドアロンの変数タイプとして宣言することはできません。理由を考えるために、このシナリオを検討してください。任意の型を格納し、それをフェッチするためのプロトコルを宣言したとします。

// a general protocol that allows for storing and retrieving
// a specific type (as defined by a Stored typealias
protocol StoringType {
    typealias Stored

    init(_ value: Stored)
    func getStored() -> Stored
}

// An implementation that stores Ints
struct IntStorer: StoringType {
    typealias Stored = Int
    private let _stored: Int
    init(_ value: Int) { _stored = value }
    func getStored() -> Int { return _stored }
}

// An implementation that stores Strings
struct StringStorer: StoringType {
    typealias Stored = String
    private let _stored: String
    init(_ value: String) { _stored = value }
    func getStored() -> String { return _stored }
}

let intStorer = IntStorer(5)
intStorer.getStored() // returns 5

let stringStorer = StringStorer("five")
stringStorer.getStored() // returns "five"

OK、これまでのところ良いです。

さて、変数の型を実際の型ではなく、型が実装するプロトコルにする主な理由は、そのプロトコルにすべて準拠するさまざまな種類のオブジェクトを同じ変数に割り当てて、多型を取得できるようにするためです。オブジェクトが実際に何であるかに応じて、実行時の動作。

ただし、プロトコルに関連付けられたタイプがある場合、これを行うことはできません。次のコードは実際にはどのように機能しますか?

// as you've seen this won't compile because
// StoringType has an associated type.

// randomly assign either a string or int storer to someStorer:
var someStorer: StoringType = 
      arc4random()%2 == 0 ? intStorer : stringStorer

let x = someStorer.getStored()

上記のコードでは、タイプは何でしょうxか?アンInt?またはString?Swiftでは、すべてのタイプをコンパイル時に修正する必要があります。関数は、実行時に決定された要因に基づいて、あるタイプから別のタイプに動的に移行することはできません。

代わりに、StoredType一般的な制約としてのみ使用できます。あらゆる種類の保存されたタイプを印刷したいとします。次のような関数を書くことができます:

func printStoredValue<S: StoringType>(storer: S) {
    let x = storer.getStored()
    println(x)
}

printStoredValue(intStorer)
printStoredValue(stringStorer)

これは問題ありません。コンパイル時に、コンパイラが2つのバージョンを書き出すように見えるからprintStoredValueです。1つはInts用、もう1つはStrings用です。これらの2つのバージョンの中xで、特定のタイプであることが知られています。


20
言い換えれば、ジェネリックプロトコルをパラメーターとして使用する方法はありません。その理由は、Swiftがジェネリックの.NETスタイルのランタイムサポートをサポートしていないためです。これは非常に不便です。
Tamerlane 2014

私の.NETの知識は少しぼんやりしています...この例で機能する.NETで同様の例がありますか?また、あなたの例のプロトコルがあなたを購入しているのかを知るのは少し難しいです。実行時に、さまざまなタイプのプリンターをp変数に割り当ててから、無効なタイプを変数に渡した場合の動作はどうなると思いますprintか?ランタイム例外?
対気速度速度2014

@AirspeedVelocity質問を更新してC#の例を含めました。私が必要とする価値については、これにより、実装ではなくインターフェースに開発できるようになるということです。printableを関数に渡す必要がある場合は、宣言でインターフェイスを使用して、関数に触れることなく多くの異なる実装を渡すことができます。また、あなたは、コードプラス型Tへの追加の制約のこの種を必要とするコレクションライブラリを実装について考える
ティムール

4
理論的には、C#のように山かっこを使用してジェネリックプロトコルを作成できる場合、プロトコルタイプの変数の作成は許可されますか?(StoringType <Int>、StoringType <String>)
GeRyCh 2016

1
Javaでは、あなたは、同等の操作を行うことができvar someStorer: StoringType<Int>たりvar someStorer: StoringType<String>と、アウトライン問題を解決します。
JeremyP 2017

42

この質問で言及されていないもう1つの解決策があります。それは、型消去と呼ばれる手法を使用することです。ジェネリックプロトコルの抽象インターフェイスを実現するには、プロトコルに準拠するオブジェクトまたは構造体をラップするクラスまたは構造体を作成します。通常は「Any {protocolname}」という名前のラッパークラス自体がプロトコルに準拠し、すべての呼び出しを内部オブジェクトに転送することでその機能を実装します。遊び場で以下の例を試してください。

import Foundation

public protocol Printer {
    typealias T
    func print(val:T)
}

struct AnyPrinter<U>: Printer {

    typealias T = U

    private let _print: U -> ()

    init<Base: Printer where Base.T == U>(base : Base) {
        _print = base.print
    }

    func print(val: T) {
        _print(val)
    }
}

struct NSLogger<U>: Printer {

    typealias T = U

    func print(val: T) {
        NSLog("\(val)")
    }
}

let nsLogger = NSLogger<Int>()

let printer = AnyPrinter(base: nsLogger)

printer.print(5) // prints 5

のタイプprinterは既知AnyPrinter<Int>であり、プリンタプロトコルの可能な実装を抽象化するために使用できます。AnyPrinterは技術的に抽象的ではありませんが、その実装は実際の実装タイプへの単なるフォールスルーであり、実装タイプをそれらを使用するタイプから分離するために使用できます。

注意すべき点の1つはAnyPrinter、ベースインスタンスを明示的に保持する必要がないことです。実際、プロパティAnyPrinterを持つことを宣言できないため、宣言することはできませんPrinter<T>。代わりに、_printbaseのprint関数への関数ポインタを取得します。呼び出しbase.printを起動せずには、ベースは自己変数としてカレーれ、そしてthusly将来の呼び出しのために保持されている関数を返します。

覚えておくべきもう1つのことは、このソリューションは本質的に動的ディスパッチの別のレイヤーであり、パフォーマンスにわずかな影響を与えることを意味します。また、型消去インスタンスは、基になるインスタンスの上に追加のメモリを必要とします。これらの理由から、型消去は費用のかからない抽象化ではありません。

明らかに、型消去を設定するための作業がいくつかありますが、一般的なプロトコルの抽象化が必要な場合は非常に便利です。このパターンは、のようなタイプの迅速な標準ライブラリにありますAnySequence。さらに読む:http//robnapier.net/erasure

ボーナス:

Printerどこにでも同じ実装を注入することにした場合は、AnyPrinterそのタイプを注入する便利な初期化子を提供できます。

extension AnyPrinter {

    convenience init() {

        let nsLogger = NSLogger<T>()

        self.init(base: nsLogger)
    }
}

let printer = AnyPrinter<Int>()

printer.print(10) //prints 10 with NSLog

これは、アプリ全体で使用するプロトコルの依存性注入を表現するための簡単で乾燥した方法です。


これをありがとう。私fatalError()は、他の型消去チュートリアルで説明されている抽象クラス(もちろん存在せず、を使用して偽造する必要があります)を使用するよりも、この型消去のパターン(関数ポインターを使用)の方が好きです。
追跡

4

更新されたユースケースへの対応:

(ところでPrintable、すでに標準のSwiftプロトコルなので、混乱を避けるために別の名前を選択することをお勧めします)

プロトコル実装者に特定の制限を適用するために、プロトコルのタイプエイリアスを制約できます。したがって、要素を印刷可能にする必要があるプロトコルコレクションを作成するには、次のようにします。

// because of how how collections are structured in the Swift std lib,
// you’d first need to create a PrintableGeneratorType, which would be
// a constrained version of GeneratorType
protocol PrintableGeneratorType: GeneratorType {
    // require elements to be printable:
    typealias Element: Printable
}

// then have the collection require a printable generator
protocol PrintableCollectionType: CollectionType {
    typealias Generator: PrintableGenerator
}

ここで、印刷可能な要素のみを含むことができるコレクションを実装する場合は、次のようにします。

struct MyPrintableCollection<T: Printable>: PrintableCollectionType {
    typealias Generator = IndexingGenerator<T>
    // etc...
}

ただし、これはおそらく実際の有用性はほとんどありません。そのような既存のSwiftコレクション構造体を制約することはできず、実装するものだけです。

代わりに、印刷可能な要素を含むコレクションへの入力を制限するジェネリック関数を作成する必要があります。

func printCollection
    <C: CollectionType where C.Generator.Element: Printable>
    (source: C) {
        for x in source {
            x.print()
        }
}

ああ、これは具合が悪いようだ。私が必要としていたのは、一般的なサポートを備えたプロトコルを持つことだけです。私はこのようなものが欲しいと思っていました:protocol Collection <T>:SequenceType。以上です。コードサンプルをありがとう私はそれを消化するのに時間がかかると思います:)
Tamerlane 2015年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.