Learning Swift
上QQ阅读APP看书,第一时间看更新

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 mentally stepping through the program yourself, one line at a time.

However, as your program gets larger, you will notice that there are many places that reuse very similar or identical code that you cannot reuse using loops. Also, the more code you write, the harder it will become to reason in your head about 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 name.

There are various 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() {
    println("Hello World!")
}

Functions are defined using the keyword func followed by a name and parenthesis (()). The code to be run within the function is surrounded by curly brackets ({}). Just as with loops, a function can contain 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 to command a function to execute is "calling a function". You call a function using its name followed by parenthesis (()):

sayHello() // Prints "Hello World!"

This is a very simple function and is not that useful, but already we can 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 finishes every line of the function, it exits out and continues on where the function was called. However, as programmers, we are often not concerned with what is happening inside a function unless something is going wrong. Especially when functions are named well, they tell you what they do and that is all you need to know in order to reason about the rest of the code. In fact, well-named functions can almost always take the place of comments in your code. This really reduces the clutter without reducing the understandability of your code.

The other advantage this function has over simply using println directly is that the code becomes more maintainable. If you use println in multiple places in your code and you change your mind about how you want to say "hello", you have to change multiple pieces of the code. If, instead, you use a function similar to the one in the previous example you can easily change how it says "hello" by changing the function, and it will be changed everywhere you use that function.

You may have noticed a similarity between how we call our sayHello function and how we use println. This is because println is actually a function that is built into Swift itself. There is actually complex code within the println function that makes printing to the console possible and accessible to all programmers. But hey, println is able to take in a value and do something with it; how do we write a function like that? The answer is with parameters.

Parameterized functions

A function can take zero or more parameters, which are input values. Let's modify our sayHello function so that it is able to say "hello" to an arbitrary name using string interpolation:

func sayHelloToName(name: String) {
    println("Hello \(name)!")
}

Now, our function takes in an arbitrary parameter called name of the type string and prints hello to it. We would say that the name of this function is now sayHelloToName:. We don't include the parameter name because when you call the method, you do not use the first parameter's name by default:

sayHelloToName("World") // Prints "Hello World!"

We include 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 all be able to communicate 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 (:), which are then separated by commas (,). On top of that, functions can not only take in values, but they can return values to the calling code as well.

Functions that return values

The type of value to be returned from a function is defined after the end of all the parameters separated by an arrow (->). Let's write a function that takes a list of invitees and a person to add to the list. If there are spots available, the function will add the person to the list and return the new version; if there are no spots available, it will just return the original list:

func addInviteeToListIfSpotAvailable
    (
    invitees: [String],
    newInvitee: String
    )
    -> [String]
{
    if invitees.count >= 20 {
        return invitees
    }
    return invitees + [newInvitee]
}

In this function, we test the number of names on the invite list and if it is greater than 20, we return the same list that is passed in to the invitees parameter. Note that return is used in a function similar to break in a loop. As soon as the program executes a return, it exits the function and provides that value to the calling code. So, the final return line is only run if the if statement doesn't get passed. In that case, it adds the new invitee parameter to the list and returns that parameter to the calling code.

You can call this function as follows:

var list = ["Sarah", "Jamison", "Marcos"]
var newInvite = "Roana"
list = addInviteeToListIfSpotAvailable(list, newInvite)

It is important that we assign the list variable to the value returned from our function because it is possible that the new value will be changed by the function. If we haven't assigned a list, nothing would actually 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 popup that suggests a name of the function that you might want to type:

Functions that return values

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 have Xcode finish typing the function for you. Not only this, it will also highlight the first parameter so that you can immediately start typing what you would like to pass in. When you are done defining the first parameter, you can press tab again to move to the next parameter. This will greatly increase the speed at which you can write your code.

Now, this is a pretty well-named function because it is clear as to 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 call a function with its named parameters. We achieved this by actually giving the second parameter two names separated by a space. The first name is the name to be used when calling the function; otherwise, it is referred to as the external name. The second name is the name to be used when referring to the constant passed in from within the function; otherwise, it is referred to as the internal name. As an exercise, try changing the function to use the same external and internal name and see what Xcode suggests you do. 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 asking them to just bring themselves. For example, it would print "Sarah, just bring yourself" for index 0 of our preceding list.

Functions with default arguments

The last feature of functions that we will discuss is the ability to provide default values for the arguments to a function. This allows the calling code to leave out the parameter from the call if they are satisfied with the default. 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 our previously used sayHelloToName: function, as follows:

func sayHelloToName(name: String = "World") {
    println("Hello \(name)!")
}

This makes it such that we can call this function with or without specifying a name:

sayHelloToName(name: "World") // Prints "Hello World!"
sayHelloToName() // Also Print "Hello World!"

Note that while using the argument, we now need to specify its name, even though it is the first argument. In fact, while using default arguments, the order of the arguments becomes unimportant. We can add default arguments to our addInvitee:ifPossibleToList: function and then we can call it with any combination or order of arguments:

func addInvitee
    (
    invitee: String = "Default Invitee",
    ifPossibleToList invitees: [String] = []
    )
    -> [String]
{
    // ...
}
list = addInvitee(ifPossibleToList: list, invitee: newInvite)
list = addInvitee(invitee: newInvite, ifPossibleToList: list)
list = addInvitee(ifPossibleToList: list)
list = addInvitee(invitee: newInvite)
list = addInvitee()

Clearly, the call still reads much better when it is written in the same order, but not all functions will be designed this way. The more important part of this feature is that you can specify only the arguments that you want to be different from the defaults.