Что такое ключевое слово `some` в SwiftUI? - PullRequest
156 голосов
/ 03 июня 2019

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

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

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

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

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

Ответы [ 5 ]

195 голосов
/ 03 июня 2019

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, возвращает значение определенного одного * 1026.* конкретный тип, который соответствует 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 return 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 для получения дополнительной информации об этой функции.

26 голосов
/ 05 июня 2019

Другой ответ хорошо объясняет технический аспект нового ключевого слова 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 используется, а пользователь класса - нет.

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

16 голосов
/ 06 июня 2019

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

some - это неa Требование!

Прежде всего, вам не нужно , чтобы объявить тип возврата 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 }
}

Это означает, что you указывает этовведите, пометив свойство body конкретным типом по вашему выбору.Единственное требование состоит в том, что этот тип должен реализовывать сам протокол View.

Это может быть тип * , который реализует View, например

  • Text
  • Image
  • Circle

или непрозрачный типкоторый реализует View, то есть

  • some View

Общие представления

Проблема возникает, когда мы пытаемся использовать представление стека в качестве *Тип возврата 1059 *, например 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?

12 голосов
/ 06 июня 2019

Ключевое слово 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 {
    ...
}
7 голосов
/ 21 июня 2019

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

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

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

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

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

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

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

...