Voir sur TensorFlow.org | Exécuter dans Google Colab | Afficher la source sur GitHub |
Ce didacticiel passera en revue la programmation orientée protocole et différents exemples de la façon dont ils peuvent être utilisés avec des génériques dans des exemples quotidiens.
Protocoles
L'héritage est un moyen puissant d'organiser le code dans des langages de programmation qui vous permet de partager du code entre plusieurs composants du programme.
Dans Swift, il existe différentes manières d'exprimer l'héritage. Vous connaissez peut-être déjà l'une de ces méthodes, issue d'autres langages : l'héritage de classe. Cependant, Swift a un autre moyen : les protocoles.
Dans ce didacticiel, nous explorerons les protocoles, une alternative au sous-classement qui vous permet d'atteindre des objectifs similaires grâce à différents compromis. Dans Swift, les protocoles contiennent plusieurs membres abstraits. Les classes, les structures et les énumérations peuvent être conformes à plusieurs protocoles et la relation de conformité peut être établie rétroactivement. Tout cela permet certaines conceptions qui ne sont pas facilement exprimables dans Swift à l'aide du sous-classement. Nous passerons en revue les idiomes qui prennent en charge l'utilisation des protocoles (extensions et contraintes de protocole), ainsi que les limitations des protocoles.
Les types de valeur de Swift 💖 !
En plus des classes qui ont une sémantique de référence, Swift prend en charge les énumérations et les structures transmises par valeur. Les énumérations et les structures prennent en charge de nombreuses fonctionnalités fournies par les classes. Nous allons jeter un coup d'oeil!
Tout d’abord, regardons en quoi les énumérations sont similaires aux classes :
enum Color: String {
case red = "red"
case green = "green"
case blue = "blue"
// A computed property. Note that enums cannot contain stored properties.
var hint: String {
switch self {
case .red:
return "Roses are this color."
case .green:
return "Grass is this color."
case .blue:
return "The ocean is this color."
}
}
// An initializer like for classes.
init?(color: String) {
switch color {
case "red":
self = .red
case "green":
self = .green
case "blue":
self = .blue
default:
return nil
}
}
}
// Can extend the enum as well!
extension Color {
// A function.
func hintFunc() -> String {
return self.hint
}
}
let c = Color.red
print("Give me a hint for c: \(c.hintFunc())")
let invalidColor = Color(color: "orange")
print("is invalidColor nil: \(invalidColor == nil)")
Give me a hint for c: Roses are this color. is invalidColor nil: true
Maintenant, regardons les structures. Notez que nous ne pouvons pas hériter de structures, mais que nous pouvons utiliser des protocoles :
struct FastCar {
// Can have variables and constants as stored properties.
var color: Color
let horsePower: Int
// Can have computed properties.
var watts: Float {
return Float(horsePower) * 745.7
}
// Can have lazy variables like in classes!
lazy var titleCaseColorString: String = {
let colorString = color.rawValue
return colorString.prefix(1).uppercased() +
colorString.lowercased().dropFirst()
}()
// A function.
func description() -> String {
return "This is a \(color) car with \(horsePower) horse power!"
}
// Can create a variety of initializers.
init(color: Color, horsePower: Int) {
self.color = color
self.horsePower = horsePower
}
// Can define extra initializers other than the default one.
init?(color: String, horsePower: Int) {
guard let enumColor = Color(color: color) else {
return nil
}
self.color = enumColor
self.horsePower = horsePower
}
}
var car = FastCar(color: .red, horsePower: 250)
print(car.description())
print("Horse power in watts: \(car.watts)")
print(car.titleCaseColorString)
This is a red car with 250 horse power! Horse power in watts: 186425.0 Red
Enfin, voyons comment ils sont transmis par types valeur contrairement aux classes :
// Notice we have no problem modifying a constant class with
// variable properties.
class A {
var a = "a"
}
func foo(_ a: A) {
a.a = "foo"
}
let a = A()
print(a.a)
foo(a)
print(a.a)
/*
Uncomment the following code to see how an error is thrown.
Structs are implicitly passed by value, so we cannot modify it.
> "error: cannot assign to property: 'car' is a 'let' constant"
*/
// func modify(car: FastCar, toColor color: Color) -> Void {
// car.color = color
// }
// car = FastCar(color: .red, horsePower: 250)
// print(car.description())
// modify(car: &car, toColor: .blue)
// print(car.description())
a foo
Utilisons des protocoles
Commençons par créer des protocoles pour différentes voitures :
protocol Car {
var color: Color { get set }
var price: Int { get }
func turnOn()
mutating func drive()
}
protocol Electric {
mutating func recharge()
// percentage of the battery level, 0-100%.
var batteryLevel: Int { get set }
}
protocol Gas {
mutating func refill()
// # of liters the car is holding, varies b/w models.
var gasLevelLiters: Int { get set }
}
Dans un monde orienté objet (sans héritage multiple), vous avez peut-être créé des classes abstraites Electric
et Gas
, puis utilisé l'héritage de classe pour que les deux héritent de Car
, puis avoir un modèle de voiture spécifique comme classe de base. Cependant, ici, les deux sont des protocoles complètement distincts avec un couplage nul ! Cela rend l’ensemble du système plus flexible dans la façon dont vous le concevez.
Définissons une Tesla :
struct TeslaModelS: Car, Electric {
var color: Color // Needs to be a var since `Car` has a getter and setter.
let price: Int
var batteryLevel: Int
func turnOn() {
print("Starting all systems!")
}
mutating func drive() {
print("Self driving engaged!")
batteryLevel -= 8
}
mutating func recharge() {
print("Recharging the battery...")
batteryLevel = 100
}
}
var tesla = TeslaModelS(color: .red, price: 110000, batteryLevel: 100)
Ceci spécifie une nouvelle structure TeslaModelS
conforme aux deux protocoles Car
et Electric
.
Définissons maintenant une voiture à essence :
struct Mustang: Car, Gas{
var color: Color
let price: Int
var gasLevelLiters: Int
func turnOn() {
print("Starting all systems!")
}
mutating func drive() {
print("Time to drive!")
gasLevelLiters -= 1
}
mutating func refill() {
print("Filling the tank...")
gasLevelLiters = 25
}
}
var mustang = Mustang(color: .red, price: 30000, gasLevelLiters: 25)
Étendre les protocoles avec des comportements par défaut
Ce que vous pouvez remarquer à partir des exemples, c'est que nous avons une certaine redondance. Chaque fois que nous rechargeons une voiture électrique, nous devons régler le niveau de pourcentage de batterie à 100. Étant donné que toutes les voitures électriques ont une capacité maximale de 100 %, mais que les voitures à essence varient selon la capacité du réservoir d'essence, nous pouvons par défaut le niveau à 100 pour les voitures électriques. .
C’est là que les extensions de Swift peuvent s’avérer utiles :
extension Electric {
mutating func recharge() {
print("Recharging the battery...")
batteryLevel = 100
}
}
Alors maintenant, toute nouvelle voiture électrique que nous créons réglera la batterie à 100 lorsque nous la rechargerons. Ainsi, nous venons de pouvoir décorer les classes, les structures et les énumérations avec un comportement unique et par défaut.
Merci à Ray Wenderlich pour la bande dessinée !
Cependant, une chose à surveiller est la suivante. Dans notre première implémentation, nous définissons foo()
comme implémentation par défaut sur A
, mais nous ne la rendons pas obligatoire dans le protocole. Ainsi, lorsque nous appelons a.foo()
, nous obtenons " A default
" imprimé.
protocol Default {}
extension Default {
func foo() { print("A default")}
}
struct DefaultStruct: Default {
func foo() {
print("Inst")
}
}
let a: Default = DefaultStruct()
a.foo()
A default
Cependant, si nous rendons foo()
obligatoire sur A
, nous obtenons " Inst
" :
protocol Default {
func foo()
}
extension Default {
func foo() {
print("A default")
}
}
struct DefaultStruct: Default {
func foo() {
print("Inst")
}
}
let a: Default = DefaultStruct()
a.foo()
Inst
Cela se produit en raison d'une différence entre la répartition statique dans le premier exemple et la répartition statique dans le second sur les protocoles dans Swift. Pour plus d'informations, reportez-vous à cet article Medium .
Remplacement du comportement par défaut
Cependant, si nous le souhaitons, nous pouvons toujours remplacer le comportement par défaut. Une chose importante à noter est que cela ne prend pas en charge la répartition dynamique .
Disons que nous avons une ancienne version d'une voiture électrique, donc la santé de la batterie a été réduite à 90 % :
struct OldElectric: Car, Electric {
var color: Color // Needs to be a var since `Car` has a getter and setter.
let price: Int
var batteryLevel: Int
func turnOn() {
print("Starting all systems!")
}
mutating func drive() {
print("Self driving engaged!")
batteryLevel -= 8
}
mutating func reCharge() {
print("Recharging the battery...")
batteryLevel = 90
}
}
Utilisations des protocoles par la bibliothèque standard
Maintenant que nous avons une idée du fonctionnement des protocoles dans Swift, passons en revue quelques exemples typiques d'utilisation des protocoles de bibliothèque standard.
Étendre la bibliothèque standard
Voyons comment nous pouvons ajouter des fonctionnalités supplémentaires aux types qui existent déjà dans Swift. Étant donné que les types dans Swift ne sont pas intégrés, mais font partie de la bibliothèque standard en tant que structures, cela est facile à faire.
Essayons de faire une recherche binaire sur un tableau d'éléments, tout en veillant également à vérifier que le tableau est trié :
extension Collection where Element: Comparable {
// Verify that a `Collection` is sorted.
func isSorted(_ order: (Element, Element) -> Bool) -> Bool {
var i = index(startIndex, offsetBy: 1)
while i < endIndex {
// The longer way of calling a binary function like `<(_:_:)`,
// `<=(_:_:)`, `==(_:_:)`, etc.
guard order(self[index(i, offsetBy: -1)], self[i]) else {
return false
}
i = index(after: i)
}
return true
}
// Perform binary search on a `Collection`, verifying it is sorted.
func binarySearch(_ element: Element) -> Index? {
guard self.isSorted(<=) else {
return nil
}
var low = startIndex
var high = endIndex
while low <= high {
let mid = index(low, offsetBy: distance(from: low, to: high)/2)
if self[mid] == element {
return mid
} else if self[mid] < element {
low = index(after: mid)
} else {
high = index(mid, offsetBy: -1)
}
}
return nil
}
}
print([2, 2, 5, 7, 11, 13, 17].binarySearch(5)!)
print(["a", "b", "c", "d"].binarySearch("b")!)
print([1.1, 2.2, 3.3, 4.4, 5.5].binarySearch(3.3)!)
2 1 2
Nous faisons cela en étendant le protocole Collection
qui définit « une séquence dont les éléments peuvent être parcourus plusieurs fois, de manière non destructive, et accessibles par un indice indexé ». Étant donné que les tableaux peuvent être indexés en utilisant la notation entre crochets, c'est le protocole que nous souhaitons étendre.
De même, nous souhaitons ajouter cette fonction utilitaire uniquement aux tableaux dont les éléments peuvent être comparés. C'est la raison pour laquelle nous avons where Element: Comparable
.
La clause where
fait partie du système de types de Swift, que nous aborderons bientôt, mais nous permet en bref d'ajouter des exigences supplémentaires à l'extension que nous écrivons, comme exiger que le type implémente un protocole, exiger que deux types soient les même, ou pour exiger qu'une classe ait une superclasse particulière.
Element
est le type associé des éléments dans un type conforme à Collection
. Element
est défini dans le protocole Sequence
, mais comme Collection
hérite de Sequence
, il hérite du type associé à Element
.
Comparable
est un protocole qui définit « un type qui peut être comparé à l'aide des opérateurs relationnels <
, <=
, >=
et >
. . Puisque nous effectuons une recherche binaire sur une Collection
triée, cela doit bien sûr être vrai, sinon nous ne savons pas s'il faut récurer/itérer à gauche ou à droite dans la recherche binaire.
En guise de remarque sur l'implémentation, pour plus d'informations sur la fonction index(_:offsetBy:)
qui a été utilisée, reportez-vous à la documentation suivante.
Génériques + protocoles = 💥
Les génériques et les protocoles peuvent être un outil puissant s’ils sont utilisés correctement pour éviter la duplication de code.
Tout d'abord, consultez un autre didacticiel, A Swift Tour , qui couvre brièvement les génériques à la fin du livre Colab.
En supposant que vous ayez une idée générale des génériques, examinons rapidement quelques utilisations avancées.
Lorsqu’un même type a plusieurs exigences comme par exemple un type conforme à plusieurs protocoles, vous disposez de plusieurs options :
typealias ComparableReal = Comparable & FloatingPoint
func foo1<T: ComparableReal>(a: T, b: T) -> Bool {
return a > b
}
func foo2<T: Comparable & FloatingPoint>(a: T, b: T) -> Bool {
return a > b
}
func foo3<T>(a: T, b: T) -> Bool where T: ComparableReal {
return a > b
}
func foo4<T>(a: T, b: T) -> Bool where T: Comparable & FloatingPoint {
return a > b
}
func foo5<T: FloatingPoint>(a: T, b: T) -> Bool where T: Comparable {
return a > b
}
print(foo1(a: 1, b: 2))
print(foo2(a: 1, b: 2))
print(foo3(a: 1, b: 2))
print(foo4(a: 1, b: 2))
print(foo5(a: 1, b: 2))
false false false false false
Notez l'utilisation de typealias
en haut. Cela ajoute un alias nommé d'un type existant dans votre programme. Une fois qu'un alias de type est déclaré, le nom de l'alias peut être utilisé à la place du type existant partout dans votre programme. Les alias de type ne créent pas de nouveaux types ; ils permettent simplement à un nom de faire référence à un type existant.
Voyons maintenant comment nous pouvons utiliser ensemble les protocoles et les génériques.
Imaginons que nous soyons un magasin d'informatique avec les exigences suivantes sur tous les ordinateurs portables que nous vendons pour déterminer comment nous les organisons à l'arrière du magasin :
enum Box {
case small
case medium
case large
}
enum Mass {
case light
case medium
case heavy
}
// Note: `CustomStringConvertible` protocol lets us pretty-print a `Laptop`.
struct Laptop: CustomStringConvertible {
var name: String
var box: Box
var mass: Mass
var description: String {
return "(\(self.name) \(self.box) \(self.mass))"
}
}
Cependant, nous avons une nouvelle exigence de regrouper nos Laptop
par masse puisque les étagères ont des restrictions de poids.
func filtering(_ laptops: [Laptop], by mass: Mass) -> [Laptop] {
return laptops.filter { $0.mass == mass }
}
let laptops: [Laptop] = [
Laptop(name: "a", box: .small, mass: .light),
Laptop(name: "b", box: .large, mass: .medium),
Laptop(name: "c", box: .medium, mass: .heavy),
Laptop(name: "d", box: .large, mass: .light)
]
let filteredLaptops = filtering(laptops, by: .light)
print(filteredLaptops)
[(a small light), (d large light)]
Cependant, et si on voulait filtrer par autre chose que Mass
?
Une option consiste à procéder comme suit :
// Define a protocol which will act as our comparator.
protocol DeviceFilterPredicate {
associatedtype Device
func shouldKeep(_ item: Device) -> Bool
}
// Define the structs we will use for passing into our filtering function.
struct BoxFilter: DeviceFilterPredicate {
typealias Device = Laptop
var box: Box
func shouldKeep(_ item: Laptop) -> Bool {
return item.box == box
}
}
struct MassFilter: DeviceFilterPredicate {
typealias Device = Laptop
var mass: Mass
func shouldKeep(_ item: Laptop) -> Bool {
return item.mass == mass
}
}
// Make sure our filter conforms to `DeviceFilterPredicate` and that we are
// filtering `Laptop`s.
func filtering<F: DeviceFilterPredicate>(
_ laptops: [Laptop],
by filter: F
) -> [Laptop] where Laptop == F.Device {
return laptops.filter { filter.shouldKeep($0) }
}
// Let's test the function out!
print(filtering(laptops, by: BoxFilter(box: .large)))
print(filtering(laptops, by: MassFilter(mass: .heavy)))
[(b large medium), (d large light)] [(c medium heavy)]
Génial! Nous sommes désormais en mesure de filtrer en fonction de n'importe quelle contrainte d'ordinateur portable. Cependant, nous ne pouvons filtrer que Laptop
.
Que diriez-vous de pouvoir filtrer tout ce qui se trouve dans une boîte et qui a une masse ? Peut-être que cet entrepôt d'ordinateurs portables sera également utilisé pour des serveurs ayant une clientèle différente :
// Define 2 new protocols so we can filter anything in a box and which has mass.
protocol Weighable {
var mass: Mass { get }
}
protocol Boxed {
var box: Box { get }
}
// Define the new Laptop and Server struct which have mass and a box.
struct Laptop: CustomStringConvertible, Boxed, Weighable {
var name: String
var box: Box
var mass: Mass
var description: String {
return "(\(self.name) \(self.box) \(self.mass))"
}
}
struct Server: CustomStringConvertible, Boxed, Weighable {
var isWorking: Bool
var name: String
let box: Box
let mass: Mass
var description: String {
if isWorking {
return "(working \(self.name) \(self.box) \(self.mass))"
} else {
return "(notWorking \(self.name) \(self.box) \(self.mass))"
}
}
}
// Define the structs we will use for passing into our filtering function.
struct BoxFilter<T: Boxed>: DeviceFilterPredicate {
var box: Box
func shouldKeep(_ item: T) -> Bool {
return item.box == box
}
}
struct MassFilter<T: Weighable>: DeviceFilterPredicate {
var mass: Mass
func shouldKeep(_ item: T) -> Bool {
return item.mass == mass
}
}
// Define the new filter function.
func filtering<F: DeviceFilterPredicate, T>(
_ elements: [T],
by filter: F
) -> [T] where T == F.Device {
return elements.filter { filter.shouldKeep($0) }
}
// Let's test the function out!
let servers = [
Server(isWorking: true, name: "serverA", box: .small, mass: .heavy),
Server(isWorking: false, name: "serverB", box: .medium, mass: .medium),
Server(isWorking: true, name: "serverC", box: .large, mass: .light),
Server(isWorking: false, name: "serverD", box: .medium, mass: .light),
Server(isWorking: true, name: "serverE", box: .small, mass: .heavy)
]
let products = [
Laptop(name: "a", box: .small, mass: .light),
Laptop(name: "b", box: .large, mass: .medium),
Laptop(name: "c", box: .medium, mass: .heavy),
Laptop(name: "d", box: .large, mass: .light)
]
print(filtering(servers, by: BoxFilter(box: .small)))
print(filtering(servers, by: MassFilter(mass: .medium)))
print(filtering(products, by: BoxFilter(box: .small)))
print(filtering(products, by: MassFilter(mass: .medium)))
[(working serverA small heavy), (working serverE small heavy)] [(notWorking serverB medium medium)] [(a small light)] [(b large medium)]
Nous sommes désormais en mesure de filtrer un tableau non seulement en fonction de n'importe quelle propriété d'une struct
spécifique, mais également de filtrer n'importe quelle structure possédant cette propriété !
Conseils pour une bonne conception d'API
Cette section est tirée de la conférence WWDC 2019 : Modern Swift API Design .
Maintenant que vous comprenez comment se comportent les protocoles, il est préférable de déterminer quand utiliser les protocoles. Aussi puissants que puissent être les protocoles, ce n’est pas toujours la meilleure idée de se lancer et de commencer immédiatement par les protocoles.
- Commencez par des cas d’utilisation concrets :
- Explorez d'abord le cas d'utilisation avec des types concrets et comprenez quel code vous souhaitez partager et trouver est répété. Ensuite, intégrez ce code partagé aux génériques. Cela pourrait signifier créer de nouveaux protocoles. Découvrez un besoin de code générique.
- Envisagez de composer de nouveaux protocoles à partir de protocoles existants définis dans la bibliothèque standard. Reportez-vous à la documentation Apple suivante pour un bon exemple.
- Au lieu d’un protocole générique, envisagez plutôt de définir un type générique.
Exemple : définition d'un type de vecteur personnalisé
Disons que nous voulons définir un protocole GeometricVector
sur les nombres à virgule flottante à utiliser dans une application de géométrie que nous créons et qui définit 3 opérations vectorielles importantes :
protocol GeometricVector {
associatedtype Scalar: FloatingPoint
static func dot(_ a: Self, _ b: Self) -> Scalar
var length: Scalar { get }
func distance(to other: Self) -> Scalar
}
Disons que nous voulons stocker les dimensions du vecteur, pour lesquelles le protocole SIMD
peut nous aider, nous allons donc faire en sorte que notre nouveau type affine le protocole SIMD
. Les vecteurs SIMD
peuvent être considérés comme des vecteurs de taille fixe qui sont très rapides lorsque vous les utilisez pour effectuer des opérations vectorielles :
protocol GeometricVector: SIMD {
associatedtype Scalar: FloatingPoint
static func dot(_ a: Self, _ b: Self) -> Scalar
var length: Scalar { get }
func distance(to other: Self) -> Scalar
}
Maintenant, définissons les implémentations par défaut des opérations ci-dessus :
extension GeometricVector {
static func dot(_ a: Self, _ b: Self) -> Scalar {
(a * b).sum()
}
var length: Scalar {
Self.dot(self, self).squareRoot()
}
func distance(to other: Self) -> Scalar {
(self - other).length
}
}
Et puis nous devons ajouter une conformité à chacun des types auxquels nous souhaitons ajouter ces capacités :
extension SIMD2: GeometricVector where Scalar: FloatingPoint { }
extension SIMD3: GeometricVector where Scalar: FloatingPoint { }
extension SIMD4: GeometricVector where Scalar: FloatingPoint { }
extension SIMD8: GeometricVector where Scalar: FloatingPoint { }
extension SIMD16: GeometricVector where Scalar: FloatingPoint { }
extension SIMD32: GeometricVector where Scalar: FloatingPoint { }
extension SIMD64: GeometricVector where Scalar: FloatingPoint { }
Ce processus en trois étapes consistant à définir le protocole, à lui donner une implémentation par défaut, puis à ajouter une conformité à plusieurs types est assez répétitif.
Le protocole était-il nécessaire ?
Le fait qu’aucun des types SIMD
n’ait d’implémentation unique est un signe d’avertissement. Donc dans ce cas, le protocole ne nous donne vraiment rien.
Le définir dans une extension de SIMD
Si on écrit les 3 opérateurs dans une extension du protocole SIMD
, cela peut résoudre le problème de manière plus succincte :
extension SIMD where Scalar: FloatingPoint {
static func dot(_ a: Self, _ b: Self) -> Scalar {
(a * b).sum()
}
var length: Scalar {
Self.dot(self, self).squareRoot()
}
func distance(to other: Self) -> Scalar {
(self - other).length
}
}
En utilisant moins de lignes de code, nous avons ajouté toutes les implémentations par défaut à tous les types de SIMD
.
Parfois, vous pourriez être tenté de créer cette hiérarchie de types, mais n'oubliez pas que ce n'est pas toujours nécessaire. Cela signifie également que la taille binaire de votre programme compilé sera plus petite et que votre code sera plus rapide à compiler.
Cependant, cette approche d’extension est idéale lorsque vous souhaitez ajouter un certain nombre de méthodes. Cependant, cela pose un problème d’évolutivité lorsque vous concevez une API plus grande.
Est un? A un?
Plus tôt, nous avons dit que GeometricVector
affinerait SIMD
. Mais s’agit-il d’une relation ? Le problème est que SIMD
définit des opérations qui permettent d'ajouter un scalaire 1 à un vecteur, mais cela n'a pas de sens de définir une telle opération dans le contexte de la géométrie.
Donc, peut-être qu'une relation has-a serait meilleure en enveloppant SIMD
dans un nouveau type générique capable de gérer n'importe quel nombre à virgule flottante :
// NOTE: `Storage` is the underlying type that is storing the values,
// just like in a `SIMD` vector.
struct GeometricVector<Storage: SIMD> where Storage.Scalar: FloatingPoint {
typealias Scalar = Storage.Scalar
var value: Storage
init(_ value: Storage) { self.value = value }
}
On peut alors être prudent et définir uniquement les opérations qui n'ont de sens que dans le contexte de la géométrie :
extension GeometricVector {
static func + (a: Self, b: Self) -> Self {
Self(a.value + b.value)
}
static func - (a: Self, b: Self) -> Self {
Self(a.value - b.value)
}
static func * (a: Self, b: Scalar) -> Self {
Self(a.value * b)
}
}
Et nous pouvons toujours utiliser des extensions génériques pour obtenir les 3 opérateurs précédents que nous voulions implémenter et qui ressemblent presque exactement à avant :
extension GeometricVector {
static func dot(_ a: Self, _ b: Self) -> Scalar {
(a.value * b.value).sum()
}
var length: Scalar {
Self.dot(self, self).squareRoot()
}
func distance(to other: Self) -> Scalar {
(self - other).length
}
}
Dans l'ensemble, nous avons pu affiner le comportement de nos trois opérations en un type en utilisant simplement une structure. Avec les protocoles, nous avons été confrontés au problème de l'écriture de conformités répétitives pour tous les vecteurs SIMD
, et nous n'avons pas non plus pu empêcher certains opérateurs comme Scalar + Vector
d'être disponibles (ce que nous ne voulions pas dans ce cas). Par conséquent, n’oubliez pas que les protocoles ne constituent pas une solution universelle. Mais parfois, des solutions plus traditionnelles peuvent s’avérer plus efficaces.
Plus de ressources de programmation orientées protocole
Voici des ressources supplémentaires sur les sujets abordés :
- WWDC 2015 : Programmation orientée protocole dans Swift : cela a été présenté en utilisant Swift 2, donc beaucoup de choses ont changé depuis (par exemple le nom des protocoles utilisés dans la présentation) mais cela reste une bonne ressource pour la théorie et les utilisations qui la sous-tendent. .
- Présentation de la programmation orientée protocole dans Swift 3 : cela a été écrit dans Swift 3, donc une partie du code devra peut-être être modifiée pour qu'il soit compilé avec succès, mais c'est une autre excellente ressource.
- WWDC 2019 : Modern Swift API Design : passe en revue les différences entre les types valeur et référence, un cas d'utilisation dans lequel les protocoles peuvent s'avérer être le pire choix dans la conception d'API (identique à la section "Conseils pour une bonne conception d'API" ci-dessus), clé recherche de membres de chemin et wrappers de propriétés.
- Génériques : la propre documentation de Swift pour Swift 5 sur les génériques.