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.”

21. Multiple types and containers

There are three types of native containers commonly used in Swift: Array, Dictionay, and Set:

 struct Array<Element> :
    RandomAccessCollection.MutableCollection {
    / /...
}
struct Dictionary<Key : Hashable.Value> :
    Collection.ExpressibleByDictionaryLiteral {
    / /...
}
public struct Set<Element : Hashable> :
    SetAlgebra.Hashable.Collection.ExpressibleByArrayLiteral {
    / /...
}
Copy the code

They’re all generic, so you can only put elements of the same type in a collection.

let numbers = [1.2.3.4.5] // numbers is of type [Int]
let strings = ["hello"."world"] // Strings is of type [String]
Copy the code

If you want to put unrelated types into the same container type, you need to do some conversion work:

The Any type can be converted implicitly
let mixed: [Any] = [1."two".3]
// Convert to [NSObject]
let objectArray = [1 as NSObject."two" as NSObject.3 as NSObject]
Copy the code

Such conversion will result in the loss of some information. When the value is taken from the container, only the result after the information is completely lost can be obtained. In use, a type conversion is required. This is the worst option when there are no other alternatives, and the compiler cannot provide warnings with such a transformation. Arbitrary objects can be added to the container at will, and values taken out of the container can be converted to arbitrary types, which is a dangerous thing:

let any = mixed[0] / / Any type
let nsObject = objectArray[0] / / NSObject type
Copy the code

Any is not a specific type. With the help of container type generics, you can not only add objects of the same concrete type to a container, but also objects that implement a type of the same protocol. Most of the time, elements put into a container will have something in common that makes it useful to specify the container type with a protocol. For example, if we want to print the description of the elements in the container, we prefer to declare the array as [CustomStringConvertible] :

let mixed: [CustomStringConvertible] = [1."two".3]
for obj in mixed {
    print(obj.description)
}
Copy the code

Although this method also loses some type information, it is better than Any or AnyObject, and is most convenient for objects with certain common characteristics. Another approach is to use the fact that enums can have values to encapsulate type information in a specific enum. The following code encapsulates an Int or String:

enum IntOrString {
    case IntValue(Int)
    case StringValue(String)}let mixed = [IntOrString.IntValue(1),
             IntOrString.StringValue("two"),
             IntOrString.IntValue(3)]
for value in mixed {
    switch value {
    case let .IntValue(i):
        print(i * 2)
    case let .StringValue(s):
        print(s.capitalized)
    } 
}
// 2 Two 6
Copy the code

In this way, different types of information are completely preserved at compile time. You can further write a simple fetch method for IntOrString using a literal conversion method.

22. The default parameters

Swift’s methods support default parameters, and you can specify a default value for a parameter when declaring a method. If this parameter is passed in when the method is called, the value is used; if the input parameter is missing, the default value is used.

 func sayHello1(str1: String = "Hello".str2: String.str3: String) {
    print(str1 + str2 + str3)
}
func sayHello2(str1: String.str2: String.str3: String = "World") {
    print(str1 + str2 + str3)
}
// Many other languages can only use the latter method, which takes the default parameter as the last parameter of the method
// If you want to use the default value when calling, just pass no corresponding value
// The following call yields the same result

sayHello1(str2: "", str3: "World")
sayHello2(str1: "Hello", str2: "")
// The output is Hello World
Copy the code

Both calls omit arguments with default values, str1 in sayHello1 is the default “Hello” and str3 in sayHello2 is the default “World”.

The NSLocalizedString and Assert methods are signed:

 func NSLocalizedString(key: String.tableName: String? = default.bundle: NSBundle = default.value: String = default.comment: String) -> String
                    
 func assert(@autoclosure condition: () - >Bool.@autoclosure _ message: () - >String = default.file: StaticString = default.line: UWord = default)
Copy the code

The default argument writes default, which is the Swift call interface generated by the method with the default argument. When you specify a compile-time constant as the default parameter value, this value is hidden within the method implementation and should not be exposed to other parts of the method.

23. Regular expressions

Swift so far does not support regular expressions at the language level.

The easiest to think of and implement, of course, is the custom =~ operator. NSRegularExpression can be used in Cocoa to do regular matching, writing a wrapper for it. Because we’re doing string regular matching, we’re doing strings on both sides of =~. A NSRegularExpression object can be generated by writing a string that accepts regular expressions. This object is then used to match the input string and the result is returned telling the caller whether the match was successful. A simplest implementation might look like this:

 struct RegexHelper {
    let regex: NSRegularExpression
    init(_ pattern: String) throws {
        try regex = NSRegularExpression(pattern: pattern,
            options: .caseInsensitive)
    }
    func match(_ input: String) -> Bool {
        let matches = regex.matches(in: input,
                               options: [],
                                 range: NSMakeRange(0, input.utf16.count))
        return matches.count > 0}}// Want to match an email address
 let mailPattern =
"^([a-z0-9_\ \. @ ([-] +)\ \da-z\ \. -] +)\ \.([a-z\ \.] {2, 6}) $"
let matcher: RegexHelper
do {
    matcher = try RegexHelper(mailPattern)
}
let maybeMailAddress = "[email protected]"
if matcher.match(maybeMailAddress) { 
    print("Valid email address")}/ / implementation = ~
precedencegroup MatchPrecedence {
    associativity: none
    higherThan: DefaultPrecedence
}
infix operator = ~: MatchPrecedence
func = ~(lhs: String.rhs: String) -> Bool {
    do {
        return try RegexHelper(rhs).match(lhs)
    } catch _ {
        return false}}if "[email protected]" = ~ "^([a-z0-9_\ \. @ ([-] +)\ \da-z\ \. -] +)\ \.([a-z\ \.] {2, 6}) $" {
    print("Valid email address")}Copy the code

24. Pattern matching

A feature somewhat similar to regular matching is built into Swift, pattern matching.

In concept, regular matching is only a subset of pattern matching, but pattern matching in Swift is still very rudimentary and simple, supporting only the simplest equality matching and range matching. In Swift, the pattern matching operator is represented by ~=. There are several versions of this operator:

func ~ = <T : Equatable>(a: T, b: T) - >Bool
func ~ = <T>(lhs: _OptionalNilComparisonType, rhs: T?). ->Bool
func ~ = <I : IntervalType>(pattern: I, value: I.Bound) - >Bool
Copy the code

The operator receives, from top to bottom, a type that can be equalized, a type that can be compared to nil, and a range input and a specific value, all returning Bool values for success or failure. Which is the switch in Swift:

// 1. The type of judgment that can be equalized
let password = "akfuv(3"
switch password {
    case "akfuv(3": print("Password approved")
    default: print("Verification failed")}// 2
let num: Int? = nil
switch num {
case nil: print("No value")
    default:  print("\(num!)")}// 3. Judgment of scope
let x = 0.5
switch x {
    case -1.0.1.0: print("In the interval")
    default: print("Out of the range")}Copy the code

Switch uses the ~= operator for pattern matching, with the pattern specified by case as the left argument and the element waiting to be matched as the right argument of the operator. This call is done implicitly by Swift.

When making case judgments in Switch, we can use our custom pattern matching method to make case judgments, which sometimes makes the code very concise and organized. The ~= operator needs to be overloaded as required. An example of using regular expressions for matching:

First override the ~= operator and accept an NSRegularExpression as a mode to match the input String:

func ~ =(pattern: NSRegularExpression.input: String) -> Bool {
    return pattern.numberOfMatches(in: input,
                              options: [],
                                range: NSRange(location: 0, length: input.count)) > 0
 }
Copy the code

Then, for simplicity, we add an operator that transforms strings into NSRegularExpression

prefix operator ~ /
prefix func ~ /(pattern: String) -> NSRegularExpression {
    return NSRegularExpression(pattern: pattern, options: nil, error: nil)}Copy the code

Use regular expressions in case statements to match the string being switched:

let contact = ("http://onevcat.com"."[email protected]")
let mailRegex: NSRegularExpression
let siteRegex: NSRegularExpression
mailRegex =
    try ~ /"^([a-z0-9_\ \. @ ([-] +)\ \da-z\ \. -] +)\ \.([a-z\ \.] {2, 6}) $"
siteRegex =
try ~ /"^(https? :\ \/\ \/)? ([\ \da-z\ \. -] +)\ \.([a-z\ \([...] {2, 6})\ \/\ \w \ \. -] * *)\ \/? $"
switch contact {
    case (siteRegex, mailRegex): print("Have both a valid website and a valid email address.") case (_, mailRegex): print("Have only valid email addresses")
    case (siteRegex, _) :print("Only have valid websites")
    default: print("没有")}Copy the code

25…. And.. <

In many scripting languages there is something like 0.. 3 or 0… The Range operator, such as 3, is used simply to specify a Range of consecutive counts from X to Y.

The most basic use is to specify numbers on both sides, 0… 3 represents the range from 0 to 3 and includes the number 3, which is called fully closed range operation; It is sometimes more common to use a range that does not include the last digit, which in Swift is written 0.. < 3.

For the range of numbers thus obtained, we can do for… Access to in:

for i in 0.3 {
    print(i, terminator: "")}Copy the code

Swift defines two operators:

/// Forms a closed range that contains both `minimum` and `maximum`.
func . <Pos : ForwardIndexType>(minimum: Pos, maximum: Pos) - >Range<Pos>
/// Forms a closed range that contains both `start` and `end`.
/// Requres: `start <= end`
func . <Pos : ForwardIndexType where Pos : Comparable>(start: Pos, end: Pos) - >Range<Pos>
/// Forms a half-open range that contains `minimum`, but not
/// `maximum`.
func ..<<Pos : ForwardIndexType>(minimum: Pos, maximum: Pos) - >Range<Pos>
/// Forms a half-open range that contains `start`, but not
/// `end`. Requires: `start <= end`
func ..<<Pos : ForwardIndexType where Pos : Comparable>(start: Pos, end: Pos) - >Range<Pos>
/// Returns a closed interval from `start` through `end`
func . <T : Comparable>(start: T, end: T) - >ClosedInterval<T>
/// Returns a half-open interval from `start` to `end`
func ..<<T : Comparable>(start: T, end: T) - >HalfOpenInterval<T>          
Copy the code

All of these methods support generics. In addition to the usual input Int or Double, which returns a Range, this operator also has an overload that accepts Comparable input and returns ClosedInterval or HalfOpenInterval.

In Swift, the other basic type that implements Comparable in addition to numbers is String. That is to say through… Or.. < to concatenate two strings. A common usage scenario is to check if a character is a valid character. For example, if you want to make sure that all characters in a word are lowercase, you can do this:

let test = "helLo"
let interval = "a"."z"
for c in test {
    if !interval.contains(String(c)) { print("\(c)Not lowercase letters.")}}Copy the code

In everyday development, you might need to determine if a character is a valid ASCII character, using \0… A ClosedInterval like ~ (\0 and ~ are the first and last ASCII characters, respectively).

AnyClass, meta-types, and.self

In Swift, the concept of “Any” can be expressed in addition to Any and AnyObject, there is also an AnyClass. AnyClass is defined in Swift by a TypeAlias:

typealias AnyClass = AnyObject.Type
Copy the code

The result of anyObject. Type is a Meta. Type is declared after the name of the Type, such as a.type, which stands for the Type of Type A. That is, we can declare A metatype to store the type A itself, and when we retrieve the type from A, we need to use.self:

class A {}
let typeA: A.Type = A.self
Copy the code

In Swift,.self can be used after a type to get the type itself, or after an instance to get the instance itself. The former method can be used to get a value representing the type, which can be useful at some point; The latter, because of the instance itself, does not seem to have many cases that need to be used in this way.

Anyobject.type (AnyClass) represents an arbitrary Type itself. For the type of A, we can force it to be an AnyClass:

 class A {}
let typeA: AnyClass = A.self

// When there is A class method in A, it can be called via typeA
class A {
    class func method(a){
        print("Hello")}}let typeA: A.Type = A.self
typeA.method()
/ / or
let anyClass: AnyClass = A.self (anyClass as! A.Type).method()
Copy the code

The concept of metatype, or metaprogramming, can become very flexible and powerful, and come in handy for writing some kind of framework code. For example, when you want to pass some type, you don’t have to constantly change the code.

In the following example, the MusicViewController and AlbumViewController meta-types are obtained as code declarations, which can be done by reading in configuration files and the like. Metatype programming is hard to replace by storing these metattypes in arrays and passing them to other methods for configuration:

class MusicViewController: UIViewController {}class AlbumViewController: UIViewController {}let usingVCTypes: [AnyClass] = [MusicViewController.self.AlbumViewController.self]
func setupViewControllers(_ vcTypes: [AnyClass]) {
    for vcType in vcTypes {
        if vcType is UIViewController.Type {
            let vc = (vcType as! UIViewController.Type).init(a)print(vc)
}
setupViewControllers(usingVCTypes)
Copy the code

You can build a framework and then configure it in a DSL way to do complex operations without touching the Swift code.

In addition, Cocoa API often need an AnyClass input, in this case should also use.self to get the required metatype, such as when registering the tableView cell type:

self.tableView.registerClass(
    UITableViewCell.self, forCellReuseIdentifier: "myCell")
Copy the code

Type is a meta Type of a certain Type. In Swift, protocol can be defined in addition to class, struct, and enum. It can be obtained by using.protocol after the name of a protocol, using the same method as.type.

27. Self in protocol and class methods

Some protocol definitions might notice that Self begins with a capital letter in the type position:

 protocol IntervalType {
    / /...
    /// Return `rhs` clamped to `self`. The bounds of the result, even
    /// if it is empty, are always within the bounds of `self`
    func clamp(intervalToClamp: Self) -> Self
/ /...
}
Copy the code

The protocol of IntervalType defines a method that takes its own type that implements the protocol and returns the same type.

In fact, the protocol itself does not have its own context type information. When declaring the protocol, it is not known what type will finally be used to implement the protocol. In Swift, generics cannot be defined in the protocol to limit it. When declaring a protocol, if the type you want to use in the protocol is the type that implements the protocol itself, you need to refer to it with Self.

But in this case, Self refers not only to the type itself that implements the protocol, but also to subclasses of that type.

Self is conceptually very simple, but actually implementing such a method takes a slight turn. Suppose you want to implement a Copyable protocol that satisfies the type of protocol required to return the same copy as the instance that received the method call. Here are some of the agreements that might be considered at the outset:

 protocol Copyable {
    func copy(a) -> Self
}
Copy the code

What you should do is create the same thing as the object that received this method, and then return it, and the return type should not change, so Self. Then start trying to implement a MyClass to satisfy this protocol:

class MyClass: Copyable {
    var num = 1
    func copy(a) -> Self { 
    // TODO:Return what?
    // return}}// Something like this might be written
// This is the error code
func copy(a) -> Self {
    let result = MyClass()
    result.num = num
    return result
}
Copy the code

Obviously the type is problematic because the method requires that an abstract Self representing the current type be returned, but its real type, MyClass, is returned, resulting in failure to compile. Try changing Self to MyClass in the method declaration, so that the declaration is the same as the actual return, but the implementation of the method is different from the protocol definition, and still does not compile.

To solve this problem, you need to initialize it in a way that is independent of the current context (that is, MyClass) and refers to the current type. We can use type(of:) to get the object type, which can also be further initialized to ensure that the method is independent of the current type context, so that both MyClass and its subclasses can correctly return the appropriate type to satisfy Self:

 func copy(a) -> Self {
    let result = type(of: self).init()
    result.num = num
    return result
}
Copy the code

The compiler says that if you want to build an object of type Self, you need an initialization method with the required keyword. This is because Swift must ensure that both the current class and its subclasses respond to the init method. Another solution is to add the final keyword before the declaration of the current class class to tell the compiler that there will no longer be subclasses to inherit the type. Choose to add the required init method in this example. Finally, the MyClass type looks like this:

 class MyClass: Copyable {
    var num = 1
    func copy(a) -> Self {
        let result = type(of: self).init()
        result.num = num
        return result
}
    required init(a){}}let object = MyClass()
object.num = 100
let newObject = object.copy()
object.num = 1
print(object.num)     / / 1
print(newObject.num)  / / 100
Copy the code

For subclasses of MyClass, copy() returns the subclass’s copied object correctly. The other place you can use Self is in class methods, and it works very similarly. The key is to make sure that subclasses also return the appropriate type.

28. Dynamic typing and multiple methods

In Swift, you can retrieve the dynamicType of an object by dynamicType (that is, the actual type at runtime, not the type specified by code or seen by the compiler). However, in use, Swift does not currently support multi-methods, that is, it cannot make appropriate overloaded method calls based on the type of the object when it is in motion.

It is possible to override methods with the same name in Swift, as long as the argument type is different:

class Pet {}
class Cat: Pet {}
class Dog: Pet {}
func printPet(_ pet: Pet) {
    print("Pet")}func printPet(_ cat: Cat) {
    print("Meow")}func printPet(_ dog: Dog) {
    print("Bark")
}
printPet(Cat()) // Meow
printPet(Dog()) // Bark
printPet(Pet()) // Pet
Copy the code

For instances of Cat or Dog, the most appropriate method is always found, rather than calling a method from a generic Pet parent. All of this happens at compile time if code like this:

func printThem(_ pet: Pet._ cat: Cat) {
    printPet(pet)
    printPet(cat)
}
printThem(Dog(), Cat())
// Pet 
// Meow
Copy the code

The type information for Dog() at print-time is not used to select the appropriate version of printPet(Dog: Dog) at run time, but is ignored and the Pet version determined at compile time is used. Because Swift does not dispatch dynamically by default, method calls can only be determined at compile time.

To circumvent this limitation, you need to evaluate and convert the input type:

func printThem(_ pet: Pet._ cat: Cat) {
    if let aCat = pet as? Cat {
        printPet(aCat)
    } else if let aDog = pet as? Dog {
        printPet(aDog)
    }
    printPet(cat)
}
// Bark 
// Meow
Copy the code

29. Attribute observation

Property Observers, a special feature in Swift, allow Observers to monitor the setting of attributes within the current type, and make some responses. Swift provides two methods for observing properties, willSet and didSet.

Using these methods, you can listen for values to be set and values to be set by adding the corresponding code block to the property declaration:

class MyClass {
    var date: NSDate {
        willSet {
            let d = date
            print("Is about to change the date from\(d)Set to\(newValue)")}didSet {
            print("Has removed the date from\(oldValue)Set to\(date)")}}init(a) {
        date = NSDate()}}let foo = MyClass()
foo.date = foo.date.addingTimeInterval(10000)
Copy the code

NewValue and oldValue can be used in willSet and didSet respectively to get the values to be set and the values already set. An important use of property observation is as a validation of setting values. For example, in the example above, you can modify the didSet if you don’t want date to exceed the current time by more than a year:

 class MyClass {
    let oneYearInSecond: TimeInterval = 365 * 24 * 60 * 60
    var date: NSDate {
/ /...
        didSet {
            if (date.timeIntervalSinceNow > oneYearInSecond) {
                print("The set time is too late!)
                date = NSDate().addingTimeInterval(oneYearInSecond)
            }
            print("Has removed the date from\(oldValue)Set to\(date)")}}/ /...
}
Copy the code

The setting of the property by the initializer method and the resetting of the property in willSet and didSet do not trigger the property observation call again.

Attributes declared in Swift include storage attributes and computed attributes. Storing attributes will actually allocate the address in memory to store the attributes, while calculating attributes does not include the storage behind, but only provides two methods of SET and GET. Attribute observation and evaluation attributes cannot coexist in the same type. That is, it is impossible to have both set and willSet or didSet in a property definition. Computed properties can be overwritten in sets to achieve the same property observations as willSet and didSet. If we can’t change the class and want to do something with property observation, we may need to subclass the class and override its properties. The overridden attribute does not know the specific implementation of the parent class attribute, but only inherits the name and type from the parent class attribute. Therefore, in the overridden attribute of the child class, it is possible to arbitrarily add attributes to observe the parent class attribute, regardless of whether the parent class is storing attributes or calculating attributes:

class A {
    var number :Int {
        get {
            print("get")
return 1 }
        set {print("set")}}}class B: A {
    override var number: Int {
        willSet {print("willSet")}
        didSet {print("didSet")}}}let b = B()
b.number = 0
/ / output
// get
// willSet 
// set
// didSet
Copy the code

The thing to notice here is that GET is called first. This is because didSet is implemented, and oldValue is used in didSet, and that value needs to be retrieved and stored for use before the entire set action, otherwise correctness cannot be guaranteed. If didSet is not implemented, this get operation will not exist.

30. final

The final keyword can be used before class, func, or var to indicate that inheritance or overwriting of the content is not allowed. There is a school of thought that banning inheritance and rewriting like this is beneficial in terms of better versioning, better performance, and more secure code. It is even argued that languages should not allow inheritance by default, subclassing only when it is explicitly indicated that inheritance is possible. But it’s also true that neither Apple nor Microsoft, nor many other languages around the world, have made the decision not to allow inheritance and rewriting by default.

Access control

Adding final to a piece of code means that the compiler guarantees that the code will not be changed again; This code is considered complete and no need to be inherited or overwritten. App development is such a big part of Cocoa development that you don’t really make it available to anyone else, and in this case it’s ok to mark all your code as final, but you can remove that keyword whenever you want to restore its heritability. When developing libraries for use by other developers, you have to think more deeply about the various usage scenarios and requirements.

In general, there are several situations when you do not want to be inherited or overridden:

  • A class or method is indeed fully functional

For many auxiliary utility classes or methods, final may be added. One big feature of such a class is that it is likely to contain only class methods and no instance methods. For example, it’s hard to think of a situation where you need to inherit or override a utility class that computes MD5 or AES encryption and decryption of a string. The algorithms for such utility classes and methods are fully validated and fixed, requiring only invocation by the user, and relatively impossible inheritance and rewriting requirements.

  • Subclass inheritance and modification is a dangerous business

When a subclass inherits or overrides some method, it can do something destructive that causes the subclass or superclass part to not work as well. In such cases, it may be better to mark the numbering method as final to ensure stability.

  • Some code must be executed for the parent class

Sometimes there is some critical code in the parent class that must be executed after being overridden by inheritance (such as state configuration, authentication, and so on), otherwise it will cause runtime errors. In general, if a subclass overrides a parent method, there is no way to force the subclass method to call the same parent. You can use a final method, do some necessary configuration in it, and then call some method that needs to be subclassed to make sure it works:

class Parent {
    final func method(a) {
        print("Start configuration")
        / /.. Necessary code
        methodImpl()
        / /.. Necessary code
        print("End configuration")}func methodImpl(a) { 
        fatalError("Subclasses must implement this method.") // The default implementation can also be given}}class Child: Parent {
    override func methodImpl(a) {
        / /.. The business logic of the subclass}}Copy the code

This way, using Method in any way ensures that the required code has been run, while giving subclasses the opportunity to inherit and override the custom implementation.

Performance considerations

Another important reason to use final is the potential performance improvement. Because the compiler can extract additional information from final, it can perform additional optimization on class or method calls. However, the potential benefit of this advantage in practice is very limited, so making classes or methods final is not recommended to pursue performance gains when there are other aspects of the project that can be optimized.