Swift-style-guide: 惰性初始化模式

创建于 2015-04-15  ·  29评论  ·  资料来源: raywenderlich/swift-style-guide

这些天,我们很多人在教程中都使用这样的模式来初始化变量:

lazy var locationManager: CLLocationManager = {
    let manager = CLLocationManager()
    manager.desiredAccuracy = kCLLocationAccuracyBest
    manager.delegate = self
    manager.requestAlwaysAuthorization()
    return manager
  }()

我已经看到它用于设置UILabels等控件。 这样做的好处是将初始化代码保存在一个方便的地方。

我想我会开始讨论,看看这是否是我们想要在风格指南中鼓励的模式?

最有用的评论

作为一个 FYI,Swift 的 Joe Groff 已经确认惰性变量是自动的@noescape所以他们不会捕获自我。

https://twitter.com/jckarter/status/704100315587477504

请记住,您仍然可以通过其他方式捕获自我(例如,在惰性变量中捕获自我的块变量)

所有29条评论

顺便说一句,自我循环保留在这里吗? 我认为应该添加[unowned self][weak self]

还有一个问题:
在这种情况下,strongSelf 的推荐名称是什么,在您的指南中没有找到:

if let strongSelf = self {

}

我认为是否保留 self 并不重要,因为每当使用locationManager变量然后被销毁时都会执行闭包。

虽然我不完全确定当你从不打电话时会发生什么locationManager 。 在这种情况下,关闭可能确实会保持自我。 有趣的。 :-)

编辑:刚刚测试了这一点,在这个闭包中使用 self 似乎没问题。 我想在您第一次调用locationManager之前,不会真正创建闭包。 然后它立即结束并再次被销毁。

顺便说一句, !是不需要的; 这很好用:

lazy var locationManager: CLLocationManager = {

谢谢,我更新了原始代码块以删除! 。 但是我们仍然需要讨论主要问题——我们是否应该在我们的风格指南中鼓励这种模式?

我喜欢。 它使事物的初始化接近事物的声明。 因此, viewDidLoad()往往会变得更短且更易于阅读。

我也喜欢这个。 👍

如果强引用周期存在问题,那么我认为[unowned self]应该解决它:

lazy var locationManager: CLLocationManager = {
  [unowned self] in
  let manager = CLLocationManager()
  manager.delegate = self
  ...
  return manager
}()

我喜欢这种方法,但我很难看到反对意见。 是否只是这种方法有点不明显,并且可能有细微差别(例如强大的保留周期的潜力)?

(参考:https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html#//apple_ref/doc/uid/TP40014097-CH20-ID57)

IMO 这只是一种模式。 也许它属于 swift-patterns 存储库。 至于保留周期,我想念它。 这只是一个典型的委托分配。 为什么这与您刚刚在初始化方法中执行 CLLocationManager 初始化内容有什么不同? 委托模式通常鼓励委托属性(在这种情况下,在 CLLocationManager 中)被定义为弱,不是吗?

我的错误是强保留周期。 这不是问题,因为执行了闭包。 如果它返回一个闭包,那么可能有必要使用[unowned self] ,尽管在这种情况下不是,因为它被分配为一个弱持有的委托。

还值得记住的是,为生成惰性 var 而指定的闭包不应依赖于类的任何可变属性。 在生成惰性变量时(通过对闭包的评估),它将捕获这些依赖项的当前值。 如果 var 不改变,那会很混乱。

请参阅http://mikebuss.com/2014/06/22/lazy-initialization-swift/中的此示例

class Person {
    var name: String

    lazy var personalizedGreeting: String = {
        [unowned self] in
        return "Hello, \(self.name)!"
        }()

    init(name: String) {
        self.name = name
    }
}

我认为这很令人困惑:

let p = Person(name: "sam")
p.personalizedGreeting
// => "Hello, sam!"

p.name = "definitely somebody else"
p.personalizedGreeting
// => "Hello, sam!"

说了这么多,我还是觉得这个模式很好,适合在教程中使用,但是作者理解这些问题很重要,以免犯这些相对复杂的错误。

/我现在就嘘

@sammyd链接的苹果文档中,苹果实际上举了一个例子:

lazy var asHTML: () -> String = {
    [unowned self] in
    if let text = self.text {
        return "<\(self.name)>\(text)</\(self.name)>"
    } else {
        return "<\(self.name) />"
    }
}

@sammyd给出的第二个链接中(他今天的链接很开心:]),作者举了一个例子:

lazy var personalizedGreeting: String = {
        [unowned self] in
        return "Hello, \(self.name)!"
        }()

我认为在这些情况下使用unowned self永远不会成为问题......也就是说,在创建惰性属性时......我想在某些情况下,虽然(正如@sammyd再次提到的那样,返回一个块,也许是其他人?)_not_ 使用unowned可能会有问题。

为简单起见,为什么不总是将unowned用于惰性属性?

无论如何,我完全赞成使用这种模式。 :+1:

注意asHTML变量_stores_ 的闭包供以后使用。 我们在这里谈论的是使用立即执行的闭包来初始化常规变量。

我喜欢它,对于lazy和非lazy属性。

不过,我不确定本指南中的指导是什么。 _总是_那样做? 如果您有一些计算的常量或颜色,我还可以看到“旧方式”在viewDidLoad()中初始化多个事物以重用对象的好处。 对我来说,这感觉像是一个个案。

多年来,我一直使用 initWithFrame 调用来实例化 ui 视图,其中包含 uilabels / textviews / 的子视图,它简化了代码并避免了视图生命周期调用——尤其是在调用超类时。
它会受到很小的性能影响吗? 或许。 这是否违背了 uikit 的流程和使用 nib 文件? 是的。 我会花费数小时对 nib 文件进行故障排除吗? 不会。我的应用程序会按时发货吗? 是的。 我担心吗? 不,其他开发人员感到震惊吗? 大概。 我在乎吗 - 不。
简单代码 = 简单逻辑 - 易于维护。

有些人会争辩说,在呈现视图控制器(它被推送到导航控制器上)之前,为了性能或内存 - 然后不要实例化控件(等待 viewDidLoad / loadView )。 那是在我们可以访问视图控制器本身的惰性变量之前。
所以这解决了抢先实例化的问题。

例如。
懒惰的 var loginVC = { return LoginVC()}()

注意 - 我在这里覆盖了 init 方法 - (没有 xib 文件)

required public init() {
    super.init(nibName: nil, bundle: nil)
}

来自stackoverflow
http://stackoverflow.com/questions/27585409/using-a-programmatically-created-uitableviewcell-subclass-in-swift/31801507#31801507

类事件单元:UITableViewCell {

var eventName: UILabel = UILabel()
var eventCity: UILabel = UILabel()
var eventTime: UILabel = UILabel()

override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)

    self.contentView.addSubview(eventName)
    self.contentView.addSubview(eventCity)
    self.contentView.addSubview(eventTime)
}

override func layoutSubviews() {
    super.layoutSubviews()

    eventName = UILabel(frame: CGRectMake(20, 10, self.bounds.size.width - 40, 25))
    eventCity = UILabel(frame: CGRectMake(0, 0, 0, 0))
    eventTime = UILabel(frame: CGRectMake(0, 0, 0, 0))

}

}

VS

class EventCell: UITableViewCell {
   lazy public  var lblName = {
    return UILabel (frame: CGRectMake(10, 0, self.bounds.size.width , 40))
  }()

  lazy public  var lblCity = {
   return UILabel (frame: CGRectMake(10, 40, self.bounds.size.width , 20))
  }()
 lazy public  var lblTime = {
      return UILabel (frame: CGRectMake(self.bounds.size.width - 80, 10, , 25))
   }()


   override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
   super.init(style: style, reuseIdentifier: reuseIdentifier)

    self.contentView.addSubview(lblName)
    self.contentView.addSubview(lblCity)
    self.contentView.addSubview(lblTime)
 }

}  

关于[unowned self]用于防止保留循环:现在不再如此。

Apple 给出并由@sammyd链接的示例代码不再使用[unowned self]

lazy var asHTML: Void -> String = {
    if let text = self.text {
        return "<\(self.name)>\(text)</\(self.name)>"
    } else {
        return "<\(self.name) />"
    }
}

事实上,我永远不会错过一个 deinit,方法是使用一个用引用裸self的闭包初始化的惰性 var。

现在我不知道在这些闭包中self是隐含的unowned还是unowned(unsafe) 。 然而,我们应该停止让它显式,因为它是隐式的。

asHTML 示例不是用闭包初始化值,而是将闭包存储在变量中。 在 asHTML 的情况下,仍然需要 [unowned self] 来防止强引用循环。 这在链接的 Swift 文档中都提到了。

Eric Chamberlain,首席架构师 - iOS
ArcTouch - 应用开发工作室

埃里克。 [email protected] 邮寄地址: [email protected]
+1-415-944-2000

为世界级品牌和财富 500 强定制应用程序
arctouch.com/work | arctouch.com/blog

2015 年 12 月 8 日上午 8:18,Gwendal Roué [email protected]写道:

关于 [unowned self] 用于防止保留循环:不再是这种情况。

示例代码https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html#//apple_ref/doc/uid/TP40014097 -CH20-ID57 由 Apple 提供并在上面链接@sammyd https://github.com/sammyd不再使用 [unowned self]:

懒惰的 var asHTML: Void -> String = {
如果让文本 = self.text {
返回“<(self.name)>(文本)(self.name)>”
} 别的 {
返回“<(self.name)/>”
}
}
事实上,我永远不会错过一个 deinit,方法是使用一个惰性 var 初始化一个引用裸自我的闭包。

现在我不知道 self 在这些闭包中是隐式无主还是无主(不安全)。 然而,我们应该停止让它显式,因为它是隐式的。


直接回复此邮件或在 GitHub 上查看https://github.com/raywenderlich/swift-style-guide/issues/88#issuecomment -162933300。

@arctouch-ericchamberlain,你是对的,示例代码现在在属性中存储了一个闭包。

然而,我重复自己一遍,“我永远不会错过一个 deinit,方法是使用一个惰性 var 初始化一个引用裸自我的闭包。”

举个例子:

class C {
    lazy var d: String = { "\(self)" }()
    deinit { print("deinit") }
}

C()   // deinit
C().d // deinit

你可以自己试试。 如果您找到保留周期,我很乐意再次使用[unowned self]

那是因为

懒惰的 var something = {..}()

不保留任何东西。 变量初始化时构造并执行闭包。 然而,

懒惰的东西= {...}

变量初始化时创建一个强引用循环。

示例代码中的 asHTML 变量属于后一种情况。

从初始化程序中删除 (),然后看看会发生什么。

Eric Chamberlain,首席架构师 - iOS
ArcTouch - 应用开发工作室

埃里克。 [email protected] 邮寄地址: [email protected]
+1-415-944-2000

为世界级品牌和财富 500 强定制应用程序
arctouch.com/work | arctouch.com/blog

2015 年 12 月 9 日下午 2:10,Gwendal Roué [email protected]写道:

@arctouch-ericchamberlain https://github.com/arctouch-ericchamberlain ,你是对的,示例代码现在在属性中存储了一个闭包。

然而,我重复自己一遍,“我永远不会错过一个 deinit,方法是使用一个惰性 var 初始化一个引用裸自我的闭包。”

举个例子:

C类{
懒惰的 var d: String = { "(self)" }()
deinit {打印(“deinit”)}
}

C() // 取消初始化
C().d // 取消初始化
你可以自己试试。 如果您找到保留周期,我很乐意再次使用 [unowned self]。


直接回复此邮件或在 GitHub 上查看https://github.com/raywenderlich/swift-style-guide/issues/88#issuecomment -163413290。

我不跟着你。

这些惰性变量都不需要[unowned self]

lazy var d: String = { "\(self)" }()
lazy var asHTML: Void -> String = { "\(self)" }

哪种惰性 var 可以?

糟糕,调用 asHTML 确实会创建保留循环。

谢谢@arctouch-ericchamberlain。 我误会了。

@groue

您发现 Apple 文档不使用[unowned self]的原因是因为他们正在设置存在问题的情况:

不幸的是,如上所述,HTMLElement 类在 HTMLElement 实例和用于其默认 asHTML 值的闭包之间创建了一个强引用循环。

然后他们继续通过添加[unowned self]来解决这个问题:

无主引用是用于解决之前 HTMLElement 示例中的强引用循环的适当捕获方法。

这导致了这个实现:

lazy var asHTML: Void -> String = {
    [unowned self] in
    if let text = self.text {
        return "<\(self.name)>\(text)</\(self.name)>"
    } else {
        return "<\(self.name) />"
    }
}

该文档页面很好地描述了何时可以使用保留周期以及何时应该使用unownedweak来摆脱它。

@johndpope

尝试与此实现进行比较,不确定我错过了什么,但我从 Apple 看到的示例倾向于避免使用封闭括号来设置内容。

class EventCell: UITableViewCell {

    lazy public var lblName = UILabel(frame: CGRectZero)
    lazy public var lblCity = UILabel(frame: CGRect())
    lazy public var lblTime = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0))

} 

不同意lazy应该用于可读性的概念。 它的唯一目的是真正的性能,因为它可能会引入状态问题,例如在闭包中使用可变属性。 您已经在一个地方进行了初始化,它称为指定的初始化。

您正在使用惰性,因为您有一些可能无法执行因此不需要的重物,或者您想延迟其执行,这恰好允许用户中止。

所以很有可能,在您拥有所述类之后,闭包将保留自我,从而造成泄漏。
即使您 100% 确定会调用闭包,也请始终使用弱/无主。
最终,我怀疑编译器会处理这个问题。 一个带有闭包的惰性属性只能执行一次,想想dispatch_once(..) 。 无论如何,这可能就是我将如何滚动。

首先,让我们把内存引用的东西排除在外。 (即使是关于lazy 的热门博客文章也会出现这个错误。我们需要更改Maxime!)由于上述原因,这不会泄漏。 即:闭包被懒惰地执行一次。 如果它被执行,self 被保留然后释放。 如果不执行,则永远不会保留。

下面的示例代码明确显示了这一点:

import XCPlayground
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

class Person {
  init(name: String) {
    self.name = name
    print("\(name) is born.")
  }

  deinit {
    print("\(name) dies.")
  }

  var name: String

  lazy var greeting: String = {
    return "Hello \(self.name)!"
  }()

}

do {
  let t = Person(name: "Ray")
  print(t.greeting)
}

print ("Next")

// Prints: 
// Ray is born.
// Hello Ray!
// Ray dies.
// Next

do {
  let t = Person(name: "Ray")
}

print ("End of the world")

// Prints:
// Ray is born.
// Ray dies.
// End of the world

接下来,让我们看看这整个懒惰的事情。 这是控制与视图控制器有关的初始化的好方法,因为您只需在使用时支付费用。 这对于CLLocationManager尤其重要,因为您可能希望控制何时发生访问,因此它不会在中间请求权限。

另一种看起来不那么 javascript-y 的模式是创建一个私有方法并调用它:

lazy var locationManager: CLLocationManager = self.makeLocationManager()

private func makeLocationManager() -> CLLocationManager
    let manager = CLLocationManager()
    manager.desiredAccuracy = kCLLocationAccuracyBest
    manager.delegate = self
    manager.requestAlwaysAuthorization()
    return manager
}

几点注意事项:

  • 我不确定哪个版本的 Swift 成为可能。
  • 没有堆泄漏; 同上的原因。 不相信我? 试试看!
  • locationManager 有一个明确的类型注释。 这是编译器当前要求的,否则您会收到有关self未解决的神秘错误消息。
  • 适用于类但不适用于不可变结构。

对于未在情节提要中分配或在viewDidLoad()中无条件分配的 UI 组件,我认为惰性是一个很好的模式。 但我不认为我已经准备好说“所有事情都懒惰”,因为它可能需要一些技巧,比如 LazyBoxes 来解决不变性错误。

我应该提一下,虽然它还没有被接受,但惰性的工作方式可能会发生相当大的变化:
https://github.com/apple/swift-evolution/blob/master/proposals/0030-property-behavior-decls.md

你怎么看待这件事:

懒惰的
考虑使用延迟初始化来更好地控制对象生命周期。

// Mark: - Location services
extension MyViewController {
  lazy var locationManager: CLLocationManager = self.makeLocationManager()

  private func makeLocationManager() -> CLLocationManager
    let manager = CLLocationManager()
    manager.desiredAccuracy = kCLLocationAccuracyBest
    manager.delegate = self
    manager.requestAlwaysAuthorization()
    return manager
  }
  // :
}

我不介意变量的块样式初始化。 事实上,通过将此类初始化放在变量声明中,可以轻松实现单元/UI 测试覆盖。

顺便说一句,由于分配 manager.delegate = self,将 locationManager 定义为类属性不起作用

你有什么建议?

只需让您的类(视图控制器?)符合 CLLocationManagerDelegate。

@rayfix我已将此片段移至超类。 我错过了将 CLLocationManagerDelegate 协议包含在基础中。

谢谢😀

作为一个 FYI,Swift 的 Joe Groff 已经确认惰性变量是自动的@noescape所以他们不会捕获自我。

https://twitter.com/jckarter/status/704100315587477504

请记住,您仍然可以通过其他方式捕获自我(例如,在惰性变量中捕获自我的块变量)

只是想让你知道,在 Xcode 8 中使用新的内存图视图时,我发现当我在下面没有使用 unowned self 构造时有一个捕获:

lazy var locationManager: CLLocationManager = {
    [unowned self] in
    var _locationManager = CLLocationManager()
    _locationManager.delegate = self
    _locationManager.desiredAccuracy = Set.shared.trackingAccuracy
    _locationManager.allowsBackgroundLocationUpdates = true
    _locationManager.activityType = .fitness      
    _locationManager.distanceFilter = Double(Set.shared.waypointInterval)
    return _locationManager
}()

但是当我包含它并且没有进行其他更改时,它并没有发生。

@rondiel请注意,CLLocationManager 的委托声明为分配(iOS 10 SDK)
@property(assign, nonatomic, nullable) id<CLLocationManagerDelegate> delegate;

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

ghost picture ghost  ·  26评论

agirault picture agirault  ·  3评论

samkim102 picture samkim102  ·  4评论

jackwu95 picture jackwu95  ·  6评论

Lweek picture Lweek  ·  5评论