如何复制结构体并同时修改其属性之一?

问题描述 投票:0回答:7

如果我想将视图控制器的状态表示为单个结构,然后实现撤消机制,我该如何更改结构上的一个属性,同时获取前一个状态的副本?

struct A {
   let a: Int
   let b: Int

   init(a: Int = 2, b: Int = 3) {
      self.a = a
      self.b = b
   }
}

let state = A()

现在我想要

state
的副本,但 b = 4。如何才能做到这一点而不构造新对象并且不必为每个属性指定一个值?

swift immutability
7个回答
40
投票

这里的答案很荒谬,尤其是在结构体成员发生变化的情况下。

让我们了解 Swift 的工作原理。

当将一个结构体从一个变量设置为另一个变量时,该结构体会自动克隆到新变量中,即相同的结构体彼此不相关。

struct A {
    let x: Int
    var y: Int
}

let a = A(x: 5, y: 10)
var a1 = a
a1.y = 69
print("a.y = \(a.y); a1.y = \(a1.y)") // prints "a.y = 10; a1.y = 69"

请记住,如果您打算更改结构体中的成员,则必须将其标记为

var
,而不是
let

更多信息在这里:https://docs.swift.org/swift-book/LanguageGuide/ClassesAndStructures.html

很好,但如果您仍然想在一行中复制和修改,请将此函数添加到您的结构中:

func changing<T>(path: WritableKeyPath<A, T>, to value: T) -> A {
    var clone = self
    clone[keyPath: path] = value
    return clone
}

现在之前的示例更改为:

let a = A(x: 5, y: 10)
let a1 = a.changing(path: \.y, to: 69)
print("a.y = \(a.y); a1.y = \(a1.y)") // prints "a.y = 10; a1.y = 69"

我发现向很多结构添加“更改”会很痛苦,但扩展会很棒:

protocol Changeable {}

extension Changeable {
    func changing<T>(path: WritableKeyPath<Self, T>, to value: T) -> Self {
        var clone = self
        clone[keyPath: path] = value
        return clone
    }
}

用“Changeable”扩展你的结构,你将拥有“改变”的功能。

使用“更改”函数方法时,您在“更改”函数的调用站点中指定的任何属性(即类型为

WritableKeyPath
)都应在原始结构中标记为
var
,而不是
let


35
投票

请注意,当您使用 constants

a
b
的占位符值时,您无法使用除此占位符之外的任何其他值构造
A
的实例。改为编写初始化程序。您也可以编写自定义方法来更改结构中的任何值:

struct A {
    let a: Int
    let b: Int

    init(a: Int = 2, b: Int = 3) {
        self.a = a
        self.b = b
    }

    func changeValues(a: Int? = nil, b: Int? = nil) -> A {
        return A(a: a ?? self.a, b: b ?? self.b)
    }
}

let state = A()
let state2 = state.changeValues(b: 4)

17
投票

如果您可以接受属性可变,这是另一种方法。优点是它适用于每个结构,并且无需在添加属性时更改函数:

struct A {
    var a: Int
    var b: Int

    func changing(change: (inout A) -> Void) -> A {
        var a = self
        change(&a)
        return a
    }
}

let state = A(a: 2, b: 3)

let nextState = state.changing{ $0.b = 4 }

您还可以为此进行扩展:

protocol Changeable {}
extension Changeable {
    func changing(change: (inout Self) -> Void) -> Self {
        var a = self
        change(&a)
        return a
    }
}

extension A : Changeable {}

您也可以使用它来完成此操作,无需任何额外的代码:

let nextState = {
    var a = state
    a.b = 4
    return a
}()

如果你不介意新结构是可变的,那就是

var nextState = state
nextState.b = 4

4
投票

我真的很喜欢@Shadow的答案,但是我很难将其适应结构的字段可以为空的场景,所以我决定改用构建器模式。我的代码看起来像这样:

struct A {
    let a: Int?
    let b: Int?

    class Builder {
        private var a: Int?
        private var b: Int?

        init(struct: A) {
            self.a = struct.a
            self.b = struct.b
        }

        func build() -> A {
            return A(a: self.a, b: self.b)
        }

        func withA(_ a: Int?) -> Builder {
            self.a = a
            return self
        }

        func withB(_ b: Int?) -> Builder {
            self.b = b
            return self
        }
    }
}

然后你可以像这样使用它:

A.Builder(struct: myA).withA(a).withB(nil).build()

有了这个,我的结构实际上是不可变的,我可以快速创建一个副本并将其一个或多个字段更改为

nil
或另一个
Int


4
投票

您可以像 Kotlin 中一样为您的结构编写“复制”函数

struct A {
   let a: Int
   let b: Int

    func copy(a: Int? = nil, b: Int? = nil) -> A {
        .init( a: a ?? self.a,
               b: b ?? self.b
        )
    }
}

let state = A(a: 3, b: 5)
let stateCopy = state.copy()
let stateCopy2 = state.copy(a: 4)
let stateCopy3 = stateCopy2.copy(b: 8)
let stateCopy4 = stateCopy2.copy(a:1, b: 2)

2
投票

我发现的最好方法(将引用保留为

let
,而不是将它们更改为
var
),是编写一个初始化方法,该方法采用相同类型的对象进行“复制”,然后用于设置您想要更改的每个单独属性的可选参数。

可选的

init
参数允许您跳过任何想要在原始结构中保持不变的属性。

struct Model {
    let a: String
    let b: String
    
    init(a: String, b: String) {
        self.a = a
        self.b = b
    }
    
    init(model: Model, a: String? = nil, b: String? = nil) {
        self.a = a ?? model.a
        self.b = b ?? model.b
    }
}

let model1 = Model(a: "foo", b: "bar")
let model2 = Model(model: model1, b: "baz")

// Output:
// model1: {a:"foo", b:"bar"}
// model2: {a:"foo", b:"baz"}

0
投票

从 Swift 5.9 开始,引入了 Variadic Types,我们可以调整 @arutyun-enfendzhyan 的答案以允许一次进行多个更改:

我们的基础结构

struct Test {
    // If the values can change, they should be declared as vars
    var string = String()
    var int = Int()
    var double = Double()
}

我们的帮手

struct KeyValuePair<Model, Value> {
    let key: WritableKeyPath<Model, Value>
    let value: Value
    
    func update(_ model: inout Model) {
        model[keyPath: key] = value
    }
}

实施

extension Test {
    func copy<each Value>(
        with pair: repeat KeyValuePair<Self, each Value>
    ) -> Self {
        var clone = self
        repeat (each pair).update(&clone)
        return clone
    }
}

let test = Test()
test.copy(with:
    .init(key: \.string, value: "string"), 
    .init(key: \.int, value: 10), 
    .init(key: \.double, value: 20)
)

>> Test(string: "string", int: 10, double: 20.0)

还有其他选择:

extension Test {
    func copy<each Value>(
        keys: repeat WritableKeyPath<Self, each Value>,
        values: repeat each Value
    ) -> Self {
        var clone = self
        repeat KeyValuePair(key: each keys, value: each values).update(model: &clone)
        return clone
    }
}

let test = Test()
test.copy(keys: \.string, \.int, \.double, values: "string", 10, 20.0)

>> Test(string: "string", int: 10, double: 20.0)
extension Test {
    func copy<each Value>(
        with tuple: repeat (key: WritableKeyPath<Self, each Value>, value: each Value)
    ) -> Self {
        var clone = self
        repeat KeyValuePair(key: (each tuple).key, value: (each tuple).value).update(model: &clone)
        return clone
    }
}

let test = Test()
test.copy(with: (\.string, "string"), (\.int, 10), (\.double, 20.0))

>> Test(string: "string", int: 10, double: 20.0)

现在关于
Changeable
协议...

如果我们能够像 @arutyun-enfendzhyan 那样将所有这些添加到协议中,那就太棒了。但可变参数类型和协议扩展似乎存在限制。

我们可以看到错误如下:

protocol Changeable {}

extension Changeable {
    func copy<each Value>(
        with pair: repeat KeyValuePair<Self, each Value>
    ) -> Self {
        var clone = self
        repeat (each pair).update(&clone) // Type of expression is ambiguous without more context
        return clone
    }
}

这很遗憾,因为我们需要复制并粘贴一些代码,而不是通过简单的协议重用它。

如果有人对如何使用此处的协议有其他想法,请随时添加评论!


更新

从 Swift 5.9 和 Xcode 15 的正式版本开始,这似乎在 xcode 项目中工作,但仍然使我的 Playground 崩溃。 完整代码:

import Foundation

protocol Changeable {}

extension Changeable {
    func copy<each Value>(
        with pair: repeat KeyValuePair<Self, each Value>
    ) -> Self {
        var clone = self
        repeat (each pair).update(&clone)
        return clone
    }
}

struct KeyValuePair<Model, Value> {
    let key: WritableKeyPath<Model, Value>
    let value: Value
    
    func update(_ model: inout Model) {
        model[keyPath: key] = value
    }
}

struct Test: Changeable {
    var string = String()
    var int = Int()
    var double = Double()
}

let test = Test()
test.copy(with:
    .init(key: \.double, value: 1),
    .init(key: \.int, value: 2),
    .init(key: \.string, value: "3")
)

>> Test(string: "3", int: 2, double: 1.0)
© www.soinside.com 2019 - 2024. All rights reserved.