第八章 函数

函数

函数是执行特定任务的自包含代码块。给定函数一个名称作为标识,并在需要的时候通过调用其名称来执行任务。

Swift 的统一函数语法十分灵活,可以表示简单的无参数的 C 风格函数,也可以表示有本地参数及外部参数的复杂 Objective-C 风格函数。参数可以为简单的函数调用提供默认值,也可以作为输入/输出参数进行传递,并在函数执行完成的时候改变参数值。

Swift 中每个函数都有一种类型,由函数的参数类型及返回值类型组成。并且可以像使用 Swift 中的其他类型那样来使用函数类型,因此将函数作为参数传递给其他函数,或者将函数作为函数返回值就变得很容易。函数可以定义在其他函数作用域中,用以封装一段有用功能。

8.1 定义和调用函数

当定义一个函数时,你可以任意定义一个或多个给定命名及类型的值作为输入(称为形参),同时可以给定一个当函数执行结束后的返回值类型(称为返回类型)。

每个函数都有一个函数名,用来描述函数所执行的任务。当要使用一个函数时,你可以调用其名称并且传递与参数类型相符的值。一个函数的实参必须与形参顺序一致。

例如下面例子中的函数 sayHello ,它以一个人名作为输入参数,返回对这个人的一句问候语。为了实现这个功能,需要定义一个名为 personName 的字符串输入参数,以及一个包含了对这个人名的问候语的字符串返回值类型:

<此处添加代码2.6.1 - 1>

所有这些信息汇集到一个以 func 关键字作为前缀的函数的定义中。使用符号 -> 来指明返回值类型(一个连字符后面跟一个右箭头),后面跟返回类型名称。

函数的定义描述了函数的功能,它期望接收什么,以及当它执行结束时会返回什么。函数的定义使其可以清晰并且无歧义地在你的代码中被调用:

<此处添加代码2.6.1 - 2>

你可以通过在圆括号内传递一个字符串变量来调用 sayHello 函数,例如 sayHello(“Anna”)。由于该函数返回一个字符串值,sayHello 可以被包含在 println 函数中来打印其返回值,如上所示。

sayHello 函数主体首先定义了一个名为 greeting 的字符串常量,并且将其设置为对 personName 的一句问候语。之后使用关键字 return 来将 greeting 作为函数返回值。当 return greeting 语句被调用的时候,函数的执行结束并且返回当前 greeting 的值。

你可以传入不同的值来调用 sayHello 函数。上面的例子展示了当传入值为 “Anna” 以及 “Brian” 时函数的行为。对于不同的情况函数返回相对应的问候语。

为了简化函数主体,可以将消息创建和返回合并到一条语句中:

<此处添加代码2.6.1 - 3>

8.2 函数的形参和返回值

在 Swift 中,函数的形参和返回值具有很高的灵活性。你可以定义任何事情,无论是仅具有一个形参的简单工具函数,还是具有丰富形参和不同形参选项的复杂函数。

多输入形参

如下函数接收一个半开区间的开始和结束位置,然后计算在此区间内有多少个元素:

<此处添加代码2.6.2 - 1>

无形参函数

函数并为要求要定义输入形参。如下是一个无形参函数,任何时候调用都返回相同的字符串消息:

<此处添加代码2.6.2 - 2>

函数定义时仍需要在函数名后面跟一对圆括号,即使它不带有任何参数。当函数被调用是,函数名后面同样需要跟一对圆括号。

无返回值的函数

函数并未要求要定义返回类型。如下是一个无返回值版本的 sayHello 函数,我们称它为 sayGoodbye,它只会打印自己的字符串值而不是返回它:

<此处添加代码2.6.2 - 3>

因为它并不返回任何值,函数定义也不需要包含返回剪头(->)及返回类型。

注意:

严格来说,即使没有定义任何返回值,函数 sayGoodbye 仍然返回了一个值。没有定义返回类型的函数返回一个特殊类型的值 Void。这是一个空的元组,没有包含任何元素,可以被写作 ()。

当一个函数被调用时,它的返回类型可以被忽略:

<此处添加代码2.6.2 - 4>

第一个函数 printAndCount,打印一个字符串,并以 Int 类型返回其字符数。第二个函数 printWithoutCounting,调用第一个函数,但忽略其返回值。当第二个函数被调用时,消息仍由第一个函数打印,但并未使用它的返回值。

注意:

返回值可以被忽略,但一个定义了返回值的函数必须有返回值。一个定义了返回类型的函数,不允许在控制流结束不返回值,这样会导致编译错误。

多返回值的函数

你可以使用一个元组作为函数的返回类型,来返回一个由多个返回值组成的复合返回值。

下面的例子定义了一个名为minMax的函数,用来查找到int类型数组中的最小值和最大值:

<此处添加代码2.6.2 - 5>

minMax 函数返回包含两个 int 类型值的一个元组。这两个值的标签为 min 和 max,当解包函数返回值的时候可以利用名字分别访问到它们。

minMax 函数主体首先将两个工作变量 currentMin 和 currentMax 设置为数组中的第一个数。然后函数遍历数组中剩余的所有值以查看是否有比 currentMin 小或比 currentMax 大的值。最后,所有值中的最大值最小值组成元组返回。

元组的成员值已经作为函数返回类型的一部分,因此可以使用点语法访问到最小值和最大值:

<此处添加代码2.6.2 - 6>

需要注意的是元组的成员不需要被命名,因为元组是由函数返回的,他们的名字已经被指定为函数返回类型的一部分。

可选元组返回类型

如果函数返回的元组类型可能出现整个元组为空值的情况,你可以使用一个可选元组返回类型来表征整个元组可能为 nil。在元组类型的圆括号后面添加一个问号可以表示可选元组返回类型,例如 (Int, Int)? 或 (String, Int, Bool)?。

注意:

一个可选元组类型例如 (Int, Int)? 和一个包含可选类型的元组例如 (Int?, Int?) 是不同的。 可选元组类型的整个元组是可选的,并不只是元组中单独的值可选。

上面 minMax 函数返回一个包含两个 Int 值的元组。但是,函数并不会对传入的数组进行任何安全检查。如果数组参数中包含有空数组 — 元素数为零的数组 — 上面定义的 minMax 函数将会在访问一个元素为空的数组时触发运行时错误。

要安全处理 “空数组” 的场景,可以将 minMax 函数写成返回可选元组,并在数组为空的时候返回nil:

<此处添加代码2.6.2 - 7>

你可以使用可选元祖来检查此版本的 minMax 函数是否返回一个真正的元组或者是返回nil: 

<此处添加代码2.6.2 - 8>

函数可以有多个输入参数,写到函数的括号内,以逗号隔开。

如下函数接收一个半开区间的开始和结束位置,然后计算在此区间内有多少个元素:

<此处添加代码2.6.2 - 9>


8.3 函数形参名

上面的所有函数都为其形参定义了行参名:

<此处添加代码2.6.3 - 1>

但是,这些参数的名称仅能在函数本身主体内部使用,不能在调用函数时使用。这些形参名被称为本地形参名,因为他们仅能在函数主体内部使用。

外部形参名 

有时,当你调用一个函数的时候,将每一个形参进行命名是非常有用的,这样可以清晰地表明每一个传入参数的含义和目的。

如果希望调用你的函数的用户提供形参名,除了定义形参名,还需要为每个参数定义外部参数名。外部形参名书写在本地形参名之前,用一个空格隔开:

<此处添加代码2.6.3 - 2>

注意:

如果你为一个参数提供外部形参名,外部形参名必须在调用时使用。

举一个例子,考虑下面这个函数,在两个字符串中间插入第三个字符串 “joiner” 来连接它们:

<此处添加代码2.6.3 - 3>

当你调用这个函数的时候,你传入的三个参数目的并不是非常明确:

<此处添加代码2.6.3 - 4>

为了让这些参数值的目的更清晰,为每一个join函数的参数定义一个外部形参名:

<此处添加代码2.6.3 - 5>

这个版本的 join 函数,第一个形参的外部名称是 string,内部名称是 s1;第二个参数的外部名称是 tostring,内部名称是 s2;第三个参数的外部名称是 withJoiner,内部名称是 joiner。

现在你可以使用这些外部形参名来明确的调用这个函数:

<此处添加代码2.6.3 - 6>

使用了外部形参名的 join 函数,能更形象、更像语句一样被用户调用,同时还使得函数主体可读性更强、意图更明确。

注意:

当别人第一次阅读你的代码可能不知道函数参数的含义时,就需要考虑使用外部形参名了。如果每个形参的目的清晰的话,就不需要指定外部形参名了。

外部形参名简写

如果你想为一个函数形参提供外部形参名,而本地形参名已经使用了一个合适的名称,那你就不需要为这个参数书写两次形参名。只需要书写一次形参名,并用一个 “#” 符号作为前缀。着能告诉 Swift 使用这个名称同时作为本地形参名和外部形参名。

下面的例子定义了一个名为 containsCharacter 的函数,通过在本地形参名前添加 # 符号来定义外部形参名:

<此处添加代码2.6.3 - 7>

这个函数对于形参名的选择使得函数主体更加清晰易读,并且在函数调用是不会有歧义:

<此处添加代码2.6.3 - 8>

默认形参值

你可以为每一个形参定义一个默认值作为函数定义的一部分。如果定义了默认值,那么在调用时就可以省略该形参。

注意:

将带默认值的形参写在函数参数列表的末尾。这能确保函数的所有调用都使用相同的无默认值形参顺序,并且在每种情况下都清晰的调用函数。

如下是一个先前的 join 函数,它为形参 joiner 提供了默认值:

<此处添加代码2.6.3 - 9>

如果 join 函数被调用时为 joiner 提供了字符串值,那么字符串值将用于连接两个字符串,跟之前一样:

<此处添加代码2.6.3 - 10>

但如果函数被调用时没有为 joiner 提供任何值,就会使用默认值空格符 “ ”:

<此处添加代码2.6.3 - 11>

带默认值的外部形参

在大多数情况下,为所有形参提供一个带默认值的外部名称是很有用。这样可以确保在函数被调用时形参提供的值所对应的实参有明确的目的。

为了让这个过程更简单,Swift 可为每个定义的形参提供一个自动外部名称。这个自动外部名和本地形参名是相同的,就如同在本地形参名前面添加 # 号一样。

如下是一个先前的 join 函数,没有为任何形参提供外部名称,但仍为 joiner 形参提供了默认值:

<此处添加代码2.6.3 - 12>

在这个例子中,Swift 自动为带默认值的形参 joiner 提供了外部名称。在函数被调用的时候外部名称必须提供,这使得函数的调用清晰无歧义:

<此处添加代码2.6.3 - 13>

可变形参

一个可变形参可以接受零个或多个指定类型的值。当函数被调用时,可变形参可以用来指定传入参数数量不固定。在参数类型名后面写三个句号 (…) 。

传递给可变形参的值在函数体内部以符合的类型存储在数组中。例如一个名为 numbers 类型为 double 的可变形参,在函数体就成为一个名为 numbers 类型为 double[] 的常量数组。

下面的例子可以计算任意长度数字数组的平均数:

<此处添加代码2.6.3 - 14>

注意:函数最多只能有一个可变形参,并且它必须出现在参数表的最后,以避免多个参数引发歧义。如果你的函数中有一个或多个带有默认值的参数,则将可变形参放在参数列表的最后面。

常量形参和变量形参

函数的形参默认是常量。试图在函数体内修改形参的值将引发编译错误。这确保了你不会误修改一个形参的值。

但是,有时函数的形参的变量副本是非常有用的。这样可以避免你自己为一个或多个参数申明变量。变量参数是变量而非常量,它能提供给函数一个可以修改的值拷贝。

在形参名前面添加 var 关键字来申明变量参数:

<此处添加代码2.6.3 - 15>

这个例子定义了一个新的方法称为 alignRight,将出入的字符串与另一个更长的字符串进行右对齐。左侧的空白使用指定的占位符来填充。在这里例子中,字符串 “Hello” 被转换为字符串 “——Hello”。

函数 alignRight 函数将输入参数 string 定义为变量参数。这意味着 string 可以作为本地变量来使用,初始化为传入字符串值, 并且可以在函数体内被修改。

函数首先计算出需要在左边添加几个字符,以使得整个字符串右对齐。这个值存储在本地的常量 amountToPad 中。如果不需要填充字符(即当 amountToPad 小于1)时,函数直接将输入值 string 返回。

否则,函数在现有字符串左边填充 amountToPad 个 pad 字符并返回。在字符串值修改的时候其一直使用 string 变量类型。

注意:

对变量参数的修改效果不会超过函数调用范围,并且在函数体外部不可见。变量参数仅存在于函数调用生命周期内。

In-Out 形参

如上所述,变量形参仅能在函数内部被修改。如果你想让一个函数改变形参值,并且让修改效果延续到函数调用结束之后,那么可以将参数定义为 in-out 形参。

在形参定义前添加 in-out 关键字来定义一个 in-out 形参。In-Out 形参有一个传递进函数的值,在函数中被修改,并传回出函数以替换原来的值。

你只能传递一个变量作为 in-out 形参对应的实参。不能传递常量或者字面量作为实参,因为常量和字面量不能被修改。当你传递一个变量作为 in-out 形参的实参时,需要在变量前面直接加上 & 符号以指示函数可以修改其值。

注意:

in-out 参数不能有默认值,并且可变形参不能被标记为 in-out 形参。如果你标记一个形参为in-out,就不能将其标记为 var 或着 let。

如下是一个示例函数称为 swapTwoInts,其有两个 in-out 整形形参 a 和 b:

<此处添加代码2.6.3 - 16>

swapTwoInts 函数只是简单地交换 a,b 的值。它将 a 的值储存在一个称为 temporaryA 的临时常量中,将 b 的值赋给 a,然后将 temporaryA 的值赋给 b。

你可以使用两个 Int 型变量调用 swapTwoInts 函数来交换他们的值。需要注意的是,当他们被传递给 swapTwoInts 函数时,someInt 和 anotherInt 名称前面需要添加 & 符号:

<此处添加代码2.6.3 - 17>

上面的例子展示了 someInt 和 anotherInt 的原始值被 swapTwoInts 函数改变,即使他们定义在函数的外部。


8.4 函数类型

每一个函数都有一个特定的函数类型,由参数类型和返回值类型组成。

例如:

<此处添加代码2.6.4 - 1>

这个例子中定义了两个简单的数学函数 addTwoInts 和 multiplyTwoInts。每个函数都接受两个 Int 值,并返回一个 Int 值,执行适当的数学运算并返回结果。

这两个函数的类型都是 (Int, Int) -> Int。可以这样理解:

“ 这是一个拥有两个 Int 型参数,且返回一个 Int 型值的函数类型。”

下面是另一个例子,该函数没有参数和返回值:

<此处添加代码2.6.4 - 2>

这个函数的类型是 () -> () ,即 “一个没有形参的函数,并且返回 Void”。没有指明返回类型的函数会返回 Void,在 Swift 中相当于一个空元组,写为 ()。

使用函数类型

你可以像使用 Swift 中的其他类型一样使用函数类型。例如,你可以定义一个常量或变量作为函数类型,并为变量指定一个函数:

<此处添加代码2.6.4 - 3>

这可以解读为:

“ 定义一个名为 mathFunction 的变量,该变量的类型是为 ‘一个接受两个 Int 型参数,返回一个 Int 值的函数’,设置这个新的变量为 addTwoInts 函数。”

addTwoInts 函数和 mathFunction 具有相同的类型,因此 Swift 在赋值时进行类型检查。

现在你可以使用 mathFunction 来调用指定的函数:

<此处添加代码2.6.4 - 4>

具有匹配的相同类型的函数可以被赋值给同一个变量,就跟非函数类型一样:

<此处添加代码2.6.4 - 5>

如同其他类型一样,当你把函数赋值给一个常量或者变量时,你可以让 Swift 自行去判断其类型:

<此处添加代码2.6.4 - 6>

作为行参的函数类型

你可以使用形如 (Int, Int) -> Int 的函数类型,来作为另一个函数的形参。这使得当函数被调用时,保留了一些功能交给调用函数去实现。

如下的例子打印出了上面数学函数的结果:

<此处添加代码2.6.4 - 7>

这个例子定义了一个有三个形参名为 printMathResult 的函数。第一个形参称为 mathFunction,类型是 (Int, Int) -> Int。你可以传递任何此类型的函数作为第一个形参的实参。第二和第三个形参成为 a 和 b,都是 Int 类型。用作数学函数的两个输入值。

当 printMathResult 被调用时,传递进了 addTwoInt 函数,整数 3 和 5。然后函数用 3 和 5 调用了传入的函数,打印出结果 8。

printMathResult 函数的功能是打印出传入的特定类型数学函数的结果。它不关心传入函数的实现,只关心传入函数的类型是否正确。这使得 printMathResult 函数能以类型的安全(type-safe)的方式将部分功能转交给调用者。

作为返回类型的函数类型

你可以将一个函数类型作为另一个函数的返回类型。可以在返回函数的返回剪头 (->) 后面添加完整的函数类型来实现。

下面的例子定义了两个简单的函数称为 stepForward 和 stepBackward。stepForward 函数返回输入值加一的结果,stepBackword 返回输入值减一的结果。这两个函数的类型都是 (Int) -> Int:

<此处添加代码2.6.4 - 8>

这个名为 chooseStepFunction 的函数,它返回一个类型为 (Int) -> Int 的函数,其根据布尔类型传入参数 backwards 的值来决定返回 stepForward 还是 stepBackward:

<此处添加代码2.6.4 - 9>

现在你可以使用 chooseStepFunction 来获取一个递增或者递减函数:

<此处添加代码2.6.4 - 10>

上面的例子可以计算出是否需要通过递增或者递减来让 currentValue 变量趋于 0. currentValue 的初始值是3,因此当 currentValue > 0 时返回真,这使 chooseStepFunction 返回 stepBackward 函数。返回函数的引用存储在名为 moveNearerToZero 的常量中。

从而 moveNearerToZero 可以执行正确的功能,可以用来计数到 0:

<此处添加代码2.6.4 - 11>


8.5 嵌套函数

前面所有章节中所涉及的函数都是全局函数,定义在全局作用域中。你也可以在函数体中定义函数,被称为嵌套函数。

嵌套函数默认对外界不可见,但仍可以通过其包裹函数(enclosing Function)调用它。 包裹函数可以通过返回一个嵌套函数使得这个嵌套函数可以被外部使用。

重写上面的 chooseStepFunction 例子以返回嵌套函数:

<此处添加代码2.6.5- 1>


Tips:

1. 函数参数名称的省略:

Objective-C 的函数在命名上用几乎完整的英文表述了函数名和详细的参数名,这使得函数的可读性极其优秀,如:UIBarButton的工厂方法:- initWithImage: style:  target: action:。Swift中也延续了这个优点,并且提供了“_” 和 “#” 符号来要求调用时添加或者不添加参数名称,为了达到和Objective-C完全一致的风格,可以在第一个参数前添加“_”符号。

2.可变参数函数的推荐写法:

Swift 和 Objective-C 中都限制最多只能有一组可变参数,可变参数必须只能作为方法中最后一个参数来使用,并且可变参数都必须是同一个类型。当需要处理多种类型的场景下,可以使用 Any 作为参数类型,这类似于 Objective-C 中的 id 类型。处理可变参数时推荐使用“_”符号,是可变参数看起来是一个匿名的参数列表,风格就和 Objective-C 统一。

3. 函数的参数修饰符需要注意的问题:

函数的参数默认是用 let 修饰的,也就是说函数内不可改变参数的值。在函数内改变值会产生编译错误,如:

func functionName(variable: Int) -> Int {

return variable *= 2;

}

如需要改变参数值,需要显示指定参数修饰符为 var:

func functionName(var variable: Int) -> Int {

return variable *= 2;

}

并且需要注意的是,参数的修饰是具有传递限制的,参数在传递调用的场景下必须保持七参数。

4. Swift 1.2变化部分及与 Objective-C 的关联:

Swift 1.2版本发布同时为 Objective-C API 中可以表示参数,返回值,属性,变量等等的“nullability”属性。这个nullability标示符影响了Objective-C API在Swift的可选类型值。

![1423711358267173.jpg](http://upload-images.jianshu.io/upload_images/63265-e883bdddaf970d5a.jpg)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容