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


259

Le nouveau tutoriel SwiftUI a le code suivant:

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

La deuxième ligne du 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 somede mot-clé, et je ne vois pas ce que le mot somepourrait 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 some- clé ?


Pour ceux qui ont été étourdis par le sujet, voici un article très décryptant et pas à pas grâce à Vadim Bulavin. vadimbulavin.com/…
Luc-Olivier

Réponses:


333

some Viewest un type de résultat opaque introduit par SE-0244 et est disponible dans Swift 5.1 avec Xcode 11. Vous pouvez le considérer 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 Pest une fonction qui renvoie une valeur d'un type concret unique 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.

Cela contraste avec une fonction renvoyant P, qui peut être utilisée pour représenter les deux S1 et S2parce qu'elle représente une Pvaleur de conformité arbitraire :

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 par rapport aux types de -> some Pretour 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 fait 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 les deux xet yont 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 s'ils Equatableétaient utilisables comme type, vous ne seriez pas en mesure de comparer deux Equatablevaleurs conformes arbitraires , 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 Equatablevaleurs 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égale parce que même si les deux fooet le barretour some Equatable, leur « inverse » génériques des espaces réservés Output1et Output2pourrait être satisfaite 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é s'il makePvenait de revenir P, car deux Pvaleurs peuvent avoir différents types concrets sous-jacents, 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 le type S un détail d'implémentation en exposant uniquement l'interface fournie parP , ce qui vous donne la possibilité de changer le type concret plus tard sur la ligne sans casser le 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é.


20
Sans relation: à partir de Swift 5.1, returnn'est pas requis dans les fonctions à expression simple
ielyamani

3
Mais quelle est la différence entre: func makeP() -> some Pet func makeP() -> P? J'ai lu la proposition et je ne vois pas non plus cette différence pour leurs échantillons.
Artem


2
La manipulation du type Swift est un gâchis. Cette spécificité est-elle vraiment quelque chose qui ne peut pas être traitée au moment de la compilation? Voir C # pour référence, il gère implicitement tous ces cas via une syntaxe simple. Les martinets doivent avoir une syntaxe presque inexplicable et presque inexplicable qui obscurcit vraiment le langage. Pouvez-vous également expliquer la justification de la conception de ceci s'il vous plaît? (Si vous avez un lien vers la proposition dans github, ce serait bien aussi) Edit: Je viens de le remarquer lié en haut.
SacredGeometry

2
@Zmaster Le compilateur traitera deux types de retour opaques comme étant différents même si l'implémentation pour les deux renvoie le même type concret. En d'autres termes, le type concret spécifique choisi est caché à l'appelant. (J'avais l'intention d'étendre la dernière moitié de ma réponse pour rendre les choses comme ça un peu plus explicites, mais je n'y suis pas encore parvenu).
Hamish

52

L'autre réponse explique bien l'aspect technique du nouveau somemot clé, mais cette réponse tentera 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 est logique 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. Cela parce qu'avant l'ajout de la fonctionnalité «certains», 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'essaye 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, il ne sait pas si les animaux sont des chiens, des chats ou autre. Pour autant que Swift le sache, animal1et animal2pourrait être une espèce animale non apparentée . Puisque nous ne pouvons pas comparer des animaux de différents types (voir ci-dessus). Ce sera une erreur

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)

animal1et ne leanimal2 sont pas Animal , 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 le sait animal1etanimal2 est du même type.

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

some Tpermet à Swift de savoir quelle implémentation Test 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é


2
Donc, votre idée est que l'appelant peut profiter du fait que deux appels à la fonction retournent le même type même si l'appelant ne sait pas de quel type il s'agit?
mat

1
@matt essentiellement yup. Même concept lorsqu'il est utilisé avec des champs, etc. - l'appelant a la garantie que le type de retour sera toujours le même type mais ne révèle pas exactement de quel type il s'agit.
Downgoat

@Downgoat merci beaucoup pour la publication et la réponse parfaites. Comme je l'ai compris somedans les retours, le type fonctionne comme une contrainte au corps de la fonction. someExige donc de renvoyer un seul type concret dans le corps de fonction entier. Par exemple: s'il y en a, return randomDogtous les autres retours doivent fonctionner uniquement avec Dog. Tous les avantages proviennent de cette contrainte: disponibilité animal1.isSibling(animal2)et bénéfice de la compilation de func animalFromAnimalFamily() -> some Animal(car Selfse définit désormais sous le capot). Est-ce correct?
Artem

5
Cette ligne était tout ce dont j'avais besoin, animal1 et animal2 ne sont pas des animaux, mais ce sont des classes qui implémentent des animaux, maintenant tout a du sens!
autour du

29

La réponse de Hamish est assez impressionnante et répond à la question d'un point de vue technique. Je voudrais ajouter quelques réflexions sur la raison pour laquelle le mot some- clé est utilisé à cet endroit particulier dans les didacticiels 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 de déclarer le bodytype de retour de 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' Viewinterface de, vous verrez que le type de retour de bodyest 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 bodypropriété avec un type particulier de votre choix. La seule exigence est que ce type doit implémenter le Viewprotocole 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.-à-d.

  • some View

Vues génériques

Le problème se pose lorsque nous essayons d'utiliser une vue de pile comme bodytype de retour de, comme VStackou 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 des types génériques ! 💡 (Et il en va de même pour les listes et autres types de vue de conteneur.)

Cela a beaucoup de sens car vous pouvez brancher n'importe quel nombre de vues de tout type (tant qu'il est conforme au Viewprotocole). Le type de béton VStackdans 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 la raison pour laquelle Apple recommande dans ses tutoriels de toujours utiliser some View, le type opaque le plus général que toutes les vues satisfont, comme le bodytype de retour du. Vous pouvez changer 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 «certains» dans SwiftUI?


2
Ce. Merci! La réponse de Hamish était très complète, mais la vôtre me dit exactement pourquoi elle est utilisée dans ces exemples.
Chris Marshall

J'adore l'idée de "certains". Une idée si l'utilisation de "certains" affecte le temps de compilation?
Tofu Warrior

@Mischa alors comment créer des vues génériques? avec un protocole qui contient des vues et des soirées d'autres comportements?
theMouk

27

Je pense que toutes les réponses manquantes jusqu'à présent sont someutiles principalement dans quelque chose comme un DSL (langage spécifique au domaine) tel que SwiftUI ou une bibliothèque / framework, qui aura des utilisateurs (autres programmeurs) différents de vous.

Vous n'utiliseriez probablement jamais somedans 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). Qu'est some- ce que 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, c'est que quelque chose est un some View, tandis que dans les coulisses, toutes sortes de mouchoirs peuvent continuer à 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, someen fait, un protocole ressemble davantage à une superclasse. C'est presque un type d'objet réel, mais pas tout à fait (par exemple, une déclaration de méthode d'un protocole ne peut pas retourner a some).

Donc, si vous deviez utiliser somequoi que ce soit, ce serait très probablement si vous écriviez un DSL ou un framework / bibliothèque à l'usage des autres, et que vous vouliez masquer les détails du type sous-jacent. 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

Le somemot-clé de Swift 5.1 ( proposition d'évolution rapide ) est utilisé en conjonction avec un protocole comme type de retour.

Les notes de version de Xcode 11 le présentent comme suit:

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 somesur iOS 12 et avant:

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

1
Merci beaucoup pour cette réponse ciblée et le problème du compilateur dans Xcode 11 beta
brainray

1
Vous êtes censé utiliser la disponibilité pour éviter somesur iOS 12 et avant. Tant que vous le faites, ça devrait aller. Le problème est seulement que le compilateur ne vous avertit pas de le faire.
mat

2
Cœur, comme vous le faites remarquer, la description concise d'Apple explique tout: les fonctions peuvent désormais masquer leur type de retour concret en déclarant à quels protocoles elles se conforment, au lieu de spécifier le type de retour exact. Et puis le code appelant la fonction peut utiliser l'interface de protocole. Neat et puis certains.
Fattie

Cela (masquer le type de retour concret) est déjà possible sans utiliser le mot clé "certains". Il n'explique pas l'effet de l'ajout de "certains" dans la signature de la méthode.
Vince O'Sullivan

@ VinceO'Sullivan Il n'est pas possible de supprimer le somemot - clé dans cet exemple de code donné dans Swift 5.0 ou Swift 4.2. L'erreur sera: "Le protocole 'Collection' ne peut être utilisé que comme une contrainte générique car il a des exigences de type Self ou associées "
Cœur

2

«certains» signifie le 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.


2

Je vais essayer de répondre à cela avec un exemple pratique très basique (de quoi s'agit-il d' un type de résultat opaque )

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 d'une ProtocolWithAssociatedType can only be used as a generic constrainterreur:

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

Mais dans Swift 5.1 c'est très bien ( someajouté):

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

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

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

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

0

Un cas d'utilisation simple qui me vient à l'esprit est l'écriture de fonctions génériques pour les types numériques.

/// Adds one to any decimal type
func addOne<Value: FloatingPoint>(_ x: Value) -> some FloatingPoint {
    x + 1
}

// Variables will be assigned 'some FloatingPoint' type
let double = addOne(Double.pi) // 4.141592653589793
let float = addOne(Float.pi) // 4.141593

// Still get all of the required attributes/functions by the FloatingPoint protocol
double.squareRoot() // 2.035090330572526
float.squareRoot() // 2.03509

// Be careful, however, not to combine 2 'some FloatingPoint' variables
double + double // OK 
//double + float // error

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.