跳到主要内容

让 mutating 从"编译报错才想起来"变成"写完就想起来"

一键三连
一键三连

· 阅读约 4 分钟

写方法改属性,一天编译个二三十次,每次都有一次是死在 mutating 上。不是不会,是写的时候根本没想"这个方法我调的是 struct 还是 class"——反正都写了七八十行了,编译器一红,哦对,得加 mutating,加上去,编译过。

这个动作烦在哪?不是加个关键字这件事本身。是它永远打断你——你正在想方法逻辑,编译器把你拽回"类型系统"那个层面,用一条红色报错提醒你:sorry,你改的是值类型。一天打断二十次,十次是同一个原因,这浪费的不是编译时间,是思路。

上个月翻到一篇 dev.to 上的老文,作者用了个"路标"的比喻讲这事的根子:类的实例是拿着一张写着地址的纸条,结构体就是那个地址本身。你对那张纸条说"你去哪儿",它因为 let 被钉死不能换地址,但纸条指向的那间屋子里,里面 var 的东西想怎么动怎么动。结构体呢,let 一钉,就是整个地址本被你按住了,里面哪一行都不许划。

写代码写了这么多年,这个道理我肯定不是不懂。但"懂"和"写的那一刻想得起来"是两回事。所以那篇文章本身没让我多震撼——评论区倒是有一个哥们儿的补充让我记到现在。他说,引用标识和状态可变性是两码事,Swift 默认推你用结构体,不只是性能上的原因,也是逼你把"这个对象的身份到底变不变"想清楚。

他说得对,但我的问题是:想清楚这个需要思考时间。写代码的时候,大部分情况根本不需要我"重新想一遍"——

你在一个结构体里写了这个方法,它改了自己的一个属性,你就知道要加 mutating。问题从来不是不知道,是"根本没意识到自己正在改结构体的属性"。

所以我给自己搞了个东西,不是什么高深玩意,一个 zsh 函数:

checkmutating() {
  grep -n "mutating" "$1" | head -20
}

没用。这玩意儿只告诉我哪里有 mutating,不告诉我哪里缺。

后来换了个思路。不是写完再查,是写之前先把地基立好。你一个 struct 里如果有五六个方法都要改属性,为什么不一开始就把它们标成 mutating?我说的"标"不是让你写完了再补——是结构体一建好,所有要改 var 属性的方法,在写函数签名那一行就直接把 mutating 写上去。

就这么简单?就这么简单。

⚡这一下,不是某个脚本,是"在写签名的时候,就把 mutating 当成函数签名的一部分"。你写 func update() 的时候,脑子里的第一反应不该是"这个方法会改属性吗",而是"这个结构体的方法,默认就当它 mutable"。写完了不是 mutating 的方法再去掉,而不是反过来。

这个顺序一换,编译报错的次数少了大概七八成——体感,没测过,但这周用下来的感觉确实如此。类那边就更不用管了,那篇 dev.to 里说得很清楚:类的方法不用标 mutated,编译器只检查属性是 let 还是 var。你有一个 class,属性是 var,方法里随便改,没有任何多余的仪式感。

为什么这一下管用?因为它把"判断"从编码中途换到了编码开头的 10 秒。你新建一个 struct,它的方法到底需不需要改属性——这不是一个中途才能知道答案的问题。属性是 var 还是 let,写类型的时候你就已经定了,后面的方法全是围绕这个决定的展开。所以"先默认 mutable,写完不需要了再摘掉"这个坏习惯看着好像会多写几个用不上的 mutating,实际成本低到可以忽略——多写一两个字,编译器又不会报错;漏写一个,那可是一整条红色报错加编译循环。

一个额外的坑。之前那篇文里作者列了类和变量/常量属性组合的四种情况,我本来想做成一张速查表贴终端里,后来放弃了——记这四种组合的工夫,够我撞十次 mutating 报错了。只记住一条就够用:类看引用,结构体看属性。类 let 了你还能改 var 属性,结构体 let 了一切止步。其他组合都是这条的变体。

这个习惯换过来花了大概一下午——前半小时是适应,后面就开始频繁"啊这次我写签名的时候就标了",然后编译一下,过。三分钟的替换成本早回本了,按每天少打断七八次的频率看,一周内稳赚。这习惯只对结构体适用,如果你全项目都用类做模型,那这篇文章对你没啥用,赶紧划走。

你们有没有自己的"写之前先立规矩"的小习惯?或者你处理 mutating 有更不打断思路的招,教教我,我也想抄。