Що таке ключове слово `some` у Swift (UI)?


259

Новий підручник SwiftUI має такий код:

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

У другому рядку слово some, а на їхньому сайті виділено так, ніби це ключове слово.

Схоже, у Swift 5.1 немає someключового слова, і я не бачу, що ще someможе там робити слово , оскільки воно йде туди, куди зазвичай йде тип. Чи є нова, не оголошена версія Swift? Це функція, яка використовується для типу способом, про який я не знав?

Що робить ключове слово some?


Для тих, хто запаморочив тему, ось дуже дешифруюча та покрокова стаття завдяки Вадиму Булавіну. vadimbulavin.com/…
Люк-Олів'є

Відповіді:


333

some Viewце непрозорий тип результатів , представлений SE-0244 і доступний у Swift 5.1 з Xcode 11. Ви можете подумати про це як про "зворотний" загальний заповнювач.

На відміну від звичайного загального заповнювача, який задовольняє абонент:

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.

Непрозорий тип результату - це неявний загальний заповнювач місця, задоволений реалізацією , тому ви можете подумати про це:

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

як виглядає так:

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

Насправді, кінцева мета цієї функції полягає в тому, щоб дозволити зворотні дженнерики в цій більш явній формі, що також дозволить вам додати обмеження, наприклад -> <T : Collection> T where T.Element == Int. Дивіться цю публікацію для отримання додаткової інформації .

Головне , щоб забрати з цього є те, що функція , яка повертає some Pодне , що повертає значення певного одного типу бетону , який відповідає P. Спроба повернути різні відповідні типи в межах функції призводить до помилки компілятора:

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

Оскільки неявний загальний заповнювач місця не може бути задоволений кількома типами.

Це на відміну від функції P, що повертається , яку можна використовувати для представлення обох S1 і S2тому, що вона представляє довільне Pвідповідне значення:

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

Гаразд, так які переваги мають непрозорі типи результатів -> some Pнад типом повернення протоколу -> P?


1. Непрозорі типи результатів можна використовувати з PAT

Основним обмеженням поточних протоколів є те, що PAT (протоколи з пов'язаними типами) не можуть використовуватися як фактичні типи. Хоча це обмеження, яке, швидше за все, буде скасовано в майбутній версії мови, оскільки непрозорі типи результатів є фактично просто загальними заповнювачами, їх можна використовувати з PAT сьогодні.

Це означає, що ви можете робити такі речі, як:

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

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

2. Непрозорі типи результатів мають тотожність

Оскільки непрозорі типи результатів примушують повернути один конкретний тип, компілятор знає, що два виклики до однієї функції повинні повернути два значення одного типу.

Це означає, що ви можете робити такі речі, як:

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

Це законно, оскільки укладач знає, що і те, xі інше yмають однаковий тип. Це важлива вимога ==, коли обидва параметри типу Self.

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

Це означає, що він очікує двох значень, які є однотипними як тип бетону. Навіть якби ви Equatableвикористовувались як тип, ви б не змогли порівняти два довільних Equatableвідповідних значення, наприклад:

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.

Оскільки компілятор не може довести, що два довільних Equatableзначення мають однаковий базовий конкретний тип.

Аналогічним чином, якщо ми ввели ще одну непрозору функцію повернення типу:

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

Приклад стає незаконним , тому що хоча обидва fooі barповернення some Equatable, їх «зворотним» загальних наповнювачі Output1і Output2може бути задоволені різними типами.


3. Непрості типи результатів складаються із загальними заповнювачами

На відміну від регулярних типових протокольних значень, непрозорі типи результатів добре поєднуються із звичайними загальними заповнювачами, наприклад:

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.

Це не спрацювало, якби makePщойно повернулися P, оскільки два Pзначення можуть мати різні основні типи конкретних, наприклад:

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.

Навіщо використовувати непрозорий тип результату над типом бетону?

У цей момент ви можете думати собі, чому б просто не написати код як:

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

Добре, використання непрозорого типу результату дозволяє зробити тип Sдеталі реалізації, викривши лише інтерфейс, наданий компанією P, надаючи гнучкість зміни конкретного типу пізніше вниз по лінії, не порушуючи коду, який залежить від функції.

Наприклад, ви можете замінити:

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

з:

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

не порушуючи жодного коду, який дзвонить makeP().

Дивіться розділ Непрозорі типи в мовному посібнику та пропозицію про швидку еволюцію для отримання додаткової інформації про цю функцію.


20
Непов’язано: Станом на Swift 5.1, returnне потрібно в
одновиражних

3
Але в чому різниця між: func makeP() -> some Pі func makeP() -> P? Я прочитав пропозицію, і не бачу цієї різниці для їхніх зразків.
Артем


2
Поводження зі швидкими рухами - це безлад. Чи справді ця специфіка є чимось, що не може бути оброблений під час компіляції? Див. C # для посилання, він обробляє всі ці випадки неявно за допомогою простого синтаксису. Стріфти повинні мати безглуздо явний майже вантажо-культовий синтаксис, що справді заплутує мову. Чи можете ви також пояснити обґрунтування проекту для цього? (Якщо у вас є посилання на пропозицію в github, це теж було б добре) Редагувати: Просто помітив, що вона пов’язана вгорі.
SacredGeometry

2
@ Zmaster Компілятор розглядає два непрозорі типи повернення як різні, навіть якщо реалізація для обох повертає один і той же конкретний тип. Іншими словами, вибраний конкретний тип приховується від абонента. (Я маю на увазі розширити свою останню половину своєї відповіді, щоб зробити такі речі трохи більш явними, але до цього часу ще не обійшли).
Гаміш

52

Інша відповідь someдопомагає пояснити технічний аспект нового ключового слова, але ця відповідь спробує легко пояснити, чому .


Скажімо, у мене є протокол Animal, і я хочу порівнювати, якщо дві тварини є рідними братами:

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

Таким чином, має сенс лише порівнювати, якщо дві тварини є рідними братами, якщо вони одного типу тварин.


Тепер дозвольте створити приклад тварини лише для довідки

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

Шлях без some T

Тепер скажімо, у мене є функція, яка повертає тварину з "сім'ї".

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

Примітка: ця функція насправді не компілюється. Це тому, що перед додаванням функції "деякий" ви не можете повернути тип протоколу, якщо в протоколі використовується "Self" або generics . Але скажімо, що ви можете ... прикидаючи це оновленням myDog на абстрактний тип Animal, давайте подивимося, що відбувається

Тепер питання виникає, якщо я спробую це зробити:

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

animal1.isSibling(animal2) // error

Це призведе до помилки .

Чому? Ну і причина в тому, що, коли ви телефонуєте animal1.isSibling(animal2)Свіфт, не знаєте, чи є тварини собаками, кішками чи ще. Наскільки Свіфт знає, animal1і це animal2можуть бути неспоріднені види тварин . Оскільки ми не можемо порівнювати тварин різних типів (див. Вище). Це призведе до помилки

Як some Tвирішує цю проблему

Перепишемо попередню функцію:

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

animal1.isSibling(animal2)

animal1і animal2це НЕ Animal , але вони відносяться до класу , який реалізує тварин .

Що тепер дозволяє вам робити це коли ви телефонуєте animal1.isSibling(animal2), Свіфт знає це animal1і animal2є одного типу.

Тож те, як мені подобається думати про це:

some Tдозволяє Swift знати, для чого використовується реалізація T, але користувач класу цього не робить.

(Відмова від самореклами ) На цій новій функції я написав публікацію в блозі, яка трохи більше заглиблюється (такий же приклад, як тут).


2
Отже, ваша ідея полягає в тому, що абонент може скористатися тим, що два виклики до функції повертають один і той же тип, навіть якщо абонент не знає, що це за тип?
мат

1
@matt по суті. Те ж саме поняття при використанні з полями тощо - абоненту надається гарантія того, що тип повернення завжди буде одного типу, але не розкриває, що саме є типом.
Пуховик

@Downgoat дякую вам за ідеальний пост та відповідь. Як я зрозумів someу відповідь, тип працює як обмеження для функції. Тому someпотрібно повернути лише один конкретний тип у цілому функціональному органі. Наприклад: якщо є, return randomDogто всі інші віддачі повинні працювати лише з Dog. Усі переваги випливають із цього обмеження: доступність animal1.isSibling(animal2)та користь компіляції func animalFromAnimalFamily() -> some Animal(бо тепер Selfце визначається під кришкою). Це правильно?
Артем

5
Цей рядок був усім, що мені було потрібно, animal1 та animal2 - це не Animal, але це клас, який реалізує Animal, тепер все має сенс!
aross

29

Відповідь Хаміша досить приголомшлива і відповідає на питання з технічної точки зору. Я хотів би додати кілька думок щодо того, чому ключове словоsome використовується саме в цьому навчальному посібнику SwiftUI від Apple, і чому слід дотримуватися належної практики.

some це не вимога!

Перш за все, вам не потрібно оголошувати bodyтип повернення як непрозорий тип. Ви завжди можете повернути тип бетону, а не використовувати some View.

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

Це також буде складено. Коли ви заглянете в Viewінтерфейс 's, ви побачите, що тип повернення bodyасоційований тип:

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

Це означає, що ви визначаєте цей тип, анотуючиbody властивість для певного типу. Єдина вимога - цей тип повинен реалізувати сам Viewпротокол.

Наприклад, це може бути конкретний тип, який реалізуєтьсяView

  • Text
  • Image
  • Circle

або непрозорий тип, який реалізує View, тобто

  • some View

Загальні погляди

Проблема виникає, коли ми намагаємось використовувати подання стека як bodyтип повернення, наприклад, VStackабо HStack:

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

Це не компілюється, і ви отримаєте помилку:

Посилання на загальний тип "VStack" вимагає аргументів у <...>

Це тому, що представлення стека в SwiftUI - це загальний тип! 💡 (І те саме стосується списків та інших типів перегляду контейнерів.)

Це має багато сенсу, оскільки ви можете підключати будь-яку кількість переглядів будь-якого типу (до тих пір, поки це відповідає Viewпротоколу). Власне конкретний тип VStackкузова в тілі

VStack<TupleView<(Text, Image)>>

Коли ми згодом вирішимо додати вигляд до стеку, його конкретний тип змінюється. Якщо після першого додамо другий текст, отримаємо

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

Навіть якщо ми внесемо незначну зміну, щось настільки тонке, як додавання проміжку між текстом і зображенням, тип стека змінюється:

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

З того, що я можу сказати, це причина, чому Apple рекомендує у своїх підручниках завжди використовувати some View, найзагальніший непрозорий тип, який задовольняє всі погляди, якbody тип повернення. Ви можете змінити реалізацію / макет вашого власного перегляду, не змінюючи тип повернення щоразу вручну.


Доплата:

Якщо ви хочете отримати більш інтуїтивне розуміння непрозорих типів результатів, я нещодавно опублікував статтю, яку, можливо, варто прочитати:

🔗 Що це за "деякі" у SwiftUI?


2
Це. Дякую! Відповідь Хаміша була дуже повною, але ваша говорить мені точно, чому вона використовується в цих прикладах.
Кріс Маршалл

Мені подобається ідея "деяких". Будь-яка ідея, якщо використання "деяких" взагалі впливає на час збирання?
Воїн Тофу

@Mischa, то як зробити загальний вигляд? з протоколом, який містить перегляди та інші способи поведінки?
theMouk

27

Я думаю, що відсутні всі відповіді поки що, що someкорисне насамперед у чомусь на зразок DSL (мови, що залежить від домену), наприклад, SwiftUI або бібліотеки / фреймворку, у якому користувачі (інші програмісти) будуть відрізнятися від вас самих.

Ви, ймовірно, ніколи не використовуєте someу своєму звичайному коді додатка, за винятком можливо, наскільки він може обернути загальний протокол, щоб він міг використовуватися як тип (а не як обмеження типу). Що someпотрібно зробити, це дозволити компілятору тримати знання про те, що конкретний тип є, при цьому ставивши перед ним фасад супертипу.

Таким чином, у SwiftUI, де ви є користувачем, все, що вам потрібно знати, - це те, що щось є some View, тоді як за лаштунками можуть продовжуватися всілякі ганчікі, від яких ви захищені. Цей об’єкт насправді дуже специфічний тип, але вам ніколи не потрібно буде чути про те, що це таке. Але, на відміну від протоколу, він є повноцінним типом, тому що де б він не з’явився, це просто фасад для якогось конкретного повноцінного типу.

У майбутній версії SwiftUI, де ви очікуєте some View, розробники можуть змінити базовий тип саме цього об’єкта. Але це не порушить ваш код, оскільки ваш код ніколи не згадував базовий тип.

Таким чином, someпо суті, протокол більше нагадує суперклас. Це майже реальний тип об'єкта, хоча і не зовсім (наприклад, декларація методу протоколу не може повернути a some).

Отже, якщо ви збиралися використовувати someдля чого завгодно, це, швидше за все, ви будете писати DSL або фреймворк / бібліотеку для використання іншими, і ви хочете замаскувати основні деталі типу. Це зробить ваш код більш простим у використанні для інших та дозволить вам змінювати деталі реалізації, не порушуючи їх код.

Однак ви також можете використовувати його у власному коді як спосіб захисту однієї області коду від деталей реалізації, захованих в іншій області коду.


22

someКлючове слово з Swift 5.1 ( скоро-еволюція пропозицію ) використовуються в поєднанні з протоколом в якості типу що повертається значення .

Примітки до випуску Xcode 11 представляють це так:

Тепер функції можуть приховати їх конкретний тип повернення, оголосивши, яким протоколам він відповідає, а не вказувати точний тип повернення:

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

Код, який викликає функцію, може використовувати інтерфейс протоколу, але не має видимості для базового типу. ( SE-0244 , 40538331)

У наведеному вище прикладі вам не потрібно говорити про те, що ви збираєтесь повернути Array. Це дозволяє вам навіть повернути загальний тип, який просто відповідає Collection.


Зверніть увагу також на цю можливу помилку, з якою ви можете зіткнутися:

"деякі" типи повернення доступні лише в iOS 13.0.0 або новіших версіях

Це означає, що ви повинні використовувати доступність, щоб уникати someна iOS 12 і раніше:

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

1
Велике спасибі за цю цілеспрямовану відповідь та проблему компілятора в Xcode 11 beta
brainray

1
Ви повинні використовувати доступність, щоб уникати someна iOS 12 і раніше. Поки ти будеш, ти повинен бути добре. Проблема полягає лише в тому, що компілятор не попереджає вас це робити.
мат

2
Cœur, так само, як ви зазначаєте, стислий опис Apple пояснює це все: Функції тепер можуть приховувати свій конкретний тип повернення, оголошуючи, яким протоколам він відповідає, замість того, щоб вказувати точний тип повернення. І тоді код виклику функції може використовувати інтерфейс протоколу. Акуратний, а потім якийсь.
Fattie

Це (приховування конкретного типу повернення) вже можливе без використання ключового слова "деякі". Це не пояснює ефект додавання "деяких" у підпис методу.
Вінс О'Салліван

@ VinceO'Sullivan Неможливо видалити someключове слово з даного зразка коду у Swift 5.0 або Swift 4.2. Помилка буде: " Протокол" Колекція "може використовуватися лише як загальне обмеження, оскільки він має вимоги до власного або асоційованого типу "
Cœur

2

'деякий' означає непрозорий тип. У SwiftUI View задекларовано як протокол

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

Коли ви створюєте свій погляд як Struct, ви відповідаєте протоколу View і повідомляєте, що var тіло поверне щось, що буде підтверджувати View Protocol. Це як узагальнена абстракція протоколу, де не потрібно визначати конкретний тип.


1

Я спробую відповісти на це дуже базовим практичним прикладом (про що це непрозорий тип результату )

Якщо припустити, що у вас є протокол із асоційованим типом, і дві структури, що реалізують його:

protocol ProtocolWithAssociatedType {
    associatedtype SomeType
}

struct First: ProtocolWithAssociatedType {
    typealias SomeType = Int
}

struct Second: ProtocolWithAssociatedType {
    typealias SomeType = String
}

Перед Swift 5.1, нижче, це незаконно через ProtocolWithAssociatedType can only be used as a generic constraintпомилку:

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

Але в Swift 5.1 це добре ( someдодано):

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

Вище є практичне використання, широко використовується в SwiftUI для some View.

Але є одне важливе обмеження - тип повернення потрібно знати під час компіляції, тому нижче знову не вийде Function declares an opaque return type, but the return statements in its body do not have matching underlying typesпомилки:

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

0

Простий варіант використання, який спадає на думку, - це написання загальних функцій для числових типів.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.