web-dev-qa-db-ja.com

汎用プロトコルを変数タイプとして使用する方法

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

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」は、自己または関連するタイプの要件があるため、一般的な制約としてのみ使用できます」

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

**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()
}
81
Tamerlane

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)

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

79

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

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>であることが知られており、Printerプロトコルの可能な実装を抽象化するために使用できます。 AnyPrinterは技術的に抽象的ではありませんが、実装は実際の実装タイプへのフォールスルーであり、実装タイプをそれらを使用するタイプから分離するために使用できます。

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

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

明らかに、型消去を設定する作業がいくつかありますが、一般的なプロトコルの抽象化が必要な場合には非常に便利です。このパターンはSwift 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

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

33
Patrick Goley

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

(ところで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()
        }
}
4