Как реализовать универсальный тип Mutable Ordered Set, ранее известный как NSMutableOrderedSet, в собственном Swift?
Я пытаюсь реализовать универсальный тип Mutable Ordered Set, и он должен соответствовать многим протоколам, чтобы вести себя так же, как Array и Set в Swift. Прежде всего для того, чтобы элемент универсального типа соответствовал 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
это даже не изменение порядка элементов.
MutableCollection
Изменение объявления
func 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]"
Как я могу это исправить?
2 ответа
Собственная реализация изменяемого упорядоченного набора на Swift:
public protocol OrderedSetProtocol: MutableCollection,
RandomAccessCollection,
SetAlgebra,
AdditiveArithmetic,
RangeReplaceableCollection
where Element: Hashable, Index == Int { }
public struct OrderedSet<Element: Hashable>: OrderedSetProtocol {
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 {
//
// needs some implementation before returning
// insert(remove(at: elements.firstIndex(of: newValue)!), at: index)
//
return
}
elements[index] = newValue
}
}
}
Соответствие протоколу RandomAccessCollection
Протокол RandomAccessCollection добавляет дополнительные ограничения на связанные типы индексов и подпоследовательностей, но в остальном не накладывает никаких дополнительных требований на протокол BidirectionalCollection. Однако, чтобы соответствовать гарантиям сложности коллекции с произвольным доступом, либо индекс для вашего настраиваемого типа должен соответствовать протоколу Strideable, либо вы должны реализовать методы index(_:offsetBy:) и distance(from:to:) с эффективностью O(1).
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`
public subscript<R: RangeExpression>(range: R) -> SubSequence where Index == R.Bound { .init(base: self, bounds: range.relative(to: self)) }
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 }
}
Соответствие протоколу 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 { filter(other.contains) }
public func symmetricDifference(_ other: Self) -> Self { filter { !other.set.contains($0) } + other.filter { !set.contains($0) } }
public mutating func formUnion(_ other: Self) { other.forEach { self.append($0) } }
public mutating func formIntersection(_ other: Self) { self = intersection(other) }
public mutating func formSymmetricDifference(_ other: Self) { self = symmetricDifference(other) }
}
Соответствие ExpressibleByArrayLiteral
Добавьте возможность инициализации с помощью литерала массива к вашим собственным пользовательским типам, объявив инициализатор init (arrayLiteral:). В следующем примере показан инициализатор литерала массива для гипотетического типа OrderedSet, который имеет семантику, подобную множеству, но поддерживает порядок своих элементов.
extension OrderedSet: ExpressibleByArrayLiteral {
public typealias ArrayLiteralElement = Element
public init(arrayLiteral: Element...) {
self.init()
for element in arrayLiteral {
self.append(element)
}
}
}
Соответствие протоколу 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: Sequence>(_ elements: S) where 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)
var index = subrange.relative(to: self).lowerBound
newElements.forEach {
if set.insert($0).inserted {
elements.insert($0, at: index)
formIndex(after: &index)
}
}
}
Соответствие протоколу CustomStringConvertible
Добавьте соответствие CustomStringConvertible вашим настраиваемым типам, определив свойство description.
extension OrderedSet: CustomStringConvertible {
public var description: String { .init(describing: elements) }
}
В соответствии с ее Slice
к CustomStringConvertible
также:
extension Slice: CustomStringConvertible where Base: OrderedSetProtocol {
public var description: String {
var description = "["
var first = true
for element in self {
if first {
first = false
} else {
description += ", "
}
debugPrint(element, terminator: "", to: &description)
}
return description + "]"
}
}
Быстрый тест на игровой площадке (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]
ordereSet1.insert(contentsOf: [1,3,4,6], at: 0) // [4, 6, 1, 2, 3]
ordereSet2.popLast() // 9
Проблема 1: Изготовление OrderedSet
а MutableCollection
В MutableCollection
вы можете изменить отдельный элемент (или часть элементов) с помощью индекса, который поддерживает доступ для записи. И вот здесь начинаются проблемы: что должен выводить
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]
отклоняется, поскольку исходный набор содержит новый член. Это, конечно, не ожидается. Сортировка ломтика
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()
}
}
Кроме того, нам нужно реализовать установщик индекса, принимающий диапазон
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
соответствие было бы более безопасным решением.
Вышеупомянутое работает, но ненадежно: мне пришлось "угадать", какие методы необходимо реализовать, поскольку реализация по умолчанию не работает. ЕслиMutableCollection
Протокол в стандартной библиотеке Swift получает новый метод с реализацией по умолчанию. все может снова сломаться.