Heterogeneous generic container in Swift

2020-06-18 07:21发布

问题:

I have a problem to put structs with a generic type in one array. I know that Swift converts the meta type of an Array into a concrete type and that this is the conflict. I tried to find a different solution but I think I need your help.

Here I define the structs and protocols:

protocol ItemProtocol {
    var id: String { get }
}

struct Section<T: ItemProtocol> {
    var items: [T]
    var renderer: Renderer<T>
}

struct Renderer<T> {
    var title: (T) -> String
}

Here two example structs that implement the ItemProtocol:

struct Book: ItemProtocol {
    var id: String
    var title: String
}

struct Car: ItemProtocol {
    var id: String
    var brand: String
}

This is how i setup the sections:

let book1 = Book(id: "1", title: "Foo")
let book2 = Book(id: "2", title: "Bar")
let books = [book1, book2]
let bookSection = Section<Book>(items: books, renderer: Renderer<Book> { (book) -> String in
    return "Book title: \(book.title)"
})
let car1 = Car(id: "1", brand: "Foo")
let car2 = Car(id: "2", brand: "Bar")
let cars = [car1, car2]
let carSection = Section<Car>(items: cars, renderer: Renderer<Car> { (car) -> String in
    return "Car brand: \(car.brand)"
})

Now i want to put the sections together. Here is what i tried. But each of these 3 lines give me an error:

let sections: [Section<ItemProtocol>] = [bookSection, carSection]
let sections2: [Section] = [bookSection, carSection]
let sections3: [Section<AnyObject: ItemProtocol>] = [bookSection, carSection]

sections.forEach({ section in
    section.items.forEach({ item in
        let renderedTitle = section.renderer.title(item)
        print("\(renderedTitle)")
    })
})

For the declaration of the sections array i get this error:

Using 'ItemProtocol' as a concrete type conforming to protocol 'ItemProtocol' is not supported

For the declaration of the sections2 array this error:

Cannot convert value of type 'Section' to expected element type 'Section'

And sections3 throws this:

Expected '>' to complete generic argument list

回答1:

The problem is there is no common ground (excepting Any) between different Section types (with different generic arguments). One possible solution would be to unify all Section types into one protocol, and use that protocol to build the array:

protocol SectionProtocol {
    var genericItems: [ItemProtocol] { get }
    var renderedTitles: [String] { get }
}

extension Section: SectionProtocol {
    var genericItems: [ItemProtocol] { return items }
    var renderedTitles: [String] {
        return items.map { renderer.title($0) }
    }
}

let sections: [SectionProtocol] = [bookSection, carSection]

sections.forEach { section in
    section.renderedTitles.forEach { renderedTitle in
        print("\(renderedTitle)")
    }
}

So instead of iterating through the elements, you iterate through the rendered titles, which each section should be able to construct.

Now this addresses the basic use case from your question, however depending on you use the section in your app it might not be enough and you'll have to recourse to type erasers, as other answerers mentioned.



回答2:

The struct Section is generic so you cannot use it as a type. One solution could be to use a type erasure:

Create any ItemProtocol wrapper:

protocol ItemProtocol {
    var id: String { get }
}

struct AnyItem : ItemProtocol {

    private let item: ItemProtocol

    init(_ item: ItemProtocol) {
        self.item = item
    }

    // MARK: ItemProtocol
    var id: String { return item.id }
}

And a type erased Section, Any section:

protocol SectionProtocol {
    associatedtype T
    var items: [T] { get }
    var renderer: Renderer<T> { get }
}

struct Section<Item: ItemProtocol>: SectionProtocol {
    typealias T = Item
    var items: [Item]
    var renderer: Renderer<Item>

    var asAny: AnySection {
        return AnySection(self)
    }
}

struct AnySection : SectionProtocol {
    typealias T = AnyItem

    private let _items: () -> [T]
    private let _renderer: () -> Renderer<T>

    var items: [T] { return _items() }
    var renderer: Renderer<T> { return _renderer() }

    init<Section : SectionProtocol>(_ section: Section) {
        self._items = { section.items as! [AnySection.T] }
        self._renderer = { section.renderer as! Renderer<AnySection.T>}
    }
} 

Now you can have a collection of AnySections:

let sections: [AnySection] = [bookSection.asAny, carSection.asAny]


回答3:

To implement it with protocols you will need to use type erasure or casting to Any, and it's pretty complex or doesn't type safe. You can take another route and implement it with algebraic enums, for example like this:

protocol ItemProtocol: CustomStringConvertible {
    var id: String { get }
}

enum ItemType {
    case book(title: String)
    case car(brand: String)
}

struct Item: ItemProtocol {
    let id: String
    let type: ItemType

    var description: String {
        switch self.type {
        case .car(let brand):
            return "Car brand: \(brand)"
        case .book(let title):
            return "Book title: \(title)"
        }
    }
}

let book1 = Item(id: "1", type: .book(title: "Title1"))
let book2 = Item(id: "2", type: .book(title: "Title2"))

let car1 = Item(id: "1", type: .car(brand: "Brand1"))
let car2 = Item(id: "2", type: .car(brand: "Brand2"))


struct Section {
    let items: [Item]
}

let section1 = Section(items: [book1, book2])
let section2 = Section(items: [car1, car2])

let sections = [section1, section2]