An ordered collection of unique elements.
import OrderedCollections
@frozen struct OrderedSet<Element: Hashable>Similar to the standard Set, ordered sets ensure that each element appears
only once in the collection, and they provide efficient tests for
membership. However, like Array (and unlike Set), ordered sets maintain
their elements in a particular user-specified order, and they support
efficient random-access traversal of their members.
OrderedSet is a useful alternative to Set when the order of elements is
important, or when you need to be able to efficiently access elements at
various positions within the collection. It can also be used instead of an
Array when each element needs to be unique, or when you need to be able to
quickly determine if a value is a member of the collection.
You can create an ordered set with any element type that conforms to the
Hashable protocol.
let buildingMaterials: OrderedSet = ["straw", "sticks", "bricks"]Two ordered sets are considered equal if they contain the same elements, and
in the same order. This matches the concept of equality of an Array, and
it is different from the unordered Set.
let a: OrderedSet = [1, 2, 3, 4]
let b: OrderedSet = [4, 3, 2, 1]
a == b // false
b.sort() // `b` now has value [1, 2, 3, 4]
a == b // trueOrderedSet implements most, but not all, SetAlgebra requirements. In
particular, it supports the membership test contains(_:) as well as all
high-level set operations such as union(_:), intersection(_:) or
isSubset(of:).
buildingMaterials.contains("glass") // false
buildingMaterials.intersection(["bricks", "straw"]) // ["straw", "bricks"]Operations that return an ordered set usually preserve the ordering of
elements in their input. For example, in the case of the intersection call
above, the ordering of elements in the result is guaranteed to match their
order in the first input set, buildingMaterials.
On the other hand, predicates such as isSubset(of:) tend to ignore element
ordering:
let moreMaterials: OrderedSet = ["bricks", "glass", "sticks", "straw"]
buildingMaterials.isSubset(of: moreMaterials) // trueHowever, OrderedSet does not implement insert(_:) nor update(with:) --
it provides its own variants for insertion that are more explicit about
where in the collection new elements gets inserted:
func insert(_ item: Element, at index: Index) -> (inserted: Bool, index: Int)
func append(_ item: Element) -> (inserted: Bool, index: Int)
func update(at index: Int, with item: Element) -> Element
func updateOrAppend(_ item: Element) -> Element?Additionally,OrderedSet has an order-sensitive definition of equality (see
above) that is incompatible with SetAlgebra's documented semantic
requirements. Accordingly, OrderedSet does not (cannot) itself conform to
SetAlgebra.
For cases where SetAlgebra conformance is desired (such as when passing an
ordered set to a function that is generic over that protocol), OrderedSet
provides an efficient unordered view of its elements that conforms to
SetAlgebra. The unordered view implements the same concept of equality as
the standard Set, ignoring element ordering.
var a: OrderedSet = [0, 1, 2, 3]
let b: OrderedSet = [3, 2, 1, 0]
a == b // false
a.unordered == b.unordered // true
func frobnicate<S: SetAlgebra>(_ set: S) { ... }
frobnicate(a) // error: `OrderedSet<String>` does not conform to `SetAlgebra`
frobnicate(a.unordered) // OKThe unordered view is mutable. Insertions into it implicitly append new elements to the end of the collection.
buildingMaterials.unordered.insert("glass") // => inserted: true
// buildingMaterials is now ["straw", "sticks", "bricks", "glass"]Accessing the unordered view is an efficient operation, with constant (minimal) overhead. Direct mutations of the unordered view (such as the insertion above) are executed in place when possible. However, as usual with copy-on-write collections, if you make a copy of the view (such as by extracting its value into a named variable), the resulting values will share the same underlying storage, so mutations of either will incur a copy of the whole set.
Ordered sets are random-access collections. Members are assigned integer
indices, with the first element always being at index 0:
let buildingMaterials: OrderedSet = ["straw", "sticks", "bricks"]
buildingMaterials[1] // "sticks"
buildingMaterials.firstIndex(of: "bricks") // 2
for i in 0 ..< buildingMaterials.count {
print("Little piggie #\(i) built a house of \(buildingMaterials[i])")
}
// Little piggie #0 built a house of straw
// Little piggie #1 built a house of sticks
// Little piggie #2 built a house of bricksBecause OrderedSet needs to keep its members unique, it cannot conform to
the full MutableCollection or RangeReplaceableCollection protocols.
Operations such as MutableCollection's subscript setter or
RangeReplaceableCollection's replaceSubrange assume the ability to
insert/replace arbitrary elements in the collection, but allowing that could
lead to duplicate values.
However, OrderedSet is able to partially implement these two protocols;
namely, there is no issue with mutation operations that merely change the
order of elements, or just remove some subset of existing members:
// Permutation operations from MutableCollection:
func swapAt(_ i: Int, _ j: Int)
func partition(by predicate: (Element) throws -> Bool) -> rethrows Int
func sort() where Element: Comparable
func sort(by predicate: (Element, Element) throws -> Bool) rethrows
func shuffle()
func shuffle<T: RandomNumberGenerator>(using generator: inout T)
func reverse()
// Removal operations from RangeReplaceableCollection:
func removeAll(keepingCapacity: Bool = false)
func remove(at index: Int) -> Element
func removeSubrange(_ bounds: Range<Int>)
func removeLast() -> Element
func removeLast(_ n: Int)
func removeFirst() -> Element
func removeFirst(_ n: Int)
func removeAll(where shouldBeRemoved: (Element) throws -> Bool) rethrowsOrderedSet also implements reserveCapacity(_) from
RangeReplaceableCollection, to allow for efficient insertion of a known
number of elements. (However, unlike Array and Set, OrderedSet does
not provide a capacity property.)
In cases where you need to pass the contents of an ordered set to a function
that only takes an array value or (or something that's generic over
RangeReplaceableCollection or MutableCollection), then the best option
is usually to directly extract the members of the OrderedSet as an Array
value using its elements property. OrderedSet uses a standard array
value for element storage, so extracting the array value has minimal
overhead.
func pickyFunction(_ items: Array<Int>)
var set: OrderedSet = [0, 1, 2, 3]
pickyFunction(set) // error
pickyFunction(set.elements) // OKIt is also possible to mutate the set by updating the value of the
elements property. This guarantees that direct mutations happen in place
when possible (i.e., without spurious copy-on-write copies).
However, the set needs to ensure the uniqueness of its members, so every
update to elements includes a postprocessing step to detect and remove
duplicates over the entire array. This can be slower than doing the
equivalent updates with direct OrderedSet operations, so updating
elements is best used in cases where direct implementations aren't
available -- for example, when you need to call a MutableCollection
algorithm that isn't directly implemented by OrderedSet itself.
Like the standard Set type, the performance of hashing operations in
OrderedSet is highly sensitive to the quality of hashing implemented by
the Element type. Failing to correctly implement hashing can easily lead
to unacceptable performance, with the severity of the effect increasing with
the size of the hash table.
In particular, if a certain set of elements all produce the same hash value, then hash table lookups regress to searching an element in an unsorted array, i.e., a linear operation. To ensure hashed collection types exhibit their target performance, it is important to ensure that such collisions cannot be induced merely by adding a particular list of members to the set.
The easiest way to achieve this is to make sure Element implements hashing
following Hashable's documented best practices. The conformance must
implement the hash(into:) requirement, and every bit of information that
is compared in == needs to be combined into the supplied Hasher value.
When used correctly, Hasher produces high-quality, randomly seeded hash
values that prevent repeatable hash collisions.
When Element implements Hashable correctly, testing for membership in an
ordered set is expected to take O(1) equality checks on average. Hash
collisions can still occur organically, so the worst-case lookup performance
is technically still O(n) (where n is the size of the set); however,
long lookup chains are unlikely to occur in practice.
An OrderedSet stores its members in a regular Array value (exposed by
the elements property). It also maintains a standalone hash table
containing array indices alongside the array; this is used to implement fast
membership tests. The size of the array is limited by the capacity of the
corresponding hash table, so indices stored inside the hash table can be
encoded into fewer bits than a standard Int value, leading to a storage
representation that can often be more compact than that of Set itself.
Inserting or removing a single member (or a range of members) needs to
perform the corresponding operation in the storage array, in addition to
renumbering any subsequent members in the hash table. Therefore, these
operations are expected to have performance characteristics similar to an
Array: inserting or removing an element to the end of an ordered set is
expected to execute in O(1) operations, while they are expected to take
linear time at the front (or in the middle) of the set. (Note that this is
different to the standard Set, where insertions and removals are expected
to take amortized O(1) time.)