简体   繁体   中英

Extending a Collection where the Element type is generic (Swift)

It's simple to extend a Collection type in swift to have a custom function for particular Element types:

struct MyStruct {
    let string: String
}

extension Set where Element == MyStruct {
    func getFirstWith(string: String) -> Element? {
        return filter({ $0.string == string }).first
    }
}

But suppose your Element type is generic?

protocol MyProtocol {}

struct MyGenericStruct<T: MyProtocol> {
    let string: String
}

// error on next line:
// error: expected '>' to complete generic argument list
// extension Set where Element == MyGenericStruct<T: MyProtocol> {
//                                                 ^
extension Set where Element == MyGenericStruct<T: MyProtocol> {
    func getFirstWith(string: String) -> Element? {
        return filter({ $0.string == string }).first
    }
}

// error on next line:
// error: use of undeclared type 'T' 
// extension Set where Element == MyGenericStruct<T> {
//                                                ^
extension Set where Element == MyGenericStruct<T> {
    func getFirstWith(string: String) -> Element? {
        return filter({ $0.string == string }).first
    }
}

It's unclear to me how to declare that my element is a generic.

If I do understand the purpose, you probably need to have more implementation in your protocol and structure .

First, element's protocol has to be hashable and equitable.

protocol MyProtocol: Hashable, Equatable {
    var string: String { get }
}

Therefore, MyGenericStruct will look like the following.

struct MyGenericStruct: MyProtocol {
    let string: String
    var hashValue: Int {
        get {
            return string.hashValue
        }
    }

    static func ==(lhs: MyGenericStruct, rhs: MyGenericStruct) -> Bool {
        return lhs.hashValue == rhs.hashValue
    }
}

Then, declare extension with constraints specified by a where clause for MyProtocol

extension Set where Element: MyProtocol {
    func getFirstWith(string: String) -> Element? {
        return filter({$0.string == string}).first
    }
}

Finally, let's do a test and see its result.

// Example
let set: Set = [
    MyGenericStruct(string: "Watermelon"),
    MyGenericStruct(string: "Orange"),
    MyGenericStruct(string: "Banana")
]
let output = set.getFirstWith(string: "Orange")
print(output)

In my playground with Xcode 8, I can get Optional(MyGenericStruct(string: "Orange")) in log.

[UPDATED1] To make an Extension on Set<MyGenericStruct> only:

extension Set where Element == MyGenericStruct {
    func getFirstWith(string: String) -> Element? {
        return filter({$0.string == string}).first
    }
}

[UPDATED2] In case to keep MyGenericStruct<T: MyProtocol> declaration as stated is necessary, another approach to implement Set extension:


protocol MyProtocol {

}

struct BaseStruct1: MyProtocol {

}

struct BaseStruct2: MyProtocol {

}

protocol ContainStringProtocol: Hashable {
    var string: String { get }
}

struct MyGenericStruct<T: MyProtocol>: ContainStringProtocol {
    var string: String
    var hashValue: Int {
        get {
            return string.hashValue
        }
    }

    init(string: String) {
        self.string = string
    }

    static func ==(lhs: MyGenericStruct, rhs: MyGenericStruct) -> Bool {
        return lhs.hashValue == rhs.hashValue
    }
}

extension Set where Element: ContainStringProtocol {
    func getFirstWith(string: String) -> Element? {
        return filter({$0.string == string}).first
    }
}

// Example
let set1: Set = [
    MyGenericStruct<BaseStruct1>(string: "Watermelon"),
    MyGenericStruct<BaseStruct1>(string: "Orange"),
    MyGenericStruct<BaseStruct1>(string: "Banana")
]
let output1 = set1.getFirstWith(string: "Orange")
print(output1!)

let set2: Set = [
    MyGenericStruct<BaseStruct2>(string: "Watermelon"),
    MyGenericStruct<BaseStruct2>(string: "Orange"),
    MyGenericStruct<BaseStruct2>(string: "Banana")
]
let output2 = set2.getFirstWith(string: "Orange")
print(output2!)

Details

Swift 3.1, xCode 8.3.3

Solution

import Foundation

struct MyStruct:Hashable {

    let string: String

    static func == (lhs: MyStruct, rhs: MyStruct) -> Bool {
        return lhs.string == rhs.string
    }

    var hashValue: Int {
        return string.hash
    }
}

extension Set where Element == MyStruct {
    func getFirstWith(string: String) -> Element? {
        return filter({ $0.string == string }).first
    }
}

var set = Set<MyStruct>()
set.insert(MyStruct(string: "123"))
set.insert(MyStruct(string: "231"))
print(String(describing:set.getFirstWith(string: "123")))
print(String(describing:set.getFirstWith(string: "231")))

///////////////////////////////////

protocol MyProtocol {}

struct MyType1: MyProtocol {}
struct MyType2: MyProtocol {}

struct MyGenericStruct<T: MyProtocol>: Hashable {
    let string: String

    static func == (lhs: MyGenericStruct, rhs: MyGenericStruct) -> Bool {
        return lhs.string == rhs.string
    }

    var hashValue: Int {
        return string.hash
    }
}

extension Set where Element == AnyHashable {
    func getFirstWith(string: String) -> Element? {


        return filter{ element -> Bool in
            if let _element = element as? MyGenericStruct<MyType1> {
                if  _element.string == string {
                    return true
                }
            }
            if let _element = element as? MyGenericStruct<MyType2> {
                if  _element.string == string {
                    return true
                }
            }
            return false
        }.first

    }
}

var set2 = Set<AnyHashable>()
set2.insert(MyGenericStruct<MyType1>(string: "abc"))
set2.insert(MyGenericStruct<MyType2>(string: "cba"))
print(String(describing: set2.getFirstWith(string: "abc")))
print(String(describing:set2.getFirstWith(string: "cba")))

Result

在此输入图像描述

Answering my own question here on request.

I was unable to figure out how to make this an extension on Set .

Instead, we wrote a little utility function. Not as clear as a extension, but gets the work done. It looks like this:

func getFirst<T: MyProtocol>(fromSet set: Set<MyGenericStruct<T>>, whereStringIs string: String) -> MyGenericStruct<T>? {
    return set.first(where: { $0.string == string })
}

As others have noted, MyGenericStruct needs to be Hashable.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM