• Swift中的计算属性和无参数函数的区别
  • 发布于 1天前
  • 20 热度
    1 评论
昨天在 Code Review 中,我又看到一位小伙伴在同一个 struct 中同时定义了 fullName 计算属性和 getFullName() 方法,代码明显冗余。这不禁让我回想起自己刚入行时也曾犯过类似的错误,那时候还因为这个问题被老大一顿"教育"。Swift 提供了两种从现有值派生新值的方式:计算属性(computed property)和无参数函数(function without parameters)。这两种方式乍看几乎完全一样,但在实际使用中,选择不当会让代码变得混乱甚至引入性能问题。

今天我就来聊聊这个看似简单但经常被忽视的问题:究竟什么时候该用计算属性,什么时候该用函数?踩过的坑,就别再踩第二次了!
傻傻分不清的两种实现方式
先来看个最常见的例子,比如有一个 User 结构体,我们想获取用户的全名:
struct User {
let givenName: String
let familyName: String

// 方式一:使用计算属性
var fullName: String {
    return"\(givenName) \(familyName)"
  }
// 堆代码 duidaima.com
// 方式二:使用无参数函数
func fullName() -> String {
    return"\(givenName) \(familyName)"
  }
}
调用时略有不同:
let user = User(givenName: "张", familyName: "三")
// 使用计算属性,看起来像在访问一个值
let name1 = user.fullName
// 使用函数,看起来像在执行一个操作
let name2 = user.fullName()
功能上完全一样,所以到底哪个更好?答案是:要看情况!我在实际项目中总结了几条实用的判断标准。

实践出真知,四个判断标准
1. 有没有"副作用"?
记得去年做的一个社交 App 项目,有个小哥伴写了这样的代码:
// 有问题的实现
var profileViews: Int = 0
var userProfile: UserProfile {
    profileViews += 1 // 每次访问都记录一次浏览量,副作用!
    return fetchUserProfile()
}
这代码的问题在哪?每次访问 userProfile 都会让 profileViews 加 1!可能看起来无害,但想想这段代码:
// 开发者只是想判断两次是否相等
if userProfile == userProfile {
    // 一行代码,profileViews加了2!
}
计算属性应该像一个普通属性一样,给人感觉是"读取一个值",而不是"执行一个操作"。如果有副作用,请用函数:
func getUserProfile() -> UserProfile {
    profileViews += 1
    return fetchUserProfile()
}
这样,函数名 getUserProfile() 暗示了这是一个"动作",开发者就会意识到可能有其他事情发生。

2. 计算有多复杂?
记得上个月接手的那个有 3 万行代码的老项目吗?里面有这样一个计算属性:
// 效率低下的计算属性
var allUsersJoinedThisMonth: [User] {
    return allUsers.filter { Calendar.current.isDate($0.joinDate, equalTo: Date(), toGranularity: .month) }
}
看起来没问题,但当 allUsers 有几千条数据时,每次访问这个属性都会进行一次完整的数组过滤,性能代价相当高!计算属性最好是 O(1) 复杂度,即常数时间。如果需要遍历大量数据或复杂计算,使用函数会更为合适:
func findUsersJoinedThisMonth() -> [User] {
    return allUsers.filter { Calendar.current.isDate($0.joinDate, equalTo: Date(), toGranularity: .month) }
}
函数名中的动词 find 暗示了这是一个"查找"操作,可能需要一些时间。

3. 代码有多长?
实话讲,这一条完全是我的主观感受,但在团队协作中却很重要。我认为计算属性应该简短明了,通常几行代码就能完成。之前我在一次 code review 中看到 60 行长的计算属性,简直要吐血! 如果你的计算属性像一篇小论文,那还是改成函数吧:
// 不太适合计算属性的长代码
var complexUserStatistics: UserStats {
    // 假设这里有20多行复杂逻辑...
    let activeTime = // 一堆计算...
    let engagement = // 更多计算...
    let retention = // 继续计算...
    returnUserStats(active: activeTime, engagement: engagement, retention: retention)
}
// 堆代码 duidaima.com
// 更合适的函数形式
func calculateUserStatistics() -> UserStats {
    // 20多行代码放这里更合适
    // ...
    return stats
}
4. 结果稳定吗?
这是最容易被忽视的一点。计算属性给人的心理预期是:短时间内多次访问,得到的结果应该是一样的。如果每次访问都可能不同,用函数更好。
// 不合适:计算属性每次结果可能不同
var randomDiscountCode: String {
    let letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    return String((0..<8).map{ _ in letters.randomElement()! })
}

// 更合适:使用函数
func generateRandomDiscountCode() -> String {
    let letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    return String((0..<8).map{ _ in letters.randomElement()! })
}
"generate"这个动词暗示了创建新内容,而不是获取现有内容。

真实场景分析
场景1:格式化文本(适合用计算属性)
我在做电商 App 时,商品价格展示就是计算属性的绝佳用例:
struct Product {
    let price: Double
    
    // 完美的计算属性应用
    var formattedPrice: String {
        return String(format: "¥%.2f", price)
    }
}
为什么?因为它简单、无副作用、O(1)复杂度,结果确定。每次访问 product.formattedPrice 都期望看到同样格式的价格字符串。

场景2:数据请求(适合用函数)
在上个项目中,有同事犯了这个错误:
// 错误示范
class ProfileViewController: UIViewController {
    // 不适合用计算属性 - 这里有网络请求!
    var currentUserFriends: [Friend] {
        return apiService.fetchFriends(for: currentUser.id)
    }
}
想象一下,这个视图控制器的其他方法中多次访问 currentUserFriends,每次都会触发网络请求!后来修改为:
func loadUserFriends() async throws -> [Friend] {
    return try await apiService.fetchFriends(for: currentUser.id)
}
动词 load 清楚地表明了这是一个加载操作,可能很耗时。

场景3:缓存数据访问(混合策略)
有时候,两种方式结合使用更妙:
class UserManager {
    privatevar cachedUsers: [User]?
    
    // 计算属性,但内部用了缓存机制
    var allUsers: [User] {
        iflet cached = cachedUsers {
            return cached
        }
        
        // 首次访问时加载
        cachedUsers = loadUsersFromDisk()
        return cachedUsers!
    }
    
    // 强制刷新用函数
    func refreshUsers() -> [User] {
        cachedUsers = loadUsersFromDisk()
        return cachedUsers!
    }
}
给 SwiftUI 开发者的特别提示
用 SwiftUI 的同学请注意,计算属性在 SwiftUI 视图中非常常见,但要小心性能陷阱。一个复杂的计算属性可能会在每次视图刷新时重复计算:
struct UserView: View {
    @ObservedObject var viewModel: UserViewModel
    
    // 危险:视图每次更新都会执行这个计算
    var filteredUsers: [User] {
        viewModel.users.filter { $0.isActive }
    }
    
    var body: some View {
        List(filteredUsers) { user in
            Text(user.name)
        }
    }
}
如果 users 数组很大,可以考虑在 ViewModel 中计算并缓存结果,或使用 @State 存储计算结果。

实战建议
经过无数次踩坑和爬坑,我总结了几条实用建议:
1.默认选计算属性:如果只是简单派生一个值,选计算属性。这样代码更简洁,意图更明确。
2.代码超过5行就考虑用函数:这不是硬性规定,但是个好习惯。长计算属性影响代码可读性。
3.留意命名风格:计算属性用名词(如 fullName),函数用动词开头(如 calculateTotal())。
4.可变状态用函数:如果你在修改状态或有副作用,请用函数明确表达这一点。
5.看团队习惯:跟随团队或项目的已有风格保持一致。不要在同一项目里忽冷忽热。

说回最初的例子
struct User {
  let givenName: String
  let familyName: String
  
  var fullName: String {
    return "\(givenName) \(familyName)"
  }
}
对于这个例子,我坚定地选择计算属性。因为它完全符合我们的标准:简单的字符串拼接,没有副作用,O(1)复杂度,结果稳定。

总结
有人可能会说:"不就是选计算属性还是函数吗,有必要讲这么多?"但正是这些小细节的积累,才造就了代码的整体品质和可维护性。
记住这些简单准则:
计算属性:适合简单、无副作用、确定性的计算
函数:适合复杂、有副作用或非确定性的操作
最后问大家一个问题,你们平时是怎么选择的?有没有因为选错而踩过坑?欢迎在评论区分享你的经验!
用户评论