Swift中计算的只读属性与函数


98

在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的功能,这些功能可以指导使用该语言时的最佳实践。


1
观看204课-“何时不使用@property”它有一些提示
Kostiantyn Koval 2014年

4
等待,您可以执行只读属性并跳过get {}?我不知道,谢谢!
Dan Rosenstark

WWDC14会议204可以在这里找到(视频和幻灯片),developer.apple.com
videos /

Answers:


53

在我看来,这主要是风格问题:我非常喜欢将属性用于:表示您可以获取和/或设置的简单值。当实际工作完成时,我使用函数(或方法)。也许必须从磁盘或数据库中计算或读取某些内容:在这种情况下,即使仅返回一个简单值,我也使用一个函数。这样一来,我可以轻松地看到一个呼叫是廉价的(属性)还是可能昂贵的(功能)。

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


12

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

在某些情况下,不带参数的函数可能会与只读属性互换。尽管语义相似,但是在何时首选一个相对另一个上有一些样式约定。

当基础算法满足以下条件时,将属性优先于函数:

  • 不扔
  • 计算复杂度很便宜(或在第一次运行时计算)
  • 通过调用返回相同的结果

1
该建议中不再包含“具有O(1)”建议。
David Pettigrew

编辑以反映Kotlin的更改。
卡斯滕·哈格曼

11

尽管计算属性与方法的问题通常比较棘手和主观,但是在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)))

1
strings.filter {!$(0).isEmpty}-返回相同的结果。它是Array.filter()上Apple文档的修改示例。而且它更容易理解。
poGUIst

7

由于运行时相同,因此此问题也适用于Objective-C。我会说,有了物业,

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

至于特定于Swift的东西,我仅有的一个例子是您可以将其@lazy用于属性。


7

区别在于:如果使用属性,则最终可以覆盖它,并在子类中对其进行读写。


9
您也可以覆盖功能。或添加二传手以提供书写能力。
Johannes Fahrenkrug 2014年

当基类将名称定义为函数时,可以添加setter或定义存储的属性?如果定义了一个属性,您当然可以做到(这正是我的观点),但是,如果它定义了一个函数,我认为您不能做到。
模拟文件

一旦Swift具有私有属性(请参阅此处stackoverflow.com/a/24012515/171933),您就可以在子类中简单地添加一个setter函数来设置该私有属性。当您的getter函数称为“名称”时,您的setter将称为“ setName”,因此没有命名冲突。
Johannes Fahrenkrug 2014年

您已经可以做到了(区别在于您用于支持的存储属性将是公共的)。但是,OP询问在基声明一个只读属性或一个函数之间是否有区别。如果声明了只读属性,则可以在派生类中将其设置为读写。在类中添加willSet和添加的扩展,而无需了解任何将来的派生类,就可以检测重写属性中的更改。但是我认为您不能用函数做类似的事情。didSet
Analog File

如何覆盖只读属性以添加设置器?谢谢。我在文档中看到这一点,“您可以通过在子类属性重写中同时提供getter和setter来将继承的只读属性呈现为读写属性”,但是... setter写入哪个变量?
Dan Rosenstark

5

在只读情况下,即使在行为相同的情况下,也不应将计算的属性视为在语义上等同于方法,因为删除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,它看起来好像xylength是平起平坐,这是不正确概念的属性。


5
这很有趣,但你可以给一个地方计算的只读属性的示例遵循此原则时,可以使用?也许我是错的,但是您的论点似乎建议不要使用它们,因为根据定义,经过计算的只读属性绝不包含状态。
斯图尔特

2

在某些情况下,您更喜欢计算属性而不是普通函数。如:返回一个人的全名。您已经知道名字和姓氏。因此,实际上,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

1

从性能角度看,似乎没有什么区别。如您在基准测试结果中所见。

要旨

main.swift 代码段:

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

在图表中:

基准


2
Date()不适用于基准测试,因为它使用计算机时钟,该时钟可能会由操作系统自动更新。mach_absolute_time将获得更可靠的结果。
Cristik '18年

1

从语义上讲,计算属性应与对象的固有状态紧密结合-如果其他属性不变,则在不同时间查询计算属性应提供相同的输出(可通过==或===进行比较)-类似在该对象上调用纯函数。

另一方面,方法开箱即用,因为我们可能无法始终获得相同的结果,因为Swift没有办法将函数标记为纯方法。同样,OOP中的方法也被视为动作,这意味着执行它们可能会导致副作用。如果该方法没有副作用,则可以安全地将其转换为计算属性。

请注意,以上两种说法纯粹是从语义的角度出发,因为计算属性很可能会发生我们所不希望的副作用,并且方法很纯净。


0

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

编辑:愤怒的投票之后,我必须澄清一些东西-如果通过点语法访问它,则可以将其视为属性。没关系的是什么。您无法使用点语法访问常规方法。

此外,调用此属性不需要额外的括号,就像Swift一样,这可能会引起混乱。


1
实际上这是不正确的- description是协议上的必需方法NSObject,因此在Objective-C中使用返回[myObject description]。无论如何,该属性description只是一个虚构的示例-我正在寻找适用于任何自定义属性/函数的更通用的答案。
Stuart 2014年

1
感谢您的澄清。我仍然不确定是否完全同意您的说法,尽管我确实理解您的理由,但任何返回值的无参数obj-c方法都可以视为属性。我现在暂时取消投反对票,但我认为这个答案是描述问题中已经提到的“语义”原因,而且跨语言一致性在这里也不是真正的问题。
Stuart
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.