Qual è la parola chiave `some` in Swift (UI)?


259

Il nuovo tutorial di SwiftUI ha il seguente codice:

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

La seconda riga della parola somee sul loro sito è evidenziata come se fosse una parola chiave.

Swift 5.1 non sembra avere someuna parola chiave e non vedo cos'altro somepotrebbe fare la parola lì, poiché va dove va di solito il tipo. Esiste una nuova versione senza preavviso di Swift? È una funzione che viene utilizzata su un tipo in un modo che non conoscevo?

Cosa fa la parola chiave some?


Per coloro che hanno avuto le vertigini sull'argomento, ecco un articolo molto decifrante e passo dopo passo grazie a Vadim Bulavin. vadimbulavin.com/…
Luc-Olivier,

Risposte:


333

some Viewè un tipo di risultato opaco come introdotto da SE-0244 ed è disponibile in Swift 5.1 con Xcode 11. Puoi considerarlo come un segnaposto generico "inverso".

A differenza di un normale segnaposto generico che è soddisfatto dal chiamante:

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 tipo di risultato opaco è un segnaposto generico implicita soddisfatto dalla realizzazione , in modo da poter pensare a questo:

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

come questo:

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

In effetti, l'obiettivo finale con questa funzione è consentire generici inversi in questa forma più esplicita, che consentirebbe anche di aggiungere vincoli, ad es. -> <T : Collection> T where T.Element == Int . Vedi questo post per maggiori informazioni .

La cosa principale da trarre da questo è che una funzione che restituisce some Pè quella che restituisce un valore di uno specifico singolo tipo concreto che è conforme a P. Il tentativo di restituire diversi tipi conformi all'interno della funzione genera un errore del compilatore:

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

Poiché il segnaposto generico implicito non può essere soddisfatto da più tipi.

Ciò è in contrasto con una funzione di ritorno P, che può essere utilizzata per rappresentare entrambi S1 e S2perché rappresenta un Pvalore conforme arbitrario :

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

Bene, quindi quali vantaggi -> some Phanno i tipi di risultati opachi rispetto ai tipi di ritorno del protocollo-> P ?


1. I tipi di risultato opachi possono essere utilizzati con i PAT

Una delle principali limitazioni attuali dei protocolli è che i PAT (protocolli con tipi associati) non possono essere usati come tipi effettivi. Sebbene questa sia una restrizione che verrà probabilmente sollevata in una versione futura della lingua, poiché i tipi di risultati opachi sono effettivamente solo segnaposto generici, possono essere usati oggi con i PAT.

Questo significa che puoi fare cose come:

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

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

2. I tipi di risultati opachi hanno identità

Poiché i tipi di risultato opachi impongono il ritorno di un singolo tipo di calcestruzzo, il compilatore sa che due chiamate alla stessa funzione devono restituire due valori dello stesso tipo.

Questo significa che puoi fare cose come:

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

Questo è legale perché il compilatore sa che entrambi xe yhanno lo stesso tipo concreto. Questo è un requisito importante per ==, in cui entrambi i parametri di tipo Self.

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

Ciò significa che prevede due valori che sono entrambi dello stesso tipo del tipo conforme concreto. Anche se Equatablefosse utilizzabile come tipo, non saresti in grado di confrontare due Equatablevalori arbitrari conformi tra loro, ad esempio:

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.

Dal momento che il compilatore non può dimostrare che due Equatablevalori arbitrari abbiano lo stesso tipo concreto sottostante.

In modo simile, se introducessimo un'altra funzione di ritorno del tipo opaco:

//   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'esempio diventa illegale perché anche se entrambi fooe barritorno some Equatable, il loro "reverse" segnaposto generici Output1e Output2potrebbero essere soddisfatte da diversi tipi.


3. I tipi di risultati opachi si compongono con segnaposti generici

A differenza dei normali valori tipizzati dal protocollo, i tipi di risultati opachi si compongono bene con segnaposto generici regolari, ad esempio:

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.

Ciò non avrebbe funzionato se makePfosse appena tornato P, poiché due Pvalori potrebbero avere diversi tipi di calcestruzzo sottostanti, ad esempio:

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.

Perché utilizzare un tipo di risultato opaco sul tipo di calcestruzzo?

A questo punto potresti pensare a te stesso, perché non scrivere semplicemente il codice come:

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

Bene, l'uso di un tipo di risultato opaco consente di rendere il tipo Sun dettaglio di implementazione esponendo solo l'interfaccia fornita daP , dandoti la flessibilità di cambiare il tipo concreto in un secondo momento lungo la linea senza rompere alcun codice che dipende dalla funzione.

Ad esempio, è possibile sostituire:

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

con:

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

senza rompere alcun codice che chiama makeP().

Consulta la sezione Tipi opachi della guida linguistica e la proposta di evoluzione di Swift per ulteriori informazioni su questa funzione.


20
Non correlato: a partire da Swift 5.1, returnnon è richiesto nelle funzioni a espressione singola
ielyamani,

3
Ma qual è la differenza tra: func makeP() -> some Pe func makeP() -> P? Ho letto la proposta e non riesco a vedere questa differenza anche per i loro campioni.
Artem,


2
La gestione dei tipi di turni è un casino. Questa specificità è davvero qualcosa che non può essere gestita in fase di compilazione? Vedi C # per riferimento gestisce tutti questi casi implicitamente attraverso una semplice sintassi. I turni devono avere una sintassi quasi esplicita, quasi cultista, che sta davvero offuscando la lingua. Puoi anche spiegare la logica del design per questo, per favore? (Se hai un link alla proposta in github sarebbe altrettanto buono) Modifica: ho appena notato che è collegato in alto.
SacredGeometry

2
@Zmaster Il compilatore tratterà due tipi di ritorno opachi come diversi anche se l'implementazione per entrambi restituisce lo stesso tipo concreto. In altre parole, il tipo concreto specifico scelto è nascosto al chiamante. (Volevo ampliare la seconda metà della mia risposta per rendere le cose un po 'più esplicite, ma non ci sono ancora arrivato).
Hamish,

52

L'altra risposta fa un buon lavoro nel spiegare l'aspetto tecnico della nuova someparola chiave, ma questa risposta cercherà di spiegare facilmente il perché .


Diciamo che ho un protocollo Animal e voglio confrontare se due animali sono fratelli:

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

In questo modo ha senso solo confrontare se due animali sono fratelli se sono lo stesso tipo di animale.


Ora lasciami solo creare un esempio di un animale solo come riferimento

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

Il modo senza some T

Ora diciamo che ho una funzione che restituisce un animale da una "famiglia".

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

Nota: questa funzione non verrà effettivamente compilata. Questo perché prima che fosse aggiunta la funzione 'alcuni' non è possibile restituire un tipo di protocollo se il protocollo usa 'Self' o generici . Ma supponiamo che tu possa ... fingendo che questo possa aumentare il mio cane di tipo astratto Animale, vediamo cosa succede

Ora il problema si presenta se provo a fare questo:

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

animal1.isSibling(animal2) // error

Questo genererà un errore .

Perché? Bene, la ragione è che quando chiami animal1.isSibling(animal2)Swift non sai se gli animali sono cani, gatti o altro. Per quanto Swift sa, animal1e animal2potrebbero essere specie animali non correlate . Dal momento che non possiamo confrontare animali di diversi tipi (vedi sopra). Questo errore

Come some Trisolve questo problema

Riscriviamo la funzione precedente:

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

animal1.isSibling(animal2)

animal1e non loanimal2 sono , ma sono la classe che implementa l'Animale . Animal

Ciò che ti consente di fare ora è quando chiami animal1.isSibling(animal2), Swift lo sa animal1e animal2sono dello stesso tipo.

Quindi il modo in cui mi piace pensarci:

some Tconsente a Swift di sapere quale implementazione Tviene utilizzata ma l'utente della classe no.

(Disclaimer sull'autopromozione) Ho scritto un post sul blog che approfondisce un po 'di più (lo stesso esempio qui) su questa nuova funzione


2
Quindi la tua idea è che il chiamante può trarre vantaggio dal fatto che due chiamate alla funzione restituiscono lo stesso tipo anche se il chiamante non sa di che tipo è?
matt

1
@matt essenzialmente yup. Stesso concetto quando viene utilizzato con campi, ecc. - al chiamante viene garantita che il tipo restituito sarà sempre dello stesso tipo ma non rivela esattamente quale sia il tipo.
Downgoat,

@Downgoat grazie mille per il post e la risposta perfetti. Come ho capito somenel tipo di ritorno funziona come vincolo al corpo della funzione. Quindi somerichiede di restituire solo un tipo concreto in tutto il corpo della funzione. Ad esempio: se esiste, return randomDogtutti gli altri ritorni devono funzionare solo con Dog. Tutti i vantaggi derivano da questo vincolo: disponibilità animal1.isSibling(animal2)e beneficio della compilazione di func animalFromAnimalFamily() -> some Animal(perché ora Selfviene definito sotto il cofano). È corretto?
Artem,

5
Questa linea era tutto ciò di cui avevo bisogno, animal1 e animal2 non sono animali, ma sono la classe che implementa gli animali, ora tutto ha un senso!
aross

29

La risposta di Hamish è piuttosto fantastica e risponde alla domanda da un punto di vista tecnico. Vorrei aggiungere alcune considerazioni sul perché la parola chiave someviene utilizzata in questo particolare posto nei tutorial SwiftUI di Apple e perché è una buona pratica da seguire.

some non è un requisito!

Prima di tutto, non è necessario dichiarare il bodytipo restituito come tipo opaco. È sempre possibile restituire il tipo concreto invece di utilizzare il some View.

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

Anche questo verrà compilato. Quando guardi Viewnell'interfaccia di, vedrai che il tipo restituito bodyè un tipo associato:

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

Ciò significa che si specifica questo tipo annotando la bodyproprietà con un tipo particolare di propria scelta. L'unico requisito è che questo tipo deve implementare il Viewprotocollo stesso.

Questo può essere un tipo specifico che implementa View, ad esempio

  • Text
  • Image
  • Circle
  • ...

o un tipo opaco che implementa View, vale a dire

  • some View

Visualizzazioni generiche

Il problema sorge quando proviamo a utilizzare una vista di stack come bodytipo di restituzione, come VStacko HStack:

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

Questo non verrà compilato e otterrai l'errore:

Il riferimento al tipo generico 'VStack' richiede argomenti in <...>

Questo perché le visualizzazioni di stack in SwiftUI sono tipi generici ! 💡 (E lo stesso vale per Elenchi e altri tipi di vista contenitore.)

Ciò ha molto senso perché è possibile collegare un numero qualsiasi di visualizzazioni di qualsiasi tipo (purché conforme al Viewprotocollo). Il tipo concreto di VStacknel corpo sopra è in realtà

VStack<TupleView<(Text, Image)>>

Quando in seguito decidiamo di aggiungere una vista allo stack, il suo tipo concreto cambia. Se aggiungiamo un secondo testo dopo il primo, otteniamo

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

Anche se facciamo una piccola modifica, qualcosa di sottile come l'aggiunta di un distanziatore tra il testo e l'immagine, il tipo di pila cambia:

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

Da quello che posso dire, questo è il motivo per cui Apple consiglia nei loro tutorial di usare sempre some View, il tipo opaco più generale che tutte le viste soddisfano, come il bodytipo di ritorno. È possibile modificare l'implementazione / il layout della vista personalizzata senza modificare manualmente il tipo di ritorno ogni volta.


Supplemento:

Se vuoi ottenere una comprensione più intuitiva dei tipi di risultati opachi, di recente ho pubblicato un articolo che potrebbe valere la pena di leggere:

🔗 Cos'è questo "alcuni" in SwiftUI?


2
Questo. Grazie! La risposta di Hamish è stata molto completa, ma la tua mi dice esattamente perché è usata in questi esempi.
Chris Marshall,

Adoro l'idea di "alcuni". Qualche idea se l'uso di "alcuni" influisce sul tempo di compilazione?
Tofu Warrior,

@Mischa, quindi come creare viste generiche? con un protocollo che contiene punti di vista e altri comportamenti?
theMouk

27

Penso che tutte le risposte finora mancanti siano someutili principalmente in qualcosa come un DSL (linguaggio specifico del dominio) come SwiftUI o una libreria / framework, che avrà utenti (altri programmatori) diversi da te.

Probabilmente non useresti mai somenel tuo normale codice app, tranne forse nella misura in cui può avvolgere un protocollo generico in modo che possa essere usato come tipo (anziché solo come vincolo di tipo). Quello che somefa è lasciare che il compilatore mantenga una conoscenza di che tipo specifico è qualcosa, mettendo una facciata di supertipo di fronte.

Quindi in SwiftUI, dove sei l'utente, tutto ciò che devi sapere è che qualcosa è un some View, mentre dietro le quinte può nascere ogni sorta di fazzoletto da cui sei protetto. Questo oggetto è in realtà un tipo molto specifico, ma non avrai mai bisogno di sapere di cosa si tratta. Tuttavia, a differenza di un protocollo, è un tipo a tutti gli effetti, perché ovunque appaia è semplicemente una facciata per un tipo specifico a tutti gli effetti.

In una versione futura di SwiftUI, dove ti aspetti un some View, gli sviluppatori potrebbero cambiare il tipo sottostante di quel particolare oggetto. Ma questo non romperà il tuo codice, perché il tuo codice non ha mai menzionato il tipo sottostante in primo luogo.

Pertanto, somein effetti rende un protocollo più simile a una superclasse. È quasi un tipo di oggetto reale, anche se non del tutto (ad esempio, la dichiarazione del metodo di un protocollo non può restituire a some).

Quindi, se si andavano a usare someper qualsiasi cosa, sarebbe molto probabilmente se si stesse scrivendo un modem DSL o quadro / libreria per l'utilizzo da parte di altri, e si voleva mascherare sottostanti dettagli tipo. Ciò renderebbe il tuo codice più semplice da usare per gli altri e ti consentirebbe di modificare i dettagli dell'implementazione senza romperne il codice.

Tuttavia, potresti anche usarlo nel tuo codice come un modo per proteggere una regione del tuo codice dai dettagli di implementazione sepolti in un'altra regione del tuo codice.


23

La someparola chiave di Swift 5.1 ( proposta di evoluzione rapida ) viene utilizzata insieme a un protocollo come tipo di ritorno.

Le note di rilascio di Xcode 11 lo presentano così:

Le funzioni possono ora nascondere il loro tipo restituito concreto dichiarando a quali protocolli è conforme, invece di specificare il tipo esatto restituito:

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

Il codice che chiama la funzione può utilizzare l'interfaccia del protocollo, ma non ha visibilità sul tipo sottostante. ( SE-0244 , 40538331)

Nell'esempio sopra, non è necessario dire che si restituirà un Array. Ciò consente di restituire anche un tipo generico a cui è appena conforme Collection.


Nota anche questo possibile errore che potresti riscontrare:

I tipi di restituzione "alcuni" sono disponibili solo in iOS 13.0.0 o versioni successive

Significa che dovresti usare la disponibilità per evitare somesu iOS 12 e prima:

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

1
Mille

1
Dovresti utilizzare la disponibilità per evitare somesu iOS 12 e versioni precedenti. Finché lo fai, dovresti stare bene. Il problema è solo che il compilatore non ti avverte di farlo.
matt

2
Inoltre, proprio come fai notare, la descrizione sintetica di Apple spiega tutto: le funzioni ora possono nascondere il loro tipo di ritorno concreto dichiarando a quali protocolli è conforme, invece di specificare il tipo di ritorno esatto. E quindi il codice che chiama la funzione può utilizzare l'interfaccia del protocollo. Pulito e poi alcuni.
Fattie,

Questo (nascondere il tipo di ritorno concreto) è già possibile senza usare la parola chiave "alcuni". Non spiega l'effetto dell'aggiunta di "alcuni" nella firma del metodo.
Vince O'Sullivan,

@ VinceO'Sullivan Non è possibile rimuovere la someparola chiave in questo esempio di codice in Swift 5.0 o Swift 4.2. L'errore sarà: "Il protocollo 'Raccolta' può essere utilizzato solo come vincolo generico perché ha requisiti di tipo Self o associati "
Cœur

2

'some' significa tipo opaco. In SwiftUI, View è dichiarato come protocollo

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

Quando crei la tua vista come Struct, ti conformi al protocollo View e dici che il corpo var restituirà qualcosa che confermerà il View Protocol. È come un'astrazione del protocollo generico in cui non è necessario definire il tipo concreto.


2

Proverò a rispondere con un esempio pratico di base (di cosa tratta questo tipo di risultato opaco )

Supponendo di avere un protocollo con tipo associato e due strutture che lo implementano:

protocol ProtocolWithAssociatedType {
    associatedtype SomeType
}

struct First: ProtocolWithAssociatedType {
    typealias SomeType = Int
}

struct Second: ProtocolWithAssociatedType {
    typealias SomeType = String
}

Prima di Swift 5.1, di seguito è illegale a causa di un ProtocolWithAssociatedType can only be used as a generic constrainterrore:

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

Ma in Swift 5.1 va bene ( someaggiunto):

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

Sopra è l'uso pratico, ampiamente utilizzato in SwiftUI per some View.

Ma c'è una limitazione importante: il tipo di ritorno deve essere conosciuto al momento della compilazione, quindi di seguito non funzionerà dando Function declares an opaque return type, but the return statements in its body do not have matching underlying typeserrore:

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

0

Un semplice caso d'uso che viene in mente è la scrittura di funzioni generiche per tipi numerici.

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.