解决一个 bug 之后对函数副作用的一点思考

09 Oct 2017 by 晓晨


下面是一段 Swift 的代码,代码做了简化。

class SomeClass {

    ...

    var accumulator: Double?

    func someFunc() {
        if let accumulator = accumulator { // A
            performPendingBinaryOperation() // B
            pendingBinaryOperation = PendingBinaryOperation(function: function, firstOperand: accumulator) // C
        }
    }

    private func performPendingBinaryOperation() {
        accumulator = pendingBinaryOperation.perform(with: accumulator)
    }

    ...

}

这段代码的问题在于,C 行中传入的参数 accumulator 是 A 行通过 optional binding 获得的成员变量 accumulator 的本地拷贝,但是 B 行执行的函数 performPendingBinaryOperation 已经修改了成员变量 accumulator,而 C 行使用的 accumulator 的本地拷贝在这里已经过时了。

如果这里 A 行使用的是 guard 语句,可能更难看出问题。

class SomeClass {

    ...

    var accumulator: Double?

    func someFunc() {
        guard let accumulator = accumulator else { // A
            return
        }
        performPendingBinaryOperation() // B
        pendingBinaryOperation = PendingBinaryOperation(function: function, firstOperand: accumulator) // C
    }

    private func performPendingBinaryOperation() {
        accumulator = pendingBinaryOperation.perform(with: accumulator)
    }

    ...

}

问题的根源来自 optional binding 对成员变量的本地拷贝在使用时过期了。

如果换成调用函数时对实参的形参拷贝也有可能出现类似的情况。

class SomeClass {

    ...

    var accumulator: Double?

    func someFunc(accumulator: Double) { // A
        performPendingBinaryOperation() // B
        pendingBinaryOperation = PendingBinaryOperation(function: function, firstOperand: accumulator) // C
    }

    private func performPendingBinaryOperation() {
        accumulator = pendingBinaryOperation.perform(with: accumulator)
    }

    ...

}

假如 someFunc 在被调用时传入的是成员变量 accumulator,那么在函数内 performPendingBinaryOperation 执行后,函数内的形参 accumulator 和传入的成员变量 accumulator 已经不一致。

以上情况,不管是 optional binding(if 或 guard),还是函数参数传递,本质上是,在拷贝变量和对新变量的使用之间,发生了对原变量的修改,即,

class SomeClass {

    ...

    var accumulator: Double?

    ... {
        let newAccumulator = accumulator // A
        performPendingBinaryOperation() // B
        pendingBinaryOperation = PendingBinaryOperation(function: function, firstOperand: accumulator) // C
    }

    private func performPendingBinaryOperation() {
        accumulator = pendingBinaryOperation.perform(with: accumulator)
    }

    ...

}

B 行即是上面提到的对原变量的修改。因为被封装在了函数里,所以很不明显。

理想情况下,在拷贝变量和对新变量的使用之间,如果调用了任何函数,且该函数或者该函数内部调用的函数(包括 computing property setter)实现中,修改了原变量,则应该给出警告。

解决方案:

还在想,没想出非常可行的方法。欢迎提供思路。 (SwiftLint 似乎不适合检查这么复杂的情况)