Foreword
Swift is actually much more complicated than Objective-C. Compared to Objective-C, which was born in the 1980s, Swift incorporates a lot of new features. This also makes it relatively difficult for us to learn and master the language. However, everything is worthwhile. Compared with Objective-C, Swift's written programs are safer and more concise, which can ultimately improve our work efficiency and quality.
There are already a lot of learning materials related to Swift. I want to introduce some of its characteristics from another angle. I call this angle "Brain Gymnastics". What does that mean? It is that we specifically pick some language details that are more cumbersome to learn. By "braining" thinking, to achieve a deeper understanding of the Swift language.
This is the second session of the gymnastics. Please prepare for exercise before exercise and keep your mind clear.
Preparing for exercise: basics
World view of object-oriented language
For many object-oriented programming languages, when thinking about problems, always take "object" as the basic starting point for thinking about the problem.
Object-oriented programming design builds the foundation of programming through the following three rules, they are:
Encapsulation, put variables and functions involved in a relatively independent logic into a class, and then expose a small number of interfaces to make it highly cohesive and low coupling.
Inheritance (Inheritance), subclasses can inherit the variables and functions of the parent class, and can modify or extend the behavior of the parent class.
Polymorphism (Polymorphism), the pointer of the parent class can point to an instance of the subclass, the program language supports to find the corresponding function of the subclass at runtime
Based on the above three criteria, some design principles are introduced, such as:
The principle of single responsibility (Single Responsibility), each class should only do one thing.
Do n’t Repeat Yourself. Do n’t repeat the same (or similar) code twice.
Good composition is better than inheritance (Better Composition over Inheritance), try to use composition instead of inheritance to design.
Therefore, based on these rules and principles, the programming world has produced design patterns that can more accurately guide our programming behavior. This is like learning geometry, first learning a few axioms, and then a large number of theorems will be proved by axioms.
For example, the Singleton Pattern is actually the product of the principle of encapsulation and single responsibility. Delegate pattern (Delegate Pattern) is also the product of the idea of interface-oriented design in a single responsibility and encapsulation.
However, in the world view of object-oriented languages, functions exist as an appendage. Functions are usually attached to a method of a concrete class. Perhaps there is a function that does not need any objects as containers at all. For the sake of unity in this world, we will still construct a class and put this function in it. For example, in the search for apes, we have a class called ImageUtils, which puts a variety of static methods for manipulating images. Some image manipulation functions are actually not very general, but we have to find a class to put No.
In some worlds of object-oriented languages, if an object is called a first-class citizen of OOP, then the function is a second-class citizen.
Functional programming
In the Swift world, functions are not second-class citizens. Yes, Swift introduces a lot of functional programming features that allow us to treat functions as first-class citizens.
What rights do first-class citizens have? That is, functions can be assigned, passed as parameters, participated in calculations, or returned as results, just like objects.
Let's first look at an example of a function being assigned. In the following example, we assign a function to a variable named myFunc and then call it.
let myFunc = {
()-> String in
return "Tang Qiao"
}
let value = myFunc ()
// value is "Tang Qiao"
Let's look at an example where a function is returned as the result of an operation. In this example, we want to construct an "adder" factory that can accept a parameter addValue and return an adder function. This adder function can add the passed parameter to addValue and return it. The following is the implemented code:
func addFactory (addValue: Int)-> (Int-> Int) {
func adder (value: Int)-> Int {
return addValue + value
}
return adder
}
With the above function, we can construct a +2 function and then use it as follows:
let add2 = addFactory (2) // construct a +2 function
let result = add2 (3) // operation, pass in 3, get 5
Function parameters
However, in this "Brain Burnout Gymnastics", it is obviously not realistic to fully introduce functional programming, so we will only study in depth from the parameters of the function to see how complex the parameters of the function can be in the Swift language.
Parameter omission
Let's first briefly look at the omission of function parameters, because there are type deductions, function parameters can often be omitted in Swift, especially when they exist in the form of anonymous functions (closures).
Let's look at an example of array sorting:
let array = [1, 3, 2, 4]
let res = array.sort {
(a: Int, b: Int)-> Bool in
return a <b
}
If a function return type can be derived, the return type can be omitted. So-> Bool in the above code can be deleted and become:
let array = [1, 3, 2, 4]
let res = array.sort {
(a: Int, b: Int) in
return a <b
}
If the parameter type of a function can be derived, the parameter type can be omitted. So: Int in the above code can be deleted and become:
let array = [1, 3, 2, 4]
let res = array.sort {
(a, b) in
return a <b
}
If the number of function parameters can be derived, the parameters can also be omitted. How do you use these parameters? You can use $ 0, $ 1 to refer to parameters. So (a, b) in the above code can be deleted, because in this case, the parameter and return value are omitted, so in can also be omitted, becoming:
let array = [1, 3, 2, 4]
let res = array.sort {
return $ 0 <$ 1
}
Swift also has a rule, if the body of the function is only one line, you can omit the return keyword, so the above code can be further simplified to:
let array = [1, 3, 2, 4]
let res = array.sort {
$ 0 <$ 1
}
The last simplification rule is more violent, because the <symbol is also a function, it accepts the same number of parameters, type, and return value as the sort function requires, so it can be simplified directly to:
let array = [1, 3, 2, 4]
let res = array.sort (<)
With this method, we can also simplify the addFactory we just wrote, and finally simplify it to the following form:
// before simplification
func addFactory (addValue: Int)-> (Int-> Int) {
func adder (value: Int)-> Int {
return addValue + value
}
return adder
}
// simplified
func addFactory (addValue: Int)-> (Int-> Int) {
return {addValue + $ 0}
}
Other keywords in function parameters
Sometimes, the parameter accepted by our function is another function, such as sort, map, so when we look at the code, we need to have the ability to be familiar with this writing.
Let's take a look at the definition of the map function of the array:
public func map <T> (@ noescape transform: (Self.Generator.Element) throws-> T) rethrows-> [T]
There are a few keywords that we didn't mention in this function definition, let's learn it first.
@noescape
@noescape, this is a keyword introduced from Swift 1.2, which is specifically used to modify the parameter type of function closure. When this parameter appears, it means that the closure will not jump out of the life cycle of this function call: That is, after the function is called, the lifetime of the closure is also over. The following is the original Apple document:
A new @noescape attribute may be used on closure parameters to functions. This indicates that the parameter is only ever called (or passed as an @noescape parameter in a call), which means that it cannot outlive the lifetime of the call. This enables some minor performance optimizations, but more importantly disables the self. requirement in closure arguments.
Under what circumstances will a closure parameter jump out of the life of the function? Very simple, we nest a closure with dispatch_async in the function implementation, so that the closure will exist in another thread, thus jumping out of the life of the current function. This is mainly to help the compiler to optimize performance.
If you are interested in this, here are some more detailed introductions for you to learn:
https://stackoverflow.com/questions/28427436/noescape-attribute-in-swift-1-2/28428521#28428521
http://nshint.io/blog/2015/10/23/noescape-attribute/
throws and rethrows
The throws keyword indicates that this function (closure) may throw an exception. The rethrows keyword indicates that if this function throws an exception, it may only be because the call to the closure passed to it caused an exception.
The existence of the throws keyword should be understood by everyone, because there are always some exceptions that may be exposed to the upper layer during design. The presence of the throws keyword makes this design possible.
So why is there a rethrows keyword? In my opinion, this is to simplify the writing of many codes. Because once a function throws an exception, in Swift's type-safe writing, we need to use try syntax. But if you need to write try in many places, it will cause the code to be very long-winded. The rethrows keyword makes it possible that in some cases, if the closure you pass in does not throw an exception, then your calling code does not need to write try.
If you are interested in this, here are some more detailed introductions for you to learn:
http://robnapier.net/re-throws
Functions as parameters of functions
As I said earlier, functions as first-class citizens mean that functions can be passed as parameters or returned as values, just like objects. In this regard, we have a name to call it, called the higher-order function (higher-order function).
In the map function of the array just now, we saw that it followed another function as a parameter. This function accepts the array element type as a parameter and returns a new type.
public func map <T> (@ noescape transform: (Self.Generator.Element) throws-> T) rethrows-> [T]
With the map function, we can easily transform array elements. As follows:
let arr = [1, 2, 4]
// arr = [1, 2, 4]
let brr = arr.map {
"No." + String ($ 0)
}
// brr = ["No.1", "No.2", "No.4"]
Brain Burning Parameters
Well, now enter the formal part of the parameter brain-burning game.
We need to construct a factory function that takes two functions as parameters and returns a new function. The new function is a stack of two function parameters Add effect.
To give a specific example, if we have a +2 function and a +3 function, then using this factory function, we can get a +5 function.
Another example is that we have a * 2 function and a * 5 function. Using this factory function, we can get a * 10 function.
How to write this function? Let's look at the answer first:
func funcBuild (f: Int-> Int, _ g: Int-> Int)
-> Int-> Int {
return {
f (g ($ 0))
}
}
let f1 = funcBuild ({$ 0 + 2}, {$ 0 + 3})
f1 (0) // get 5
let f2 = funcBuild ({$ 0 * 2}, {$ 0 * 5})
f2 (1) // get 10
This function fully reflects the status of the function as a first-class citizen. However, we also see that the existence of functions as parameters poses a challenge to the readability of the program. Fortunately, we have typealias. Through typealias, we can write the type of the function more readable. For example, the above code can be modified into the following form:
typealias IntFunction = Int-> Int
func funcBuild (f: IntFunction, _ g: IntFunction)
-> IntFunction {
return {
f (g ($ 0))
}
}
Now look at the code, is it a lot clearer?
Paradigm in parameter
When the parameters in the function are reintroduced into the paradigm, the function of the function is more powerful, but the readability is further reduced. For example, in the previous example, the restriction function can only be Int-> Int. In fact, it is not necessary. We put the two functions into one function. We only need to ensure that the output type of one function matches the input type of the other function. So, the example just now can be further transformed with a paradigm:
func funcBuild <T, U, V> (f: T-> U, _ g: V-> T)
-> V-> U {
return {
f (g ($ 0))
}
}
let f3 = funcBuild ({"No." + String ($ 0)}, {$ 0 * 2})
f3 (23) // The result is "No.46"
In the above example, we ensure that the output type of function g is T, and the input type of function f is T. Thus, in the example, we concatenate a function of * 2 and a function of converting a number to a string to construct a function of multiplying by 2 and then converting the string.
There are many corresponding examples. For example, WWDC introduced a code that adds a cache mechanism to a function. In this code, any function without a cache function can be transformed into a function with a cache function after modification. The code is as follows, you can learn it yourself:
func memoize <T: Hashable, U> (body: (T)-> U)-> (T-> U) {
var memo = Dictionary <T, U> ()
return {x in
if let q = memo [x] {return q}
let r = body (x)
memo [x] = r
return r
}
}
to sum up
Summarize the brain cells exercised by this brain-burning exercise:
Swift is a language that combines object-oriented programming and functional programming features.
Functions are first-class citizens in Swift, and can be assigned, passed as parameters, participated in calculations, returned as results, or created dynamically.
Because of type deduction, the parameters of the function have various omission rules.
When the function is used as a parameter, there are @noescape, throw and rethrow keywords to understand.
When a function is used as a parameter, it is not easy to read. Reasonable use of typealias can make the source code structure clearer.
Swift Brain Gymnastics (2)-Parameters of the function