- Learning Swift(Second Edition)
- Andrew J Wagner
- 2017字
- 2021-07-16 12:33:07
Functions
All of the code we have explored so far is very linear down the file. Each line is processed one at a time and then the program moves onto the next. This is one of the great things about programming: everything the program does can be predicted by stepping through the program yourself mentally, one line at a time.
However, as your program gets larger, you will notice that there are places that reuse very similar or identical code that you cannot reuse by using loops. Moreover, the more code you write, the harder it becomes to know exactly what it is doing. Code comments can help with that but there is an even better solution to both of these problems and they're called functions. A function is essentially a named collection of code that can be executed and reused by using that name.
There are various different types of functions but each builds on the previous type.
Basic functions
The most basic type of function simply has a name with some static code to be executed later. Let's look at a simple example. The following code defines a function named sayHello:
func sayHello() { print("Hello World!") }
Functions are defined using the keyword func followed by a name and parentheses (()). The code to be run in the function is surrounded by curly brackets ({}). Just like in loops, a function can consist of any number of lines of code.
From our knowledge of printing, we know that this function will print out the text Hello World!. However, when will it do that? The terminology used for telling a function to execute is "calling a function." You call a function by using its name followed by parentheses (()):
sayHello() // Prints "Hello World!"
This is a very simple function that is not that useful but we can already see some pretty great benefits of functions. In reality, what happens when you call this function is that the execution moves into the function and, when it has finished executing every line of the function, it exits out and continues on from where the function was called. However, as programmers, we are often not concerned with what is happening inside a function unless something has gone wrong. If functions are named well, they tell you what they will do and that is all you need to know to follow the rest of the code. In fact, well-named functions can almost always take the place of comments in your code. This really reduces clutter without harming the legibility of your code.
The other advantage this function has over using print directly is that the code becomes more maintainable. If you use print in multiple places in your code and then change your mind about how you want to say Hello, you have to change a lot of code. However, if you use a function like the one above, you can easily change how it says Hello by changing the function and it will then be changed in each place you use that function.
You may have noticed some similarity in how we have named our sayHello function and how we used print. This is because print is a function that is built into Swift itself. There is complex code in the print function that makes printing to the console possible and accessible to all programmers. But hey, print is able to take in a value and do something with it, how do we write a function like that? The answer is: parameters.
Parameterized functions
A function can take zero or more parameters, which are input values. Let's modify our sayHello function to be able to say Hello to an arbitrary name using string interpolation:
func sayHelloToName(name: String) { print("Hello \(name)!") }
Now our function takes in an arbitrary parameter called name of the type String and prints hello to it. The name of this function is now sayHelloToName:. We didn't include the parameter name because, when you call the method, you don't use the first parameter's name by default:
sayHelloToName("World") // Prints "Hello World!"
We included a colon (:) at the end of the name to indicate that it takes a parameter there. This makes it different from a function named sayHelloToName that does not take a parameter. The naming may seem unimportant and arbitrary but it is very important that we are all able to communicate about our code using common and precise terminology, so that we can more effectively learn from and collaborate with each other.
As mentioned before, a function can take more than one parameter. A parameter list looks a lot like a tuple. Each parameter is given a name and a type separated by a colon (:), and these are then separated by commas (,). On top of that, functions can not only take in values but can also return values to the calling code.
Functions that return values
The type of value to be returned from a function is defined after the end of all of the parameters separated by an arrow ->. Let's write a function that takes a list of invitees and one other person to add to the list. If there are spots available, the function adds the person to the list and returns the new version. If there are no spots available, it just returns the original list, as shown here:
func addInviteeToListIfSpotAvailable ( invitees: [String], newInvitee: String ) -> [String] { if invitees.count >= 20 { return invitees } return invitees + [newInvitee] }
In this function, we tested the number of names on the invitee list and, if it was greater than 20, we returned the same list as was passed in to the invitees parameter. Note that return is used in a function in a similar way to break in a loop. As soon as the program executes a line that returns, it exits the function and provides that value to the calling code. So, the final return line is only run if the if statement does not pass. It then adds the newinvitee parameter to the list and returns that to the calling code.
You would call this function like so:
var list = ["Sarah", "Jamison", "Marcos"] var newInvite = "Roana" list = addInviteeToListIfSpotAvailable(list, newInvite: newInvitee)
It is important to note that we must assign list to the value returned from our function because it is possible that the new value will be changed by the function. If we did not do this, nothing would happen to the list.
If you try typing this code into a playground, you will notice something very cool. As you begin typing the name of the function, you will see a small pop-up that suggests the name of the function you might want to type, as shown:
You can use the arrow keys to move up and down the list to select the function you want to type and then press the Tab key to make Xcode finish typing the function for you. Not only that, but it highlights the first parameter so that you can immediately start typing what you want to pass in. When you are done defining the first parameter, you can press Tab again to move on to the next parameter. This greatly increases the speed with which you can write your code.
This is a pretty well-named function because it is clear what it does. However, we can give it a more natural and expressive name by making it read more like a sentence:
func addInvitee ( invitee: String, ifPossibleToList invitees: [String] ) -> [String] { if invitees.count >= 20 { return invitees } return invitees + [invitee] } list = addInvitee(newInvite, ifPossibleToList: list)
This is a great feature of Swift that allows you to have a function called with named parameters. We can do this by giving the second parameter two names, separated by a space. The first name is the one to be used when calling the function, otherwise referred to as the external name. The second name is the one to be used when referring to the constant being passed in from within the function, otherwise referred to as the internal name. As an exercise, try to change the function so that it uses the same external and internal names and see what Xcode suggests. For more of a challenge, write a function that takes a list of invitees and an index for a specific invitee to write a message to ask them to just bring themselves. For example, it would print Sarah, just bring yourself for the index 0 in the preceding list.
Functions with default arguments
Sometimes we write functions where there is a parameter that commonly has the same value. It would be great if we could provide a value for a parameter to be used if the caller did not override that value. Swift has a feature for this called default arguments. To define a default value for an argument, you simply add an equal sign after the argument, followed by the value. We can add a default argument to the sayHelloToName: function, as follows:
func sayHelloToName(name: String = "World") { print("Hello \(name)!") }
This means that we can now call this function with or without specifying a name:
sayHelloToName("World") // Prints "Hello World!" sayHelloToName() // Also Print "Hello World!"
When using default arguments, the order of the arguments becomes unimportant. We can add default arguments to our addInvitee:ifPossibleToList: function and then call it with any combination or order of arguments:
func addInvitee ( invitee: String = "Default Invitee", ifPossibleToList invitees: [String] = [] ) -> [String] { // ... } list = addInvitee(ifPossibleToList: list, newInvite) list = addInvitee(newInvite, ifPossibleToList: list) list = addInvitee(ifPossibleToList: list) list = addInvitee(newInvite) list = addInvitee()
Clearly, the call still reads much better when it is written in the same order but not all functions are designed in that way. The most important part of this feature is that you can specify only the arguments that you want to be different from the defaults.
Guard statement
The last feature of functions that we are going to discuss is another type of conditional called a guard statement. We have not discussed it until now because it doesn't make much sense unless it is used in a function or loop. A guard statement acts in a similar way to an if statement but the compiler forces you to provide an else condition that must exit from the function, loop, or switch case. Let's rework our addInvitee:ifPossibleToList: function to see what it looks like:
func addInvitee ( invitee: String, ifPossibleToList invitees: [String] ) -> [String] { guard invitees.count < 20 else { return invitees } return invitees + [newInvitee] }
Semantically, the guard statement instructs us to ensure that the number of invitees is less than 20 or else return the original list. This is a reversal of the logic we used before, when we returned the original list if there were 20 or more invitees. This logic actually makes more sense because we are stipulating a prerequisite and providing a failure path. The other nice thing about using the guard statement is that we can't forget to return out of the else condition. If we do, the compiler will give us an error.
It is important to note that guard statements do not have a block of code that is executed if it passes. Only an else condition can be specified with the assumption that any code you want to run for the passing condition will simply come after the statement. This is safe only because the compiler forces the else condition to exit the function and, in turn, ensures that the code after the statement will not run.
Overall, guard statements are a great way of defining preconditions to a function or loop without having to indent your code for the passing case. This is not a big deal for us yet but, if you have lots of preconditions, it often becomes cumbersome to indent the code far enough to handle them.