353

新的SwiftUI 教程有以下代码:

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

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

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

关键字some有什么作用?

4

13 回答 13

447

some ViewSE-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. 有关更多信息,请参阅此帖子

主要的一点是,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.

这是合法的,因为编译器知道两者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)
}

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

例如,您可以替换:

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

和:

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

不破坏任何调用makeP().

有关此功能的更多信息,请参阅语言指南的不透明类型部分和Swift 进化提案

于 2019-06-03T20:06:41.263 回答
78

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

没有的方式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)Swift 时,它不知道动物是狗、猫还是什么。据斯威夫特所知,animal1可能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 知道这一点animal1并且animal2是同一类型。

所以我喜欢这样思考:

some TSwift知道正在使用什么实现,T但类的用户不知道。

(自我推销免责声明)我写了一篇博客文章,对这个新功能进行了更深入的介绍(与此处相同的示例)

于 2019-06-05T06:34:12.080 回答
39

Hamish 的回答非常棒,从技术角度回答了这个问题。我想补充一些关于为什么在 Apple 的SwiftUI 教程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
  • …</li>

或实现的不透明类型View,即

  • some View

通用视图

当我们尝试使用堆栈视图作为body的返回类型时,就会出现问题,例如VStackor 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 中的“一些”是什么?

于 2019-06-06T15:50:33.110 回答
36

我认为到目前为止所有答案都缺少的是,some它主要在诸如 SwiftUI 或库/框架之类的 DSL(域特定语言)之类的东西中有用,它们的用户(其他程序员)与您不同。

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

因此,在您作为用户的 SwiftUI 中,您只需要知道某些东西是 a some View,而在幕后,您可以免受各种手帕的影响。这个对象实际上是一个非常特殊的类型,但你永远不需要知道它是什么。然而,与协议不同的是,它是一种成熟的类型,因为无论它出现在哪里,它都只是某些特定的成熟类型的外观。

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

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

因此,如果您要使用some任何东西,很可能是正在编写供他人使用的 DSL 或框架/库,并且您想掩盖底层类型的详细信息。这将使您的代码更易于其他人使用,并允许您在不破坏他们的代码的情况下更改实现细节。

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

于 2019-06-21T19:20:14.180 回答
28

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

Xcode 11发行说明是这样呈现的:

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

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

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

在上面的例子中,你不需要告诉你要返回一个Array. 这甚至允许您返回一个仅符合Collection.


还要注意您可能面临的这个可能的错误:

'some' 返回类型仅适用于 iOS 13.0.0 或更高版本

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

@available(iOS 13.0, *)
func makeACollection() -> some Collection {
    ...
}
于 2019-06-06T03:50:27.743 回答
7

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

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

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()
    }
}
于 2020-01-02T20:05:27.647 回答
2

'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 协议的内容。它就像一个通用的协议抽象,您不必定义具体的类型。

于 2019-08-05T19:39:56.323 回答
1

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

var x = 5

对比

int x =5

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

于 2020-08-20T02:07:08.930 回答
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

所以

一些协议

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

于 2020-12-31T03:48:41.053 回答
0

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

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

于 2020-06-01T21:07:09.267 回答
0

简单的理解方式,例如kindOf在 Objc 中

于 2020-08-28T03:03:21.243 回答
0

您可以在 swift 中假设为泛型。

于 2021-03-04T08:19:32.680 回答
0

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

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

于 2021-03-18T13:47:17.640 回答