【问题标题】:Computed read-only property vs function in SwiftSwift 中计算的只读属性与函数
【发布时间】:2014-07-24 23:25:15
【问题描述】:

在 Swift WWDC 会话简介中,演示了一个只读属性 description

class Vehicle {
    var numberOfWheels = 0
    var description: String {
        return "\(numberOfWheels) wheels"
    }
}

let vehicle = Vehicle()
println(vehicle.description)

选择上述方法而不是使用方法是否有任何影响:

class Vehicle {
    var numberOfWheels = 0
    func description() -> String {
        return "\(numberOfWheels) wheels"
    }
}

let vehicle = Vehicle()
println(vehicle.description())

在我看来,选择只读计算属性的最明显原因是:

  • 语义 - 在本例中,description 成为类的属性而不是它执行的操作是有意义的。
  • 简洁/清晰 - 在获取值时无需使用空括号。

显然,上面的示例过于简单,但还有其他充分的理由可以选择其中一个吗?例如,是否有一些函数或属性的特性可以指导您决定使用哪个?


注意乍一看,这似乎是一个很常见的 OOP 问题,但我很想知道任何 Swift 特定的特性,这些特性可以指导使用这种语言时的最佳实践。

【问题讨论】:

  • 观看 204 会话 - “何时不使用 @property” 它有一些提示
  • 等等,你可以做一个只读属性并跳过get {}?我不知道,谢谢!
  • WWDC14 Session 204 可以在这里找到(视频和幻灯片),developer.apple.com/videos/play/wwdc2014/204
  • 查看相关Kotlin question

标签: swift methods properties language-agnostic semantics


【解决方案1】:

在我看来,这主要是风格问题:我非常喜欢使用 properties 来实现这一点:properties;表示您可以获得和/或设置的简单值。当实际工作完成时,我使用函数(或方法)。也许必须从磁盘或数据库中计算或读取某些内容:在这种情况下,我使用一个函数,即使只返回一个简单的值。这样我就可以很容易地看出一个调用是便宜的(属性)还是可能昂贵的(功能)。

当 Apple 发布一些 Swift 编码约定时,我们可能会变得更加清晰。

【讨论】:

    【解决方案2】:

    好吧,你可以应用 Kotlin 的建议 https://kotlinlang.org/docs/reference/coding-conventions.html#functions-vs-properties

    在某些情况下,没有参数的函数可以互换 具有只读属性。虽然语义相似,但有 是一些关于何时更喜欢一个而不是另一个的风格约定。

    当底层算法优先使用属性而不是函数:

    • 不扔
    • 复杂性计算起来很便宜(或缓存 第一次运行)
    • 通过调用返回相同的结果

    【讨论】:

    • 该建议中不再包含“有一个 O(1)”的建议。
    • 编辑以反映 Kotlin 的变化。
    【解决方案3】:

    虽然一般来说计算属性与方法的问题是困难且主观的,但目前在 Swift 的案例中有一个重要的论点是更喜欢方法而不是属性。您可以将 Swift 中的方法用作纯函数,这不适用于属性(从 Swift 2.0 beta 开始)。这使得方法更加强大和有用,因为它们可以参与功能组合。

    func fflat<A, R>(f: (A) -> () -> (R)) -> (A) -> (R) {
        return { f($0)() }
    }
    
    func fnot<A>(f: (A) -> Bool) -> (A) -> (Bool) {
        return { !f($0) }
    }
    
    extension String {
        func isEmptyAsFunc() -> Bool {
            return isEmpty
        }
    }
    
    let strings = ["Hello", "", "world"]
    
    strings.filter(fnot(fflat(String.isEmptyAsFunc)))
    

    【讨论】:

    • strings.filter {!$(0).isEmpty} - 返回相同的结果。它是苹果文档中关于 Array.filter() 的修改示例。而且更容易理解。
    【解决方案4】:

    由于运行时相同,因此这个问题也适用于 Objective-C。我会说,你得到的属性

    • 可以在子类中添加设置器,使属性readwrite
    • 能够使用 KVO/didSet 进行更改通知
    • 更一般地说,您可以将属性传递给需要关键路径的方法,例如获取请求排序

    至于 Swift 特有的东西,我唯一的例子是你可以使用 @lazy 作为属性。

    【讨论】:

      【解决方案5】:

      有一个区别: 如果你使用一个属性,你最终可以覆盖它并使其在子类中读/写。

      【讨论】:

      • 您也可以覆盖函数。或者添加一个 setter 来提供写作能力。
      • 基类定义名称为函数时,可以添加setter或者定义存储属性吗?如果它定义了一个属性,你当然可以这样做(这正是我的观点),但如果它定义了一个函数,我认为你不能这样做。
      • 一旦 Swift 拥有私有属性(参见此处stackoverflow.com/a/24012515/171933),您可以简单地向您的子类添加一个 setter 函数来设置该私有属性。当您的 getter 函数称为“name”时,您的 setter 将称为“setName”,因此不会发生命名冲突。
      • 您已经可以做到了(不同的是您用于支持的存储属性将是公开的)。但是 OP 询问在基础中声明只读属性或函数之间是否有区别。如果您声明一个只读属性,则可以在派生类中使其成为可读写的。将willSetdidSet 添加到base 类的扩展,在不知道任何未来派生类的情况下,可以检测被覆盖属性的变化。但我认为你不能用函数做类似的事情。
      • 如何覆盖只读属性来添加 setter?谢谢。我在文档中看到了这一点,“您可以通过在子类属性覆盖中提供 getter 和 setter 来将继承的只读属性呈现为读写属性”但是...... setter 写入什么变量?
      【解决方案6】:

      在只读情况下,计算属性不应被认为在语义上等同于方法,即使它们的行为相同,因为删除 func 声明模糊了包含的数量之间的区别实例的状态和仅仅是状态的函数的数量。您无需在调用站点输入(),但您的代码可能会失去清晰度。

      作为一个简单的例子,考虑以下向量类型:

      struct Vector {
          let x, y: Double
          func length() -> Double {
              return sqrt(x*x + y*y)
          }
      }
      

      通过将长度声明为一个方法,很明显它是一个状态的函数,它只依赖于xy

      另一方面,如果您要将length 表示为计算属性

      struct VectorWithLengthAsProperty {
          let x, y: Double
          var length: Double {
              return sqrt(x*x + y*y)
          }
      }
      

      然后,当您在 IDE 中对 VectorWithLengthAsProperty 的实例进行 dot-tab-complete 时,看起来 xylength 是平等地位的属性,这在概念上是不正确的。

      【讨论】:

      • 这很有趣,但是你能举一个例子说明当遵循这个原则时,一个计算的只读属性会在哪里使用?也许我错了,但您的论点似乎表明它们应该永远被使用,因为根据定义,计算的只读属性永远不会包含状态。
      【解决方案7】:

      在某些情况下,您更喜欢计算属性而不是普通函数。如:返回一个人的全名。您已经知道名字和姓氏。所以真的 fullName 属性是一个属性而不是一个函数。在这种情况下,它是计算属性(因为你不能设置全名,你可以使用名字和姓氏来提取它)

      class Person{
          let firstName: String
          let lastName: String
          init(firstName: String, lastName: String){
              self.firstName = firstName
              self.lastName = lastName
          }
          var fullName :String{
              return firstName+" "+lastName
          }
      }
      let william = Person(firstName: "William", lastName: "Kinaan")
      william.fullName //William Kinaan
      

      【讨论】:

        【解决方案8】:

        从性能的角度来看,似乎没有区别。正如您在基准测试结果中看到的那样。

        gist

        main.swift代码sn-p:

        import Foundation
        
        class MyClass {
            var prop: Int {
                return 88
            }
        
            func foo() -> Int {
                return 88
            }
        }
        
        func test(times: u_long) {
            func testProp(times: u_long) -> TimeInterval {
                let myClass = MyClass()
                let starting = Date()
                for _ in 0...times {
                    _ = myClass.prop
                }
                let ending = Date()
                return ending.timeIntervalSince(starting)
            }
        
        
            func testFunc(times: u_long) -> TimeInterval {
                let myClass = MyClass()
                let starting = Date()
                for _ in 0...times {
                    _ = myClass.prop
                }
                let ending = Date()
                return ending.timeIntervalSince(starting)
            }
        
            print("prop: \(testProp(times: times))")
            print("func: \(testFunc(times: times))")
        }
        
        test(times: 100000)
        test(times: 1000000)
        test(times: 10000000)
        test(times: 100000000)
        

        输出:

        prop: 0.0380070209503174 func: 0.0350250005722046 prop: 0.371925950050354 func: 0.363085985183716 prop: 3.4023300409317 func: 3.38373708724976 prop: 33.5842199325562 func: 34.8433820009232 Program ended with exit code: 0

        在图表中:

        【讨论】:

        • Date() 不适合基准测试,因为它使用计算机时钟,该时钟受操作系统自动更新的影响。 mach_absolute_time 会得到更可靠的结果。
        【解决方案9】:

        从语义上讲,计算属性应该与对象的内在状态紧密耦合 - 如果其他属性没有改变,那么在不同时间查询计算属性应该给出相同的输出(可通过 == 或 === 比较) - 类似于在该对象上调用纯函数。

        另一方面,开箱即用的方法假设我们可能并不总是得到相同的结果,因为 Swift 没有办法将函数标记为纯函数。此外,OOP 中的方法被视为操作,这意味着执行它们可能会导致副作用。如果该方法没有副作用,则可以安全地将其转换为计算属性。

        请注意,以上两种说法都是纯粹从语义的角度来看的,因为计算属性很可能会出现我们不期望的副作用,而方法是纯粹的。

        【讨论】:

          【解决方案10】:

          从历史上看,描述是 NSObject 的一个属性,许多人希望它在 Swift 中继续存在。在它之后添加括号只会增加混乱。

          编辑: 在激烈的投票之后,我必须澄清一些事情——如果它是通过点语法访问的,它可以被认为是一个属性。引擎盖下的内容并不重要。您无法使用点语法访问常用方法。

          此外,调用此属性不需要额外的括号,就像在 Swift 的情况下那样,这可能会导致混淆。

          【讨论】:

          • 实际上这是不正确的——descriptionNSObject 协议上必需的方法,因此在objective-C 中使用[myObject description] 返回。无论如何,description 属性只是一个人为的例子——我正在寻找一个适用于任何自定义属性/功能的更通用的答案。
          • 感谢您的澄清。我仍然不确定我是否完全同意您的说法,即任何返回值的无参数 obj-c 方法都可以被视为属性,尽管我确实理解您的推理。我暂时撤回我的反对票,但我认为这个答案描述了问题中已经提到的“语义”原因,跨语言一致性也不是这里真正的问题。
          猜你喜欢
          • 2014-07-27
          • 1970-01-01
          • 2014-11-20
          • 1970-01-01
          • 2017-11-04
          • 2014-01-09
          • 2017-06-06
          • 2018-06-11
          相关资源
          最近更新 更多