Swift(UI) 中的 "some" 关键字是什么?

What is the "some" keyword in Swift(UI)?

新的SwiftUI tutorial代码如下:

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

第二行单词 some,并且在他们的站点上突出显示,就好像它是关键字一样。

Swift 5.1 似乎没有 some 作为关键字,而且我看不出 some 这个词还能做什么,因为它出现在类型通常去。 Swift 是否有新的未公布版本?它是以我不知道的方式在类型上使用的函数吗?

关键字 some 有什么作用?

some Viewan opaque result type as introduced by SE-0244 并且在 Swift 5.1 和 Xcode 11 中可用。您可以将其视为 "reverse" 通用占位符。

与调用者满意的常规通用占位符不同:

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 == IntSee this post for more info.

要摆脱这个的主要事情是 returning some P 的函数 return 是特定 单个 [=124] 的值=] 符合 P 的具体类型。在函数中尝试 return 不同的符合类型会产生编译器错误:

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

由于隐式泛型占位符不能被多种类型满足。

这与函数 returning P 形成对比,后者可用于表示 both S1S2 因为它表示任意 P 符合值:

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

好的,那么不透明结果类型 -> some P 比协议 return 类型有什么好处 -> P


1。不透明结果类型可与 PAT 一起使用

当前协议的一个主要限制是 PAT(具有关联类型的协议)不能用作实际类型。尽管此限制可能会在该语言的未来版本中取消,但由于不透明结果类型实际上只是通用占位符,因此它们现在可以与 PAT 一起使用。

这意味着您可以执行以下操作:

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

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

2。不透明结果类型具有身份

因为不透明的结果类型强制单个具体类型被 returned,编译器知道对同一个函数的两次调用必须 return 两个相同类型的值。

这意味着您可以执行以下操作:

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

这是合法的,因为编译器知道 xy 具有相同的具体类型。这是 == 的重要要求,其中两个参数都是 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 值具有相同的底层具体类型。

以类似的方式,如果我们引入另一个不透明类型 returning 函数:

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

该示例变得非法,因为尽管 foobar return some Equatable,它们的 "reverse" 通用占位符 Output1Output2 可以通过不同的类型来满足。


3。不透明结果类型由通用占位符组成

与常规 protocol-typed 值不同,不透明结果类型可以与常规通用占位符很好地组合,例如:

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 刚刚 returned 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)
}

好吧,使用不透明的结果类型允许您通过仅公开 P 提供的接口来使类型 S 成为实现细节,从而使您可以在以后灵活地更改具体类型在不破坏任何依赖于函数的代码的情况下下线。

例如,您可以替换:

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

与:

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

不破坏任何调用 makeP().

的代码

有关此功能的更多信息,请参阅 the Opaque Types section of the language guide and the Swift evolution proposal

另一个答案很好地解释了新 some 关键字的技术方面,但这个答案将尝试轻松解释 为什么 .


假设我有一个动物协议,我想比较两只动物是否是兄弟姐妹:

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

现在假设我有一个函数 returns 来自 'family' 的动物。

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

Note: this function won't actually compile. This because before the 'some' feature was added you cannot return a protocol type if the protocol uses 'Self' or generics. But let's say you can... pretending this upcasts myDog to abstract type Animal, let's see what happens

现在问题来了,如果我尝试这样做:

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

animal1.isSibling(animal2) // error

这将引发错误

为什么?好吧,原因是,当您调用 animal1.isSibling(animal2) 时,Swift 不知道这些动物是狗、猫还是其他动物。 据Swift所知,animal1animal2可能是不相关的动物物种。由于我们无法比较不同类型的动物(见上文)。这会出错

如何some T解决这个问题

让我们重写之前的函数:

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

animal1.isSibling(animal2)

animal1animal2不是Animal而是它们是实现 Animal.

的 class

这让你现在做的是当你调用 animal1.isSibling(animal2) 时,Swift 知道 animal1animal2 是同一类型。

所以我喜欢这样想:

some T lets Swift know the what implementation of T is being used but the user of the class doesn't.

(Self-promotion 免责声明)我已经写了一个 blog post 关于这个新功能

更深入一点(与这里相同的例子)

Swift 5.1 (swift-evolution proposal) 中的 some 关键字作为 return 类型与协议结合使用。

Xcode 11 release notes 这样呈现:

Functions can now hide their concrete return type by declaring what protocols it conforms to, instead of specifying the exact return type:

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

Code that calls the function can use the interface of the protocol, but doesn’t have visibility into the underlying type. (SE-0244, 40538331)

在上面的示例中,您不需要告诉您要 return 和 Array。这允许您甚至 return 一个仅符合 Collection.

的通用类型

另请注意您可能会遇到的错误:

'some' return types are only available in iOS 13.0.0 or newer

这意味着您应该使用可用性来避免 some 在 iOS 12 和之前:

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

is pretty awesome and answers the question from a technical perspective. I would like to add some thoughts on why the keyword some is used in this particular place in Apple's SwiftUI tutorials 以及为什么这是一个值得遵循的好习惯。

some 不是必需的!

首先,您不需要body的return类型声明为不透明类型。您始终可以 return 具体类型而不是使用 some View.

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

这也会编译。当您查看 View 的界面时,您会看到 body 的 return 类型是关联类型:

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

或实现 Viewopaque 类型,即

  • some View

一般视图

当我们尝试将堆栈视图用作 body 的 return 类型时,如 VStackHStack:

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

这不会编译,你会得到错误:

Reference to generic type 'VStack' requires arguments in <...>

那是因为 SwiftUI 中的堆栈视图是 generic 类型! (Lists 和其他容器视图类型也是如此。)

这很有意义,因为您可以插入任意数量的任意类型的视图(只要它符合 View 协议)。上面正文中VStack的具体类型实际上是

VStack<TupleView<(Text, Image)>>

当我们稍后决定将视图添加到堆栈时,它的具体类型会发生变化。如果我们在第一个文本之后添加第二个文本,我们得到

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

即使我们进行微小的更改,比如在文本和图像之间添加一个间隔符,堆栈的类型也会发生变化:

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

据我所知,这就是 Apple 在其教程中建议始终使用 some View 的原因,这是所有视图都满足的最通用的不透明类型,如body 的 return 类型。您可以更改自定义视图的实现/布局,而无需每次都手动更改 return 类型。


补充:

如果你想更直观地了解不透明的结果类型,我最近发表了一篇可能值得一读的文章:

What’s this “some” in SwiftUI?

我认为到目前为止所有答案都缺少的是 some 主要在 DSL(domain-specific 语言)之类的东西中有用,例如 SwiftUI 或 library/framework,它会有用户(其他程序员)与你不同。

你可能永远不会在你的普通应用程序代码中使用 some,除非它可以包装一个通用协议,以便它可以用作一个类型(而不是仅仅作为一个类型约束)。 some 所做的是让编译器知道某些东西是什么特定类型,同时在它前面放置超类型外观。

因此在 SwiftUI 中,你是用户, 需要知道的是 some View,而在幕后各种 hanky-panky 可以继续你被屏蔽的地方。该对象实际上是一种非常特殊的类型,但您永远不需要知道它是什么。然而,与协议不同的是,它是一种 full-fledged 类型,因为无论它出现在哪里,它都只是某些特定 full-fledged 类型的外观。

在 SwiftUI 的未来版本中,您期望 some View,开发人员可以更改该特定对象的基础类型。但这不会破坏您的代码,因为您的代码从一开始就从未提及基础类型。

因此,some 实际上使协议更像超类。它是几乎一个真正的对象类型,虽然不完全是(例如,一个协议的方法声明不能return一个some)。

因此,如果您打算将 some 用于任何事情,则很可能是 正在编写 DSL 或 framework/library 供其他人,并且您想掩盖基础类型的详细信息。这将使您的代码更易于其他人使用,并允许您更改实现细节而不破坏他们的代码。

但是,您也可以在自己的代码中使用它作为一种方式,使代码的一个区域免受隐藏在代码另一区域的实现细节的影响。

'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 主体将 return 确认 View 协议的内容。它就像一个通用协议抽象,您不必在其中定义具体类型。

我将尝试用非常基本的实际示例来回答这个问题(这是什么不透明的结果类型

假设您有关联类型的协议,以及实现它的两个结构:

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

对于那些对这个主题感到头晕的人,这里是一篇非常解密和一步一步的文章,感谢 Vadim Bulavin。

https://www.vadimbulavin.com/opaque-return-types-and-the-some-keyword-in-swift/

为了简化,如果你知道两者之间的区别

var x = 5

int x =5

那你就知道了some。 编译器知道,你也知道。在没有指定细节(它使用的通用类型)的情况下说你遵守某事的最小努力

简单易懂,如kindOf in Objc

我的理解(可能是错误的)

我的电话

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

所以

some Protocol

可以将泛型 class 用于在自己的代码中作为协议的子类使用该协议的泛型

您可以假设 swift 中的泛型。

Mischa 在 post 上面(抱歉,我还不能直接添加评论)指出 some 是可选的,除非您使用泛型类型作为 VStack 等。那是因为 some 是所有视图都满足的最一般的不透明类型。所以在这里使用它有助于解决编译错误。

似乎 some 与 Combine 的 eraseToAnyPublisher() 方法非常接近。