Swift 3中的dispatch_once去哪了?

58

好的,我了解到在Xcode 8中有一个新的Swifty Dispatch API。我正在使用DispatchQueue.main.async并且已经浏览了Xcode中的Dispatch模块以查找所有新的API。

但是我还使用dispatch_once来确保诸如单例创建和一次性设置不会被执行多次(即使在多线程环境中),而在新的Dispatch模块中找不到dispatch_once

static var token: dispatch_once_t = 0
func whatDoYouHear() {
    print("All of this has happened before, and all of it will happen again.")
    dispatch_once(&token) {
        print("Except this part.")
    }
}
7个回答

61
自从Swift 1.x以来,Swift一直在使用dispatch_once幕后来执行全局变量和静态属性的线程安全懒加载。
所以上面的static var已经在使用dispatch_once了,这使得它有点奇怪(并且可能会在将其再次用作另一个dispatch_once的标记时出现问题)。事实上,没有一种安全的方法可以使用dispatch_once而不出现这种递归,因此他们将其删除了。相反,只需使用构建在其上的语言功能即可。
// global constant: SomeClass initializer gets called lazily, only on first use
let foo = SomeClass()

// global var, same thing happens here
// even though the "initializer" is an immediately invoked closure
var bar: SomeClass = {
    let b = SomeClass()
    b.someProperty = "whatever"
    b.doSomeStuff()
    return b
}()

// ditto for static properties in classes/structures/enums
class MyClass {
    static let singleton = MyClass()
    init() {
        print("foo")
    }
}

这很好,如果你一直在使用dispatch_once来进行一次性的初始化以产生某个值,那么你可以将该值设置为全局变量或静态属性进行初始化。
但是,如果你使用dispatch_once来执行不一定有结果的工作呢?你仍然可以使用全局变量或静态属性来实现:只需将该变量的类型设置为Void即可。
let justAOneTimeThing: () = {
    print("Not coming back here.")
}()

如果访问全局变量或静态属性来执行一次性工作让您感觉不对劲,比如说您希望客户在使用您的库之前调用一个“初始化我”的函数,那么只需将该访问包装在一个函数中:

func doTheOneTimeThing() {
    justAOneTimeThing
}

查看迁移指南获取更多信息。


1
考虑到单例“是一种将类的实例化限制为一个对象的设计模式”,将private添加到类的构造函数中不是很好吗?请注意,如果在此类中有一个变量,您可能会有多个实例保持对该变量的不同值。一旦构造函数是private的,只有static let singleton行才能初始化它。 - henrique
如果我必须在dispatch_once块中使用实例变量怎么办? - Inder Kumar Rathore
@InderKumarRathore :这条评论的背后实际上包含了很多内容。为什么不将其作为一个单独的问题发布呢? - rickster
@rickster,我在 Stack Overflow 上搜索后已经在 Apple 开发者论坛上发布了。一旦我得到解决方案,我会尽快发布。这是帖子链接:https://forums.developer.apple.com/thread/69433?sr=stream - Inder Kumar Rathore

22

虽然“懒惰变量”模式使我不再关心调度令牌,通常比dispatch_once()更方便,但我不喜欢它在调用位置的外观:

_ = doSomethingOnce

我希望这个语句看起来更像一个函数调用(因为它意味着动作),但实际上并不是。而且,不得不写 _ = 来显式地丢弃结果非常让人烦恼。

有一种更好的方法:

lazy var doSomethingOnce: () -> Void = {
  print("executed once")
  return {}
}()

这使得以下内容成为可能:

doSomethingOnce()

这可能不够高效(因为它调用了一个空闭包而不是仅丢弃一个Void),但对我来说提高了清晰度是完全值得的。


1
同使用 Void 一样,有一个小细节需要注意:如果在第一次调用之前有人写了 doSomethingOnce = {},它将不会被调用。 - Cœur
请指导如何消除编译器在声明“doSomethingOnce”时生成的错误,如果它包含的代码可能会抛出异常。 “lazy var doSomethingOnce:()throws-> Void” - eklektek
@user2196409 从函数中抛出的错误可以通过在调用处使用try关键字来处理,就像处理普通的抛出函数一样(我在Swift 4.2中验证过)。 - Andrii Chernenko

17

这里和互联网上的其他答案都非常好,但我认为也应该提到这个小细节:

dispatch_once 的优点在于其高度优化,一旦运行后就会消除代码,这种方式我几乎无法理解,但我比较确信这比设置和检查(真实的)全局标记要快得多。

虽然在 Swift 中可以合理地实现标记方法,但需要声明另一个存储的布尔值并不是很好。更不用说线程不安全了。正如文档所述,您应该使用“惰性初始化的全局变量”。是啊,但是为什么要在全局范围内混杂呢?

除非有人能说服我更好的方法,否则我倾向于在我将要使用它的范围内或者接近该范围内声明我的 do-once 闭包,如下所示:

private lazy var foo: Void = {
    // Do this once
}()

基本上我是在说“当我读到这句话时,foo应该是运行这个代码块的结果。” 它的行为方式与全局let常量完全相同,只不过在正确的作用域内。而且看起来更漂亮。然后我可以在任何地方调用它,通过将其读入永远不会被使用的变量中。我喜欢Swift中的_。像这样:

_ = foo
这个很酷的小技巧早已存在,但并没有得到太多关注。它基本上会在运行时将变量保留为未调用的闭包,直到有东西想要查看其Void结果。在读取时,它调用闭包,丢弃它并将其结果保留在foo中。Void在内存方面实际上几乎不使用,所以后续读取(例如_ = foo)对CPU不造成任何影响。(别引用我说的话,请有人检查汇编是否正确!)你可以有很多这样的变量,Swift在第一次运行后就几乎不再关心它们了!抛弃那个旧的dispatch_once_t,让你的代码像圣诞节开箱时一样漂亮!
我的一个问题是,在第一次读取之前,您可以将foo设置为其他内容,然后您的代码将永远不会被调用!因此需要使用全局的let常量来防止这种情况发生。问题是,类作用域中的常量与self不兼容,因此不能使用实例变量……但说真的,您什么时候将任何东西设置为Void呢?
此外,您需要将返回类型指定为Void(),否则它仍会抱怨self。谁能想到呢?
lazy只是为了使变量像它应该的那样懒惰,这样Swift就不会在init()上直接运行它。
相当时髦,只要记得不要写入它就可以了!:P

5
你和 @rickster 的方法有微妙的不同。你的“惰性初始化局部变量”每实例执行一次操作,而“惰性初始化全局变量”每类型执行一次操作。详见 这个代码片段 - Yevhen Dubinin

8

Swift 3.0中的"dispatch_once"示例

步骤1:只需用下面的代码替换您的Singleton.swift(单例类)即可

// Singleton Class
class Singleton: NSObject { 
var strSample = NSString()

static let sharedInstance:Singleton = {
    let instance = Singleton ()
    return instance
} ()

// MARK: Init
 override init() {
    print("My Class Initialized")
    // initialized with variable or property
    strSample = "My String"
}
}

步骤2:从ViewController.swift调用Singleton

单例模式示例图片

在ViewController.swift文件中,您可以通过以下方式调用单例:
``` let singletonInstance = Singleton.sharedInstance ```
这将返回一个指向Singleton类的实例的引用。然后,您可以使用该引用访问Singleton类中的属性和方法。
// ViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        let mySingleton = Singleton.sharedInstance
        print(mySingleton.strSample)

        mySingleton.strSample = "New String"
        
        print(mySingleton.strSample)
        
        let mySingleton1 = Singleton.sharedInstance
        print(mySingleton1.strSample)

    }

ViewController示例图像

输出结果如下

My Class Initialized
My String
New String
New String

请问为什么这只会被调度一次?为什么两个访问sharedInstance的线程之间不会发生冲突?此外,作为“静态”变量,它在某个未知时间被初始化 - 无论我们是否需要它。dispatch_once的好处是,我可以控制它何时执行。 - Motti Shneor

6

适用于Xcode 8 GA Swift 3编译

创建一个dispatch_once单例类实例的推荐且优雅的方式:

final class TheRoot {
static let shared = TheRoot()
var appState : AppState = .normal
...

使用方法:

if TheRoot.shared.appState == .normal {
...
}

这些代码行是做什么的?

final - 这使得该类无法被覆盖或扩展,也使得代码运行更快,减少了间接性。

static let shared = TheRoot() - 此行执行了延迟初始化,且仅运行一次。

此解决方案是线程安全的。


1
请问这是如何实现“懒加载”的?我的经验告诉我,在其他语言中,编译器会在某个随机时间点插入静态变量的初始化代码,从未出现过懒加载。那么在Swift中的定义在哪里呢? - Motti Shneor

4
根据迁移指南:

在Swift中已不再提供免费函数dispatch_once。 在Swift中,您可以使用延迟初始化全局变量或静态属性,并获得与dispatch_once提供的线程安全和一次调用保证相同的效果。

示例:
  let myGlobal = { … global contains initialization in a call to a closure … }()

  // using myGlobal will invoke the initialization code only the first time it is used.
  _ = myGlobal  

-3

线程安全的 dispatch_once:

private lazy var foo: Void = {
    objc_sync_enter(self)
    defer { objc_sync_exit(self) }

    // Do this once
}()

现有的惰性机制已经是线程安全的(在执行最多一次的意义上)。这个额外的“线程安全”部分没有帮助,反而可能会损害。 - polytopia
@polytopia 对于以下变量 private lazy var foo: Void = { print("hello") }(),这段代码会多次打印 "hello"(仅在模拟器上重现):for _ in 0 ... 1000 { DispatchQueue.global(qos: .default).async { _ = self.foo } } } - pigmasha

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接