什么是“一些”;关键字在Swift(UI)?

新的SwiftUI教程有以下代码:

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

第二行是some,在他们的网站上突出显示,就好像它是一个关键字一样。

Swift 5.1似乎没有some作为关键字,而且我看不出some这个词还能在那里做什么,因为它位于类型通常所在的位置。有没有一个新的、未公布的Swift版本?它是一个我不知道的被用在类型上的函数吗?

关键字some的作用是什么?

67037 次浏览

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的函数相反,后者可用于表示这两个 S1S2,因为它表示任意的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.

这是合法的,因为编译器知道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值具有相同的底层具体类型。

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

//   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都返回some Equatable,但它们的“反向”泛型占位符Output1Output2可以由不同的类型满足。


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

不透明结果类型的使用允许你通过只公开P提供的接口来使类型S成为一个实现细节,使你可以灵活地在后面更改具体类型,而不会破坏任何依赖于函数的代码。

例如,你可以替换:

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

:

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

而不会破坏任何调用makeP()的代码。

有关此特性的进一步信息,请参阅语言指南中的不透明类型部分快速进化方案

另一个答案很好地解释了新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的方法

现在,假设我有一个函数,从一个“家族”中返回一个动物。

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

注意:这个函数实际上不会编译。这是因为在'some'特性之前添加了如果协议使用'Self'或泛型,则不能返回协议类型。但是假设你可以…假设这将myDog向上转换为抽象类型Animal,让我们看看会发生什么

现在问题来了,如果我试着这么做:

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


animal1.isSibling(animal2) // error

这将抛出一个错误

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

some T如何解决这个问题

让我们重写之前的函数:

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


animal1.isSibling(animal2)

animal1animal2分别是 Animal 它们是实现Animal的类

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

所以我喜欢这样思考:

some T斯威夫特知道正在使用的是T的哪个实现,但类的用户不知道。

(自我推广免责声明)我已经写了一个博客,它更深入地讨论了这个新特性(示例与这里相同)

Swift 5.1 (swift-evolution提议)中的some关键字与协议一起作为返回类型使用。

Xcode 11 发布说明是这样表示的:

函数现在可以通过声明它遵循什么协议来隐藏具体的返回类型,而不是指定确切的返回类型:

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

调用该函数的代码可以使用协议的接口,但不能看到底层类型。(se - 0244, 40538331)

在上面的例子中,你不需要告诉你将返回一个Array。这甚至允许你返回一个只符合Collection的泛型类型。


还要注意你可能会遇到的这个错误:

'some'返回类型仅在iOS 13.0.0或更新版本中可用

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

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

哈米什的回答非常棒,从技术角度回答了这个问题。我想补充一些关于为什么关键字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的返回类型,如VStackHStack时,问题就出现了:

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的返回类型的原因,some View是所有视图都满足的最通用的不透明类型。您可以更改自定义视图的实现/布局,而不必每次都手动更改返回类型。


补充:

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

🔗SwiftUI里的“一些”是什么?

我认为到目前为止所有的答案都遗漏了,some主要是在像DSL(领域特定语言)这样的东西中有用,比如SwiftUI或库/框架,它们会有用户(其他程序员)与你不同。

你可能永远不会在正常的应用程序代码中使用some,除非它可以包装一个泛型协议,这样它就可以用作类型(而不仅仅是类型约束)。some所做的是让编译器保持特定类型的知识,同时在它前面放置一个超类型facade。

因此,在SwiftUI中,你是用户,所有需要知道的是某个东西是some View,而在幕后,各种各样的恶作剧可以继续进行,而你是被屏蔽的。这个对象实际上是一个非常特定的类型,但您永远不需要知道它是什么。然而,与协议不同的是,它是一个成熟的类型,因为无论它出现在哪里,它都只是某个特定成熟类型的外观。

在未来版本的SwiftUI中,当你期望some View时,开发人员可以改变该特定对象的底层类型。但这不会破坏您的代码,因为您的代码一开始就没有提到底层类型。

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

因此,如果你打算将some用于任何事情,很可能是当正在编写供他人使用的DSL或框架/库,并且你想掩盖底层类型细节时。这将使您的代码更便于他人使用,并且允许您在不破坏他人代码的情况下更改实现细节。

但是,您也可以在自己的代码中使用它,作为一种方法,将代码的一个区域与隐藏在代码的另一个区域中的实现细节隔离开来。

'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时,你要遵守视图协议,并告诉var主体将返回一些将确认视图协议的内容。它就像一个通用的协议抽象,你不必定义具体的类型。

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

假设你有关联类型的协议,并且有两个结构实现它:

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

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

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

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

var x = 5

vs

int x =5

那么你就会知道some。 编译器知道它,您也知道它。在不指定具体细节(它使用的泛型类型)的情况下,最小限度地说明您遵守了某些内容

简单理解,就像Objc中的kindOf

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

这是我拥有的

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

所以

一些协议

使用该协议的泛型类是否可以在自己的代码中作为协议的子类处理

你可以假设swift是通用的。

上面Mischa的帖子(抱歉,我还不能直接添加注释)指出some是可选的,除非你使用泛型类型如VStack等。这是因为some是所有视图都满足的最通用的不透明类型。因此在这里使用它有助于解决编译错误。

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

不透明的返回类型

如果你看一下我的例子,你会发现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