The article is from collated notes on objccn. IO/related books. “Thank you objC.io and its writers for selflessly sharing their knowledge with the world.”
In all programming languages, collections of elements are the most important data types. A programming language’s good support for different collection types is an important factor in determining programming efficiency and happiness. As a result, Swift places a special emphasis on sequences and collections, which the library developers put more effort into than any other section, even making it seem like the library is designed to deal with collection types almost exclusively. As a result of this effort, we were able to use a very powerful collection model, which is much more extensible than collections in other languages that you’re used to, but it’s also quite complex.
An array of
Arrays and variability
There are several advantages to differentiating var and let. Constants defined using let are easier to understand because they are immutable. When you read something like let fibs =… When you declare this, you can be sure that the fiBS value will never change, which is enforced by the compiler. This can be very helpful when reading code. Note, however, that this applies only to types that have value semantics. When you use let to define a reference type to an instance of a class, it guarantees that the reference will never change. That is, you cannot assign a new value to the reference, but the object to which the reference refers can change.
Like all collection types in the library, arrays have value semantics. When you assign an existing array to another variable, the contents of the array are copied.
Compare NSArray’s approach to mutable features in the Foundation framework. There’s no change method in NSArray, so to change an array, you have to use NSMutableArray. But even if you have an immutable NSArray, its reference nature doesn’t guarantee that the array won’t change.
Var muArray = NSMutableArray(array:[1,2,3]) var array: NSArray = muArray muarray. add(4) print(array) // 1,2,3,4Copy the code
The correct way to do this is to copy the values manually before assigning them.
Var array = NSMutableArray(array:[1,2,3]) var array: NSArray = muarray.copy () as! Add (4) print(array) // 1,2,3Copy the code
In Swift, there is only one uniform type for arrays, which can be defined as mutable by using var instead of let when declared. When you declare the second array with let and assign the first array to it, you can guarantee that the new array will not change because there are no shared references.
Array index
The Swift array provides all the usual manipulation methods you can think of, like isEmpty and count. Arrays also allow direct access to elements on a given index by subscript, such as fibs[3]. Remember, however, to ensure that the index value is not out of range before using subscripts to retrieve elements. Otherwise the program will crash.
Swift also has a number of ways to manipulate arrays without evaluating indexes:
For value in array.dropFirst(2) {print(value)} for value in array.dropFirst(2) {print(value)} // For value in array.dropLast(2) {print(value)} enumerated() {print("\(index) : \(value)")} if let index = array.firstindex (where: {$0 == 5}) {print(index)} var temp = array.map{$0 * 2} print(temp) {$0 * 2} print(temp array.filter{ $0 % 2 == 0 } print(temp)Copy the code
Other operations behave slightly differently. The first and last attributes return an optional value, and nil when the array is empty. First equals isEmpty, right? Nil: self [0]. Similarly, calling removeLast when the array is empty causes a crash; However, popLast removes the last element and returns it if the array is not empty. When the array is empty, it does nothing and returns nil. You should choose which method to use depending on your needs: when using arrays as stacks, you may always want to use a combination of checking empty and removing the last element; On the other hand, if you already know whether the array is empty, there is no need to deal with the optional values.
An array of deformation
Let array = [1,2,3,4,5] let temp = array.map{num in num * num} print(temp)Copy the code
The Swift array has the Map method, which comes from the world of functional programming.
This version has three big advantages. First of all, it’s short. Shorter length generally means fewer errors, but more importantly, it’s clearer than the original. All extraneous stuff has been removed, and once you get used to the map flying around, the map is like a signal, and once you see it, you know that there is going to be a function applied to each element of the array that will return another array that will contain all the converted results.
Second, temp will be the result of the map, and we will not change its value, so we no longer need to declare var, we can declare it as let. Also, since the type of the returned array elements can be inferred from the function passed to map, we no longer need to explicitly type Temp.
Element is a placeholder for the type of the Element contained in the array, and T is the type placeholder for the converted Element. The map function itself does not care what elements and T are, they can be of any type. The exact type of T is determined by the return type of the transform method passed to the map by the caller.
extension Array { func myMap<T>(_ transform: (Element) -> T) -> [T] { var res: [T] = [] res.reservecapacity (count) for x in self {res.append(transform(x))} return res}} let array = [1,2,3,4,5] let temp = array.myMap{ $0 + 1 } print(temp) // [2, 3, 4, 5, 6]Copy the code
Swift source: github.com/apple/swift…
Use functions to parameterize behavior
Map tries to separate out the template code, which does not change with each call. What does change is the functional code — the logic of how to transform each element. Map does this by taking the transformation function provided by the caller as an argument.
Throughout the library, there are many design patterns that parameterize behavior. For example, in Array and other collection types, there are more than a dozen methods that take a function as an argument to define their behavior:
Var temp = array.compactMap {$0 * 2} print(temp) // Filter {$0%2 == 0} print(temp) // [2, Var flag = array.allSatisfy {$0 < 6} print(flag) // true // reduce -- aggregate elements into a value var sum = array.reduce(0){$0 + $1} print(sum) // 15 // access each element array.forEach {print($0)} // 1 2 3 4 5 // Rearrange elements temp = array temp.sort(by: { $0 > $1 }) print(temp) // [5, 4, 3, 2, 1] temp = array.sorted(by: {$0 > $1}) print (temp) / / [5, 4, 3, 2, 1] var I = [0, 2], J = (0, 5) print (i.l exicographicallyPrecedes (j) {return $0 < $1}) / / true temp = var poi =,4,3,5,1 [2] Partition (by: {$0 > 2}) print(poI) // print(temp) // [2, 1, 3, 5, 4] // Print (array.firstindex (where: {$0 = = 5})! // 4 print(array.lastIndex(of: 1)!) // 0 print(array.first(where: { $0 == 2 })!) // 2 print(array.last(where: { $0 == 2 })!) Print (array.min(by: {$0 < $1})) print(array.min(by: {$0 < $1})) // 1 print(array.max(by: { $0 < $1 })!) = [0,1,2,3,4] print(array.elementsequal (temp, by: { $0 > $1 })) // true print(array.starts(with: temp, by: {$0 == $1 + 1})) print(array.split(whereSeparator: whereSeparator) {$0 > 2})) // [ArraySlice([1, 2])] print(array.prefix(while: {$0 < 3})) // [1, 2] // Discard element when condition is true; Print (array.drop(while: {$0 < 3})) / / [3, 4, 5] / / delete all the eligible elements temp =,1,2,3,4 [0] temp. RemoveAll (where: {$2 0%! = 0 }) print(temp) // [0, 2, 4]Copy the code
All of these functions are designed to get rid of uninteresting parts of the code, such as creating new arrays or doing a for loop over source data. These sections have been replaced by a single word that describes what to do. This highlights the logical code that the programmer really wants to express.
Some of these functions have default behavior. Unless you specify it, sort by default will sort the elements that can be compared in ascending order. Contains For elements that can be evaluated equal, it directly checks whether two elements are equal. These default behaviors make code more readable. Ascending order is very natural, so the sense of array.sort() is intuitive. Array. index(of: “foo”) is also clearer than array.index {$0 == “foo”}.
But in the example above, they are just shorthand for the general case. The elements in a collection don’t have to be comparable, they don’t have to be equal, you don’t even have to operate on the entire element, for example, on an array of Person objects, You can sort by their age (people.sort {$0.age < $1.age}) or check to see if the set contains minors (people.contains {$0.age < 18}). For example, people.sort {$0.name.uppercased() < $1.name.uppercased()} is not efficient for sorting the cased cases.
If you find multiple places in your code that iterate over an Array and do the same or similar things, consider writing an extension to Array. For example, the following code splits the elements of an array as adjacent and equal:
/ /,2,2,2,3,4,4 [1] - > [[1], [2,2,2], [3], [4]] extension Array {func mySplit (the where condition: (Element, Element) -> Bool) -> [[Element]] { var result: [[Element]] = self.isEmpty ? [] : [[self[0]]] for (previous, current) in zip(self, self.dropFirst()) { if condition(previous, current) { result.append([current]) } else { result[result.endIndex - 1].append(current) } } return result } } var array ,2,2,2,3,4,4 = [1] print (array. MySplit (where: {$0! = $1})) // [[1], [2, 2, 2], [3], [4, 4]] print(array.mySplit(where: ! =) // [1], [2, 2, 2], [3], [4, 4]]Copy the code
The benefit of doing this is the same as we described in the map introduction, the split(where:) version is much more readable than the for loop. Even though the for loop is simple, you still have to do it in your head, which adds to the burden of understanding. Using split(where:) reduces the likelihood of errors, and it allows you to declare result variables using lets instead of var.
Closures with mutable and state
When iterating through an array, you can use a map to perform some side effects (such as inserting elements into a lookup table). We don’t recommend doing this. Here’s an example:
array.map { item in table.append(item) }
Copy the code
This hides the side effects (changing the lookup table) in what looks like just an array distortion. If you see code like the one above, using a simple for loop is obviously a better choice than using a function like map. In this case, the forEach method is also more appropriate than Map, but forEach itself has some problems, which we’ll discuss in more detail in a moment.
Doing this with side effects is fundamentally different from deliberately giving a closure a local state, which is a very useful technique. Closures are functions that can capture and modify variables outside of their scope, and can be a powerful tool when combined with higher-order functions. For example, the accumulate function can be implemented using map combined with a closure containing the states:
extension Array { func myAccumulate<Result>(_ initialResult: Result, _ nextPartialResult: (Result, Element) -> Result) -> [Result] { var running = initialResult return map { next in running = nextPartialResult(running, Next) return running}}} print ([1, 2, 3, 4] myAccumulate (0, + $1} {$0)) / / [1, 3, 6, 10]Copy the code
Note that this code assumes that map executes the deformation functions in the original order of the sequence. In our map above, that’s exactly what happened. But it is also possible that the deformation of a sequence is unordered, for example we can have an implementation that handles element deformation in parallel. The map version of the official library does not specify whether it will process sequences sequentially, but it seems safe to do so for now.
filter
Another common operation is to examine an array and filter out the elements that meet certain criteria and use them to create a new array.
Let nums = [1,2,3,4,5,6,7,8,9,10] nums.filter {num in num % 2 == 0} // 10] nums.filter { $0 % 2 == 0 } // [2, 4, 6, 8, 10]Copy the code
For very short closures, this helps readability. However, if the closure is more complex, it is better to explicitly write the parameter names as we did before. But this is more of a personal choice, just use the version that looks easier to read at first glance. A good rule of thumb is to use abbreviations if closures can be written well on a single line.
By combining map and filter, we can now easily do many array operations without introducing intermediate variables. This makes the resulting code much shorter and more readable. For example, to find all even squares up to 100, we can find 0.. <10 map to get all squares and filter out all odd numbers:
print((1.. <10).map({ $0 * $0 }).filter({ $0 % 2 == 0})) // [4, 16, 36, 64]Copy the code
The implementation of filter looks similar to map:
extension Array { func myFilter(_ isIncluded: (Element) -> Bool) -> [Element] { var res: [Element] = [] for self where isIncluded(x) {res.append(x)} return res}} var array = [1,2,3,4,5] print(array.myFilter({ $0 % 2 == 0 })) // [2, 4]Copy the code
A performance tip: If you’re writing code like this, don’t do it!
bigArray.filter { someCondition }.count > 0
Copy the code
Filter creates a brand new array and operates on each element in the array. In the above code, however, this is clearly not necessary. The above code only needs to check that at least one element satisfies the condition, in which case it is more appropriate to use contains(where:) :
bigArray.contains { someCondition }
Copy the code
This is much faster for two main reasons: it doesn’t create a whole new array just to count, and it exits prematurely once the first matching element is found. In general, you should choose to use a Filter only when you need all the results.
reduce
Both map and filter act on an array and produce a new, modified array. Sometimes, though, you might want to combine all elements into a single new value. For example, if we wanted to add up all the elements, we could write:
Var array = [1, 2, 3, 4, 5] print (array. Reduce (0, + $1} {$0)) / / 15 print (array. Reduce (0, +)) / / 15Copy the code
The implementation of Reduce looks like this:
extension Array {
func myRedece<Result>(_ initialResult: Result, _ nextRartialResult:(Result, Element) -> Result) -> Result {
var result = initialResult
for x in self {
result = nextRartialResult(result, x)
}
return result
}
}
var array = [1, 2, 3, 4, 5]
print(array.myRedece(0, { $0 + $1 })) // 15
Copy the code
Another performance tip: Reduce is quite flexible, so it’s not surprising to see reduce when building arrays or performing other operations. For example, you can implement map and filter using reduce alone:
extension Array {
func myMap<T>(_ transform:(Element) -> T) -> [T] {
return reduce([], { $0 + [transform($1)] })
}
func myFilter(_ isIncluded:(Element) -> Bool) -> [Element] {
return reduce([], { isIncluded($1) ? $0 + [$1] : $0 })
}
}
var array: [Int] = [1, 2, 3, 4, 5] + [6]
print(array.myMap( { $0 + 1 } )) // [2, 3, 4, 5, 6, 7]
print(array.myFilter( { $0 % 2 == 0 } )) // [2, 4, 6]
Copy the code
This implementation is aesthetic and eliminates the need for verbose, imperative for loops. But Swift is not Haskell, and Swift’s array is not a list. Here, a new array is created each time a merge is performed by appending a transformed or qualified element to the previous result. This means that the complexity of the two implementations above is O(n2), not O(n). As the array length increases, the time taken to execute these functions increases in a square relation.
There is another version of Reduce that is of a different type. Specifically, the function that merges the intermediate Result with an element now accepts an inout Result as an argument. When inout is used, the compiler does not create a new array each time, so the time complexity of this version of filter is once again O(n). When the reduce(into:_:) call is inlined by the compiler, the generated code is usually the same as the code obtained using the for loop.
A flattened map
Sometimes we want to map an array, but the deformation function returns another array instead of individual elements. For example, let’s say we have a function called extractLinks that reads a Markdown file and returns an array of urls containing all the links in that file. The signature of this function looks like this:
func extractLinks(markdownFile: String) -> [URL]
Copy the code
If we have a bunch of Markdown files and want to extract all the links in those files into a single array, we can try markdownFiles.map(extractLinks). The problem is that this method returns an array of urls, each element of which is an array of urls in a file. Now you can call the joined group to flatten the two-dimensional group into a one-dimensional array after mapping gets an array of arrays:
let markdownFiles: [String] = // ...
let nestedLinks = markdownFiles.map(extractLinks)
let links = nestedLinks.joined()
Copy the code
The flatMap method combines the transformation and flattening operations into one step. Markdownfiles.flatmap (links) returns urls from all Markdown files in an array.
Var a = [[1, 1], [2], [3]] let b: (Int) b = a.f latMap ({$0. The map {$0 + 2}}) print (b) / / [3, 3, 4, 4, 5, 5]Copy the code
The function signature of flatMap looks basically the same as that of Map, except that its transform function returns an array. In the implementation, it uses append(contentsOf:) instead of append(_:) so that the returned array is flattened
extension Array {
func myFlatMap<T>(_ transform: (Element) -> [T]) -> [T] {
var result: [T] = []
for x in self {
result.append(contentsOf: transform(x))
}
return result
}
}
Copy the code
Another common use of flatMap is to combine elements from different arrays. To get all the paired combinations of the elements in two arrays, we can flatMap one array and then map the other array in the transform function:
Let a = [" ♠, "" has" and "♣ ♦"] let b = [" J ", "Q", "K", "a"] let res = a.f latMap {x b.m ap in {y in (x, y)}} / / [(" ♠ "and" J "), (" ♠ ", "Q"), (" ♠ ", "K"), (" ♠, "" A"), (" has "and" J "), (" has ", "Q"), (" has ", "K"), (" has ", "A"), (" ♣ "and" J "), (" ♣ ", "Q"), (" ♣ ", "K"), ♣ "("," A "), (" ♦ "and" J "), (" ♦ ", "Q"), (" ♦ ", "K"), (" ♦ ", "A")"Copy the code
Use forEach for iteration
The last operation we will discuss is forEach. It works very much like a for loop: the passed function is executed once for each element in the sequence. Unlike map, forEach does not return any values, which is especially useful for operations that have side effects. Let’s start off with forEach instead of the for loop:
For element in [1,2,3] {print(element)} [1,2,3].Copy the code
This is nothing special, but if you want to call a function on every element in a collection, forEach is appropriate. Passing a function name to forEach makes code much cleaner and more compact than passing a closure expression. For example, if you are implementing a View Controller on iOS and want to add an array of views to the current view, just write theviews.foreach (view.addSubView).
However, there are some subtle differences between the for loop and forEach that are worth noting. For example, when a for loop has a return statement, rewriting it to forEach can make a big difference in code behavior.
A return in forEach does not make an external function return, it just makes the closure itself return. In this case, the compiler will notice that the parameters of the return statement are not being used and issue a warning, so we can find the problem. But we shouldn’t rely on the compiler to find all of these errors.
(1.. <10).forEach { number in print(number) if number > 2 {return} }Copy the code
You may not realize it at first, but this code will print out all the numbers entered. The return statement does not terminate the loop, it simply returns from the closure, so the next iteration of the loop is started in forEach’s implementation.
In some cases, like the addSubview example above, forEach might be better than the for loop. However, we recommend not using forEach in most other cases because of the ambiguous behavior of return. In such cases, it might be better to use a regular for loop.
Array slice
In addition to accessing individual elements in an array with individual subscripts (such as fibs[0]), we can also retrieve elements in a range by subscripts. For example, if you want to get an array element other than the first one, you can do this:
Let a = [0,1,2,3,4,5] let b = [1... print(b) // [1, 2, 3, 4, 5] print(type(of: b)) // ArraySlice<Int>Copy the code
It returns a slice of the array containing all parts of the original array starting with the second element. The resulting result is of type ArraySlice, not Array. A slice type is just a representation of an array. The data behind it is still the same array, but it is represented as a slice. Because the elements of the array are not copied, the cost of creating a slice is minimal.
Because ArraySlice and Array satisfy the same protocol (Collection protocol being the most important), they have the same methods, so you can treat slices as arrays. If you need to convert a slice to an Array, you can do so by passing it to the Array build method:
let newArray = Array(b)
type(of: newArray) // Array<Int>
Copy the code
Keep in mind that the slice and the array behind it refer to elements using the same index. Therefore, slice indexes do not need to start at zero. For example, above we use a[1…] The index of the first element of the slice created is 1, so accessing the element B [0] incorrectly would crash our program for overstepping the bounds. If you do slicing, we recommend that you always index based on the startIndex and endIndex attributes. Do this even if you are dealing with a normal array with two properties of 0 and count-1, because the implicit assumption is easy to break.
The dictionary
Another key data structure is the Dictionary. Dictionaries contain keys and their corresponding values, where each key is unique. The average time it takes to retrieve a value by key is of a constant order of magnitude, whereas the time it takes to search an array for a particular element will be proportional to the array size. Unlike arrays, dictionaries are unordered, and when you use a for loop to enumerate key-value pairs in a dictionary, the order is indeterminate.
We use subscripts to get the value of a setting. Dictionary lookup always returns an optional value, and returns nil when the specified key does not exist. This is different from arrays, where access with out-of-bounds subscripts causes the program to crash.
In theory, the reason for this difference is that array indexes and dictionary keys are used very differently. As we’ve already discussed, with arrays, you rarely need to use the index of an array directly. Even if you use an index, the index is usually computed in some way (e.g., from 0.. <array.count). That said, using an invalid index is usually a programmer’s mistake. Dictionary keys, on the other hand, tend to come from other sources, and it’s rare to get keys from the dictionary itself.
Unlike arrays, dictionaries are a sparse structure. For example, the fact that there is a value under the “name” key does nothing to determine whether there is a value under the “address” key.
In the example below, we make up a setup interface for an app and use the dictionary as the model data layer. The interface consists of a series of Settings, each with its own name (that is, the key in our dictionary) and value. A value can be a text, a number, or a Boolean. We use an enum with associated values to represent it:
enum Setting {
case text(String)
case int(Int)
case bool(Bool)
}
let defaultSettings: [String : Setting] = [
"Airplane Model" : .bool(false),
"Name" : .text("My iPhone"),
]
print(defaultSettings["Name"]) // Optional(SwiftTest.Setting.text("My iPhone"))
Copy the code
variability
Like arrays, dictionaries defined using lets are immutable: you cannot add, delete, or modify entries to them. We can also define a mutable dictionary using var. To remove a value from the dictionary, either set the value to nil by subscript or call removeValue(forKey:). The latter method also returns the deleted value (or nil if the key to be deleted does not exist). For an immutable dictionary, if we want to modify it, we need to copy it:
var userSettings = defaultSettings
userSettings["Name"] = .text("Jared's iPhone")
userSettings["Do Not Disturb"] = .bool(true)
Copy the code
Some useful dictionary methods
What if we want to merge a default Settings dictionary with a custom Settings dictionary that has been changed by a user?
The custom Settings should override the default Settings, while the resulting dictionary should still contain keys that have not been customized. In other words, we need to merge two dictionaries, and the dictionary used to do the merge needs to override duplicate keys.
Dictionary has a merge(_:uniquingKeysWith:), which takes two arguments, the first is the key-value pair to be merged, and the second is a function that defines how to merge two values of the same key. We can use this method to merge one dictionary into another, as shown in the following example:
var settings = defaultSettings
let overriddenSettings : [String : Setting] = ["Name" : .text("Your iPhone")]
settings.merge(overriddenSettings, uniquingKeysWith: { $1 })
print(settings)
// ["Airplane Model": SwiftTest.Setting.bool(false), "Name": SwiftTest.Setting.text("Your iPhone")]
Copy the code
In the example above, we used {$1} as a strategy to merge two values. That is, if a key exists in both Settings and overriddenSettings, we use the value in overriddenSettings.
We can also build a new dictionary from a sequence of (Key,Value) key-value pairs. If we can ensure that the key is unique, we can use Dictionary(uniqueKeysWithValues:). However, in cases where a key can exist more than once in a sequence, as above, we need to provide a function to merge two values corresponding to the same key.
For example, to count the number of occurrences of an element in a sequence, we can map each element, match them to 1, and then create a dictionary from the resulting sequence of key-value pairs (element, degree). If we encounter two values under the same key (that is, we see the same element a number of times), we simply add up The Times with + :
extension Sequence where Element: Hashable {
var frequencies: [Element : Int] {
let frequencyPairs = self.map{ ($0, 1) }
return Dictionary(frequencyPairs, uniquingKeysWith: +)
}
}
let frequencies = "hello".frequencies // ["o": 1, "h": 1, "e": 1, "l": 2]
print(frequencies.filter { $0.value > 1 }) // ["l": 2]
Copy the code
Another useful method is to map dictionary values. Because Dictionary is a type that implements a Sequence, it already has a map method to generate an array. But sometimes we want to keep the structure of the dictionary, and only perform row operations on the values in it. The mapValues method does just that:
let settingsAsStrings = settings.mapValues { setting -> String in switch setting {
case .text(let text): return text
case .int(let number): return String(number)
case .bool(let value): return String(value)
} }
print(settingsAsStrings) // ["Name": "Jane\'s iPhone", "Airplane Mode": "false"]
Copy the code
Hashable requirements
Dictionaries are actually hash tables. The dictionary uses the key’s hashValue to specify a location for each key in the underlying array of storage. This is why Dictionary requires that its Key type comply with the Hashable protocol. All of the basic data types in the library follow the Hashable protocol, including strings, integers, floating-point numbers, and Booleans. In addition, types such as arrays, collections, and optional values automatically become hashable if their elements are all hashable.
To ensure performance, hash tables require that the types stored in them provide a good hash function, meaning that this function does not cause too many conflicts. It is not easy to implement a good hash function that evenly distributes its input over the whole integer range. Fortunately, we hardly need to implement this function ourselves. In many cases, the compiler can generate an implementation of Hashable, even if it doesn’t work for a particular type, and the library comes with built-in hash functions to hook custom types.
For structures and enumerations, as long as they are made up of Hashable types, Swift can automatically compose the implementation needed for the Hashable protocol. If all the stored properties of a structure are Hashable, then the structure already implements the Hashable protocol without manually implementing it. Similarly, this protocol can be implemented automatically as long as the enumeration contains hashable associated values; For enumerations without associated values, you don’t even have to explicitly declare that you want to implement the Hashable protocol. This not only saves the initial implementation effort, but also automatically updates the implementation as properties are added or removed.
If you can’t take advantage of automatic Hashable composition (either because you’re implementing a class; Either there are a few attributes that need to be ignored in your custom structure for hashing purposes), then you first need to make the type implement the Equatable protocol, You can then implement the hash(into:) method to satisfy the Hashable protocol (deprecated prior to Swift 4.2 by implementing the hashValue attribute). This method takes an argument of type Hasher, which encapsulates a generic hash function and captures the state of the hash function when the consumer provides data to it. It has a combine that accepts any hashable value. You should pass all the basic components of a type to Hasher one by one by calling the Combine method. The base components are the properties that make up the essence of a type, and you often want to exclude temporary properties that can be lazily reconstructed.
You should use the same basic components for equality checking, because the following invariable principle must be followed: two instances of the same (which you implement with the same definition of ==) must have the same hash value. But the reverse need not be true: two instances of the same hash need not be equal. The number of different hashes is finite, while the number of types that can be hashed (such as strings) is infinite.
The library’s general-purpose hash function uses a random seed as one of its inputs. That is, the hash of the string “ABC” will be different each time the program executes. Random seeding is a security measure used to prevent targeted hash flood denial of service attacks. Because dictionaries and collections iterate over their elements in the order stored in the hash table, and because the order is determined by the hash value, this means that the same code will produce a different iteration order each time it executes. If you want hash values to be the same every time, for example for testing, you can disable random seeds by setting the environment variable SWIFT_DETERMINISTIC_HASHING=1, but you should not do this in a formal environment.
Finally, you need to be careful when using dictionary keys for types that do not have value semantics, such as mutable objects. If you change the contents of an object after using it as a dictionary key, its hash value and/or equality properties tend to change as well. You won’t be able to find it in the dictionary. The dictionary will store the object in the wrong location, which will cause the internal storage of the dictionary to fail. For value types, there is no problem because the key in the dictionary is not shared with the copied value and therefore cannot be changed externally.
Set
The third main Set type in the library is Set. A collection is an unordered set of elements, each of which appears only once. You can think of a collection as a dictionary that stores only keys and no values. Like Dictionary, sets are implemented through hash tables and have similar performance features and requirements. A constant time operation that tests whether a collection contains an element. Just like keys in dictionaries, elements in a collection must satisfy Hashable.
Collections are a better choice if you need to efficiently test whether an element exists in a sequence and the order of the elements doesn’t matter (the same operation in arrays is O(n)). Collections can also be used when you need to ensure that there are no duplicate elements in a sequence.
Set to observe the ExpressibleByArrayLiteral agreement, that is to say, we can use an array literal way to initialize a collection:
let naturals: Set = [1, 2, 3, 2]
naturals // [1, 2, 3]
naturals.contains(3) // true
naturals.contains(0) // false
Copy the code
Note that the number 2 appears only once in the set, and the duplicate number is not inserted into the set.
Collections, like other collection types, support the basic operations we’ve seen: you can iterate through a for loop, map or filter it, or do various other things.
The set of algebraic
As its name suggests, a Set is closely related to a mathematical concept of a Set; It supports the basic set operations you learn in math class. For example, we can find the complement of one set of another:
let iPods: Set = ["iPod touch", "iPod nano", "iPod mini", "iPod shuffle", "iPod Classic"]
let discontinuedIPods: Set = ["iPod mini", "iPod Classic", "iPod nano", "iPod shuffle"]
let currentIPods = iPods.subtracting(discontinuedIPods) // ["iPod touch"]
Copy the code
We can also find the intersection of two sets, in other words, find elements in both sets:
let touchscreen: Set = ["iPhone", "iPad", "iPod touch", "iPod nano"]
let iPodsWithTouch = iPods.intersection(touchscreen)
// ["iPod nano", "iPod touch"]
Copy the code
Or, we can find the union of two sets, that is, merge the two sets into one (removing the redundant ones, of course):
var discontinued: Set = ["iBook", "Powerbook", "Power Mac"]
discontinued.formUnion(discontinuedIPods)
discontinued
/*
["iPod shuffle", "iBook", "iPod Classic", "Powerbook", "iPod mini", "iPod nano", "Power Mac"]
*/
Copy the code
Here we use a mutable version of formUnion to change the original collection (which is why we need to declare the original collection with var). Almost all collection operations take the form of immutable versions and mutable versions, both of which start with form. To learn more about collection operations, look at the SetAlgebra protocol.
Index collection and character collection
Set and OptionSet are the only types in the library that implement SetAlgebra, but the protocol is also implemented in Foundation by two other interesting types: IndexSet and CharacterSet. Both were things that existed long before Swift. Several other Objective-C classes, including these two, are now imported entirely into Swift as value types, and in the process they also adhere to some common standard library protocols. This is very friendly for Swift developers, and these types instantly become familiar.
IndexSet represents a set of positive integers. Of course, you could do this with Set
, but IndexSet is more efficient because it uses a list of ranges internally. For example, now that you have a table view with 1000 elements, you want a collection to manage the indexes of the elements that have been selected by the user. With Set
, up to 1000 elements may be stored, depending on how many are selected. IndexSet, however, stores contiguity ranges, meaning that in the case of the first 500 rows, only the first and last integer values are stored in the IndexSet.
However, as a user of IndexSet, you don’t need to care about the internal implementation, all of which is hidden under the well-known SetAlgebra and Collection interfaces. (Unless you really need to manipulate internal scopes directly, for this purpose IndexSet exposes its view through the rangeView property, which is a collection type). For example, you can add ranges to a collection of indexes and then map the indexes as if they were separate elements:
var indices = IndexSet() indices.insert(integersIn: 1.. <5) indices.insert(integersIn: 11.. <15) let evenIndices = indices.filter { $0 % 2 == 0 } print(evenIndices) // [2, 4, 12, 14]Copy the code
Similarly, CharacterSet is an efficient collection of Stored Unicode code points. It is often used to check whether a particular string contains only characters from a subset of characters, such as alphanumerics or decimalDigits. However, unlike IndexSet, CharacterSet is not a collection type. Its name, CharacterSet, is generated when it is imported from Objective-C and is not compatible with Swift’s Character type in Swift. Perhaps UnicodeScalarSet would be a better name.
Use collections in closures
Dictionaries and collections can be useful data structures in functions, even if they are not exposed to function callers. If we want to write an extension for a Sequence that retrits all unique elements of the Sequence, we simply place the elements in a Set and return the contents of the Set. However, because the set does not define an order, this is unstable, and the order of the input elements may not be consistent in the result. To solve this problem, we can create an extension to solve this problem. Inside the extension method we still use Set to verify uniqueness:
extension Sequence where Element: Hashable { func myUnique() -> [Element] { var seen: Set<Element> = [] return filter { element in if (seen.contains(element)) { return false } else { seen.insert(element) Return true}}}} print (,2,3,12,1,3,4,5,6,4,6 [1]. MyUnique ()) / / [1, 2, 3, 12, 4, 5, 6])Copy the code
The above method allows us to find all non-repeating elements in a sequence and maintain their original order by specifying that the elements must satisfy the Hashable constraint. In the closure we pass to filter, we use an external SEEN variable whose value we can access and modify in the closure.
Range
A range represents an interval of two values, defined by upper and lower boundaries. You can use the.. < to create a half-open range with no upper bounds, or use… Create a closed range that contains both upper and lower boundaries:
// let singleDigitNumbers = 0.. <10 Array(singleDigitNumbers) // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] let lowercaseLetters = Character(" A ")... Character("z")Copy the code
There are also prefix and postfix variants of these operators to denote unilateral ranges:
let fromZero = 0... let upToZ = .. <Character("z")Copy the code
There are five different concrete types that can be used to represent ranges, each representing a different constraint on a value. The two most commonly used types are Range (made up of.. < half-open range created) and ClosedRange (created by… Closed range created). Both have a generic argument for Bound: the only requirement for Bound is that it comply with the Comparable protocol. For example, the lowercaseLetters expression above is of type ClosedRange.
The most basic operation on a scope is to check whether it contains certain elements:
singleDigitNumbers.contains(9) // true lowercaseLetters.overlaps("c".. <"f") // trueCopy the code
The half-open range and the closed range have their own uses:
- Only a half-open range can express the space interval (that is, the lower bound and the upper bound are equal, for example
5.. < 5
). - Only a closed range can contain the maximum that its element type can express (e.g. 0… Int. Max). And a half range
The upper bound of the range is one more than the maximum value it contains.
Count range
A range would naturally look like a sequence or collection type. And you can indeed iterate over a range of integers, or treat it like a set type:
for i in 0.. <10 { print("\(i)", terminator: " ") } // 0 1 2 3 4 5 6 7 8 9 singleDigitNumbers.last // Optional(9)Copy the code
But not all scopes can use this approach. For example, the compiler does not allow us to iterate over a Character range: (The reason we cannot iterate over a Character range directly is Unicode related.)
// Error: type 'Character' does not implement the 'Strideable' protocol. for c in lowercaseLetters { ... }Copy the code
What’s going on here? For a Range to meet the set type protocol, the conditions are that its elements meet the Strideable protocol (you can move from one element to another by adding offsets) and that the stride step is an integer.
In other words, in order to traverse a range, it must be countable. For countable ranges (where those constraints are satisfied), since there is a constraint for the Stride integer type, valid boundaries include integer and pointer types, but cannot be floating-point types. If you want to iterate over successive floating-point values, you can create a sequence to iterate over by using the Stride (from:to:by) and stride(from:through:by) methods.
Range expression
The relative(to:) method uses the startIndex of the collection type as the range lower bound for the part of the range with the missing lower bound. Again, it uses endIndex as the upper bound for the part of the range where the upper bound is missing. In this way, partial ranges make the syntax of set slicing quite compact:
Let arr = [1,2,3,4] arr[2... // [3, 4] arr[..<1] // [1] arr[1...2] // [2, 3]Copy the code
This works because the subscript operator declaration in the Collection protocol receives a type that implements RangeExpression, rather than one of the five specific range types mentioned above. You can even omit both boundaries to get a slice representing the entire set:
arr[...] // [1, 2, 3, 4] type(of: arr) // Array<Int>Copy the code
review
In this chapter, we introduced a number of different collection types :Array, Dictionary, Set, IndexSet, and Range. We looked at some of the methods for these collection types and learned that Swift’s built-in collection types allow you to control the variability of collections using lets and var. In addition, various Range types have been introduced.
We’ll revisit the topic of this chapter in collection type protocols, where we’ll delve into those protocols built into Swift over collection types.