web-dev-qa-db-fra.com

Quel est le mot clé `some` dans Swift (UI)?

Le nouveau tutoriel SwiftUI a le code suivant:

struct ContentView: View {
    var body: some View {
        Text("Hello World")
    }
}

La deuxième ligne, le mot some, et sur leur site est mise en évidence comme s'il s'agissait d'un mot-clé.

Swift 5.1 ne semble pas avoir some comme mot-clé, et je ne vois pas ce que le mot some pourrait faire d'autre, car il va là où le type va habituellement. Existe-t-il une nouvelle version inopinée de Swift? Est-ce une fonction qui est utilisée sur un type d'une manière que je ne connaissais pas?

Que fait le mot clé some?

250
Nicholas

some View Est n type de résultat opaque tel qu'introduit par SE-0244 et est disponible en Swift 5.1 avec Xcode 11. Vous pouvez considérer cela comme un espace réservé générique "inversé".

Contrairement à un espace réservé générique régulier qui est satisfait par l'appelant:

protocol P {}
struct S1 : P {}
struct S2 : P {}

func foo<T : P>(_ x: T) {}
foo(S1()) // Caller chooses T == S1.
foo(S2()) // Caller chooses T == S2.

Un type de résultat opaque est un espace réservé générique implicite satisfait par l'implémentation , vous pouvez donc y penser:

func bar() -> some P {
  return S1() // Implementation chooses S1 for the opaque result.
}

comme ressemblant à ceci:

func bar() -> <Output : P> Output {
  return S1() // Implementation chooses Output == S1.
}

En fait, l'objectif final de cette fonctionnalité est d'autoriser les génériques inversés sous cette forme plus explicite, ce qui vous permettrait également d'ajouter des contraintes, par exemple -> <T : Collection> T where T.Element == Int. Voir cet article pour plus d'informations .

La principale chose à retenir est qu'une fonction renvoyant some P Est une fonction qui renvoie une valeur d'un type concret spécifique unique qui conforme à P. Tenter de renvoyer différents types conformes dans la fonction génère une erreur de compilation:

// error: Function declares an opaque return type, but the return
// statements in its body do not have matching underlying types.
func bar(_ x: Int) -> some P {
  if x > 10 {
    return S1()
  } else {
    return S2()
  }
}

Comme l'espace réservé générique implicite ne peut pas être satisfait par plusieurs types.

Ceci contraste avec une fonction renvoyant P, qui peut être utilisée pour représenter à la fois S1 Et S2 Car il représente une valeur arbitraire P conforme:

func baz(_ x: Int) -> P {
  if x > 10 {
    return S1()
  } else {
    return S2()
  }
}

D'accord. Quels sont les avantages des types de résultats opaques -> some P Par rapport aux types de retour de protocole -> P?


1. Les types de résultats opaques peuvent être utilisés avec les PAT

Une limitation actuelle majeure des protocoles est que les PAT (protocoles avec des types associés) ne peuvent pas être utilisés comme types réels. Bien qu'il s'agisse d'une restriction qui sera probablement levée dans une future version du langage, car les types de résultats opaques ne sont en réalité que des espaces réservés génériques, ils peuvent être utilisés avec les PAT aujourd'hui.

Cela signifie que vous pouvez faire des choses comme:

func giveMeACollection() -> some Collection {
  return [1, 2, 3]
}

let collection = giveMeACollection()
print(collection.count) // 3

2. Les types de résultats opaques ont une identité

Étant donné que les types de résultats opaques appliquent un seul type concret est renvoyé, le compilateur sait que deux appels à la même fonction doivent renvoyer deux valeurs du même type.

Cela signifie que vous pouvez faire des choses comme:

//   foo() -> <Output : Equatable> Output {
func foo() -> some Equatable { 
  return 5 // The opaque result type is inferred to be Int.
}

let x = foo()
let y = foo()
print(x == y) // Legal both x and y have the return type of foo.

Ceci est légal car le compilateur sait que x et y ont le même type concret. Il s'agit d'une exigence importante pour == , où les deux paramètres de type Self.

protocol Equatable {
  static func == (lhs: Self, rhs: Self) -> Bool
}

Cela signifie qu'il attend deux valeurs qui sont à la fois du même type que le type conforme au béton. Même si Equatable était utilisable comme type, vous ne pourriez pas comparer deux valeurs arbitraires Equatable conformes, par exemple:

func foo(_ x: Int) -> Equatable { // Assume this is legal.
  if x > 10 {
    return 0
  } else {
    return "hello world"      
  }
}

let x = foo(20)
let y = foo(5)
print(x == y) // Illegal.

Comme le compilateur ne peut pas prouver que deux valeurs Equatable arbitraires ont le même type concret sous-jacent.

De manière similaire, si nous introduisions une autre fonction de retour de type opaque:

//   foo() -> <Output1 : Equatable> Output1 {
func foo() -> some Equatable { 
  return 5 // The opaque result type is inferred to be Int.
}

//   bar() -> <Output2 : Equatable> Output2 {
func bar() -> some Equatable { 
  return "" // The opaque result type is inferred to be String.
}

let x = foo()
let y = bar()
print(x == y) // Illegal, the return type of foo != return type of bar.

L'exemple devient illégal car bien que foo et bar renvoient some Equatable, Leurs espaces réservés génériques "inversés" Output1 Et Output2 Pourraient être satisfaits par différents types.


3. Les types de résultats opaques composent avec des espaces réservés génériques

Contrairement aux valeurs de type protocole standard, les types de résultats opaques se composent bien avec des espaces réservés génériques réguliers, par exemple:

protocol P {
  var i: Int { get }
}
struct S : P {
  var i: Int
}

func makeP() -> some P { // Opaque result type inferred to be S.
  return S(i: .random(in: 0 ..< 10))
}

func bar<T : P>(_ x: T, _ y: T) -> T {
  return x.i < y.i ? x : y
}

let p1 = makeP()
let p2 = makeP()
print(bar(p1, p2)) // Legal, T is inferred to be the return type of makeP.

Cela n'aurait pas fonctionné si makeP venait de renvoyer P, car deux valeurs de P peuvent avoir des types concrets sous-jacents différents, par exemple:

struct T : P {
  var i: Int
}

func makeP() -> P {
  if .random() { // 50:50 chance of picking each branch.
    return S(i: 0)
  } else {
    return T(i: 1)
  }
}

let p1 = makeP()
let p2 = makeP()
print(bar(p1, p2)) // Illegal.

Pourquoi utiliser un type de résultat opaque sur le type de béton?

À ce stade, vous pensez peut-être à vous-même, pourquoi ne pas simplement écrire le code comme suit:

func makeP() -> S {
  return S(i: 0)
}

Eh bien, l'utilisation d'un type de résultat opaque vous permet de faire du type S un détail d'implémentation en exposant uniquement l'interface fournie par P, vous donnant la possibilité de changer le type de béton plus tard sur la ligne. sans casser aucun code qui dépend de la fonction.

Par exemple, vous pouvez remplacer:

func makeP() -> some P {
  return S(i: 0)
}

avec:

func makeP() -> some P { 
  return T(i: 1)
}

sans casser aucun code qui appelle makeP().

Voir la section Types Opaques du guide de langue et la proposition d'évolution Swift pour plus d'informations sur cette fonctionnalité.

301
Hamish

L'autre réponse explique bien l'aspect technique du nouveau mot clé some mais cette réponse essaiera d'expliquer facilement pourquoi.


Disons que j'ai un protocole Animal et que je veux comparer si deux animaux sont frères et sœurs:

protocol Animal {
    func isSibling(_ animal: Self) -> Bool
}

De cette façon, il n'a de sens que de comparer si deux animaux sont frères et sœurs s'ils sont du même type d'animal.


Maintenant, permettez-moi de créer un exemple d'un animal juste pour référence

class Dog: Animal {
    func isSibling(_ animal: Dog) -> Bool {
        return true // doesn't really matter implementation of this
    }
}

Le chemin sans some T

Supposons maintenant que j'ai une fonction qui renvoie un animal d'une "famille".

func animalFromAnimalFamily() -> Animal {
    return myDog // myDog is just some random variable of type `Dog`
}

Remarque: cette fonction ne se compilera pas réellement. Ceci parce qu'avant l'ajout de la fonctionnalité 'some' , vous ne pouvez pas retourner un type de protocole si le protocole utilise 'Self' ou des génériques . Mais disons que vous pouvez ... prétendre que cela transforme myDog en animal de type abstrait, voyons ce qui se passe

Maintenant, le problème vient si j'essaie de faire ceci:

let animal1: Animal = animalFromAnimalFamily()
let animal2: Animal = animalFromAnimalFamily()

animal1.isSibling(animal2) // error

Cela générera une erreur .

Pourquoi? Eh bien, la raison en est que lorsque vous appelez animal1.isSibling(animal2) Swift ne sait pas si les animaux sont des chiens, des chats ou autre chose. autant que Swift sait, animal1 et animal2 pourraient être des espèces animales non apparentées . Puisque nous ne pouvons pas comparer les animaux de différents types (voir ci-dessus).

Comment some T Résout ce problème

Réécrivons la fonction précédente:

func animalFromAnimalFamily() -> some Animal {
    return myDog
}
let animal1 = animalFromAnimalFamily()
let animal2 = animalFromAnimalFamily()

animal1.isSibling(animal2)

animal1 Et animal2 Sont pasAnimal, mais ce sont des classes qui implémentent Animal .

Ce que cela vous permet de faire maintenant, c'est lorsque vous appelez animal1.isSibling(animal2), Swift sait que animal1 Et animal2 Sont du même type.

Donc, la façon dont j'aime y penser:

some T Permet à Swift de savoir quelle implémentation de T est utilisée mais pas l'utilisateur de la classe.

(Avis de non-promotion) J'ai écrit un article de blog qui va un peu plus en profondeur (même exemple qu'ici) sur cette nouvelle fonctionnalité

43
Downgoat

réponse de Hamish est assez génial et répond à la question d'un point de vue technique. Je voudrais ajouter quelques réflexions sur la raison pour laquelle le mot clé some est utilisé à cet endroit particulier dans Tutoriels SwiftUI d'Apple et pourquoi c'est une bonne pratique à suivre.

some n'est pas une exigence!

Tout d'abord, vous n'avez pas besoin pour déclarer le type de retour de body comme un type opaque. Vous pouvez toujours renvoyer le type concret au lieu d'utiliser le some View.

struct ContentView: View {
    var body: Text {
        Text("Hello World")
    }
}

Cela compilera également. Lorsque vous regardez dans l'interface de View, vous verrez que le type de retour de body est un type associé:

public protocol View : _View {

    /// The type of view representing the body of this view.
    ///
    /// When you create a custom view, Swift infers this type from your
    /// implementation of the required `body` property.
    associatedtype Body : View

    /// Declares the content and behavior of this view.
    var body: Self.Body { get }
}

Cela signifie que vous spécifiez ce type en annotant la propriété body avec un type particulier de votre choix. La seule exigence est que ce type doit implémenter le protocole View lui-même.

Cela peut être soit un type spécifique qui implémente View, par exemple

  • Text
  • Image
  • Circle

ou un type opaque qui implémente View, c'est-à-dire.

  • some View

Vues génériques

Le problème se pose lorsque nous essayons d'utiliser une vue de pile comme type de retour de body, comme VStack ou HStack:

struct ContentView: View {
    var body: VStack {
        VStack {
            Text("Hello World")
            Image(systemName: "video.fill")
        }
    }
}

Cela ne se compilera pas et vous obtiendrez l'erreur:

La référence au type générique 'VStack' nécessite des arguments dans <...>

C'est parce que les vues de pile dans SwiftUI sont génériques types! ???? (Et la même chose est vraie pour Listes et d'autres types de vue de conteneur.)

Cela a beaucoup de sens car vous pouvez brancher n'importe quel nombre de vues de n'importe quel type (tant qu'il est conforme au protocole View). Le type concret de VStack dans le corps ci-dessus est en fait

VStack<TupleView<(Text, Image)>>

Lorsque nous décidons plus tard d'ajouter une vue à la pile, son type concret change. Si nous ajoutons un deuxième texte après le premier, nous obtenons

VStack<TupleView<(Text, Text, Image)>>    

Même si nous apportons une modification mineure, quelque chose d'aussi subtil que l'ajout d'un espaceur entre le texte et l'image, le type de la pile change:

VStack<TupleView<(Text, _ModifiedContent<Spacer, _FrameLayout>, Image)>>

D'après ce que je peux dire, c'est ça la raison pour laquelle Apple recommande dans leurs tutoriels de toujours utiliser some View, le type opaque le plus général que toutes les vues satisfont, comme le type de retour de body. Vous pouvez modifier l'implémentation/la disposition de votre vue personnalisée sans changer manuellement le type de retour à chaque fois.


Supplément:

Si vous souhaitez obtenir une compréhension plus intuitive des types de résultats opaques, j'ai récemment publié un article qui pourrait être intéressant à lire:

???? Qu'est-ce que c'est que "certains" dans SwiftUI?

28
Mischa

Je pense que toutes les réponses manquent jusqu'à présent, c'est que some est principalement utile dans quelque chose comme un DSL (langage spécifique au domaine) tel que SwiftUI ou une bibliothèque/framework, qui aura tilisateurs (autres programmeurs) différent de vous.

Vous n'utiliseriez probablement jamais some dans votre code d'application normal, sauf peut-être dans la mesure où il peut encapsuler un protocole générique afin qu'il puisse être utilisé comme type (au lieu de simplement comme contrainte de type). Ce que some fait, c'est de laisser le compilateur garder une connaissance de quel type spécifique quelque chose est, tout en mettant une façade de supertype devant lui.

Ainsi, dans SwiftUI, où vous êtes l'utilisateur, tout ce que vous devez savoir est que quelque chose est un some View, tandis que dans les coulisses, toutes sortes de mouchoirs peuvent continuer de vous protéger. Cet objet est en fait un type très spécifique, mais vous n'aurez jamais besoin de savoir de quoi il s'agit. Pourtant, contrairement à un protocole, c'est un type à part entière, car partout où il apparaît, ce n'est qu'une façade pour un type spécifique à part entière.

Dans une future version de SwiftUI, où vous attendez un some View, les développeurs pourraient changer le type sous-jacent de cet objet particulier. Mais cela ne cassera pas votre code, car votre code n'a jamais mentionné le type sous-jacent en premier lieu.

Ainsi, some rend en fait un protocole plus semblable à une superclasse. C'est presque un type d'objet réel, mais pas tout à fait (par exemple, la déclaration d'une méthode de protocole ne peut pas retourner un some).

Donc, si vous deviez utiliser some pour quoi que ce soit, ce serait très probablement si vous écriviez un DSL ou un framework/bibliothèque pour une utilisation par d'autres, et que vous vouliez masquer le type sous-jacent détails. Cela rendrait votre code plus simple à utiliser pour les autres et vous permettrait de modifier les détails d'implémentation sans casser leur code.

Cependant, vous pouvez également l'utiliser dans votre propre code pour protéger une région de votre code des détails d'implémentation enfouis dans une autre région de votre code.

23
matt

Le mot clé some de Swift 5.1 ( proposition Swift-evolution ) est utilisé conjointement avec un protocole comme type de retour).

Xcode 11 notes de version le présenter comme ça:

Les fonctions peuvent désormais masquer leur type de retour concret en déclarant à quels protocoles elles sont conformes, au lieu de spécifier le type de retour exact:

func makeACollection() -> some Collection {
    return [1, 2, 3]
}

Le code qui appelle la fonction peut utiliser l'interface du protocole, mais n'a pas de visibilité sur le type sous-jacent. ( SE-0244 , 40538331)

Dans l'exemple ci-dessus, vous n'avez pas besoin de dire que vous allez retourner un Array. Cela vous permet même de renvoyer un type générique qui est juste conforme à Collection.


Notez également cette erreur possible que vous pourriez rencontrer:

'certains' types de retour ne sont disponibles que dans iOS 13.0.0 ou plus récent

Cela signifie que vous êtes censé utiliser la disponibilité pour éviter some sur iOS 12 et avant:

@available(iOS 13.0, *)
func makeACollection() -> some Collection {
    ...
}
22
Cœur

"certains" signifie de type opaque. Dans SwiftUI, View est déclaré en tant que protocole

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol View {

    /// The type of view representing the body of this view.
    ///
    /// When you create a custom view, Swift infers this type from your
    /// implementation of the required `body` property.
    associatedtype Body : View

    /// Declares the content and behavior of this view.
    var body: Self.Body { get }
}

Lorsque vous créez votre vue en tant que Struct, vous vous conformez au protocole View et dites que le corps var renverra quelque chose qui confirmera à View Protocol. C'est comme une abstraction générique de protocole où vous n'avez pas à définir le type concret.

1
varunrathi28

Je vais essayer de répondre à cela avec un exemple pratique très basique (qu'est-ce que c'est n type de résultat opaque à propos)

En supposant que vous ayez un protocole avec le type associé et deux structures l'implémentant:

protocol ProtocolWithAssociatedType {
    associatedtype SomeType
}

struct First: ProtocolWithAssociatedType {
    typealias SomeType = Int
}

struct Second: ProtocolWithAssociatedType {
    typealias SomeType = String
}

Avant Swift 5.1, ci-dessous est illégal en raison de ProtocolWithAssociatedType can only be used as a generic constraint Erreur:

func create() -> ProtocolWithAssociatedType {
    return First()
}

Mais en Swift 5.1 c'est très bien (some ajouté):

func create() -> some ProtocolWithAssociatedType {
    return First()
}

Ci-dessus est une utilisation pratique, largement utilisée dans SwiftUI pour some View.

Mais il y a n limitation importante - le type retourné doit être connu au moment de la compilation, donc ci-dessous ne fonctionnera plus en donnant Function declares an opaque return type, but the return statements in its body do not have matching underlying types Erreur:

func create() -> some ProtocolWithAssociatedType {
    if (1...2).randomElement() == 1 {
        return First()
    } else {
        return Second()
    }
}
0
tzaloga