In this tutorial, you’re going to build a multiset, otherwise known as a bag, from scratch.
Along the way, you’ll learn how to:
- Adopt these protocols:
Hashable, Sequence, Collection, CustomStringConvertible, ExpressibleByArrayLiteral
andExpressibleByDictionaryLiteral
. - Create custom initializations for your collections.
- Improve your custom collections with custom methods.
Getting Started
A Bag is like a Set in that it does not store repeated values. The difference is this: A Bag keeps a running count of any repeated values while a Set does not.
Set vs BagInitializing Collections
To avoid this, Swift supplies two protocols that enable initialization with sequence literals. Literals give you a shorthand way to write data without explicitly creating an object.
// 1
extension Bag: ExpressibleByArrayLiteral {
init(arrayLiteral elements: Element...) {
self.init(elements)
}
}
// 2
extension Bag: ExpressibleByDictionaryLiteral {
init(dictionaryLiteral elements: (Element, Int)...) {
self.init(elements.map { (key: $0.0, value: $0.1) })
}
}
-
ExpressibleByArrayLiteral
is used to create a Bag from an array style literal. Here you use the initializer you created earlier and pass in the elements collection. -
ExpressibleByDictionaryLiteral
does the same but for dictionary style literals. The map converts elements to the named-tuple the initializer expects.
Enforcing Non-destructive Iteration
One caveat: Sequence
does not require conforming types to be non-destructive. This means that after iteration, there’s no guarantee that future iterations will start from the beginning. That’s a huge issue if you plan on iterating over your data more than once.
To enforce non-destructive iteration, your object needs to conform to the Collection
protocol.
Collection
inherits from Indexable
and Sequence
.
Final Codes
import Foundation
struct Bag<Thing: Hashable> {
fileprivate var contents: [Thing: Int] = [:]
var thingsKinds: Int {
return contents.count
}
var thingsCount: Int {
return contents.values.reduce(0){ $0 + $1 }
}
init(){}
init<S: Sequence>(_ list: S) where S.Iterator.Element == Thing {
list.forEach{ put($0) }
}
init<S: Sequence>(_ tuple: S) where S.Iterator.Element == (key: Thing, value: Int) {
tuple.forEach{ put($0.key, num: $0.value) }
}
mutating func put(_ thing: Thing, num: Int = 1) {
precondition(num > 0, "Should put at least 1 thing in bag!")
if let currentNum = contents[thing] {
contents[thing] = currentNum + num
}
else {
contents[thing] = num
}
}
mutating func take(_ thing: Thing, num: Int = 1) {
precondition(num > 0, "Should take at least 1 thing from bag!")
guard
let currentCount = contents[thing],
currentCount >= num else {
return
}
if currentCount > num {
contents[thing] = currentCount - num
}
else {
contents.removeValue(forKey: thing)
}
}
}
extension Bag: CustomStringConvertible {
var description: String {
return String(describing: contents)
}
}
extension Bag: ExpressibleByDictionaryLiteral {
init(dictionaryLiteral elements: (Thing, Int)...) {
self.init(elements.map{ (key: $0.0, value: $0.1) })
}
}
extension Bag: ExpressibleByArrayLiteral {
init(arrayLiteral elements: Thing...) {
self.init(elements)
}
}
struct BagIndex<Element: Hashable> {
fileprivate let index: DictionaryIndex<Element, Int>
fileprivate init(
_ dictionaryIndex: DictionaryIndex<Element, Int>) {
self.index = dictionaryIndex
}
}
extension BagIndex: Comparable {
static func == (lhs: BagIndex, rhs: BagIndex) -> Bool {
return lhs.index == rhs.index
}
static func < (lhs: BagIndex, rhs: BagIndex) -> Bool {
return lhs.index < rhs.index
}
}
extension Bag: Sequence {
typealias Iterator = AnyIterator<(thing: Thing, num: Int)>
func makeIterator() -> Iterator {
var iterator = contents.makeIterator()
return AnyIterator {
return iterator.next()
}
}
}
extension Bag: Collection {
typealias Index = BagIndex<Thing>
var startIndex: BagIndex<Thing> {
return .init(contents.startIndex)
}
var endIndex: BagIndex<Thing> {
return .init(contents.endIndex)
}
subscript (position: Index) -> Iterator.Element {
precondition((startIndex ..< endIndex).contains(position),
"out of bounds")
// 3
let dictionaryElement = contents[position.index]
return (thing: dictionaryElement.key,
num: dictionaryElement.value)
}
func index(after i: Index) -> Index {
// 4
return Index(contents.index(after: i.index))
}
}
var bag = Bag<String>()
bag.put("Book")
bag.put("Food", num: 2)
let bag1: Bag = ["A", "B"]
bag1
let bag2: Bag = ["A": 1, "B": 2]
bag2
bag.forEach{
print($0)
}
网友评论