Что такое ключевое слово some в SwiftUI? - программирование

Что такое ключевое слово some в SwiftUI?

Новый учебник SwiftUI имеет следующий код:

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

Во второй строке слово some, и на их сайте подсвечивается, как будто это ключевое слово.

Swift 5.1, похоже, не содержит some в качестве ключевого слова, и я не вижу, что еще может быть слово, которое some могут использовать, поскольку оно идет туда, где обычно идет тип. Есть ли новая, необъявленная версия Swift? Это функция, которая используется с типом способом, о котором я не знал?

Что делает ключевое слово some?

4b9b3361

Ответ 1

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 для получения дополнительной информации об этой функции.

Ответ 2

Другой ответ хорошо объясняет технический аспект нового ключевого слова some, но этот ответ попытается легко объяснить почему.


Допустим, у меня есть протокол Animal, и я хочу сравнить, если два животных являются братьями и сестрами:

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

Таким образом, имеет смысл сравнивать, только если два животных являются братьями и сестрами, если они одного типа животного.


Теперь позвольте мне создать пример животного только для справки.

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

Путь без some T

Теперь допустим, у меня есть функция, которая возвращает животное из "семьи".

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

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

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

Ответ 3

Хэмиш ответ довольно удивительный и отвечает на вопрос с технической точки зрения. Я хотел бы добавить некоторые соображения о том, почему ключевое слово 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 являются общими типами! And (И то же самое верно для списков и других типов контейнеров.)

Это делает много смысла, потому что вы можете подключить любое количество просмотров любого типа (если он соответствует View протокол). Конкретный тип VStack в теле выше на самом деле

VStack<TupleView<(Text, Image)>>

Когда мы позже решим добавить представление в стек, его конкретный тип изменится. Если мы добавим второй текст после первого, мы получим

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

Даже если мы сделаем небольшое изменение, такое же тонкое, как добавление разделителя между текстом и изображением, тип стека изменится:

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

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


Дополнение:

Если вы хотите лучше понять непрозрачные типы результатов, я недавно опубликовал статью, которую стоит прочитать:

🔗 Что это за "некоторые" в SwiftUI?

Ответ 4

Ключевое слово 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 {
    ...
}

Ответ 5

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

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

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

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

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

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

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

Ответ 6

Из выступления на WWDC в 25:35 причины возврата some типа, а не, например, типа protocol:

limitations of returning a protocol type

Ответ 7

"Некоторые" означает:

"Это новый Swift 5.1 ключевого слово некоторые означает, что вычисленное свойство может вернуть что - либо, когда что - то по крайней мере соответствует Посмотреть протокол. Что касается текста. И с Свифтом 5.1 мы также не должен больше добавить возвратное ключевое слово. Последнюю строку функции или замыкания будут возвращены автоматически. "

Ответ 8

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