Что такое ключевое слово `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().

См. Раздел «Непрозрачные типы» в руководстве по языку и предложении Swift Evolution для получения дополнительной информации об этой функции.

Хэмиш
источник
20
Не связано: с Swift 5.1 returnне требуется в функциях с одним выражением
ielyamani
3
Но в чем разница между: func makeP() -> some Pи func makeP() -> P? Я прочитал предложение и не вижу этой разницы для своих образцов тоже.
Артем
2
Обработка типа Swifts - беспорядок. Действительно ли эта специфичность не может быть обработана во время компиляции? См. 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`
}

Примечание: эта функция на самом деле не будет компилироваться. Это потому, что до того, как была добавлена ​​функция some, вы не можете вернуть тип протокола, если протокол использует 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), Swift знает это animal1и animal2относится к тому же типу.

Итак, как мне нравится думать об этом:

some Tпозволяет Swift знать, какая реализация Tиспользуется, а пользователь класса - нет.

(Отказ от ответственности за саморекламу) Я написал пост в блоге , в котором подробно рассказывается об этой новой функции (тот же пример, что и здесь)

Downgoat
источник
2
Итак, ваша идея заключается в том, что вызывающая сторона может использовать тот факт, что два вызова функции возвращают один и тот же тип, даже если вызывающая сторона не знает, какой это тип?
матовый
1
@matt по сути да. Та же концепция при использовании с полями и т. Д. Вызывающей стороне предоставляется гарантия того, что тип возвращаемого значения всегда будет одного и того же типа, но не раскрывается точно, что это за тип.
Вниз
@ Downgoat большое спасибо за идеальный пост и ответ. Как я понял someв return type работает как ограничение на тело функции. Поэтому someтребуется возвращать только один конкретный тип во всем теле функции. Например: если есть, return randomDogвсе остальные возвраты должны работать только с Dog. Все преимущества проистекают из этого ограничения: доступность animal1.isSibling(animal2)и польза от компиляции func animalFromAnimalFamily() -> some Animal(потому что теперь она Selfопределяется под капотом). Это правильно?
Артем
5
Эта строка была всем, что мне было нужно, animal1 и animal2 не Animal, но это класс, который реализует Animal, теперь все имеет смысл!
aross
29

Ответ Хэмиша довольно удивительный и отвечает на вопрос с технической точки зрения. Я хотел бы добавить некоторые соображения о том, почему ключевое слово someиспользуется в этом конкретном месте в учебных пособиях Apple по SwiftUI, и почему это хорошая практика для подражания.

some не является требованием!

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

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

Это также скомпилируется. Когда вы посмотрите на Viewинтерфейс, вы увидите, что возвращаемый тип 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?

Mischa
источник
2
Это. Спасибо! Ответ Хэмиша был очень полным, но ваш точно говорит мне, почему он используется в этих примерах.
Крис Маршалл
Мне нравится идея "некоторые". Любая идея, если использование «некоторых» влияет на время компиляции вообще?
Воин тофу
@ Миша так как сделать дженерики просмотров? с протоколом, который содержит представления и другие виды поведения?
TheMouk
27

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

Вы, вероятно, никогда не someбудете использовать в своем обычном коде приложения, за исключением, возможно, того, что он может обернуть универсальный протокол, чтобы его можно было использовать как тип (а не просто как ограничение типа). То, что someнужно, это дать компилятору возможность узнать, что это за тип, что-то особенное, в то же время поместив перед ним фасад супертипа.

Таким образом, в SwiftUI, где вы являетесь пользователем, все, что вам нужно знать, это то, что что-то есть some View, в то время как за кулисами может происходить всякое ханж-панки, от которого вы защищены. Этот объект на самом деле очень специфический тип, но вам никогда не придется слышать о том, что это такое. И все же, в отличие от протокола, это полноценный тип, потому что где бы он ни появлялся, это просто фасад для какого-то определенного полноценного типа.

В будущей версии SwiftUI, где вы ожидаете some View, разработчики могут изменить базовый тип этого конкретного объекта. Но это не сломает ваш код, потому что он никогда не упоминал базовый тип.

Таким образом, someфактически делает протокол больше похожим на суперкласс. Это почти реальный тип объекта, хотя и не совсем (например, объявление метода протокола не может вернуть a some).

Поэтому, если вы собираетесь использовать someчто-либо, скорее всего, если бы вы писали DSL или фреймворк / библиотеку для использования другими, и вы хотели замаскировать детали базового типа. Это упростит ваш код для использования другими и позволит вам изменить детали реализации, не нарушая их код.

Однако вы также можете использовать его в своем собственном коде для защиты одной области кода от деталей реализации, скрытых в другой области кода.

матовый
источник
23

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
brainray
1
Вы должны использовать доступность, чтобы избежать someна iOS 12 и раньше. Пока вы делаете, у вас все будет хорошо. Проблема только в том, что компилятор не предупреждает вас об этом.
матовый
2
Конечно, как вы указали, краткое описание Apple объясняет все это: теперь функции могут скрывать свой конкретный тип возврата, указав, каким протоколам он соответствует, вместо указания точного типа возврата. И тогда код, вызывающий функцию, может использовать интерфейс протокола. Аккуратный, а затем некоторые.
Толстяк
Это (скрытие конкретного возвращаемого типа) уже возможно без использования ключевого слова «некоторые». Это не объясняет эффект добавления «некоторых» в сигнатуру метода.
Винс О'Салливан
@ VinceO'Sullivan Невозможно удалить someключевое слово в данном примере кода в Swift 5.0 или Swift 4.2. Ошибка будет: « Протокол« Коллекция »может использоваться только в качестве общего ограничения, потому что он имеет Self или связанные требования типа »
Cœur
2

'some' означает непрозрачный тип. В 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. Это как общая абстракция протокола, где вам не нужно определять конкретный тип.

varunrathi28
источник
2

Я попытаюсь ответить на это очень простым практическим примером (что это за непрозрачный тип результата )

Предполагая, что у вас есть протокол со связанным типом и две структуры, реализующие его:

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()
    }
}
tzaloga
источник
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
Артем Илюмжинов
источник