新的SwiftUI tutorial具有以下代码:
struct ContentView: View {
var body: some View {
Text("Hello World")
}
}
第二行中的单词some
及其在其网站上被高亮显示,就好像它是关键字一样。
Swift 5.1似乎没有使用some
作为关键字,而且我看不到some
这个词还能做什么,因为它通常在类型所在的地方出现。是否有Swift的未发布新版本?是否以我不知道的方式在类型上使用了该函数?
关键字some
的作用是什么?
答案 0 :(得分:195)
some View
是an opaque result type的SE-0244,在带有Xcode 11的Swift 5.1中可用。您可以将其视为“反向”通用占位符。
与调用方可以满足的常规通用占位符不同:
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
的函数相反,该函数可以用来表示两者 S1
和S2
,因为它表示任意{{1 }}符合价值:
P
好吧,不透明结果类型func baz(_ x: Int) -> P {
if x > 10 {
return S1()
} else {
return S2()
}
}
比协议返回类型-> some P
有什么好处?
当前协议的主要限制是PAT(具有关联类型的协议)不能用作实际类型。尽管此限制在将来的语言版本中可能会取消,但由于不透明的结果类型实际上只是通用的占位符,因此它们现在可用于PAT。
这意味着您可以执行以下操作:
-> P
由于不透明的结果类型强制返回单个具体类型,因此编译器知道两次调用同一函数必须返回两个相同类型的值。
这意味着您可以执行以下操作:
func giveMeACollection() -> some Collection {
return [1, 2, 3]
}
let collection = giveMeACollection()
print(collection.count) // 3
这是合法的,因为编译器知道// 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
如果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)
}
的代码。
有关此功能的更多信息,请参见语言指南的the Opaque Types section和the Swift evolution proposal。
答案 1 :(得分:26)
另一个答案很好地解释了新some
关键字的技术方面,但是该答案将试图轻松解释为什么。
假设我有一个协议“动物”,我想比较两个动物是否是兄弟姐妹:
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`
}
注意:此函数实际上不会编译。这是因为在添加“某些”功能之前,如果协议使用“ Self”或泛型,则无法返回协议类型。但是,假设您可以...假装将myDog转换为动物抽象类型,让我们看看会发生什么情况
现在问题来了,如果我尝试这样做:
let animal1: Animal = animalFromAnimalFamily()
let animal2: Animal = animalFromAnimalFamily()
animal1.isSibling(animal2) // error
这将引发错误。
为什么?很好的原因是,当您致电animal1.isSibling(animal2)
时,Swift并不知道这些动物是狗,猫还是其他动物。 据Swift所知,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)
时,Swift知道animal1
和animal2
是同一类型。
所以我喜欢这样想:
some T
让 Swift 知道正在使用T
的实现方式,但该类的用户未使用。
(自我宣传免责声明)我写了一个blog post,对该功能进行了更深入的介绍(与此处相同的示例)
答案 2 :(得分:16)
Hamish's answer很棒,可以从技术角度回答问题。我想补充一下为什么在苹果SwiftUI tutorials的这个特定位置使用关键字some
的原因,以及为什么要遵循这样的好习惯。
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 中的堆栈视图是泛型类型! ?(列表和其他容器视图类型也是如此。)
这很有意义,因为您可以插入任何数量的任何类型的视图(只要符合View
协议)。上方正文中VStack
的具体类型实际上是
VStack<TupleView<(Text, Image)>>
当我们稍后决定将视图添加到堆栈时,其具体类型会更改。如果我们在第一个文本之后添加第二个文本,则会得到
VStack<TupleView<(Text, Text, Image)>>
即使我们进行了微小的更改(就像在文本和图像之间添加间隔一样细微),堆栈的类型也会发生变化:
VStack<TupleView<(Text, _ModifiedContent<Spacer, _FrameLayout>, Image)>>
据我所知,这是苹果在其教程中建议始终使用some View
(所有视图都满足的最通用的不透明类型)作为{{1}的原因。 }的返回类型。您可以更改自定义视图的实现/布局,而无需每次都手动更改返回类型。
如果您想更直观地了解不透明的结果类型,我最近发表了一篇文章,可能值得一读:
答案 3 :(得分:12)
Swift 5.1(swift-evolution proposal)中的some
关键字与协议一起用作返回类型。
Xcode 11 release notes像这样显示:
函数现在可以通过声明其遵循的协议来隐藏其具体的返回类型,而不用指定确切的返回类型:
func makeACollection() -> some Collection { return [1, 2, 3] }
调用该函数的代码可以使用协议的接口,但无法查看底层类型。 (SE-0244,40538331)
在上面的示例中,您无需告诉您将要返回Array
。这样,您甚至可以返回仅符合Collection
的通用类型。
还请注意您可能会遇到的以下错误:
“某些”返回类型仅在iOS 13.0.0或更高版本中可用
这意味着您应该在iOS 12及更高版本上使用可用性来避免some
:
@available(iOS 13.0, *)
func makeACollection() -> some Collection {
...
}
答案 4 :(得分:7)
我认为到目前为止缺少的所有答案是,some
主要用于诸如DSL(域特定语言)之类的东西,例如SwiftUI或库/框架,它们将具有个用户(其他程序员)与您自己有所不同。
您可能永远不会在常规应用程序代码中使用some
,除非它可以包装通用协议,以便可以将其用作类型(而不是用作类型约束)。 some
的作用是让编译器在将超类型外观放在其前面的同时,了解什么是特定类型。
因此,在SwiftUI(您是用户)中,您您所需要知道的只是某物是some View
,而在幕后,各种麻烦的事情可以从你被屏蔽了。实际上,该对象是一个非常特定的类型,但是您永远不需要了解它是什么。但是,与协议不同的是,它是完整类型,因为无论出现在哪里,它都只是某些特定完整类型的基础。
在SwiftUI的未来版本中,您期望使用some View
,开发人员可以更改该特定对象的基础类型。但这不会破坏您的代码,因为您的代码最初从未提到过底层类型。
因此,some
实际上使协议更像是超类。它几乎是一个真正的对象类型,尽管不是很真实(例如,协议的方法声明不能返回some
)。
因此,如果您要使用some
做任何事情,很可能是您正在编写供他人使用的DSL或框架/库,并且您想屏蔽基础类型详细信息。这将使您的代码更易于他人使用,并允许您在不破坏其代码的情况下更改实现细节。
但是,您也可以在自己的代码中使用它,以保护代码的一个区域免受埋在代码另一区域的实现细节的影响。
答案 5 :(得分:1)
“ 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进行确认。就像通用的协议抽象一样,您无需定义具体的类型。
答案 6 :(得分:1)
我将尝试用一个非常基本的实际示例来回答这个问题(这是一种不透明的结果类型有关的内容)
假设您具有关联类型的协议,并且有两个实现它的结构:
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()
}
}
答案 7 :(得分:0)
想到的一个简单用例是为数字类型编写泛型函数。
/// Adds one to any decimal type
func addOne<Value: FloatingPoint>(_ x: Value) -> some FloatingPoint {
x + 1
}
// Variables will be assigned 'some FloatingPoint' type
let double = addOne(Double.pi) // 4.141592653589793
let float = addOne(Float.pi) // 4.141593
// Still get all of the required attributes/functions by the FloatingPoint protocol
double.squareRoot() // 2.035090330572526
float.squareRoot() // 2.03509
// Be careful, however, not to combine 2 'some FloatingPoint' variables
double + double // OK
//double + float // error
答案 8 :(得分:0)
对于那些对这个主题感到头晕的人,这里要感谢Vadim Bulavin,这是一篇非常解密和循序渐进的文章。
https://www.vadimbulavin.com/opaque-return-types-and-the-some-keyword-in-swift/
答案 9 :(得分:0)
为简化起见,如果您知道两者之间的区别
var x = 5
vs
int x =5
然后您将知道some
。
编译器知道它,您也知道。只需花费很少的努力就可以说您遵守某件事,而没有指定具体细节(它使用的通用类型)
答案 10 :(得分:0)
一种简单的理解方式,例如Objc中的kindOf
答案 11 :(得分:0)
以我的理解(可能是错误的)
我打过的电话
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
所以
<块引用>一些协议
可以在自己的代码中将使用该协议的泛型类作为协议的子类进行处理
答案 12 :(得分:0)
你可以在 swift 中假设为泛型。
答案 13 :(得分:0)
Mischa 的上述帖子(抱歉,我还不能直接添加评论)指出 some
是可选的,除非您使用泛型类型作为 VStack 等。这是因为 some
是最通用的不透明所有视图都满足的类型。所以在这里使用它有助于解决编译错误。
似乎 some
与 Combine 的 eraseToAnyPublisher()
方法所做的非常接近。