Penggunaan protokol sebagai tipe array dan parameter fungsi dengan cepat

139

Saya ingin membuat kelas yang dapat menyimpan objek yang sesuai dengan protokol tertentu. Objek harus disimpan dalam array yang diketik. Menurut protokol dokumentasi Swift dapat digunakan sebagai tipe: 

Karena ini adalah tipe, Anda dapat menggunakan protokol di banyak tempat di mana tipe lain diperbolehkan, termasuk:

  • Sebagai tipe parameter atau tipe kembalian dalam sebuah fungsi, metode, atau penginisialisasi
  • Sebagai jenis konstanta, variabel, atau properti
  • Sebagai tipe item dalam larik, kamus, atau wadah lainnya

Namun yang berikut ini menghasilkan kesalahan kompilator:

Protokol 'SomeProtocol' hanya dapat digunakan sebagai batasan umum karena memiliki Persyaratan Jenis Sendiri atau terkait

Bagaimana Anda bisa menyelesaikan ini:

protocol SomeProtocol: Equatable {
    func bla()
}

class SomeClass {
    
    var protocols = [SomeProtocol]()
    
    func addElement(element: SomeProtocol) {
        self.protocols.append(element)
    }
    
    func removeElement(element: SomeProtocol) {
        if let index = find(self.protocols, element) {
            self.protocols.removeAtIndex(index)
        }
    }
}
snod
sumber
2
Di Swift ada kelas protokol khusus yang tidak memberikan polimorfisme atas jenis yang mengimplementasikannya. Protokol tersebut menggunakan Self atau tipe terkait dalam definisinya (dan Equatable adalah salah satunya). Dalam beberapa kasus, dimungkinkan untuk menggunakan pembungkus tipe-terhapus untuk membuat koleksi Anda homomorfik. Lihat di sini misalnya.
werediver

Jawaban:

51

Anda telah mengalami varian masalah dengan protokol di Swift yang belum ada solusi yang baik.

Lihat juga Memperluas Array untuk memeriksa apakah itu diurutkan di Swift? , berisi saran tentang cara mengatasinya yang mungkin sesuai untuk masalah spesifik Anda (pertanyaan Anda sangat umum, mungkin Anda dapat menemukan solusi menggunakan jawaban ini).

DarkDust
sumber
1
Saya pikir ini adalah jawaban yang benar untuk saat ini. Solusi Nate berhasil tetapi tidak menyelesaikan masalah saya sepenuhnya.
snod
33

Anda ingin membuat kelas generik, dengan batasan tipe yang mengharuskan kelas yang digunakan dengannya sesuai SomeProtocol, seperti ini:

class SomeClass<T: SomeProtocol> {
    typealias ElementType = T
    var protocols = [ElementType]()

    func addElement(element: ElementType) {
        self.protocols.append(element)
    }

    func removeElement(element: ElementType) {
        if let index = find(self.protocols, element) {
            self.protocols.removeAtIndex(index)
        }
    }
}
Nate Cook
sumber
Bagaimana Anda membuat contoh objek dari kelas itu?
snod
Hmmm ... Cara ini mengunci Anda untuk menggunakan satu jenis yang sesuai dengan SomeProtocol-let protocolGroup: SomeClass<MyMemberClass> = SomeClass()
Nate Cook
Dengan cara ini Anda hanya bisa menambahkan objek kelas MyMemberClasske array?
snod
ataulet foo = SomeClass<MyMemberClass>()
DarkDust
@snod Ya, bukan itu yang Anda cari. Masalahnya adalah Equatablekesesuaian - tanpanya Anda dapat menggunakan kode persis Anda. Mungkin mengajukan bug / permintaan fitur?
Nate Cook
17

Di Swift ada kelas protokol khusus yang tidak memberikan polimorfisme atas jenis yang mengimplementasikannya. Protokol semacam itu menggunakan Selfatau associatedtypekata kunci dalam definisinya (dan Equatablemerupakan salah satunya).

Dalam beberapa kasus, dimungkinkan untuk menggunakan pembungkus tipe-terhapus untuk membuat koleksi Anda homomorfik. Berikut ini contohnya.

// This protocol doesn't provide polymorphism over the types which implement it.
protocol X: Equatable {
    var x: Int { get }
}

// We can't use such protocols as types, only as generic-constraints.
func ==<T: X>(a: T, b: T) -> Bool {
    return a.x == b.x
}

// A type-erased wrapper can help overcome this limitation in some cases.
struct AnyX {
    private let _x: () -> Int
    var x: Int { return _x() }

    init<T: X>(_ some: T) {
        _x = { some.x }
    }
}

// Usage Example

struct XY: X {
    var x: Int
    var y: Int
}

struct XZ: X {
    var x: Int
    var z: Int
}

let xy = XY(x: 1, y: 2)
let xz = XZ(x: 3, z: 4)

//let xs = [xy, xz] // error
let xs = [AnyX(xy), AnyX(xz)]
xs.forEach { print($0.x) } // 1 3
werediver
sumber
13

Solusi terbatas yang saya temukan adalah menandai protokol sebagai protokol khusus kelas. Ini akan memungkinkan Anda untuk membandingkan objek menggunakan operator '==='. Saya mengerti ini tidak akan berfungsi untuk struct, dll, tetapi itu cukup baik dalam kasus saya.

protocol SomeProtocol: class {
    func bla()
}

class SomeClass {

    var protocols = [SomeProtocol]()

    func addElement(element: SomeProtocol) {
        self.protocols.append(element)
    }

    func removeElement(element: SomeProtocol) {
        for i in 0...protocols.count {
            if protocols[i] === element {
                protocols.removeAtIndex(i)
                return
            }
        }
    }

}
almas
sumber
Tidakkah ini mengizinkan entri duplikat protocols, jika addElementdipanggil lebih dari sekali dengan objek yang sama?
Tom Harrington
Ya, array dengan cepat mungkin berisi entri duplikat. Jika Anda berpikir bahwa ini mungkin terjadi dalam kode Anda, maka gunakan Set, bukan larik, atau pastikan larik tersebut belum berisi objek itu.
almas
Anda dapat memanggil removeElement()sebelum menambahkan elemen baru jika Anda ingin menghindari duplikat.
Georgios
Maksud saya, bagaimana Anda mengontrol array Anda di udara, bukan? Terima kasih atas jawabannya
Reimond Hill
8

Solusinya cukup sederhana:

protocol SomeProtocol {
    func bla()
}

class SomeClass {
    init() {}

    var protocols = [SomeProtocol]()

    func addElement<T: SomeProtocol where T: Equatable>(element: T) {
        protocols.append(element)
    }

    func removeElement<T: SomeProtocol where T: Equatable>(element: T) {
        protocols = protocols.filter {
            if let e = $0 as? T where e == element {
                return false
            }
            return true
        }
    }
}
bzz
sumber
4
Anda melewatkan hal penting: OP ingin protokol mewarisi Equatableprotokol. Itu membuat perbedaan besar.
werediver
@werver Saya rasa tidak. Dia ingin menyimpan objek yang sesuai dengan SomeProtocoltipe array. Equatablekesesuaian diperlukan hanya untuk menghapus elemen dari larik. Solusi saya adalah versi perbaikan dari solusi @almas karena dapat digunakan dengan semua jenis Swift yang sesuai dengan Equatableprotokol.
bzz
2

Saya menganggap bahwa tujuan utama Anda adalah menyimpan koleksi objek yang sesuai dengan beberapa protokol, menambah koleksi ini dan menghapusnya. Ini adalah fungsionalitas seperti yang dinyatakan dalam klien Anda, "SomeClass". Pewarisan yang setara membutuhkan diri sendiri dan itu tidak diperlukan untuk fungsi ini. Kami dapat membuat ini berfungsi dalam array di Obj-C menggunakan fungsi "index" yang dapat menggunakan komparator kustom tetapi ini tidak didukung di Swift. Jadi solusi paling sederhana adalah dengan menggunakan kamus alih-alih array seperti yang ditunjukkan pada kode di bawah ini. Saya telah menyediakan getElements () yang akan memberi Anda kembali array protokol yang Anda inginkan. Jadi siapa pun yang menggunakan SomeClass tidak akan tahu bahwa kamus digunakan untuk implementasi.

Karena bagaimanapun, Anda akan memerlukan beberapa properti pembeda untuk memisahkan keberatan Anda, saya berasumsi itu adalah "nama". Harap pastikan bahwa do element.name = "foo" Anda saat membuat instance SomeProtocol baru. Jika namanya tidak disetel, Anda masih bisa membuat instance, tetapi tidak akan ditambahkan ke collection dan addElement () akan mengembalikan "false".

protocol SomeProtocol {
    var name:String? {get set} // Since elements need to distinguished, 
    //we will assume it is by name in this example.
    func bla()
}

class SomeClass {

    //var protocols = [SomeProtocol]() //find is not supported in 2.0, indexOf if
     // There is an Obj-C function index, that find element using custom comparator such as the one below, not available in Swift
    /*
    static func compareProtocols(one:SomeProtocol, toTheOther:SomeProtocol)->Bool {
        if (one.name == nil) {return false}
        if(toTheOther.name == nil) {return false}
        if(one.name ==  toTheOther.name!) {return true}
        return false
    }
   */

    //The best choice here is to use dictionary
    var protocols = [String:SomeProtocol]()


    func addElement(element: SomeProtocol) -> Bool {
        //self.protocols.append(element)
        if let index = element.name {
            protocols[index] = element
            return true
        }
        return false
    }

    func removeElement(element: SomeProtocol) {
        //if let index = find(self.protocols, element) { // find not suported in Swift 2.0


        if let index = element.name {
            protocols.removeValueForKey(index)
        }
    }

    func getElements() -> [SomeProtocol] {
        return Array(protocols.values)
    }
}
Jitendra Kulkarni
sumber
0

Saya menemukan solusi Swift yang tidak murni-murni di entri blog itu: http://blog.inferis.org/blog/2015/05/27/swift-an-array-of-protocols/

Triknya adalah menyesuaikan diri dengan NSObjectProtocolyang diperkenalkan isEqual(). Oleh karena itu, alih-alih menggunakan Equatableprotokol dan penggunaan defaultnya, ==Anda dapat menulis fungsi Anda sendiri untuk menemukan elemen dan menghapusnya.

Berikut adalah implementasi dari find(array, element) -> Int?fungsi Anda :

protocol SomeProtocol: NSObjectProtocol {

}

func find(protocols: [SomeProtocol], element: SomeProtocol) -> Int? {
    for (index, object) in protocols.enumerated() {
        if (object.isEqual(element)) {
            return index
        }
    }

    return nil
}

Catatan: Dalam hal ini objek Anda yang sesuai dengan SomeProtocolmust inherit from NSObject.

Kevin Delord
sumber