Гетерогенный универсальный контейнер в Swift

У меня проблема с помещением структур с универсальным типом в один массив. Я знаю, что Swift преобразует метатип массива в конкретный тип, и в этом заключается конфликт. Я пытался найти другое решение, но думаю, мне нужна ваша помощь.

Здесь я определяю структуры и протоколы:

protocol ItemProtocol {
    var id: String { get }
}

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

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

Вот два примера структур, реализующих ItemProtocol:

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

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

Вот как я настраиваю разделы:

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)"
})

Теперь я хочу соединить разделы. Вот что я пробовал. Но каждая из этих трех строк выдает ошибку:

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)")
    })
})

Для объявления массива sections я получаю эту ошибку:

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

Для объявления массива sections2 эта ошибка:

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

И sections3 подбрасывает это:

Expected '>' to complete generic argument list

Структурированный массив Numpy
Структурированный массив Numpy
Однако в реальных проектах я чаще всего имею дело со списками, состоящими из нескольких типов данных. Как мы можем использовать массивы numpy, чтобы...
T - 1Bits: Генерация последовательного массива
T - 1Bits: Генерация последовательного массива
По мере того, как мы пишем все больше кода, мы привыкаем к определенным способам действий. То тут, то там мы находим код, который заставляет нас...
Что такое деструктуризация массива в JavaScript?
Что такое деструктуризация массива в JavaScript?
Деструктуризация позволяет распаковывать значения из массивов и добавлять их в отдельные переменные.
6
0
1 326
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Чтобы реализовать это с протоколами, вам нужно будет использовать стирание типа или приведение к Any, а это довольно сложно или небезопасно по типу. Вы можете выбрать другой путь и реализовать его с помощью алгебраических перечислений, например так:

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]

Структура Section является общей, поэтому ее нельзя использовать как тип. Одним из решений может быть использование стирания типа:

Создайте любую оболочку ItemProtocol:

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

И типа стертый Раздел, Любой раздел:

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

Теперь у вас может быть коллекция AnySection:

let sections: [AnySection] = [bookSection.asAny, carSection.asAny]
Ответ принят как подходящий

Проблема в том, что нет общего (за исключением Any) между разными типами Section (с разными универсальными аргументами). Одним из возможных решений было бы объединить все типы Section в один протокол и использовать этот протокол для построения массива:

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)")
    }
}

Таким образом, вместо того, чтобы перебирать элементы, вы перебираете визуализированные заголовки, которые каждый раздел должен иметь возможность создавать.

Теперь это касается основного варианта использования из вашего вопроса, однако, в зависимости от того, какой раздел вы используете в своем приложении, этого может быть недостаточно, и вам придется прибегать к типу ластиков, как упоминали другие ответчики.

Как renderer определяется в вашем примере?

Lukas Würzburger 13.03.2018 13:26

@lukwuerz Это то же свойство рендерера, что и в вопросе.

Cristik 13.03.2018 14:00

Другие вопросы по теме