Что такое ключевое слово `some` в SwiftUI?
Новый учебник SwiftUI имеет следующий код:
struct ContentView: View {
var body: some View {
Text("Hello World")
}
}
Вторая строка слова some
и на их сайте выделяется как бы ключевое слово.
Swift 5.1 не имеет some
в качестве ключевого слова, и я не вижу, что еще слово some
мог бы делать там, так как он идет туда, куда обычно идет тип. Есть ли новая, необъявленная версия Swift? Это функция, которая используется в типе способом, о котором я не знал?
Что значит ключевое слово some
делать?
17 ответов
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 для получения дополнительной информации об этой функции.
Другой ответ хорошо объясняет технический аспект нового some
ключевое слово, но этот ответ будет пытаться легко объяснить, почему.
Допустим, у меня есть протокол Animal, и я хочу сравнить, являются ли два животных братьями и сестрами:
protocol Animal {
func isSibling(with animal: Self) -> Bool
}
Таким образом, имеет смысл сравнивать только двух братьев и сестер, если они относятся к одному типу животных.
Теперь позвольте мне создать пример животного только для справки.
class Dog: Animal {
func isSibling(with 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
, но они класс, который реализует Animal.
Что это позволяет вам сделать сейчас, когда вы звоните animal1.isSibling(animal2)
Свифт знает, что animal1
а также animal2
того же типа.
Итак, как мне нравится думать об этом:
some T
позволяет Swift знать, что реализацияT
используется, но пользователь класса нет.
(Отказ от ответственности за саморекламу) Я написал пост в блоге, в котором рассказывается об этой новой функции немного подробнее (тот же пример, что и здесь)
Я думаю, что все ответы на данный момент отсутствуют, что some
полезен прежде всего в чем-то вроде DSL (предметно-ориентированного языка), такого как SwiftUI или библиотека / инфраструктура, в которой пользователи (другие программисты) будут отличаться от вас.
Вы, вероятно, никогда не будете использовать some
в вашем обычном коде приложения, за исключением, возможно, постольку, поскольку он может обернуть универсальный протокол, чтобы его можно было использовать как тип (а не просто как ограничение типа). какая some
Это означает, что компилятор должен знать, что это за конкретный тип, в то же время помещая перед ним фасад супертипа.
Таким образом, в SwiftUI, где вы являетесь пользователем, все, что вам нужно знать, это то, что some View
в то время как за кулисами может продолжаться всякое видение платка, от которого вы защищены. Этот объект на самом деле очень специфический тип, но вам никогда не придется слышать о том, что это такое. Тем не менее, в отличие от протокола, это полноценный тип, потому что везде, где он появляется, это просто фасад для какого-то определенного полноценного типа.
В будущей версии SwiftUI, где вы ожидаете some View
разработчики могут изменить базовый тип этого конкретного объекта. Но это не сломает ваш код, потому что он никогда не упоминал базовый тип.
Таким образом, some
фактически делает протокол более похожим на суперкласс. Это почти реальный тип объекта, хотя и не совсем (например, объявление метода протокола не может вернуть some
).
Так что, если вы собираетесь использовать some
скорее всего, это было бы, если бы вы писали DSL или фреймворк / библиотеку для использования другими, и вы хотели замаскировать детали базового типа. Это сделает ваш код более простым для использования другими и позволит вам изменить детали реализации, не нарушая их код.
Тем не менее, вы также можете использовать его в своем собственном коде для защиты одной области кода от деталей реализации, скрытых в другой области кода.
Ответ Хэмиша довольно удивительный и отвечает на вопрос с технической точки зрения. Я хотел бы добавить некоторые мысли о том, почему ключевое слово 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?
some
ключевое слово из Swift 5.1 (предложение swift-evolution) используется вместе с протоколом в качестве типа возврата.
Примечания к выпуску 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 {
...
}
Я постараюсь ответить на это на очень простом практическом примере (что это за непрозрачный тип результата)
Предполагая, что у вас есть протокол со связанным типом и две структуры, реализующие его:
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()
}
}
'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. Это как общая абстракция протокола, где вам не нужно определять конкретный тип.
Предварительное условие
Эта проблема на самом деле предназначена для помощи протоколам со связанными типами. Например, у объекта есть связанный тип (Self).
тип переменной - не разрешен ❌
var x: Equatable = 10 // Int
var y: Equatable = "10" // String
Это потому, что компилятор не может знать, соответствует ли ассоциированный тип x ассоциированному типу y. Так что это просто запрещает. Компилятор просто не хочет оказаться в ситуации, когда вы попробуете что-то вроде:
if x == y { print("Equal") }
Хотя оба равноправны. Один - в то время как другой -String
.
тип возвращаемого значения - не разрешен ❌
func x() -> Equatable {
return 10 // Use of protocol 'Equatable' as a type must be written 'any Equatable'
}
общее ограничение - разрешено ✅
Это позволяет вам делать это только как общее ограничение:
func compare<T: Equatable>(_ x: T, _ y: T) -> Bool {
return x == y
}
Разрешено, поскольку оба x,y имеют один общий тип, который также может бытьEquatable
. Это означает, что вы никогда не сможете оказаться в ситуации, когдаx
являетсяInt
, покаy
является строкой. Оба должны быть одного и того же конкретного типа.
уменьшает некоторые из предыдущих синтаксических ограничений
тип возвращаемого значения переменной - разрешен ✅
Следующее запрещено.
var x: Equatable = 10
Однако с добавлением , разрешено следующее:
var x: some Equatable = 10
Но само по себе это не очень полезно. Вы все еще не можете и никогда не должны делать что-то вроде:
var x: some Equatable = 10
var y: some Equatable = 10
if x == y {
print("equal") // ERROR: Cannot convert value of type 'some Equatable' (type of 'y') to expected argument type 'some Equatable' (type of 'x')
}
тип возврата - разрешен ✅ &
let y = 8
func foo() -> some Equatable { // THIS is where `some` really shines
if y > 10 {
return 10
} else {
return 9
}
}
Но он поставляется с некоторыми необходимыми и хорошими средствами защиты.
let y = 8
func foo() -> some Equatable { // ERROR: Function declares an opaque return type 'some Equatable', but the return statements in its body do not have matching underlying types
if y > 10 {
return 10
} else {
return "ten"
}
}
это означает, что он не позволит вам иметь разные типы возврата , такие как:10
и"ten"
вместе. Вы можете вернуть что угодно — при условии, что все возвращаемые вами вещи относятся к одному и тому же конкретному типу.
на самом деле это не обязательно.
Просто очень полезно избегать сложного синтаксиса.
Смотрите фантастический ответ Миши
Дополнительное примечание
Это немного выходит за рамки этого вопроса, но я хотел бы показать, какsome
иany
работать в тандеме:
var x: some Equatable = 10
var y: some Equatable = "ten"
func handle(thing: any Equatable) {
print(thing)
}
handle(thing: x) // 10
handle(thing: y) // "ten"
Но и:
var x: some Equatable = 10
var y: some Equatable = "ten"
func handle(thing1: any Equatable, thing2: any Equatable ) {
print(thing1 == thing2) // ❌
}
handle(thing1: x, thing2: y)
handle(thing1: y, thing2: y)
Ошибка, которую вы получаете,
ОШИБКА: бинарный оператор «==» не может быть применен к двум «любым равным» операндам.
Это потому, что на данный момент связанный тип, который управляет логикой==
оператор не определен. Он определяется только тогда, когда знает, что это Int или String, но это неизвестно. Так что это просто ошибка.
Для упрощения, если вы знаете разницу между
var x = 5
против
int x =5
Тогда ты узнаешь some
. Компилятор это знает, и вы это знаете. Минимальные усилия, чтобы сказать, что вы что-то соблюдаете, без указания специфики (общих типов, которые он использует)
Ключевое слово «some» используется для указания непрозрачного типа возвращаемого значения, которым обычно является протокол.
Это означает, что тип возвращаемого значения может быть любым типом, соответствующим протоколу, но он всегда должен возвращать один и тот же тип при разных вызовах.
Это отличается от возврата только протокола или использования дженериков, где он может возвращать разные типы для разных вызовов.
Вот пример кода, поясняющий различия:
protocol Animal {
init() // required to make createSpecificAnimal work
func makeSound()
}
struct Lion: Animal {
func makeSound() { print("roar") }
}
struct Dog: Animal {
func makeSound() { print("bark") }
}
// 1 - Function decides what type of Animal to return everytime this
// function is called and can choose different Animals on each call because
// return type is Animal.
func createRandomAnimal() -> Animal {
return Bool.random() ? Lion() : Dog()
}
let animal1 = createRandomAnimal()
animal1.makeSound() // May "roar" or "bark"
let animal2 = createRandomAnimal()
animal2.makeSound() // May "roar" or "bark"
// 2 - Function decides what type of Animal to return BUT CAN'T return a
// Lion OR a Dog like above because "some Animal" is an opaque return type
// and can't return 2 different types across different calls. It must
// choose a type and stick with it everytime this function is called. This
// function has decided the Animal that will always return from this function
// will be a Lion.
func createAnimal() -> some Animal { // <- OPAQUE RETURN TYPE
return Lion()
}
let animal3 = createAnimal()
animal3.makeSound() // Will always "roar"
let animal4 = createAnimal()
animal4.makeSound() // Will always "roar"
// 3 - Caller decides what type of Animal to return. If the caller passes
// Lion.self the return type will be Lion. If the caller passes Dog.self the
// return type will be Dog.
func createSpecificAnimal<T: Animal>(_: T.Type) -> T {
return T()
}
let animal5 = createSpecificAnimal(Lion.self)
animal5.makeSound() // Will always "roar"
let animal6 = createSpecificAnimal(Dog.self)
animal6.makeSound() // Will always "bark"
На ум приходит простой пример использования - написание общих функций для числовых типов.
/// 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
Для тех, кого тема закружила, вот очень расшифровывающая и пошаговая статья спасибо Вадиму Булавину.
https://www.vadimbulavin.com/opaque-return-types-and-the-some-keyword-in-swift/
Вышеупомянутое сообщение Миши (извините, я пока не могу напрямую добавить комментарий) заявляет, что это необязательно, если вы не используете общие типы как VStack и т. Д. И это потому, что это наиболее общий непрозрачный тип, которому удовлетворяют все представления. Таким образом, его использование здесь помогает устранить ошибку компиляции.
Похоже, что это
Непрозрачные возвращаемые типы
Если вы посмотрите на мой пример, вы увидите, чтоsome Gesture
Значит этоmyGesture
свойство всегда будет реализовыватьGesture
протокол, однако конкретный тип реализации не должен быть известен вызывающей стороне (он скрыт). То же самое верно и в отношенииbody
свойство – вместо предоставления конкретного типа возвращаемое значение описывается с точки зрения поддерживаемых им протоколов, т.е.View
.
Вот код:
import SwiftUI
struct ContentView: View {
@State private var rotate: Angle = .zero
var myGesture: some Gesture {
RotationGesture()
.onChanged { rotate = $0 }
.onEnded { angle in rotate = angle }
}
var body: some View {
Rectangle()
.frame(width: 200, height: 200)
.foregroundColor(.blue)
.rotationEffect(rotate)
.gesture(myGesture)
}
}
В дополнение к вышесказанному, все модификаторы SwiftUI, применяемые к прямоугольнику, также используютsome
ключевое слово при возврате значения. Например:
func foregroundColor(_ color: Color?) -> some View
В моем понимании (может и не так)
Позвоните, что у меня было
Protocol View{}
class Button: View { // subclass of View }
//this class not a subclass of View
class ButtonBuilder<T> where T:View { //using T as View here }
потом
var body: View = Button() // ok
var body: View = ButtonBilder() //not ok
var body: some View = ButtonBilder() //ok
Так
какой-то протокол
Может обрабатывать общий класс, который использует этот протокол как общий в своем собственном коде в качестве подкласса протокола.