Swift链式调用可变成员报错的差异原因及解决办法
问题描述
我在Swift中自定义了Deque结构体,包含多个带mutating关键字的添加、移除元素方法。当我尝试直接用链式调用Deque().pushBack(contentsOf: 1, 2, 1, 2, 1)作为参数传给全局函数match时,触发了如下错误:
无法对不可变值使用可变成员:函数调用返回不可变值
但如果先把Deque实例赋值给var变量,再执行相同的链式调用并传入match,代码就能正常运行。我想搞清楚两者差异的原因、是否和match函数有关,以及解决办法。
相关代码如下:
struct Deque { private var arrayDeque: [Int] = [] mutating func pushFront(_ element: Int) { arrayDeque.insert(element, at: 0) } @discardableResult mutating func pushFront(contentsOf elements: Int...) -> Deque { for element in elements.reversed() { pushFront(element) } return self } mutating func pushBack(_ element: Int) { arrayDeque.append(element) } @discardableResult mutating func pushBack(contentsOf elements: Int...) -> Deque { for element in elements { pushBack(element) } return self } @discardableResult mutating func popFront() -> Deque? { guard !isEmpty else { return nil } var copy = self copy.arrayDeque.removeFirst() return copy } @discardableResult mutating func popBack() -> Deque? { guard !isEmpty else { return nil } var copy = self copy.arrayDeque.removeLast() return copy } var isEmpty: Bool { return arrayDeque.isEmpty } var count: Int { return arrayDeque.count } func peekFront() -> Int? { return arrayDeque.first } func peekBack() -> Int? { return arrayDeque.last } } func match(_ xs: Deque) -> Bool { ... } // 报错代码 let a = match(Deque().pushBack(contentsOf: 1, 2, 1, 2, 1)) // 正常运行代码 var b = Deque() let b2 = b.pushBack(contentsOf: 1, 2, 1, 2, 1) let b3 = match(b2)
原因分析
1. 值类型的可变性规则
Swift里的结构体是值类型,值类型的实例有两种状态:
- 用
let声明的实例是不可变的,不能修改它的属性或调用带mutating的方法 - 用
var声明的实例是可变的,可以修改属性、调用mutating方法
带mutating关键字的方法,本质是要修改实例本身的内容,所以只能被可变实例调用。
2. 临时实例的默认不可变性
Deque()直接创建的是一个临时匿名实例,Swift默认把这种临时实例当作不可变的(相当于隐式用let声明)。当你调用它的pushBack(contentsOf:)方法时,这个mutating方法需要修改这个临时实例,但它是不可变的,所以就会触发报错。
3. 和match函数无关
这个错误和match函数没有关系,只是因为你把链式调用的结果直接传给match时,链式调用的第一步生成了不可变的临时实例,导致mutating方法无法执行。就算不调用match,直接写Deque().pushBack(contentsOf: 1,2,3)也会报同样的错。
解决方法
方法一:先将实例赋值给var变量(最直观的写法)
就是你已经试过的正常代码,先把Deque实例用var声明为可变,再调用mutating方法:
var tempDeque = Deque() let modifiedDeque = tempDeque.pushBack(contentsOf: 1, 2, 1, 2, 1) let result = match(modifiedDeque)
方法二:修改方法,去掉mutating改为返回新实例(更符合值类型设计)
值类型的设计习惯通常是不修改自身,而是返回修改后的新实例。调整你的pushBack(contentsOf:)等方法,移除mutating关键字,内部创建新实例并返回:
// 修改后的pushBack(contentsOf:)方法 @discardableResult func pushBack(contentsOf elements: Int...) -> Deque { var newDeque = self for element in elements { newDeque.arrayDeque.append(element) } return newDeque } // 同理修改pushFront(contentsOf:)等方法 @discardableResult func pushFront(contentsOf elements: Int...) -> Deque { var newDeque = self for element in elements.reversed() { newDeque.arrayDeque.insert(element, at: 0) } return newDeque }
修改后,即使是临时实例也能直接调用链式方法,代码可以正常运行:
let a = match(Deque().pushBack(contentsOf: 1, 2, 1, 2, 1))
方法三:显式创建可变临时实例(可读性较差,不推荐)
Swift支持在表达式中创建可变临时实例,不过这种写法可读性不高,一般不建议使用:
let a = match({ var temp = Deque() temp.pushBack(contentsOf: 1, 2, 1, 2, 1) return temp }())
内容的提问来源于stack exchange,提问作者Adrian

