1
votes

Comment obtenir une classe avec un type générique accepter un tableau de différents par les mêmes types génériques?

Je suis en train d'essayer d'apprendre et de comprendre les protocoles avec les types associés dans Swift.

En même temps, j'apprends SwiftUI et je suis un cours sur Udemy.

L'application que nous allons construire est une application de commande de café.

Cela étant dit, je ne suis pas le tutoriel du "T" car j'ai essayé de structurer l'application différemment, donc je peux apprendre à penser par moi-même. L'application n'a rien d'extraordinaire.

Le didacticiel n'utilise pas de génériques et de protocoles pour représenter ou structurer les données. C'est juste un tutoriel pour présenter SwiftUI.

J'ai créé un protocole appelé Coffee qui a un type associé CupSize.

Chaque café Cappuccino, Espresso et Brewed Coffee confirme le protocole Coffee .

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

Ensuite, j'ai créé une structure Order et une classe OrderManager.

La structure Order a un générique et doit être un article Priceable. L'idée d'un article générique à un prix abordable est de prendre en charge d'autres articles à l'avenir au cas où je voudrais étendre l'application ... pas seulement du café.

L'idée derrière OrderManager est de suivre toutes les commandes et de gérer les opérations CRUD des commandes (il faut encore implémenter la suppression, la lecture et la mise à jour).

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.
])

Mon problème est l'utilisation de OrderManager.

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


0 commentaires

3 Réponses :


1
votes

Vous n'avez pas besoin de définir le type générique sur la classe, vous devez le mettre sur une méthode comme la suivante:

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)

et pour envoyer à la classe vous venez d'envoyer votre modèle par exemple

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)

Voici un exemple avec deux objets génériques

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

Aussi si vous voulez l'utiliser sur une classe, vous pouvez le faire comme suit:

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


2 commentaires

Merci beaucoup pour votre réponse! Même si j'ai accepté et que je vais utiliser la réponse de Matt, je vais utiliser votre code pour expérimenter un peu.


@ FNMT8L9IN82 OK, votons pour si cela vous est utile



3
votes

C'est bien que vous souhaitiez expérimenter des génériques, mais ce n'est pas l'occasion. Vous dites:

L'idée ... est de soutenir d'autres articles à l'avenir au cas où je voudrais développer l'application ... pas seulement du café.

Mais vous n'avez pas besoin d'un générique pour cela. La seule exigence pour gérer une commande est que l'article de la commande ait un prix. Priceable est déjà un type; vous n'avez pas besoin d'ajouter un type générique au mix.

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


1 commentaires

Décidément, je me sens idiot! Je ne peux pas croire que j'étais tellement accro aux génériques que j'ai totalement oublié d'utiliser Priceable comme produit. Merci beaucoup pour la réponse.



2
votes

Je suis en train d'essayer d'apprendre et de comprendre les génériques avec les types associés dans Swift.

Il n'existe pas de "génériques avec des types associés dans Swift". Il existe des génériques et des protocoles avec des types associés (PAT). Ils ont certaines choses en commun, mais sont des concepts profondément différents utilisés pour des choses très différentes.

Le but d'un générique est de permettre au type de varier selon les types choisis par l'appelant .

Le but d'un PAT est de permettre à un type d'être utilisé par des algorithmes existants, en utilisant des types choisis par l'implémenteur . Compte tenu de cela, Coffee n'a pas de sens en tant que protocole. Vous essayez de le traiter comme un type hétérogène. Ce n'est pas ce qu'est un PAT. Un PAT est un hook pour permettre aux types d'être utilisés par des algorithmes.

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

Ceci dit que OrderManager peut contenir n'importe quoi ; littéralement rien du tout. Il n'est pas nécessaire que ce soit Pricable. Dans votre cas, Item est contraint à Any, ce qui n'est certainement pas ce que vous vouliez (et pourquoi Person travaille alors qu'il ne devrait pas). Mais cela n'a pas beaucoup de sens que OrderManager soit lié à un type d'élément. Voulez-vous vraiment un OrderManager pour le café et un OrderManager complètement différent pour l'espresso? Cela ne correspond pas du tout à ce que vous faites. OrderManager devrait fonctionner sur un ordre de n'importe quoi, non?

Il n'est pas vraiment possible de déterminer de quels protocoles et génériques vous avez besoin ici car vous ne faites jamais rien avec OrderManager.orders . Commencez par le code d'appel. Commencez sans génériques ni protocoles. Laissez simplement le code se dupliquer, puis extrayez cette duplication en génériques et protocoles. Si vous n'avez pas d'algorithme clair (cas d'utilisation) en tête, vous ne devriez pas encore créer de protocole.

Voir la réponse de matt pour un point de départ, mais je suis sûr que ce n'est pas suffisant pour votre problème. Vous aurez probablement besoin de plus de choses (probablement le nom de l'élément par exemple). Commencez par quelques structures simples ( Espresso , BrewedCoffee , etc.), puis commencez à élaborer votre code d'appel, et vous aurez probablement d'autres questions dont nous pourrons discuter.


Pour répondre à votre question de savoir comment attaquer ce genre de problème, je commencerais comme ceci.

Premièrement, nous avons quelques articles à vendre. Je les modélise de leurs manières les plus évidentes:

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

Terminé!

OK, pas vraiment fait, mais sérieusement, en quelque sorte fait. Nous pouvons maintenant faire des boissons au café. Jusqu'à ce que vous ayez un autre problème à résoudre, vous avez vraiment terminé. Mais nous avons un autre problème:

Nous voulons construire une commande, afin que nous puissions donner une facture au client. Une commande est composée d'articles. Et les articles ont des noms et des prix. De nouvelles choses peuvent être ajoutées à une commande et je peux obtenir une représentation textuelle de l'ensemble. Nous modélisons donc d'abord ce dont nous avons besoin:

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

Et pour implémenter cela, nous avons besoin d'un protocole qui fournit le nom et le prix:

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

Maintenant, nous aimerions qu'un espresso soit un objet. Nous appliquons donc une modélisation rétroactive pour en faire un:

extension Espresso: Item {
    var name: String { "Espresso" }
    var price: Decimal { 3.00 }
}

Et la même chose avec BrewedCoffee:

protocol Item {
    var name: String { get }
    var price: Decimal { get }
}

Et bien sûr Cappuccino ... mais vous savez, au moment où je commence à écrire, je veux vraiment couper-coller BrewedCoffee. Cela suggère peut-être qu'il y a un protocole caché là-dedans.

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

Avec cela, nous pouvons implémenter les exigences de Item:

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

Et maintenant les conformités ne nécessitent aucune duplication de code.

class OrderManager<Item> { ... }

Ces deux exemples sont de deux utilisations différentes des protocoles. La première consiste à implémenter une collection hétérogène ( [Item] ). Ces types de protocoles ne peuvent pas avoir de types associés. Le second est de faciliter le partage de code entre les types. Ces types peuvent. Mais dans les deux cas, je n'ai ajouté aucun protocole avant d'avoir un cas d'utilisation clair: je devais pouvoir les ajouter à Order et récupérer certains types de données. Cela nous a conduit à chaque étape du processus.

Pour commencer, modélisez simplement vos données et concevez vos algorithmes. Puis adaptez vos données aux algorithmes avec des protocoles. Les protocoles arrivent tard, pas tôt.


6 commentaires

Tu as tout à fait raison. Ce que je voulais dire, c'est PAT (protocoles avec des types associés) et non des génériques avec des types associés. Je ne sais pas pourquoi j'ai dit des génériques avec des types associés en premier lieu :). "Le but d'un générique est de permettre au type de varier selon les types choisis par l'appelant." - Je suppose que la bibliothèque est un bon exemple? En fait, j'allais utiliser Coffee comme classe parent et les autres comme sous-classes. Cependant, je continue à lire des trucs comme commencer par une structure et utiliser la composition plutôt que l'héritage. J'ai essayé de me forcer à apprendre et à utiliser POP au lieu de POO. Merci pour la réponse.


Mettez à jour le message pour indiquer les protocoles avec les types associés.


"Je suppose que la bibliothèque est un bon exemple." Non; être dans une bibliothèque ou pas n'a rien à voir avec cela. Il s'agit de savoir si les types sont choisis par l'appelant (génériques) ou par l'implémentation (PAT). La simple conversion de l'héritage basé sur les classes en protocoles conduit à de très mauvais protocoles. Les PAT ne concernent pas l'héritage; ils ne remplacent pas les classes. Ils permettent de résoudre un autre type de problème de réutilisation de code (l'application d'algorithmes génériques aux types). Si vous n'avez pas d'algorithme qui doit fonctionner sur plusieurs types non liés, vous ne voulez pas de PAT.


Merci pour la réponse. Cela peut être tiré par les cheveux, mais comment structureriez-vous le code dans ce cas ci-dessus? Quelques exemples seraient très utiles pour apprendre.


LOL ... je ne vous ai pas vu modifier votre contenu. Merci de prendre le temps de montrer quelques exemples. Je vais les étudier et les examiner. Merci beaucoup!


Je n'aimais pas le fait que je duplique la taille et que j'allais le résoudre plus tard, mais il semble que tu m'as battu :)