如何在本机 Swift 中实现以前称为 NSMutableOrderedSet 的可变有序集泛型类型?
Posted
技术标签:
【中文标题】如何在本机 Swift 中实现以前称为 NSMutableOrderedSet 的可变有序集泛型类型?【英文标题】:How to implement a Mutable Ordered Set generic type formerly known as NSMutableOrderedSet in native Swift? 【发布时间】:2020-01-23 21:49:04 【问题描述】:我正在尝试实现一个通用的 Mutable Ordered Set 类型,它需要符合许多协议才能像 Swift 中的 Array 和 Set 一样。首先要实现泛型类型元素需要符合Hashable
,泛型结构需要符合RandomAccessCollection
、SetAlgebra
、ExpressibleByArrayLiteral
、AdditiveArithmetic
、RangeReplaceableCollection
和MutableCollection
。我还想允许下标访问它的SubSequence
,并添加对PartialRangeThrough
、PartialRangeUpTo
、PartialRangeFrom
和UnboundedRange
的支持。
这是我的通用 OrderedSet
结构声明:
public struct OrderedSet<Element: Hashable>
public init()
private var elements: [Element] = []
private var set: Set<Element> = []
尽管这是一个自我回答的问题,但我真的很感激并鼓励新的答案,对此实施的一些反馈和/或关于如何修复/改进它的建议:
编辑/更新:
sorted
方法按预期工作,但变异的sort
甚至没有改变元素顺序。
可变集合
声明变异
函数排序()
当 Self 符合 RandomAccessCollection 且 Element 符合 Comparable 时可用。
var numbers: OrderedSet = [15, 40, 10, 30, 60, 25, 5, 100]
numbers[0..<4] // [15, 40, 10, 30]
numbers[0..<4].sorted() // [10, 15, 30, 40]
numbers[0..<4].sort() // [15, 40, 10, 30, 60, 25, 5, 100]
print(numbers)
// Prints "[15, 40, 10, 30, 60, 25, 5, 100]"
// But it should print "[10, 15, 30, 40, 60, 25, 5, 100]"
我该如何解决?
【问题讨论】:
比codereview.stackexchange.com/questions/tagged/swift更好? 【参考方案1】:可变有序集的原生 Swift 实现:
public struct OrderedSet<Element: Hashable>
public init()
private var elements: [Element] = []
private var set: Set<Element> = []
符合 MutableCollection 协议 要将 MutableCollection 协议的一致性添加到您自己的自定义集合中,请升级您的类型的下标以支持读取和写入访问。存储在 MutableCollection 实例的下标中的值随后必须可以在同一位置访问。也就是说,对于可变集合实例 a、索引 i 和值 x,以下代码示例中的两组赋值必须相等:
extension OrderedSet: MutableCollection
public subscript(index: Index) -> Element
get elements[index]
// set
// guard set.update(with: newValue) == nil else
// insert(remove(at: elements.firstIndex(of: newValue)!), at: index)
// return
//
// elements[index] = newValue
//
set
guard let newMember = set.update(with: newValue) else return
elements[index] = newMember
符合 RandomAccessCollection 协议
RandomAccessCollection
协议对相关的 Indices 和 SubSequence 类型增加了进一步的约束,但对BidirectionalCollection
协议没有额外的要求。但是,为了满足随机访问集合的复杂性保证,您的自定义类型的索引必须符合Strideable
协议,或者您必须以 O(1) 的效率实现index(_:offsetBy:)
和distance(from:to:)
方法.
extension OrderedSet: RandomAccessCollection
public typealias Index = Int
public typealias Indices = Range<Int>
public typealias SubSequence = Slice<OrderedSet<Element>>
public typealias Iterator = IndexingIterator<Self>
// Generic subscript to support `PartialRangeThrough`, `PartialRangeUpTo`, `PartialRangeFrom` and `FullRange`
public subscript<R: RangeExpression>(range: R) -> SubSequence where Index == R.Bound .init(elements[range])
public var endIndex: Index elements.endIndex
public var startIndex: Index elements.startIndex
public func formIndex(after i: inout Index) elements.formIndex(after: &i)
public var isEmpty: Bool elements.isEmpty
@discardableResult
public mutating func append(_ newElement: Element) -> Bool insert(newElement).inserted
符合哈希协议 要在集合中使用您自己的自定义类型或作为字典的键类型,请将 Hashable 一致性添加到您的类型。 Hashable 协议继承自 Equatable 协议,因此您还必须满足该协议的要求。当您在类型的原始声明中声明 Hashable 一致性并且您的类型满足以下条件时,编译器会自动综合您自定义类型的 Hashable 和要求:对于结构,其所有存储的属性都必须符合 Hashable。对于一个枚举,它的所有关联值都必须符合 Hashable。 (没有关联值的枚举即使没有声明也具有 Hashable 一致性。)
extension OrderedSet: Hashable
public static func ==(lhs: Self, rhs: Self) -> Bool lhs.elements.elementsEqual(rhs.elements)
符合 SetAlgebra 协议 在实现符合 SetAlgebra 协议的自定义类型时,您必须实现所需的初始化程序和方法。为了使继承的方法正常工作,符合类型必须满足以下公理。假设: • S 是符合 SetAlgebra 协议的自定义类型,x 和 y 是 S 的实例,e 是 S.Element 类型——集合所持有的类型。 • S() == [ ] • x.intersection(x) == x • x.intersection([ ]) == [ ] • x.union(x) == x • x.union([ ]) == x x.contains(e) 暗示 • x.union(y).contains(e) • x.union(y).contains(e) 意味着 x.contains(e) || y.contains(e) • x.contains(e) && y.contains(e) 当且仅当 x.intersection(y).contains(e) • x.isSubset(of: y) 意味着 x.union(y) == y • x.isSuperset(of: y) 意味着 x.union(y) == x • x.isSubset(of: y) 当且仅当 y.isSuperset(of: x) • x.isStrictSuperset(of: y) 当且仅当 x.isSuperset(of: y) && x != y • x.isStrictSubset(of: y) 当且仅当 x.isSubset(of: y) && x != y
extension OrderedSet: SetAlgebra
public mutating func insert(_ newMember: Element) -> (inserted: Bool, memberAfterInsert: Element)
let insertion = set.insert(newMember)
if insertion.inserted
elements.append(newMember)
return insertion
public mutating func remove(_ member: Element) -> Element?
if let index = elements.firstIndex(of: member)
elements.remove(at: index)
return set.remove(member)
return nil
public mutating func update(with newMember: Element) -> Element?
if let index = elements.firstIndex(of: newMember)
elements[index] = newMember
return set.update(with: newMember)
else
elements.append(newMember)
set.insert(newMember)
return nil
public func union(_ other: Self) -> Self
var orderedSet = self
orderedSet.formUnion(other)
return orderedSet
public func intersection(_ other: Self) -> Self
var orderedSet = self
orderedSet.formIntersection(other)
return orderedSet
public func symmetricDifference(_ other: Self) -> Self
var orderedSet = self
orderedSet.formSymmetricDifference(other)
return orderedSet
public mutating func formUnion(_ other: Self)
other.forEach append($0)
public mutating func formIntersection(_ other: Self)
self = .init(filter other.contains($0) )
public mutating func formSymmetricDifference(_ other: Self)
self = .init(filter !other.set.contains($0) + other.filter !set.contains($0) )
符合 ExpressibleByArrayLiteral 通过声明 init(arrayLiteral:) 初始化程序,将使用数组文字初始化的功能添加到您自己的自定义类型。下面的示例展示了一个假设的 OrderedSet 类型的数组字面量初始值设定项,它具有类似集合的语义,但保持其元素的顺序。
extension OrderedSet: ExpressibleByArrayLiteral
public init(arrayLiteral: Element...)
self.init()
for element in arrayLiteral
self.append(element)
extension OrderedSet: CustomStringConvertible
public var description: String .init(describing: elements)
符合 AdditiveArithmetic 协议 要将 AdditiveArithmetic 协议一致性添加到您自己的自定义类型,请实现所需的运算符,并使用可以表示自定义类型的任何值的大小的类型提供静态零属性。
extension OrderedSet: AdditiveArithmetic
public static var zero: Self .init()
public static func + (lhs: Self, rhs: Self) -> Self lhs.union(rhs)
public static func - (lhs: Self, rhs: Self) -> Self lhs.subtracting(rhs)
public static func += (lhs: inout Self, rhs: Self) lhs.formUnion(rhs)
public static func -= (lhs: inout Self, rhs: Self) lhs.subtract(rhs)
符合 RangeReplaceableCollection 协议 要将 RangeReplaceableCollection 一致性添加到您的自定义集合,请添加一个空初始化程序和 replaceSubrange(:with:) 方法到您的自定义类型。 RangeReplaceableCollection 使用此初始化程序和方法提供了所有其他方法的默认实现。例如,removeSubrange(:) 方法是通过调用 replaceSubrange(_:with:) 来实现的,其中 newElements 参数为空集合。您可以覆盖任何协议所需的方法来提供您自己的自定义实现。
extension OrderedSet: RangeReplaceableCollection
public init<S>(_ elements: S) where S: Sequence, S.Element == Element
elements.forEach set.insert($0).inserted ? self.elements.append($0) : ()
mutating public func replaceSubrange<C: Collection, R: RangeExpression>(_ subrange: R, with newElements: C) where Element == C.Element, C.Element: Hashable, Index == R.Bound
elements[subrange].forEach set.remove($0)
elements.removeSubrange(subrange)
newElements.forEach set.insert($0).inserted ? elements.append($0) : ()
OrderedSet Playground Sample
快速 Playground 测试(OrderedSet 应该具有 Swift 原生 Array
和 Set
结构可用的所有方法)
var ordereSet1: OrderedSet = [1,2,3,4,5,6,1,2,3] // [1, 2, 3, 4, 5, 6]
var ordereSet2: OrderedSet = [4,5,6,7,8,9,7,8,9] // [4, 5, 6, 7, 8, 9]
ordereSet1 == ordereSet2 // false
ordereSet1.union(ordereSet2) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
ordereSet1.intersection(ordereSet2) // [4, 5, 6]
ordereSet1.symmetricDifference(ordereSet2) // [1, 2, 3, 7, 8, 9]
ordereSet1.subtract(ordereSet2) // [1, 2, 3]
ordereSet2.popLast() // 9
【讨论】:
令人印象深刻的工作,赞 ?。一个小建议:我会将SubSequence
类型别名声明为Slice<OrderedSet<T>>
@vadian 如果我将SubSequence
类型从Self
更改为Slice<OrderedSet<Element>>
,则会有一个缺点。它不会使用自定义描述方法打印切片
我明白了。好吧,由您决定什么更重要。
var os: OrderedSet = [1, 2, 3] ; os[0] = 2 ; print(os) // [2, 2, 3]
– 这是预期的行为吗?
@MartinR 我已经编辑了我的问题。你能看一下吗?谢谢【参考方案2】:
问题1:将OrderedSet
变成MutableCollection
在MutableCollection
中,您可以通过支持write 访问的下标更改单个元素(或元素片段)。这就是问题开始的地方:
var oset: OrderedSet = [1, 2, 3, 4]
oset[0] = 3
print(oset)
是吗?我们不能简单地替换第一个元素,因为这样集合成员不再是唯一的。您当前的实现返回[1, 2, 3, 4]
,即如果新成员已存在于集合中,它拒绝该设置。
这使得MutableCollection
方法的许多默认实现失败:sort()
、swapAt()
、shuffle()
可能还有更多:
var oset: OrderedSet = [4, 3, 2, 1]
oset.swapAt(0, 2)
print(oset) // [4, 3, 2, 1]
oset.sort()
print(oset) // [4, 3, 2, 1]
oset.shuffle()
print(oset) // [4, 3, 2, 1]
问题 2:切片
在您的实现中,您选择了Slice<OrderedSet<Element>>
作为SubSequence
类型。 Slice
使用来自原始(基本)集合的存储,并且只维护自己的 startIndex
和 endIndex
。这会导致意想不到的结果:
let oset: OrderedSet = [1, 2, 3, 4, 5]
var oslice = oset[0..<3]
oslice[0] = 5
print(oslice) // [1, 2, 3]
设置oslice[0]
被拒绝,因为originating 集包含新成员。这当然不是预期的。对切片进行排序
var oset: OrderedSet = [6, 5, 4, 3, 2, 1]
oset[0..<4].sort()
print(oset) // [6, 5, 4, 3, 2, 1]
失败是因为排序的元素被一一写回,而失败是因为成员已经存在于集合中。切片赋值也是如此:
var o1: OrderedSet = [1, 2]
let o2: OrderedSet = [2, 1]
o1[0..<2] = o2[0..<2]
print(o1) // [1, 2]
另一个问题是切片oset[0..<3]
不符合OrderedSetProtocol
:它是一个(可变)集合,但例如不是SetAlgebra
,因此它不能用于形成联合、交叉或对称差异。
您真的需要MutableCollection
一致性吗?
我会认真考虑不采用MutableCollection
协议。这不会使有序集不可变:它仅意味着不能通过下标设置器修改单个成员。您仍然可以插入或删除元素,或与其他集合形成联合或交集。只有对于像排序这样的“复杂”操作,你必须通过一个额外的临时集合:
var oset: OrderedSet = [4, 3, 2, 1]
oset = OrderedSet(oset.sorted())
print(oset) // [1, 2, 3, 4]
最大的优点是不再有不清楚的行为。
是的,我想要 MutableCollection
一致性!
好的,你要求的 - 让我们看看我们能做些什么。我们可以尝试通过“修复”下标设置器来解决这个问题。一种尝试是您注释掉的代码:
set
guard set.update(with: newValue) == nil else
insert(remove(at: elements.firstIndex(of: newValue)!), at: index)
return
elements[index] = newValue
这具有将现有成员移动到给定位置,移动其他元素的效果:
var oset: OrderedSet = [1, 2, 3, 4]
oset[0] = 3
print(oset) // [3, 1, 2, 4]
这似乎让大多数方法都能正常工作:
var oset: OrderedSet = [4, 3, 2, 1]
oset.swapAt(0, 2)
print(oset) // [2, 3, 4, 1]
oset.sort()
print(oset) // [1, 2, 3, 4]
oset.shuffle()
print(oset) // [1, 4, 3, 2]
甚至下标排序:
var oset: OrderedSet = [6, 5, 4, 3, 2, 1]
oset[0..<4].sort()
print(oset) // [3, 4, 5, 6, 2, 1]
但我看到两个缺点:
用户可能不期望下标设置器的这种副作用。 它破坏了下标设置器所需的 O(1) 一致性。另一种选择是将下标设置器保持原样(即拒绝无效设置),并实现有问题的方法,而不是使用MutableCollection
的默认实现:
extension OrderedSet
public mutating func swapAt(_ i: Index, _ j: Index)
elements.swapAt(i, j)
public mutating func partition(by belongsInSecondPartition: (Element) throws -> Bool) rethrows -> Index
try elements.partition(by: belongsInSecondPartition)
public mutating func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows
try elements.sort(by: areInIncreasingOrder)
extension OrderedSet where Element : Comparable
public mutating func sort()
elements.sort()
另外我们需要实现下标setter取一个范围
public subscript(bounds: Range<Index>) -> SubSequence
这样一个排序的切片作为一个操作被分配回集合,而不是单独分配给每个元素。
这在我的测试中有效,但我可能会忽略某些东西。
对于切片,我会将OrderedSet
设为自己的SubSequence
类型。这意味着元素是重复的。这可以通过将element
存储设置为ArraySlice
来避免,但是——正如我们在上面看到的——无论如何都必须复制不同成员的set
,以避免在原始集发生突变时产生不必要的副作用。
代码
这是我目前所拥有的。据我所知,它可以正常工作,但需要更多测试。
请注意,有些方法不需要实现,例如ExpressibleByArrayLiteral
在SetAlgebra
中已经有了默认实现,各种索引计算都有默认实现,因为Index
就是Strideable
。
public struct OrderedSet<Element: Hashable>
private var elements: [Element] = []
private var set: Set<Element> = []
public init()
extension OrderedSet
public init<S>(distinctElements elements: S) where S : Sequence, S.Element == Element
self.elements = Array(elements)
self.set = Set(elements)
precondition(self.elements.count == self.set.count, "Elements must be distinct")
extension OrderedSet: SetAlgebra
public func contains(_ member: Element) -> Bool
set.contains(member)
@discardableResult public mutating func insert(_ newMember: Element) -> (inserted: Bool, memberAfterInsert: Element)
let insertion = set.insert(newMember)
if insertion.inserted elements.append(newMember)
return insertion
@discardableResult public mutating func remove(_ member: Element) -> Element?
if let oldMember = set.remove(member)
let index = elements.firstIndex(of: member)!
elements.remove(at: index)
return oldMember
else
return nil
@discardableResult public mutating func update(with newMember: Element) -> Element?
if let member = set.update(with: newMember)
return member
else
elements.append(newMember)
return nil
public mutating func formUnion(_ other: Self)
other.elements.forEach self.insert($0)
public mutating func formIntersection(_ other: Self)
for element in elements
if !other.contains(element)
remove(element)
public mutating func formSymmetricDifference(_ other: Self)
for member in other.elements
if set.contains(member)
remove(member)
else
insert(member)
public func union(_ other: Self) -> Self
var orderedSet = self
orderedSet.formUnion(other)
return orderedSet
public func intersection(_ other: Self) -> Self
var orderedSet = self
orderedSet.formIntersection(other)
return orderedSet
public func symmetricDifference(_ other: Self) -> Self
var orderedSet = self
orderedSet.formSymmetricDifference(other)
return orderedSet
public init<S>(_ elements: S) where S : Sequence, S.Element == Element
elements.forEach insert($0)
extension OrderedSet: CustomStringConvertible
public var description: String elements.description
extension OrderedSet: MutableCollection, RandomAccessCollection
public typealias Index = Int
public typealias SubSequence = OrderedSet
public subscript(index: Index) -> Element
get
elements[index]
set
if !set.contains(newValue) || elements[index] == newValue
set.remove(elements[index])
set.insert(newValue)
elements[index] = newValue
public subscript(bounds: Range<Index>) -> SubSequence
get
return OrderedSet(distinctElements: elements[bounds])
set
replaceSubrange(bounds, with: newValue.elements)
public var startIndex: Index elements.startIndex
public var endIndex: Index elements.endIndex
public var isEmpty: Bool elements.isEmpty
extension OrderedSet
public mutating func swapAt(_ i: Index, _ j: Index)
elements.swapAt(i, j)
public mutating func partition(by belongsInSecondPartition: (Element) throws -> Bool) rethrows -> Index
try elements.partition(by: belongsInSecondPartition)
public mutating func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows
try elements.sort(by: areInIncreasingOrder)
extension OrderedSet where Element : Comparable
public mutating func sort()
elements.sort()
extension OrderedSet: RangeReplaceableCollection
public mutating func replaceSubrange<C>(_ subrange: Range<Index>, with newElements: C) where C : Collection, C.Element == Element
set.subtract(elements[subrange])
let insertedElements = newElements.filter
set.insert($0).inserted
elements.replaceSubrange(subrange, with: insertedElements)
结论
我已经说过,放弃MutableCollection
一致性将是更安全的解决方案。
上述方法可行但很脆弱:我不得不“猜测”必须实现哪些方法,因为默认实现不起作用。如果 Swift 标准库中的MutableCollection
协议获得了一个具有默认实现的新方法。事情可能会再次破裂。
【讨论】:
以上是关于如何在本机 Swift 中实现以前称为 NSMutableOrderedSet 的可变有序集泛型类型?的主要内容,如果未能解决你的问题,请参考以下文章