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

Functions

In Swift, a function is a self-contained block of code that performs a specific task. Functions are generally used to logically break our code into reusable named blocks. The function's name is used to call the function.

When we define a function, we can also optionally define one or more parameters (also known as arguments). Parameters are named values that are passed into the function by the code that calls it. These parameters are generally used within the function to perform the task of the function. We can also define default values for the parameters to simplify how the function is called.

Every Swift function has a type associated with it. This type is referred to as the return type and it defines the type of data returned from the function to the code that called it. If a value is not returned from a function, the return type is Void.

Let's look at how to define functions in Swift.

Using a single parameter function

The syntax used to define a function in Swift is very flexible. This flexibility makes it easy for us to define simple C style functions or more complex functions, with local and external parameter names. Let's look at some examples of how to define functions. The following example accepts one parameter and does not return any value back to the code that called it (return type- Void):

func sayHello(name: String) -> Void { 
   let retString = "Hello " + name 
   print( retString) 
} 

In the preceding example, we defined a function named sayHello that accepts one variable that is named name. Inside the function, we print out a Hello greeting to the name of the person. Once the code within the function gets executed, the function exits and control is returned to the code that called it. Rather than printing out the greeting, if we want to return the greeting to the code that called it, we can add a return type, as follows:

func sayHello2(name: String) ->String { 
    let retString = "Hello " + name 
    return retString 
} 

The -> string defines that the return type associated with the function is a string. This means that the function must return an instance of the String type to the code that calls it. Inside the function, we build a string constant with the greeting message and then use the return keyword to return the string constant.

Calling a Swift function is very similar to how we call functions or methods in other languages such as C or Java. The following example shows how to call the sayHello(name:) function that prints the greeting message to the screen from within the function:

sayHello(name:"Jon") 

Now, let's look at how to call the sayHello2(name:) function that returns a value back to the code that called it:

var message = sayHello2(name:"Jon") 
print(message) 

In the preceding example, we call the sayHello2(name:) function and put the value returned in the message variable. If a function defines a return type, such as the sayHello2(name:) function does, it must return a value of that type to the code that called it. Therefore, every possible conditional path within the function must end by returning a value of the specified type. This does not mean that the code that called the function has to retrieve the returned value. As an example, both lines in the following example are valid:

sayHello2(name:"Jon") 
var message = sayHello2("Jon") 

If you do not specify a variable for the return value to go into, the value is dropped. When you compile your code you will receive a warning if a function returns a value and you do not put it into a variable or a constant. You can avoid the warning by using the underscore, as shown in this next example:

_ = sayHello2(name:"Jon") 

The underscore tells the compiler that you are aware of the return value but you do not want to use it. Let's look at how we would define multiple parameters for our functions.

Using a multi-parameter function

We are not limited to just one parameter with our functions, we can also define multiple parameters. To create a multi-parameter function, we list the parameters in the parentheses and separate the parameter definitions with commas. Let's look at how to define multiple parameters in a function:

func sayHello(name: String, greeting: String) { 
    print("\(greeting) \(name)") 
} 

In the preceding example, the function accepts two arguments: name and greeting. We then print a greeting to the console using both the parameters.

Calling a multi-parameter function is a little different from calling a single-parameter function. When calling a multi-parameter function, we separate the parameters with commas. We also need to include the parameter name for all the parameters. The following example shows how to call a multi-parameter function:

sayHello(name:"Jon", greeting:"Bonjour") 

We do not need to supply an argument for each parameter of the function if we define default values. Let's look at how to configure default values for our parameters.

Defining a parameter's default values

We can define default values for parameters by using the equal to operator (=) within the function definition when we declare the variables. The following example shows how to declare a function with parameter default values:

func sayHello(name: String, greeting: String = "Bonjour") { 
    print("\(greeting) \(name)") 
} 

In the function declaration, we define one parameter without a default value (name: String) and one parameter with a default value (greeting: String = "Bonjour"). When a parameter has a default value declared, we are able to call the function with or without setting a value for that parameter. The following example shows how to call the sayHello() function without setting the greeting parameter, and also how to call it setting the greeting parameter:

sayHello(name:"Jon") 
sayHello(name:"Jon", greeting: "Hello") 

In the sayHello(name:"Jon") line, the sayHello() function will print out the message Bonjour Jon since it uses the default value for the greeting parameter. In the sayHello(name:"Jon", greeting: "Hello") line, the sayHello() function will print out the message Hello Jon since we override the default value for the greeting parameter.

We can declare multiple parameters with default values and override only the ones we want by using the parameter names. The following example shows how we would do this by overriding one of the default values when we call it:

func sayHello4(name: String, name2: String = "Kim", greeting: String = "Bonjour") { 
    print("\(greeting) \(name) and \(name2)") 
} 
 
sayHello(name:"Jon", greeting: "Hello") 

In the preceding example, we declare one parameter without a default value (name: String) and two parameters with default values (name2: String = "Kim", greeting: String = "Bonjour"). We then call the function leaving the name2 parameter with its default value, but override the default value of the greeting parameter.

The preceding example will print out this message: Hello Jon and Kim.

Returning multiple values from a function

There are a couple of ways to return multiple values from a Swift function. One of the most common ways is to put the values into a collection type (array or dictionary) and return the collection. The following example shows how to return a collection type from a Swift function:

func getNames() -> [String] { 
    var retArray = ["Jon", "Kim", "Kailey", "Kara"] 
    return retArray 
} 
 
var names = getNames() 

In the preceding example, we declare the getNames() function with no parameters and a return type of [String]. The return type of [String] specifies the return type to be an array of string types.

One of the drawbacks of returning a collection type is that the values of the collection must be of the same type, or we must declare our collection type to be of the Any type. In the preceding example, our array could only return string types. If we needed to return numbers with our strings, we could return an array of Any types and then use typecasting to specify the object type. However, this would not be a very good design for our application since it would be very prone to errors. A better way to return values of different types would be to use a tuple type.

When we return a tuple from a function, it is recommended that we use a named tuple to allow us to use the dot syntax to access the returned values. The following example shows how to return a named tuple from a function and access the values from the named tuple that is returned:

func getTeam() -> (team:String, wins:Int, percent:Double) { 
    let retTuple = ("Red Sox", 99, 0.611) 
 return retTuple 
} 
 
var t = getTeam() 
print("\(t.team) had \(t.wins) wins") 

In the preceding example, we define the getTeam() function that returns a named tuple containing three values—String, Int, and Double. Within the function, we create the tuple that we are going to return. Notice that we do not need to define the tuple that we are going to return as a named tuple as long as the value types within the tuple match the value types in the function definition. We can then call the function, as we would any other function, and use the dot syntax to access the values of the tuple that is returned. In the preceding example, the code would print out the following line:

Red Sox had 99 wins 

Returning optional values

In the previous sections, we returned non-nil values from our function; however, that is not always what we need our code to do. What happens if we need to return a nil value from a function? The following code would not be valid and would cause an expression does not conform to type 'NilLiteralConvertible' exception:

func getName() ->String { 
    return nil 
} 

The reason this code throws an exception is we define the return type as a String value; however, we are attempting to return nil. If there is a reason to return nil, we need to define the return type as an optional type to let the code calling it know that the value may be nil. To define the return type as an optional type, we use the question mark (?) the same way as we did when we defined a variable as an optional type. The following example shows how to define an optional return type:

func getName() ->String? { 
    return nil 
} 

The preceding code would not cause an exception.

We can also set a tuple as an optional type or any value within a tuple as an optional type. The following example shows how we would return a tuple as an optional type:

func getTeam2(id: Int) -> (team:String, wins:Int, percent:Double)? { 
    if id == 1 { 
        return ("Red Sox", 99, 0.611) 
    } 
    return nil 
} 

In the following example, we could return a tuple as defined within our function definition or a nil; either option is valid. If we needed an individual value within our tuple to be nil, we would need to add an optional type within our tuple. The following example shows how to return a nil within our tuple:

func getTeam() -> (team:String, wins:Int, percent:Double?) { 
    let retTuple: (String, Int, Double?) = ("Red Sox", 99, nil) 
    return retTuple 
} 

In the preceding example, we can set the percent value to either a Double value or nil.

Adding external parameter names

In the preceding examples in this section, the parameters were defined similarly to how we would define the parameters in C code, where we define the parameter names and value types. In Swift, we are not limited to this syntax; we can also use external parameter names.

External parameter names are used when we call a function to indicate the purpose of each parameter. If we want to use external parameter names with our functions, we would need to define an external parameter name for each parameter in addition to its local parameter name. The external parameter name is added before the local parameter name in the function definition. The external and local parameter names are separated by a space.

Let's look at how to use external parameter names. But before we do so, let's review how we have previously defined functions. In the next two examples, we will define a function without external parameter names, and then we will redefine that function with external parameter names:

func winPercentage(team: String, wins: Int, loses: Int) -> Double  { 
    return Double(wins) / Double(wins + loses) 
} 

In the preceding example, we define the winPercentage() function; it accepts three parameters. These parameters are team, wins, and loses. The team parameter is a String type and the wins and loses parameters are Int types. The following line of code shows how to call the winPercentage() function:

var per = winPercentage("Red Sox", wins: 99, loses: 63) 

Now, let's define the same function with external parameter names:

func winPercentage(BaseballTeam team: String, withWins wins: Int, andLoses losses: Int) -> Double { 
  return Double(wins) / Double(wins + losses) 
} 

In the preceding example, we redefine the winPercentage function with external parameter names. In this redefinition, we have the same three parameters: team, wins, and losses. The difference is how we define the parameters. When using external parameters, we define each parameter with both an external parameter name and a local parameter name separated by a space. In the preceding example, the first parameter has an external parameter name of BaseballTeam, an internal parameter name of team, and a type of String.

When we call a function with external parameter names, we need to include the external parameter names in the function call. The following code shows how to call the function in the preceding example:

var per = winPercentage(BaseballTeam:"Red Sox", withWins:99, andLoses:63) 

While using external parameter names requires more typing, it does make your code easier to read. In the preceding example, it is easy to see that the function is looking for the name of a baseball team, the second parameter is the number of wins, and the last parameter is the number of losses.

Using variadic parameters

A variadic parameter is one that accepts zero or more values of a specified type. Within the functions definition, we define a variadic parameter by appending three periods (...) to the parameter's type name. The values of a variadic parameter are made available to the function as an array of the specified type. The following example shows how we would use a variadic parameter with a function:

func sayHello(greeting: String, names: String...) { 
  for name in names { 
    print("\(greeting) \(name)") 
  } 
} 

In the preceding example, the sayHello() function takes two parameters. The first parameter is a String type, which is the greeting to use. The second parameter is a variadic parameter of the String type, which are the names to send the greeting to. Within the function, a variadic parameter is an array that contains the type specified; therefore, in our example, the names parameter is an array of String values. In this example, we use a for-in loop to access the values within the names parameter.

The following line of code shows how to call the sayHello() function with a variadic parameter:

sayHello(greeting:"Hello", names: "Jon", "Kim") 

The preceding line of code will print two greetings: Hello Jon and Hello Kim.

Inout parameters

If we want to change the value of a parameter and we want those changes to persist once the function ends, we need to define the parameter as an inout parameter. Any changes made to an inout parameter are passed back to the variable that was used in the function call.

Two items to keep in mind when we use inout parameters are that these parameters cannot have default values and they cannot be a variadic parameter.

Let's look at how to use the inout parameters to swap the values of two variables:

func reverse( first: inout String, second: inout String) { 
    let tmp = first 
    first = second 
    second = tmp 
} 

This function will accept two parameters and swap the values of the variables that are used in the function call. When we make the function call, we put an ampersand (&) in front of the variable name, indicating that the function can modify its value. The following example shows how to call the reverse function:

var one = "One" 
var two = "Two" 
reverse(first: &one, second: &two) 
print("one: \(one)  two: \(two)") 

In the preceding example, we set variable one to value One and variable two to value Two. We then call the reverse function with the one and two variables. Once the reverse function returns, the variable named one will contain the value Two, while the variable named two will contain the value One.

Nesting functions

All the functions that we have shown so far are examples of global functions. Global functions are the ones that are defined at a global scope within the class or file that they are in. Swift also allows us to nest one function within another. Nested functions can only be called within the enclosed function; however, the enclosed function can return a nested function that allows it to be used outside the scope of the enclosed function. We will cover returning a function in Chapter 12, Working with Closures, later in this book.

Let's look at how to nest functions by creating a simple sort function that will take an array of integers and sort it:

func sort( numbers: inout [Int]) { 
    func reverse( first: inout Int, second: inout Int) { 
        let tmp = first 
        first = second 
        second = tmp 
    } 
     
    var count = numbers.count 
     
    while count > 0 { 
        for var i in 1..<count { 
            if numbers[i] < numbers[i-1] { 
                reverse(first: &numbers[i], second: &numbers[i-1]) 
            } 
        } 
        count -= 1 
    } 
} 

In the preceding code, we begin by creating a global function named sort that accepts an inout parameter, that is, an array of Ints. Within the sort function, the first thing we do is to define the nested function that is named reverse. A function needs to be defined in the code prior to calling it, so it is good practice to put all the nested functions at the start of the global function so that we know they are defined prior to calling them. The reverse function simply swaps the two values that are passed in.

Within the body of the sort function, we implement the logic for the simple sort. Within that logic, we compare two numbers in the array, and if the numbers need to be reversed, we call the nested reverse function to swap the two numbers. This example shows how we can effectively use a nested function to organize our code to make it easy to maintain and read. Let's look at how to call the global sort function:

var nums: [Int] = [6,2,5,3,1] 
 
sort(&nums) 
 
for value in nums { 
    print("--\(value)") 
} 

The preceding code creates an array of five integers and then passes the array to the sort function. When the sort function returns the nums array, it contains a sorted array.

Note

Nested functions, when used properly, can be very useful. However, it is really easy to overuse them. Before creating a nested function, you might want to ask yourself why you want to use a nested function and what problem are you solving by using a nested function.