Cara menggunakan protokol generik sebagai tipe variabel

89

Katakanlah saya memiliki protokol:

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

Dan inilah implementasinya

class Printer<T> : Printable {

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

Harapan saya adalah saya harus bisa menggunakan Printablevariabel untuk mencetak nilai seperti ini:

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

Kompiler mengeluhkan kesalahan ini:

"protokol 'Dapat dicetak' hanya dapat digunakan sebagai batasan umum karena memiliki persyaratan Jenis Sendiri atau terkait"

Apakah saya melakukan sesuatu yang salah? Pokoknya untuk memperbaikinya?

**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)

EDIT 2: Contoh dunia nyata dari apa yang saya inginkan. Perhatikan bahwa ini tidak akan dikompilasi, tetapi menyajikan apa yang ingin saya capai.

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()
}
Tamerlane
sumber
1
Berikut adalah utas yang relevan di Forum Pengembang Apple dari 2014 di mana pertanyaan ini ditangani (sampai tingkat tertentu) oleh pengembang Swift di Apple: devforums.apple.com/thread/230611 (Catatan: Akun Pengembang Apple diperlukan untuk melihat ini halaman.)
titaniumdecoy

Jawaban:

88

Seperti yang ditunjukkan Thomas, Anda dapat mendeklarasikan variabel Anda dengan tidak memberikan tipe sama sekali (atau Anda dapat secara eksplisit memberikannya sebagai tipe Printer<Int>. Tapi berikut adalah penjelasan mengapa Anda tidak dapat memiliki tipePrintable protokol.

Anda tidak dapat memperlakukan protokol dengan tipe terkait seperti protokol biasa dan mendeklarasikannya sebagai tipe variabel mandiri. Untuk memikirkan mengapa, pertimbangkan skenario ini. Misalkan Anda mendeklarasikan protokol untuk menyimpan beberapa jenis arbitrer dan kemudian mengambilnya kembali:

// 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"

Oke, sejauh ini bagus.

Sekarang, alasan utama Anda akan memiliki tipe variabel menjadi protokol yang diterapkan tipe, daripada tipe sebenarnya, adalah agar Anda dapat menetapkan berbagai jenis objek yang semuanya sesuai dengan protokol itu ke variabel yang sama, dan mendapatkan polimorfik perilaku saat runtime bergantung pada objek sebenarnya.

Tetapi Anda tidak dapat melakukan ini jika protokol memiliki tipe yang terkait. Bagaimana kode berikut bekerja dalam praktiknya?

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

Pada kode di atas, akan seperti apakah tipe dari xitu? An Int? Atau String? Di Swift, semua jenis harus diperbaiki pada waktu kompilasi. Sebuah fungsi tidak dapat bergeser secara dinamis dari mengembalikan satu jenis ke jenis lainnya berdasarkan faktor yang ditentukan pada waktu proses.

Sebaliknya, Anda hanya dapat menggunakan StoredTypesebagai batasan umum. Misalkan Anda ingin mencetak semua jenis jenis yang disimpan. Anda bisa menulis fungsi seperti ini:

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

printStoredValue(intStorer)
printStoredValue(stringStorer)

Ini tidak masalah, karena pada waktu kompilasi, kompilator seperti menulis dua versi printStoredValue: satu untuk Ints, dan satu untuk Strings. Di dalam dua versi itu, xdikenal tipe tertentu.

Kecepatan Kecepatan Udara
sumber
20
Dengan kata lain, tidak ada cara untuk memiliki protokol generik sebagai parameter dan alasannya adalah bahwa Swift tidak mendukung dukungan runtime gaya .NET untuk obat generik? Ini cukup merepotkan.
Tamerlane
Pengetahuan .NET saya agak kabur ... apakah Anda memiliki contoh sesuatu yang serupa di .NET yang akan berfungsi dalam contoh ini? Selain itu, agak sulit untuk melihat protokol dalam contoh Anda membeli Anda. Saat runtime, perilaku apa yang Anda harapkan jika Anda menetapkan printer dari jenis yang berbeda ke pvariabel Anda dan kemudian meneruskan jenis yang tidak valid ke print? Pengecualian waktu proses?
Kecepatan Udara
@AirspeedVelocity Saya telah memperbarui pertanyaan untuk menyertakan contoh C #. Adapun nilai mengapa saya membutuhkannya adalah bahwa ini akan memungkinkan saya untuk mengembangkan ke antarmuka bukan ke implementasi. Jika saya perlu meneruskan printable ke suatu fungsi, saya dapat menggunakan antarmuka dalam deklarasi dan melewatkan banyak implementasi yang berbeda tanpa menyentuh fungsi saya. Juga pikirkan tentang penerapan perpustakaan koleksi, Anda akan memerlukan kode semacam ini ditambah batasan tambahan pada tipe T.
Tamerlane
4
Secara teoritis, jika dimungkinkan untuk membuat protokol generik menggunakan tanda kurung sudut seperti di C #, apakah pembuatan variabel jenis protokol akan diizinkan? (StoringType <Int>, StoringType <String>)
GeRyCh
1
Di Java, Anda dapat melakukan yang setara var someStorer: StoringType<Int>atau var someStorer: StoringType<String>dan memecahkan masalah yang Anda garis besarkan.
JeremyP
43

Ada satu solusi lagi yang belum disebutkan pada pertanyaan ini, yaitu menggunakan teknik yang disebut penghapusan tipe . Untuk mencapai antarmuka abstrak untuk protokol generik, buat kelas atau struct yang membungkus objek atau struct yang sesuai dengan protokol. Kelas pembungkus, biasanya bernama 'Any {protocol name}', itu sendiri sesuai dengan protokol dan mengimplementasikan fungsinya dengan meneruskan semua panggilan ke objek internal. Coba contoh di bawah ini di taman bermain:

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

Jenis dari printertelah diketahui AnyPrinter<Int>dan dapat digunakan untuk mengabstraksi segala kemungkinan implementasi dari protokol Printer. Meskipun AnyPrinter tidak abstrak secara teknis, implementasinya hanyalah jatuh ke tipe implementasi nyata, dan dapat digunakan untuk memisahkan tipe implementasi dari tipe yang menggunakannya.

Satu hal yang perlu diperhatikan adalah AnyPrintertidak harus secara eksplisit mempertahankan instance dasar. Faktanya, kami tidak bisa karena kami tidak bisa menyatakan AnyPrintermemiliki Printer<T>properti. Sebagai gantinya, kita mendapatkan penunjuk fungsi _printke printfungsi basis . Memanggil base.printtanpa memanggilnya mengembalikan fungsi di mana basis di-curried sebagai variabel mandiri, dan karenanya dipertahankan untuk pemanggilan di masa mendatang.

Hal lain yang perlu diingat adalah bahwa solusi ini pada dasarnya adalah lapisan lain dari pengiriman dinamis yang berarti sedikit berpengaruh pada kinerja. Selain itu, instance penghapus tipe memerlukan memori tambahan di atas instance yang mendasarinya. Untuk alasan ini, penghapusan tipe bukanlah abstraksi bebas biaya.

Jelas ada beberapa pekerjaan untuk mengatur penghapusan tipe, tetapi ini bisa sangat berguna jika abstraksi protokol umum diperlukan. Pola ini ditemukan di pustaka standar swift dengan tipe seperti AnySequence. Bacaan lebih lanjut: http://robnapier.net/erasure

BONUS:

Jika Anda memutuskan ingin memasukkan implementasi yang sama di semua Printertempat, Anda dapat menyediakan penginisialisasi praktis AnyPrinteryang memasukkan jenis tersebut.

extension AnyPrinter {

    convenience init() {

        let nsLogger = NSLogger<T>()

        self.init(base: nsLogger)
    }
}

let printer = AnyPrinter<Int>()

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

Ini bisa menjadi cara yang mudah dan KERING untuk mengekspresikan injeksi dependensi untuk protokol yang Anda gunakan di seluruh aplikasi Anda.

Patrick Goley
sumber
Terima kasih untuk ini. Saya menyukai pola penghapusan tipe ini (menggunakan pointer fungsi) lebih baik daripada menggunakan kelas abstrak (yang tentu saja, tidak ada dan harus dipalsukan menggunakan fatalError()) yang dijelaskan dalam tutorial penghapusan tipe lainnya.
Mengejar
4

Menangani kasus penggunaan terbaru Anda:

(btw Printablesudah menjadi protokol Swift standar jadi Anda mungkin ingin memilih nama yang berbeda untuk menghindari kebingungan)

Untuk menerapkan batasan khusus pada pelaksana protokol, Anda dapat membatasi typealias protokol. Jadi untuk membuat koleksi protokol Anda yang membutuhkan elemen agar dapat dicetak:

// 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
}

Sekarang jika Anda ingin menerapkan koleksi yang hanya dapat berisi elemen yang dapat dicetak:

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

Namun, ini mungkin utilitas kecil yang sebenarnya, karena Anda tidak dapat membatasi struct koleksi Swift yang ada seperti itu, hanya yang Anda terapkan.

Sebagai gantinya, Anda harus membuat fungsi umum yang membatasi inputnya ke koleksi yang berisi elemen yang dapat dicetak.

func printCollection
    <C: CollectionType where C.Generator.Element: Printable>
    (source: C) {
        for x in source {
            x.print()
        }
}
Kecepatan Kecepatan Udara
sumber
Ya ampun, ini terlihat sakit. Yang saya butuhkan hanyalah memiliki protokol dengan dukungan umum. Saya berharap memiliki sesuatu seperti ini: Protocol Collection <T>: SequenceType. Dan itu dia. Terima kasih untuk contoh kodenya, saya pikir perlu beberapa saat untuk mencernanya :)
Tamerlane