【问题标题】:Simple way to replace an item in an array if it exists, append it if it doesn't如果数组中的项目存在,则替换它的简单方法,如果不存在则追加它
【发布时间】:2019-08-11 08:19:07
【问题描述】:

斯威夫特 4.2

我有多个函数可以替换数组中的对象或结构(如果存在),如果不存在,则添加它。

func updateFruit(_ fruit: Fruit)
{
    if let idx = fruitArray.firstIndex(where: { $0.id == fruit.id })
    {
        fruitArray[idx] = fruit
    }
    else
    {
        fruitArray.append(fruit)
    }
}

显然我可以把它变成 Array 的扩展:

extension Array
{
    mutating func replaceOrAppend(_ item: Element, whereFirstIndex predicate: (Element) -> Bool)
    {
        if let idx = self.firstIndex(where: predicate)
        {
            self[idx] = item
        }
        else
        {
            append(item)
        }
    }
}

但是,有没有更简单、更容易的表达方式?最好使用闭包或内置函数。

注意:当前的实现不允许使用集合。

【问题讨论】:

  • 您的代码简单易行:)
  • 仅供参考 - 由于您实际上是在寻找更多工作代码的代码审查,您可能希望查看Code Review 站点。如果您最终确实在那里发布了您的问题,请删除此问题。
  • 你可以让你的生活“更轻松”,让你的Fruit符合Equatable。这样做你不需要谓词。如果您想使用Set,只需使其符合Hashable
  • @LeoDabus 我喜欢那个解决方案。
  • 当您说您拥有“多个功能”时,其他功能是什么样的?我怀疑您正在沿错误的轴使此代码通用。他们都有$0.id == newthing.id 之类的东西,还是有其他谓词?是所有谓词$0.<prop> == newthing.<prop>(即使不只是id)?还是这些谓词更复杂?通用代码必须始终从它的调用方式开始,否则你会走错路。

标签: arrays swift


【解决方案1】:

鉴于您的用例,您总是在检查$0.<prop> == newthing.<prop>,您可以通过添加以下内容进一步提升:

mutating func replaceOrAppend<Value>(_ item: Element, 
                                     firstMatchingKeyPath keyPath: KeyPath<Element, Value>)
    where Value: Equatable
{
    let itemValue = item[keyPath: keyPath]
    replaceOrAppend(item, whereFirstIndex: { $0[keyPath: keyPath] == itemValue })
}

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

struct Student {
    let id: Int
    let name: String
}

let alice0 = Student(id: 0, name: "alice")
let alice1 = Student(id: 1, name: "alice")
let bob = Student(id: 0, name: "bob")

var array = [alice0]

array.replaceOrAppend(alice1, firstMatchingKeyPath: \.name) // [alice1]
array.replaceOrAppend(bob, firstMatchingKeyPath: \.name)    // [alice1, bob]

当然,如果你经常这样做,你可以不断提升。

protocol Identifiable {
    var id: Int { get }
}

extension Student: Identifiable {}

extension Array where Element: Identifiable {
    mutating func replaceOrAppendFirstMatchingID(_ item: Element)
    {
        replaceOrAppend(item, firstMatchingKeyPath: \.id)
    }
}

array.replaceOrAppendFirstMatchingID(alice0) // [alice1, alice0]

【讨论】:

    【解决方案2】:

    假设您的类型是 Equatable,这是一个通用扩展:

    extension RangeReplaceableCollection where Element: Equatable {
    
        mutating func addOrReplace(_ element: Element) {
            if let index = self.firstIndex(of: element) {
                self.replaceSubrange(index...index, with: [element])
            }
            else {
                self.append(element)
            }
        }
    }
    
    

    不过,请记住,我(和您的)函数只会替换 一个 匹配项。

    完整的工作操场测试:

    【讨论】:

    • TestType.== 的示例实现不是对 Equatable 的有效一致性。 Equatable 要求“为了保持可替换性,== 运算符应考虑 Equatable 类型的所有可见方面。”你的 == 只考虑first
    • @RobNapier 这是一个测试实现来验证addOrReplace的功能......它并不意味着“正确”
    • 但如果它更正确,它就没有用了。如果element 可以完全替代集合中的元素,为什么要替换它?它们彼此无法区分。
    • 如果这很重要,就会违反 Equatable。 “平等意味着可替代性——任何两个比较相等的实例都可以在取决于它们的值的任何代码中互换使用。”您描述的问题仅与可变引用类型有关。这就是为什么除了 === 之外的任何东西都很难使可变引用类型 Equatable。
    • @RobNapier 如果您想对未强制执行的规则持教条主义的态度,那就去做吧。抛开关于封装的工具脱落,我只想说像这样的 API 是公开的,因此我们可以在深奥的理想满足现实世界使用时以一种有意义的方式动态地使用它们。
    【解决方案3】:

    我可以建议使用replaceValue 创建protocol Replacable,这将表示我们可以用来枚举对象的标识符。

    protocol Replacable {
        var replaceValue: Int { get }
    }
    

    现在我们可以为Array 创建扩展,但现在我们可以像这样从示例代码中删除谓词

    extension Array where Element: Replacable {
        mutating func replaceOrAppend(_ item: Element) {
            if let idx = self.firstIndex(where: { $0.replaceValue == item.replaceValue }) {
                self[idx] = item
            }
            else {
                append(item)
            }
        }
    }
    

    由于Set 不是有序集合,我们可以简单地删除对象,如果集合包含它并插入新值

    extension Set where Element: Replacable {
        mutating func replaceOrAppend(_ item: Element) {
            if let existItem = self.first(where: { $0.replaceValue == item.replaceValue }) {
                self.remove(existItem)
            }
            self.insert(item)
        }
    }
    

    【讨论】:

    • 虽然数组的解决方案可能有效,但我更喜欢我们符合 equatable 的解决方案,因为它更简单
    • 您需要通过仅检查对象ids 是否相等来覆盖Equatable== 方法。在所有情况下都不正确。例如,您有两个具有相同ids 但具有不同names 的对象。他们应该平等吗?这就是为什么我也更喜欢有特殊设计的协议
    • 我同意在这里使用 Equatable 是不正确的,但是这个协议是非常单一的(特别是对于像“可替换”这样的通用名称)。问题中的原始代码比IMO好得多。如果需要扩展,where Element == Fruit 将是适当的限制,直到您可以在同一程序中找到足够多的其他用例来提取有意义的协议。
    • @TarasChernyshenko 在重新考虑 Equatable 之后,我同意这不是最好的解决方案。
    猜你喜欢
    • 2012-03-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-28
    • 2014-07-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多