新的 SwiftUI tutorial 具有以下代码:
struct ContentView: View {
var body: some View {
Text("Hello World")
}
}
第二行单词 some
,并在他们的网站上突出显示,就好像它是一个关键字一样。
Swift 5.1 似乎没有 some
作为关键字,而且我看不出 some
这个词还有什么用处,因为它位于类型通常所在的位置。是否有新的未发布版本的 Swift?它是以我不知道的方式在类型上使用的函数吗?
关键字 some
有什么作用?
some View
是由 SE-0244 引入的 an opaque result type,在 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
。 See this post for more info。
主要的一点是返回 some P
的函数是返回符合 P
的特定 single 具体类型的值的函数。尝试在函数中返回不同的符合类型会产生编译器错误:
// 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
的函数形成对比,后者可用于表示 both 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)
}
好吧,使用不透明结果类型允许您通过仅公开 P
提供的接口来使类型 S
成为实现细节,从而使您可以灵活地在以后更改具体类型,而不会破坏任何依赖的代码功能上。
例如,您可以替换:
func makeP() -> some P {
return S(i: 0)
}
和:
func makeP() -> some P {
return T(i: 1)
}
不会破坏任何调用 makeP()
的代码。
有关此功能的详细信息,请参阅语言指南的 the Opaque Types section 和 the Swift evolution proposal。
另一个答案很好地解释了新 some
关键字的技术方面,但这个答案将尝试轻松解释为什么。
假设我有一个协议 Animal 并且我想比较两只动物是否是兄弟姐妹:
protocol Animal {
func isSibling(_ animal: Self) -> Bool
}
这样,只有当两只动物是同一类型的动物时,比较它们是否是兄弟姐妹才有意义。
现在让我创建一个动物的例子,仅供参考
class Dog: Animal {
func isSibling(_ animal: Dog) -> Bool {
return true // doesn't really matter implementation of this
}
}
没有一些T的方式
现在假设我有一个从“家庭”返回动物的函数。
func animalFromAnimalFamily() -> Animal {
return myDog // myDog is just some random variable of type `Dog`
}
注意:这个函数实际上不会编译。这是因为在添加“某些”功能之前,如果协议使用“自我”或泛型,则无法返回协议类型。但是假设你可以......假装这将 myDog 向上转换为抽象类型 Animal,让我们看看会发生什么
现在问题来了,如果我尝试这样做:
let animal1: Animal = animalFromAnimalFamily()
let animal2: Animal = animalFromAnimalFamily()
animal1.isSibling(animal2) // error
这将引发错误。
为什么?原因是,当您调用 animal1.isSibling(animal2)
时,Swift 不知道动物是狗、猫还是其他什么。 据 Swift 所知,animal1
和 animal2
可能是不相关的动物物种。因为我们无法比较不同类型的动物(见上文)。这会出错
一些T如何解决这个问题
让我们重写之前的函数:
func animalFromAnimalFamily() -> some Animal {
return myDog
}
let animal1 = animalFromAnimalFamily()
let animal2 = animalFromAnimalFamily()
animal1.isSibling(animal2)
animal1
和 animal2
不是 Animal
,但 它们是实现 Animal 的类。
这让您现在可以做的是,当您调用 animal1.isSibling(animal2)
时,Swift 知道 animal1
和 animal2
是同一类型。
所以我喜欢这样思考:
some T 让 Swift 知道正在使用 T 的什么实现,但类的用户不知道。
(自我推销免责声明)我写了一个blog post,对这个新功能进行了更深入的介绍(与此处相同的示例)
some
作为函数体的约束。所以 some
只需要在整个函数体中返回一个具体类型。例如:如果有 return randomDog
,则所有其他返回必须仅适用于 Dog
。所有好处都来自此约束:animal1.isSibling(animal2)
的可用性和 func animalFromAnimalFamily() -> some Animal
的编译好处(因为现在 Self
已在后台定义)。这是对的吗?
我认为到目前为止所有答案都缺少的是,some
主要在诸如 SwiftUI 或库/框架之类的 DSL(域特定语言)之类的东西中很有用,它们将拥有 用户(其他程序员)不同于你自己。
您可能永远不会在您的普通应用程序代码中使用 some
,除非它可以包装通用协议,以便可以将其用作类型(而不仅仅是作为类型约束)。 some
的作用是让编译器知道某物是什么特定类型,同时在其前面放置一个超类型外观。
因此,在您作为用户的 SwiftUI 中,您 只需要知道某物是 some View
,而在幕后,您可以免受各种手帕的影响。这个对象实际上是一个非常特殊的类型,但你永远不需要知道它是什么。然而,与协议不同的是,它是一种成熟的类型,因为无论它出现在哪里,它都只是某些特定的成熟类型的外观。
在您期待 some View
的 SwiftUI 的未来版本中,开发人员可以更改该特定对象的基础类型。但这不会破坏您的代码,因为您的代码从一开始就没有提到底层类型。
因此,some
实际上使协议更像一个超类。它几乎是一个真正的对象类型,虽然不完全是(例如,协议的方法声明不能返回 some
)。
因此,如果您打算将 some
用于任何事情,那很可能是您正在编写供他人使用的 DSL 或框架/库,并且您想要掩盖底层类型的详细信息。这将使您的代码更易于其他人使用,并允许您在不破坏他们的代码的情况下更改实现细节。
但是,您也可以在自己的代码中使用它,以保护您的代码的一个区域免受隐藏在代码的另一个区域中的实现细节的影响。
Hamish's answer 非常棒,从技术角度回答了这个问题。我想补充一些关于为什么在 Apple 的 SwiftUI tutorials 中的这个特定位置使用关键字 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
的 特定 类型
文本
图片
圆圈
…
或实现 View
的 opaque 类型,即
一些观点
通用视图
当我们尝试使用堆栈视图作为 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
的返回类型的原因。您可以更改自定义视图的实现/布局,而无需每次都手动更改返回类型。
补充:
如果你想更直观地了解 opaque 结果类型,我最近发表了一篇文章,可能值得一读:
🔗 SwiftUI 中的“一些”是什么?
Swift 5.1 (swift-evolution proposal) 中的 some
关键字与协议一起用作返回类型。
Xcode 11 release notes 是这样呈现的:
函数现在可以通过声明它遵循的协议来隐藏它们的具体返回类型,而不是指定确切的返回类型: 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
。只要你这样做,你应该没问题。问题只是编译器不会警告您这样做。
some
关键字。错误将是:“Protocol 'Collection' 只能用作通用约束,因为它具有 Self 或关联的类型要求”
我将尝试用非常基本的实际示例来回答这个问题(这是什么不透明的结果类型)
假设您有关联类型的协议,以及实现它的两个结构:
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()
}
以上是实际用法,在 some View
的 SwiftUI 中广泛使用。
但是有 一个 重要的限制 - 需要在编译时知道返回类型,所以下面再次无法给出 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 协议的内容。它就像一个通用的协议抽象,您不必定义具体的类型。
为了简化,如果您知道两者之间的区别
var x = 5
对比
int x =5
然后你就会知道some
。编译器知道,你也知道。在不指定细节(它使用的通用类型)的情况下说你遵守某事的最小努力
对于那些对这个主题感到头晕目眩的人,这里感谢 Vadim Bulavin,这是一篇非常解密和逐步的文章。
https://www.vadimbulavin.com/opaque-return-types-and-the-some-keyword-in-swift/
简单的理解方式,比如 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)
}
}
除了上述之外,所有应用于 Rectangle 的 SwiftUI 修饰符在返回值时也使用 some
关键字。例如:
func foregroundColor(_ color: Color?) -> some View
return
语句。也许这只是惯性,但在我看来,如果没有return
语句,它的某些东西看起来很奇怪。我确实喜欢从计算属性中省略return
!func makeP() -> some P
和func makeP() -> P
之间有什么区别?我已经阅读了提案,也看不出他们的样本有这种差异。some P
would be needed