Как заставить класс с универсальным типом принять массив различных одинаковых универсальных типов? - PullRequest
1 голос
/ 26 октября 2019

Я пытаюсь изучить и понять протоколы со связанными типами в swift.

В то же время я изучаю SwiftUI и прохожу курс обучения по Udemy.

Приложение, которое мы создадим, является приложением заказа кофе.

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

В учебном пособии не используются обобщения и протоколы для представления или структурирования данных. Это просто учебное пособие для демонстрации SwiftUI.

Я создал протокол под названием Coffee, связанный с типом CupSize.

Каждый кофе Cappuccino, Espresso и Brewed Coffee подтверждает протокол Coffee.

protocol Priceable {
  var cost: Double { get }
}

protocol Coffee {
  associatedtype CupSize
  var cupSize: CupSize { get }
  init(cupSize: CupSize)
}

enum EspressoCupSize {
  case small
}

struct Espresso: Coffee, Priceable {
  var cupSize = EspressoCupSize.small
  var cost: Double { return 3.00 }
}

enum BrewedCoffeeCupSize {
  case small
  case medium
  case large
}

struct BrewedCoffee: Coffee, Priceable {
  var cupSize: BrewedCoffeeCupSize
  var cost: Double {
    switch self.cupSize {
      case .small: return 1.00
      case .medium: return 2.00
      case .large: return 3.00
    }
  }
}

enum CappuccinoCupSize {
  case small
  case medium
  case large
}

struct Cappuccino: Coffee, Priceable {
  var cupSize: CappuccinoCupSize
  var cost: Double {
    switch self.cupSize {
      case .small: return 2.00
      case .medium: return 3.00
      case .large: return 4.00
    }
  }
}

Затем я создал структуру Order и класс OrderManager.

Структура заказа имеет универсальный характер и должна быть элементом Priceable. Идея универсального ценового элемента заключается в поддержке других элементов в будущем на случай, если я захочу расширить приложение, а не только кофе.

Идея OrderManager - отслеживать все заказы и управлять ими. Операции CRUD для заказов (все еще необходимо реализовать удаление, чтение и обновление).

struct Order<Item: Priceable> {
  var name: String
  var item: Item
}

class OrderManager<Item> {
  private var orders: [Item] 

  init(orders: [Item]) {
    self.orders = orders
  }

  func add(_ order: Item) {
    self.orders.append(order)
  } 
}

Моя проблема связана с использованием OrderManager.

let maryOrder = Order(name: "Mary", item: Espresso())
let sueOrder = Order(name: "Sue", item: BrewedCoffee(cupSize: .medium))

// Dummy Structure
struct Person {}

let orderManager = OrderManager(orders: [
  maryOrder,
  sueOrder,
  Person() // This works!!! Which is not what I want.
])

Я хочу, чтобы универсальный тип для OrderManagerбыть Орденом, но поскольку у Ордена есть собственный родовой тип Priceable, я не могу найти правильный ответ или правильный синтаксис.

Вещи, которые я пытался заставить OrderManager работать

class OrderManager<Order> {} // Does not work because Order needs a generic type
class OrderManager<Order<Priceable>> // Still does not work
class OrderManager<Item: Priceable, Order<Item> {} // Still does not work.
// and I tried other solutions, but I cannot get this to work
// Also, when I think I got the right syntax, I cannot add Mary and Sue's orders to
// OrderManager because Mary's item is Espresso and Sue's item is BrewedCoffee

Как я могу заставить OrderManager принимать только массив заказов?

Ответы [ 3 ]

3 голосов
/ 26 октября 2019

Хорошо, что вы хотите поэкспериментировать с дженериками, но это не повод для этого. Вы говорите:

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

Новам не нужен универсальный для этого. Единственным требованием для управления заказом является то, чтобы его позиция была доступной по цене. Цена уже тип;вам не нужно добавлять универсальный тип к миксу.

struct Order {
  var name: String
  var item: Priceable
}

class OrderManager {
    private var orders: [Order]

  init(orders: [Order]) {
    self.orders = orders
  }

  func add(_ order: Order) {
    self.orders.append(order)
  }
}
2 голосов
/ 26 октября 2019

Я пытаюсь выучить и понять дженерики со связанными типами в 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 и возвращать определенные виды данных. Это привело нас к каждому шагу на этом пути.

В качестве отправной точки просто смоделируйте свои данные и разработайте свои алгоритмы. Затем адаптируйте свои данные к алгоритмам с протоколами. Протоколы приходят поздно, а не рано.

1 голос
/ 26 октября 2019

Вам не нужно определять тип Generic в классе, вы должны поместить его в метод, подобный следующему:

class OrderManager {
   func doOrder<T: Priceable, L: Protocol2 >(obj: T) -> L {
     // Use obj as a Priceable model
     // return a Protocol2 model
   }
}

, а для отправки в класс вы просто отправляете свою модель, например

varProtocol2 = OrderManager.doOrder(obj: maryOrder.item)

Вот пример с двумя универсальными объектами

protocol prot1 {
    var a: Int {get set}
}

protocol protRet {
    var b: String {get set}
    init()
}

struct prot1Struct: prot1 {
    var a: Int
}

struct prot2Struct: protRet {
    init() {
        b = ""
    }
    var b: String
}

class Manage {
    func Do<T: prot1, L: protRet>(obj: T) -> L {
        var ret: L = L()
        ret.b = "\(obj.a)"
        return ret
    }
}


var obj1: prot2Struct?
var paramItem = prot1Struct(a: 10)
obj1 = Manage().Do(obj: paramItem)

Также, если вы хотите использовать его в классе, вы можете сделать это следующим образом:

class manageb<T: prot1, L: protRet> {
    func Do(obj: T) -> L {
        var ret: L = L()
        ret.b = "\(obj.a)"
        return ret
    }
}

var obj1: prot2Struct?
var paramItem = prot1Struct(a: 10)

let classB = manageb<prot1Struct, prot2Struct>()
obj1 = classB.Do(obj: paramItem)
...