Я пытаюсь выучить и понять дженерики со связанными типами в swift.
Не существует такого понятия, как «дженерики со связанными типами в Swift». Есть дженерики, и есть протоколы со связанными типами (PAT). У них есть некоторые общие черты, но они представляют собой совершенно разные понятия, используемые для совершенно разных вещей.
Цель универсального шаблона - позволить типу варьироваться в зависимости от типов, выбранных вызывающей .
Цель PAT состоит в том, чтобы позволить типу использоваться существующими алгоритмами , используя типы , выбранные исполнителем . Учитывая это, Coffee
не имеет смысла в качестве протокола. Вы пытаетесь относиться к этому как к разнородному типу. Это не то, что PAT. PAT - это ловушка, позволяющая алгоритмам использовать типы.
class OrderManager<Item> { ... }
Это говорит о том, что OrderManager может содержать что угодно ;буквально что-нибудь вообще. Это не должно быть ценным. В вашем случае Item принудительно вводится в Any, что, безусловно, не то, что вы хотели (и почему Person работает, а не должен). Но не имеет особого смысла, что OrderManager привязан к какому-либо типу элементов. Вы действительно хотите один OrderManager для кофе и совершенно другой OrderManager для эспрессо? Это совсем не соответствует тому, что вы делаете. OrderManager должен работать над Order of что-нибудь, верно?
На самом деле невозможно определить, какие протоколы и генерики вам нужны здесь, потому что вы никогда ничего не делаете с OrderManager.orders
. Начните с кода вызова. Начните без дженериков или протоколов. Просто позвольте коду дублироваться, а затем распакуйте его в дженерики и протоколы. Если у вас нет четкого алгоритма (варианта использования), вам пока не следует создавать протокол.
См. Ответ Мэтта для начальной точки, но я уверен, что этого недостаточно для вашей проблемы,Скорее всего, вам понадобится больше вещей (например, название предмета). Начните с некоторых простых структур (Espresso
, BrewedCoffee
и т. Д.), А затем начните разрабатывать свой код вызова, и тогда у вас, вероятно, возникнут дополнительные вопросы, которые мы можем обсудить.
К вашемуВопрос о том, как атаковать такого рода проблемы, я бы начал так:
Сначала у нас есть несколько предметов для продажи. Я моделирую их наиболее очевидными способами:
// An Espresso has no distinguishing characteristics.
struct Espresso {}
// But other coffees have a size.
enum CoffeeSize: String {
case small, medium, large
}
// You must know the size in order to create a coffee. You don't need to know
// its price, or its name, or anything else. But you do have to know its size
// or you can't pour one. So "size" is a property of the type.
struct BrewedCoffee {
let size: CoffeeSize
}
struct Cappuccino {
let size: CoffeeSize
}
Готово!
Хорошо, не совсем сделано, но серьезно, вроде сделано. Теперь мы можем делать кофейные напитки. Пока вы не решите какую-то другую проблему, вы действительно готовы. Но у нас есть еще одна проблема:
Мы хотим построить Заказ, чтобы мы могли выставить клиенту счет. Заказ состоит из предметов. И предметы имеют названия и цены. Новые вещи могут быть добавлены в Орден, и я могу получить текстовое представление всего этого. Итак, мы сначала смоделируем то, что нам нужно:
struct Order {
private (set) var items: [Item]
mutating func add(_ item: Item) {
items.append(item)
}
var totalPrice: Decimal { items.map { $0.price }.reduce(0, +) }
var text: String { items.map { "\($0.name)\t\($0.price)" }.joined(separator: "\n") }
}
И чтобы реализовать это, нам нужен протокол, который предоставляет имя и цену:
protocol Item {
var name: String { get }
var price: Decimal { get }
}
Теперь мы бы хотели, чтобы эспрессо былопункт. Поэтому мы применяем ретроактивное моделирование, чтобы сделать его единым целым:
extension Espresso: Item {
var name: String { "Espresso" }
var price: Decimal { 3.00 }
}
И то же самое с BrewedCoffee:
extension BrewedCoffee {
var name: String { "\(size.rawValue.capitalized) Coffee" }
var price: Decimal {
switch size {
case .small: return 1.00
case .medium: return 2.00
case .large: return 3.00
}
}
}
И, конечно, капучино ... но вы знаете, когда я начинаюнапишите, что я действительно хочу вырезать и вставить BrewedCoffee. Это говорит о том, что, возможно, там скрывается протокол.
// Just a helper to make syntax prettier.
struct PriceMap {
var small: Decimal
var medium: Decimal
var large: Decimal
}
protocol SizedCoffeeItem: Item {
var size: CoffeeSize { get }
var baseName: String { get }
var priceMap: PriceMap { get }
}
С этим мы можем реализовать требования Item:
extension SizedCoffeeItem {
var name: String { "\(size.rawValue.capitalized) \(baseName)" }
var price: Decimal {
switch size {
case .small: return priceMap.small
case .medium: return priceMap.medium
case .large: return priceMap.large
}
}
}
И теперь соответствия не требуют дублирования кода.
extension BrewedCoffee: SizedCoffeeItem {
var baseName: String { "Coffee" }
var priceMap: PriceMap { PriceMap(small: 1.00, medium: 2.00, large: 3.00) }
}
extension Cappuccino: SizedCoffeeItem {
var baseName: String { "Cappuccino" }
var priceMap: PriceMap { PriceMap(small: 2.00, medium: 3.00, large: 4.00) }
}
Эти два примера имеют два разных использования протоколов. Первый - реализация гетерогенной коллекции ([Item]
). Эти типы протоколов не могут иметь связанные типы. Второе - облегчить совместное использование кода между типами. Эти виды могут. Но в обоих случаях я не добавлял никаких протоколов до тех пор, пока у меня не было четкого варианта использования: мне нужно было иметь возможность добавлять их в Order и возвращать определенные виды данных. Это привело нас к каждому шагу на этом пути.
В качестве отправной точки просто смоделируйте свои данные и разработайте свои алгоритмы. Затем адаптируйте свои данные к алгоритмам с протоколами. Протоколы приходят поздно, а не рано.