Co to jest słowo kluczowe `some` w Swift (UI)?


259

Nowy samouczek SwiftUI zawiera następujący kod:

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

W drugim wierszu słowo somei na ich stronie jest podświetlone, jakby było słowem kluczowym.

Swift 5.1 nie wydaje się być somesłowem kluczowym i nie widzę, co jeszcze słowo somemogłoby tam robić, ponieważ idzie tam, gdzie zwykle idzie typ. Czy jest nowa, niezapowiedziana wersja Swift? Czy jest to funkcja używana w typie w sposób, o którym nie wiedziałam?

Co robi słowo kluczowe some?


Dla tych, którzy byli oszołomieni tematem, tutaj bardzo odszyfrowujący i krok po kroku artykuł dzięki Vadimowi Bulavinowi. vadimbulavin.com/…
Luc-Olivier

Odpowiedzi:


333

some Viewjest nieprzezroczystym typem wyniku wprowadzonym przez SE-0244 i jest dostępny w Swift 5.1 z Xcode 11. Można to traktować jako ogólny symbol zastępczy „odwrotnej”.

W przeciwieństwie do zwykłego ogólnego symbolu zastępczego, który jest wywoływany przez osobę dzwoniącą:

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.

Nieprzezroczysty typ wyniku to domyślny ogólny symbol zastępczy spełniony przez implementację , więc możesz pomyśleć o tym:

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

wygląda tak:

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

W rzeczywistości ostatecznym celem tej funkcji jest zezwolenie na odwrotne generyczne w tej bardziej wyraźnej formie, co pozwoliłoby również na dodanie ograniczeń, np -> <T : Collection> T where T.Element == Int. Zobacz ten post, aby uzyskać więcej informacji .

Najważniejszą rzeczą do usunięcia jest some Pto, że zwracana funkcja to taka, która zwraca wartość określonego pojedynczego konkretnego typu, który jest zgodny P. Próba zwrócenia różnych zgodnych typów w ramach funkcji powoduje błąd kompilatora:

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

Ponieważ domyślny ogólny symbol zastępczy nie może być spełniony przez wiele typów.

Jest to w przeciwieństwie do zwracanej funkcji P, która może być używana do reprezentowania obu S1 i S2ponieważ reprezentuje dowolną Pzgodną wartość:

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

Okej, więc jakie korzyści mają nieprzejrzyste typy wyników w -> some Pporównaniu do typów zwracanych przez protokół -> P?


1. Nieprzezroczyste typy wyników mogą być używane z PAT

Głównym bieżącym ograniczeniem protokołów jest to, że PAT (protokoły z powiązanymi typami) nie mogą być używane jako rzeczywiste typy. Chociaż jest to ograniczenie, które prawdopodobnie zostanie zniesione w przyszłej wersji języka, ponieważ nieprzezroczyste typy wyników są w rzeczywistości zwykłymi symbolami zastępczymi, można je dziś stosować z PAT.

Oznacza to, że możesz robić takie rzeczy jak:

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

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

2. Nieprzezroczyste typy wyników mają tożsamość

Ponieważ nieprzezroczyste typy wyników wymuszają zwrócenie jednego konkretnego typu, kompilator wie, że dwa wywołania tej samej funkcji muszą zwrócić dwie wartości tego samego typu.

Oznacza to, że możesz robić takie rzeczy jak:

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

Jest to legalne, ponieważ kompilator wie o tym xi yma ten sam konkretny typ. Jest to ważny wymóg ==, w przypadku gdy oba parametry typu Self.

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

Oznacza to, że oczekuje dwóch wartości, które są tego samego typu, co typ zgodny z betonem. Nawet jeśli Equatablebyłyby użyteczne jako typ, nie byłoby możliwe porównanie dwóch dowolnych Equatablezgodnych wartości, na przykład:

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.

Ponieważ kompilator nie może udowodnić, że dwie dowolne Equatablewartości mają ten sam podstawowy typ betonu.

W podobny sposób, jeśli wprowadzimy inną funkcję zwracania typu nieprzezroczystego:

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

Przykład staje się nielegalny, ponieważ mimo fooi oba barzwracają some Equatable, ich „odwrócone” ogólne symbole zastępcze Output1i Output2mogą być zaspokojone przez różne typy.


3. Nieprzezroczyste typy wyników składają się z ogólnych symboli zastępczych

W przeciwieństwie do zwykłych wartości typowych dla protokołu, nieprzezroczyste typy wyników dobrze komponują się ze zwykłymi rodzajowymi symbolami zastępczymi, na przykład:

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.

To nie zadziałałoby, gdyby makePwłaśnie wróciło P, ponieważ dwie Pwartości mogą mieć różne podstawowe typy betonu, na przykład:

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.

Dlaczego warto stosować nieprzejrzysty typ wyniku zamiast rodzaju betonu?

W tym momencie możesz pomyśleć, dlaczego nie napisać kodu jako:

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

Cóż, użycie nieprzezroczystego typu wyniku pozwala uczynić ten typ Sszczegółem implementacji, odsłaniając tylko interfejs zapewniony przez P, co daje elastyczność zmiany konkretnego typu później w dół linii bez łamania kodu zależnego od funkcji.

Na przykład możesz zastąpić:

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

z:

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

bez łamania wywoływanego kodu makeP().

Więcej informacji na temat tej funkcji znajduje się w sekcji Typy nieprzezroczyste przewodnika językowego i propozycji Szybkiej ewolucji .


20
Niespowiązany: od wersji Swift 5.1 returnnie jest wymagany w funkcjach z jednym wyrażeniem
ielyamani,

3
Ale jaka jest różnica między: func makeP() -> some Pa func makeP() -> P? Przeczytałem propozycję i nie widzę tej różnicy także dla ich próbek.
Artem


2
Obsługa jerzyków to bałagan. Czy ta specyfika jest naprawdę czymś, z czym nie można sobie poradzić w czasie kompilacji? Zobacz C # w celach informacyjnych, obsługuje domyślnie wszystkie te przypadki za pomocą prostej składni. Jerzyki muszą mieć niepotrzebnie wyraźną składnię, niemal kultystyczną, która naprawdę zaciemnia język. Czy możesz również wyjaśnić uzasadnienie projektu? (Jeśli masz link do propozycji w github, to też byłoby dobre) Edycja: Zauważyłem, że link jest na górze.
SacredGeometry

2
@Zmaster Kompilator będzie traktował dwa nieprzezroczyste typy zwrotów jako różne, nawet jeśli implementacja dla obu zwraca ten sam konkretny typ. Innymi słowy, wybrany konkretny rodzaj betonu jest ukryty przed dzwoniącym. (Chciałem rozwinąć drugą połowę mojej odpowiedzi, aby uczynić takie rzeczy nieco bardziej wyraźnymi, ale jeszcze się do tego nie przyzwyczaiłem).
Hamish

52

Druga odpowiedź dobrze wyjaśnia techniczny aspekt nowego somesłowa kluczowego, ale ta odpowiedź będzie próbowała łatwo wyjaśnić, dlaczego .


Powiedzmy, że mam protokół Animal i chcę porównać, czy dwa zwierzęta są rodzeństwem:

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

W ten sposób sensowne jest porównanie, czy dwa zwierzęta są rodzeństwem, jeśli są tego samego rodzaju .


Teraz pozwól mi po prostu stworzyć przykład zwierzęcia tylko dla odniesienia

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

Droga bez some T

Powiedzmy teraz, że mam funkcję, która zwraca zwierzę z „rodziny”.

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

Uwaga: ta funkcja nie będzie się właściwie kompilować. Dzieje się tak, ponieważ przed dodaniem funkcji „niektóre” nie można zwrócić typu protokołu, jeśli protokół używa „Ja” lub ogólnych . Ale powiedzmy, że możesz ... udając, że to upcasty myDog do abstrakcyjnego typu Animal, zobaczmy, co się stanie

Teraz pojawia się problem, jeśli spróbuję to zrobić:

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

animal1.isSibling(animal2) // error

Spowoduje to błąd .

Czemu? Powodem jest to, że kiedy dzwonisz do animal1.isSibling(animal2)Swift, nie wie, czy zwierzęta to psy, koty, czy cokolwiek innego. O ile Swift wie animal1i animal2mogą to być niepowiązane gatunki zwierząt . Ponieważ nie możemy porównywać zwierząt różnych typów (patrz wyżej). To spowoduje błąd

Jak some Trozwiązuje ten problem

Przepiszmy poprzednią funkcję:

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

animal1.isSibling(animal2)

animal1i nieanimal2 są , ale są klasą, która implementuje Animal . Animal

Teraz możesz to zrobić, kiedy dzwonisz animal1.isSibling(animal2), Swift wie o tym animal1i animal2jest tego samego typu.

Tak więc lubię o tym myśleć:

some Tinformuje Swift o tym, jaka implementacja Tjest używana, ale użytkownik klasy nie.

(Wyłączenie odpowiedzialności za autopromocję) Napisałem post na blogu, który bardziej szczegółowo przedstawia tę nową funkcję


2
Twoim pomysłem jest to, że program wywołujący może skorzystać z faktu, że dwa wywołania funkcji zwracają ten sam typ, nawet jeśli program wywołujący nie wie, jaki to typ?
mat

1
@matt zasadniczo tak. Ta sama koncepcja, gdy jest używana z polami itp. - osoba wywołująca ma gwarancję, że typ zwracany będzie zawsze tego samego typu, ale nie ujawnia dokładnie, jaki jest typ.
Downgoat

@Downgoat dziękuję bardzo za doskonały post i odpowiedź. Jak rozumiem somew zamian, typ działa jako ograniczenie treści funkcji. someWymaga więc zwrócenia tylko jednego konkretnego typu w całym ciele funkcji. Na przykład: jeśli tak, return randomDogwszystkie inne zwroty muszą działać tylko z Dog. Wszystkie korzyści wynikają z tego ograniczenia: dostępność animal1.isSibling(animal2)i korzyść z kompilacji func animalFromAnimalFamily() -> some Animal(ponieważ teraz Selfzostanie zdefiniowana pod maską). Czy to jest poprawne?
Artem

5
Ta linia była wszystkim, czego potrzebowałem, animal1 i animal2 nie są Animal, ale są klasą, która implementuje Animal, teraz wszystko ma sens!
aross

29

Odpowiedź Hamisha jest niesamowita i odpowiada na pytanie z technicznego punktu widzenia. Chciałbym dodać kilka przemyśleń na temat tego, dlaczego słowo kluczowe somejest używane w tym konkretnym miejscu w samouczkach Apple SwiftUI i dlaczego warto go przestrzegać.

some nie jest wymaganiem!

Przede wszystkim nie musisz deklarować bodytypu zwracanego jako nieprzejrzystego. Zawsze możesz zwrócić konkretny typ zamiast używać some View.

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

To również się skompiluje. Gdy spojrzysz na Viewinterfejs, zobaczysz, że zwracanym typem bodyjest typ powiązany:

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

Oznacza to, że określasz ten typ, dodając adnotację do bodywłaściwości wybranego rodzaju. Jedynym wymaganiem jest to, że ten typ musi implementować Viewsam protokół.

Które mogą być albo specyficzny typ, który implementuje Viewna przykład

  • Text
  • Image
  • Circle

lub nieprzezroczysty typ, który implementuje View, tj

  • some View

Ogólne widoki

Problem pojawia się, gdy staramy się korzystać z widoku stosu jako body„s typ zwracany, jak VStacki HStack:

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

To się nie skompiluje i pojawi się błąd:

Odwołanie do typu ogólnego „VStack” wymaga argumentów w <...>

To dlatego, że widoki stosu w SwiftUI są rodzajami rodzajowymi ! 💡 (To samo dotyczy list i innych typów widoków kontenerów).

Ma to sens, ponieważ można podłączyć dowolną liczbę widoków dowolnego typu (o ile jest to zgodne z Viewprotokołem). Konkretny typ VStackpowyższego ciała jest w rzeczywistości

VStack<TupleView<(Text, Image)>>

Kiedy później decydujemy się dodać widok do stosu, zmienia się jego konkretny typ. Jeśli dodamy drugi tekst po pierwszym, otrzymamy

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

Nawet jeśli dokonamy drobnej zmiany, czegoś tak subtelnego jak dodanie odstępu między tekstem a obrazem, zmieni się rodzaj stosu:

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

Z tego, co mogę powiedzieć, to jest powód, dla którego Apple zaleca w swoich tutorialach, aby zawsze używać some Viewnajbardziej ogólnego nieprzezroczystego typu, który spełniają wszystkie widoki, jako bodytypu zwracanego. Możesz zmienić implementację / układ niestandardowego widoku bez ręcznej zmiany typu zwrotu za każdym razem.


Suplement:

Jeśli chcesz bardziej intuicyjnie zrozumieć nieprzejrzyste typy wyników, niedawno opublikowałem artykuł, który warto przeczytać:

🔗 Co to jest „trochę” w SwiftUI?


2
To. Dzięki! Odpowiedź Hamisha była bardzo kompletna, ale twoja mówi mi dokładnie, dlaczego została użyta w tych przykładach.
Chris Marshall

Podoba mi się pomysł „niektórych”. Masz jakiś pomysł, jeśli użycie „trochę” w ogóle wpływa na czas kompilacji?
Tofu Warrior

@Mischa, więc jak tworzyć widoki ogólne? z protokołem zawierającym widoki i inne zachowania?
theMouk

27

Myślę, że do tej pory brakuje wszystkich odpowiedzi, które somesą przydatne przede wszystkim w czegoś takiego jak DSL (język specyficzny dla domeny), taki jak SwiftUI lub biblioteka / framework, w którym użytkownicy (inni programiści) będą inni niż ty.

Prawdopodobnie nigdy nie użyłbyś somenormalnego kodu aplikacji, z wyjątkiem być może o tyle, o ile może on zawrzeć ogólny protokół, aby można go było używać jako typu (zamiast ograniczenia typu). Co somerobi to niech kompilator zachować wiedzę o tym, co specyficzny rodzaj coś jest, a oddanie supertypem elewację przed nim.

Zatem w SwiftUI, gdzie jesteś użytkownikiem, wszystko, co musisz wiedzieć, to że coś jest some View, podczas gdy za kulisami może się dziać wszelkiego rodzaju chanke-panky, przed którymi jesteś osłonięty. Ten obiekt jest w rzeczywistości bardzo specyficznym typem, ale nigdy nie musisz słyszeć o tym, co to jest. Jednak w przeciwieństwie do protokołu, jest to pełnoprawny typ, ponieważ gdziekolwiek się pojawia, jest jedynie fasadą dla jakiegoś konkretnego pełnoprawnego typu.

W przyszłej wersji SwiftUI, w której oczekujesz a some View, programiści mogą zmienić podstawowy typ tego konkretnego obiektu. Ale to nie zepsuje twojego kodu, ponieważ twój kod nigdy nie wspominał o typie podstawowym.

W someefekcie protokół bardziej przypomina nadklasę. Jest to prawie prawdziwy typ obiektu, choć niezupełnie (na przykład deklaracja metody protokołu nie może zwrócić a some).

Więc jeśli zamierzasz używać somedo czegokolwiek, najprawdopodobniej byłoby to, gdybyś pisał DSL lub framework / bibliotekę do użytku przez innych i chciałbyś zamaskować podstawowe szczegóły typu. Ułatwi to korzystanie z kodu innym użytkownikom i pozwoli na zmianę szczegółów implementacji bez łamania ich kodu.

Możesz jednak również użyć go we własnym kodzie jako sposobu ochrony jednego regionu kodu przed szczegółami implementacji zakopanymi w innym regionie kodu.


22

Słowo somekluczowe z Swift 5.1 ( propozycja swift-evolution ) jest używane w połączeniu z protokołem jako typ zwracany.

Xcode 11 Uwagi do wydania przedstawić to tak:

Funkcje mogą teraz ukryć swój konkretny typ zwrotu, deklarując, z którymi protokołami się zgadza, zamiast określać dokładny typ zwrotu:

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

Kod wywołujący funkcję może korzystać z interfejsu protokołu, ale nie ma wglądu w typ podstawowy. ( SE-0244 , 40538331)

W powyższym przykładzie nie musisz mówić, że zwrócisz Array. To pozwala nawet zwrócić ogólny typ, który jest po prostu zgodny Collection.


Zwróć również uwagę na ten możliwy błąd, który możesz napotkać:

„niektóre” typy zwrotów są dostępne tylko w systemie iOS 13.0.0 lub nowszym

Oznacza to, że należy korzystać z dostępności, aby uniknąć somena iOS 12 i wcześniej:

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

1
Wielkie dzięki za tę skoncentrowaną odpowiedź i problem kompilatora w Xcode 11 beta
brainray

1
Powinieneś korzystać z dostępności, aby uniknąć somena iOS 12 i wcześniejszych. Tak długo, jak to robisz, powinno być w porządku. Problem polega tylko na tym, że kompilator nie ostrzega cię przed tym.
mat

2
Oczywiście, jak wskazałeś, zwięzły opis Apple wyjaśnia wszystko: funkcje mogą teraz ukrywać swój konkretny typ zwracania, deklarując, z którymi protokołami się zgadza, zamiast określać dokładny typ zwrotu. Następnie funkcja wywołująca kod może użyć interfejsu protokołu. Schludnie, a potem trochę.
Fattie,

To (ukrywanie konkretnego typu zwrotu) jest już możliwe bez użycia słowa kluczowego „some”. Nie tłumaczy efektu dodania „części” do podpisu metody.
Vince O'Sullivan

@ VinceO'Sullivan Nie można usunąć somesłowa kluczowego w podanym przykładzie kodu w Swift 5.0 lub Swift 4.2. Błąd będzie: „ protokołem«zbieranie»może być używany tylko jako ogólne ograniczenia, ponieważ ma Jaźni lub związane wymagania typu
Coeur

2

„niektóre” oznaczają rodzaj nieprzezroczysty. W SwiftUI View jest deklarowany jako protokół

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

Kiedy tworzysz swój widok jako Struct, zgadzasz się z protokołem View i mówisz, że ciało var zwróci coś, co potwierdzi protokół View. To jak ogólna abstrakcja protokołu, w której nie trzeba definiować konkretnego typu.


1

Spróbuję odpowiedzieć na to bardzo podstawowym praktycznym przykładem (o czym jest ten nieprzejrzysty typ wyniku )

Zakładając, że masz protokół z powiązanym typem i dwie struktury go implementujące:

protocol ProtocolWithAssociatedType {
    associatedtype SomeType
}

struct First: ProtocolWithAssociatedType {
    typealias SomeType = Int
}

struct Second: ProtocolWithAssociatedType {
    typealias SomeType = String
}

Przed wersją Swift 5.1 poniżej jest nielegalne z powodu ProtocolWithAssociatedType can only be used as a generic constraintbłędu:

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

Ale w Swift 5.1 jest w porządku ( somedodano):

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

Powyżej znajduje się praktyczne zastosowanie, szeroko stosowane w SwiftUI dla some View.

Ale jest jedno ważne ograniczenie - zwracany typ musi być znany w czasie kompilacji, więc poniżej znowu nie będzie działać, dając Function declares an opaque return type, but the return statements in its body do not have matching underlying typesbłąd:

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

0

Prostym przypadkiem, który przychodzi mi do głowy, jest pisanie ogólnych funkcji dla typów numerycznych.

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

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.