今天我们开始一起学习函数 Function。

在编程语言中,函数的作用是让我们把代码片段包起来,这样它们就可以在很多地方使用,而我们不必重复写被包起来的那些代码片段。我们还可以将数据发送到函数中,自定义它们的工作方式,并取回最终的计算结果。

函数最大的贡献是我们不必再重复复制和粘贴相同的代码,就可以在不同的地方使用同一段代码。这意味着更少的代码重复,也意味着更改工作的便利性大幅提高,且可以避免漏改的错误发生。

今天,我们将学习如何编写我们自己的函数,如何接受参数以及如何返回数据。

让我们开始吧!

如何使用函数重复使用代码

当你写了一些非常喜欢的代码,并想反复使用时,你应该考虑将其放入函数中。函数就是从程序的其他部分分割出来的代码块,并为其命名,这样就可以方便地引用它们。

例如,假设我们有这样一段简单漂亮的代码:

print("Welcome to my app!")
print("By default This prints out a conversion")
print("chart from centimeters to inches, but you")
print("can also set a custom range if you want.")

这是一个应用程序的欢迎信息,你可能希望在应用程序启动时打印出来,或者在用户寻求帮助时打印出来。

但如果你希望在两个地方都打印呢?是的,你可以复制这四行 print() 并将它们放在两个地方,但如果你想将这些文字放在十个地方呢?或者,如果您以后想修改措辞——你真的会记得修改代码中出现的所有地方吗?

这就是函数的作用所在:我们可以调出代码,给它一个名字,然后随时随地运行它。这意味着所有的 print()行都会留在一个地方,并在其他地方重复使用。

如下所示:

func showWelcome() {
    print("Welcome to my app!")
    print("By default This prints out a conversion")
    print("chart from centimeters to inches, but you")
    print("can also set a custom range if you want.")
}

让我们来分析一下…

  1. 首先是 func 关键字,它标志着一个函数的开始。
  2. 我们将函数命名为 showWelcome。这个名字可以是你想要的任何名字,但尽量让人记住——printInstructions()displayHelp() 等都是不错的选择。
  3. 函数的主体包含在打开和关闭的大括号中,就像循环的主体和条件的主体一样。

这里还有一个额外的东西,你可能会从我们迄今为止的工作中认出它:showWelcome 后面的**()。早在我们学习字符串的时候,我就说过 count 后面没有(),但upercased()**却有。

现在你知道为什么了:这些 () 是在函数中使用的。它们不仅在创建函数时使用(如上图所示),还在调用函数时使用(即要求 Swift 运行其代码时)。在我们的例子中,我们可以这样调用函数:

showWelcome()

这就是函数的调用位置,这是一个花哨的名字,意思是 “函数被调用的地方”。

那么,括号的实际作用是什么呢?这就是我们为函数添加配置选项的地方——我们可以传递数据,自定义函数的工作方式,从而使函数变得更加灵活。

举个例子,我们已经使用过这样的代码:

let number = 139

if number.isMultiple(of: 2) {
    print("Even")
} else {
    print("Odd")
}

isMultiple(of:) 是一个属于整数的函数。如果它不允许任何形式的自定义,那就没有意义了——它是什么的倍数?当然,苹果本可以让它成为类似 isOdd() 或 isEven() 的函数,这样它就不需要配置选项了,但通过编写 (of: 2),这个函数变得更加强大,因为现在我们可以检查 2、3、4、5、50 或任何其他数字的倍数。

同样,当我们掷虚拟骰子时,我们使用了这样的代码:

let roll = Int.random(in: 1...20)

同样,random() 是一个函数,**(in: 1...20)**部分标志着配置选项——如果没有它,我们就无法控制随机数的范围,这将大大降低其实用性。

我们可以在创建函数时,在括号中加入额外的代码,创建自己的函数,并对其进行配置。这个函数应该给定一个整数,比如 8,然后计算 1 到 12 的乘法表。

代码如下:

func printTimesTables(number: Int) {
    for i in 1...12 {
        print("\(i) x \(number) is \(i * number)")
    }
}

printTimesTables(number: 5)

注意到我在括号内输入的数字:Int 吗?这就是所谓的参数,也是我们的自定义点。我们的意思是,无论谁调用这个函数,都必须在这里传递一个整数,Swift 会强制执行。在函数内部,number 可以像其他常量一样使用,因此它会出现在 print() 调用中。

正如你所看到的,在调用 printTimesTables() 时,我们需要明确写入 number:5 ——我们需要将参数名称作为函数调用的一部分。这在其他语言中并不常见,但我认为这在 Swift 中非常有用,可以提醒我们每个参数的作用。

当你有多个参数时,参数命名就变得更加重要。例如,如果我们想自定义乘法表的高度,我们可以使用第二个参数设置范围的终点,就像这样:

func printTimesTables(number: Int, end: Int) {
    for i in 1...end {
        print("\(i) x \(number) is \(i * number)")
    }
}

printTimesTables(number: 5, end: 20)

这需要两个参数:一个叫 number 的整数和一个叫 end 的终点。在运行 printTablesTable() 时,这两个参数都需要特别命名,我希望你现在能明白为什么它们很重要——想象一下,如果我们的代码是这样的:

printTimesTables(5, 20)

你还记得哪个号码是哪个吗?可能记得 但六个月后你还记得吗?可能记不住。

现在,从技术上讲,我们给发送数据和接收数据起的名字略有不同,虽然很多人都忽略了这种区别,但我至少要让你意识到这一点,以免以后措手不及。

请看这段代码:

func printTimesTables(number: Int, end: Int) {

这里的 number 和 end 都是参数:它们是占位符名称,在调用函数时会被填入值,这样我们就有了函数内部这些值的名称。

现在看看这段代码:

printTimesTables(number: 5, end: 20)

在这里,5 和 20 是参数:它们是发送到函数中的实际值,用于填充数字和结尾。

因此,我们既有参数又有参数值:一个是占位符,另一个是实际值,所以如果你分不清楚,只需记住参数/占位符,参数值/实际值。

这种名称上的区别重要吗?并不重要,事实上,在 Swift 中你很快就会发现,这种区别会让人格外困惑,所以根本不值得考虑。

当你要求 Swift 调用函数时,都必须按照创建函数时列出的顺序传递参数。

因此,对于这段代码:

func printTimesTables(number: Int, end: Int) {

这不是有效代码,因为它将结束置于数字之前:

printTimesTables(end: 20, number: 5)

提示:在函数内创建的任何数据都会在函数结束时自动销毁。

函数中应包含哪些代码?

函数的设计目的是让我们可以轻松地重复使用代码,这意味着我们不必复制和粘贴代码来获得常见的行为。如果你愿意,可以很少使用函数,但老实说,没有必要——函数是帮助我们编写更清晰、更灵活代码的绝佳工具。

有三种情况你会想要创建自己的函数:

  1. 最常见的情况是,你需要在多个地方使用相同的功能。在这种情况下,使用函数意味着你可以修改一段代码,然后更新所有使用你的函数的地方。
  2. 函数对于分割代码也很有用。如果你有一个很长的函数,就很难跟上所有的内容,但如果你把它分成三到四个较小的函数,就会变得更容易跟上。
  3. 最后一个原因更为先进: Swift 允许我们在现有函数的基础上构建新函数,这是一种称为函数组合的技术。通过将工作分割成多个小函数,函数组合让我们可以通过以各种方式组合这些小函数来构建大函数,这有点像乐高积木。

函数应接收多少个参数?

乍一看,这个问题就像是 “一段字符串有多长?”。也就是说,这个问题没有真正的、硬性的答案——一个函数可以不带参数,也可以带 20 个参数。

这当然没错,但当一个函数需要很多参数时——也许是 6 个或更多,但这是非常主观的——你就需要开始问这个函数是否做了太多的工作。

  • 它需要所有六个参数吗?
  • 能否将该函数拆分成参数更少的小函数?
  • 是否应该以某种方式对这些参数进行分组?

我们稍后将讨论解决这个问题的一些技巧,但这里有一个重要的教训:这就是所谓的 “代码气味”——代码中的某些东西暗示着我们构建程序的方式存在潜在问题。

如何从函数中返回值

我们已经了解了如何创建函数以及如何为函数添加参数,但函数通常也会返回数据——它们会执行一些计算,然后将计算结果返回给调用者。

Swift 内置了大量这样的函数,而苹果的框架中还有数以万计的函数。例如,我们的 playground 顶部一直有 import Cocoa,其中包括各种数学函数,如用于计算数字平方根的 sqrt()

**sqrt()**函数只接受一个参数,即我们要计算平方根的数字,然后它会继续计算,并将平方根返回给我们。

例如,我们可以这样写:

let root = sqrt(169)
print(root)

如果想从函数中返回自己的值,需要做两件事:

  1. 在函数的开头括号前写一个箭头,然后写一个数据类型,告诉 Swift 将返回哪种数据。
  2. 使用 return 关键字发回数据。

例如,也许你想在程序的不同部分掷骰子,但与其总是强制使用 6 面骰子掷骰子,不如将其作为一个函数:

func rollDice() -> Int {
    return Int.random(in: 1...6)
}

let result = rollDice()
print(result)

因此,函数必须返回一个整数,而实际值会通过**return**关键字发送回来。

使用这种方法,你可以在程序的许多地方调用 rollDice(),它们都将使用 6 面骰子。但如果将来您决定使用 20 面骰子,您只需修改这一个函数,就可以更新程序的其他部分。

重要提示:当你说你的函数将返回一个 Int 时,Swift 会确保它始终返回一个 Int —— 你不能忘记返回一个值,否则你的代码将无法构建。

让我们举一个更复杂的例子:两个字符串是否包含相同的字母,无论它们的顺序如何?这个函数应该接受两个字符串参数,如果它们的字母相同,则返回 true – 因此,”abc “和 “cab “应该返回 true,因为它们都包含一个 “a”、一个 “b”和一个 “c”。

实际上,你已经知道了足够多的知识来自己解决这个问题,但是你已经学了太多,可能已经忘记了让这个任务变得如此简单的一件事:如果你在任何字符串上调用 sorted(),你会得到一个新的字符串,其中所有字母都是按字母顺序排列的。因此,如果对两个字符串都调用 sorted(),就可以使用 == 来比较它们的字母是否相同。

请继续尝试自己编写函数。同样,如果你写得很吃力,也不用担心——这对你来说都是新知识,努力记住新知识也是学习过程的一部分。

这里有一个示例解决方案:

func areLettersIdentical(string1: String, string2: String) -> Bool {
    let first = string1.sorted()
    let second = string2.sorted()
    return first == second
}

让我们来分析一下:

  1. 它创建了一个名为 areLettersIdentical() 的新函数。
  2. 该函数接受两个字符串参数:string1 和 string2
  3. 函数说它返回一个 Bool,所以在某些时候我们必须总是返回 true 或 false
  4. 在函数体中,我们对两个字符串进行排序,然后使用 == 对字符串进行比较——如果相同,则返回 true,否则返回 false

这段代码对 string1 和 string2 进行了排序,并将它们的排序值赋值给新常量 first 和 second。然而,这并不是必需的,我们可以跳过这些临时常量,直接比较 sorted() 的结果,就像下面这样:

func areLettersIdentical(string1: String, string2: String) -> Bool {
    return string1.sorted() == string2.sorted()
}

代码少了,但我们可以做得更好。我们告诉 Swift 这个函数必须返回布尔值,因为函数中只有一行代码,所以 Swift 知道这一行必须返回数据。正因为如此,当函数只有一行代码时,我们可以完全删除 return 关键字,就像这样:

func areLettersIdentical(string1: String, string2: String) -> Bool {
    string1.sorted() == string2.sorted()
}

我们还可以对 rollDice() 函数进行同样的处理:

func rollDice() -> Int {
    Int.random(in: 1...6)
}

请记住,只有当你的函数只包含一行代码时,这才有效,尤其是这行代码必须实际返回您承诺返回的数据。

让我们举第三个例子。还记得学校里的勾股定理吗?它指出,如果一个三角形内有一个直角,那么可以通过将三角形的其他两边平方、相加,然后计算结果的平方根,来计算斜边的长度。

你已经学会了如何使用 sqrt(),所以我们可以建立一个 pythagoras() 函数,接受两个Double数并返回另一个Double数:

func pythagoras(a: Double, b: Double) -> Double {
    let input = a * a + b * b
    let root = sqrt(input)
    return root
}

let c = pythagoras(a: 3, b: 4)
print(c)

因此,这是一个名为 pythagoras() 的函数,它接受两个 Double 参数,并返回另一个 Double。该函数将 a 和 b 相乘,然后将其传入 sqrt(),并返回结果。

这个函数也可以精简为一行,去掉返回关键字——试试看吧。

这就是解决方案:

func pythagoras(a: Double, b: Double) -> Double {
    sqrt(a * a + b * b)
}

在我们继续之前,我还想提到最后一件事:如果函数不返回值,你仍然可以使用 return 本身来强制函数提前退出。例如,你可能要检查输入是否与预期一致,如果不一致,你就想在继续之前立即退出函数。

Swift 函数中何时不需要返回关键字?

在 Swift 中,我们使用 return 关键字从函数发回值,但有一种特殊情况不需要:当我们的函数只包含一个表达式时。

现在,”表达式 “并不是经常使用的一个词,但在这里理解它很重要。当我们编写程序时,我们会这样做:

5 + 8

或这样:

greet("Alan")

这些代码行会被解析为单个值:5 + 8 会被解析为 13,而 greet(“Alan”) 可能会返回字符串 “Hi, Alan!”。

甚至一些较长的代码也会被解析为一个值。例如,如果我们有这样三个布尔常量:

let isAdmin = true 
let isOwner = false
let isEditingEnabled = false

那么这行代码就会解析为一个单一的值:

isOwner == true && isEditingEnabled || isAdmin == true

这将变为 “true”,因为即使 isOwner 为假,isAdmin 为真,所以整个表达式变为 true。

因此,我们编写的许多代码都可以解析为单一值。但也有很多代码无法解析为单一值。例如,这里的值是什么:

let name = "Otis"

是的,这创建了一个常量,但它本身并不成为一个值——我们不能写 return let name = "Otis"

同样,我们也可以这样写条件:

if name == "Maeve" {
    print("Hello, Maeve!")
}

这也不能成为一个单一的值,因为其中有一个条件。

现在,所有这些都很重要,因为这些分隔符都有名字:当我们的代码可以归结为一个单一的值,如 true、false、”Hello”或 19 时,我们称之为表达式。表达式是可以赋值给变量或使用 print() 打印的东西。另一方面,当我们执行创建变量、启动循环或检查条件等操作时,我们称之为语句。

这一切之所以重要,是因为当我们的函数中只有一个表达式时,Swift 允许我们跳过使用 return 关键字。因此,这两个函数做的是同一件事:

func doMath() -> Int {
    return 5 + 5
}

func doMoreMath() -> Int {
    5 + 5
}

记住,里面的表达式可以很长,但不能包含任何语句——不能有循环、条件、新变量等。

现在,你可能会认为这样做毫无意义,因为你总是会使用 return 关键字。但是,这个功能在 SwiftUI 中非常常用,所以值得牢记。

在结束之前,我还想提一件事。你已经看到我们如何在表达式中使用 +、&& 和 || 等运算符,因为它们仍然解析为单个值。那么,三元操作符在这里也可以使用,事实上,这也是它的主要用例:当你想使用单个表达式,但又不想使用完整的 if 时。

为了演示这一点,请看下面的函数:

func greet(name: String) -> String {
    if name == "Taylor Swift" {
        return "Oh wow!"
    } else {
        return "Hello, \(name)"
    }
}

如果我们想删除其中的返回语句,就无法写出这样的内容:

func greet(name: String) -> String {
    if name == "Taylor Swift" {
        "Oh wow!"
    } else {
        "Hello, \(name)"
    }
}

这是不允许的,因为我们有实际的语句 —— if 和 else。

不过,我们可以这样使用三元运算符:

func greet(name: String) -> String {
    name == "Taylor Swift" ? "Oh wow!" : "Hello, \(name)"
}

这是一个单一表达式。如果 name 等于 “Taylor Swift”,那么它的解析结果是这样的:

  • Swift 将检查 name 是否为 Taylor Swift。
  • 是,所以 name ==”Taylor Swift “为真。
  • 三元运算符会意识到它的条件现在为真,所以它会选择 “哦哇”,而不是 “你好,(名字)”。

三元运算符的真正优势在于可以将条件功能放入一行代码中。而且,由于 SwiftUI 经常使用单表达式函数,这意味着三元运算符在 SwiftUI 中也会经常使用。

如何从函数中返回多个值

当你想从函数中返回单个值时,你可以在函数的开头括号前写入箭头和数据类型,就像这样:

func isUppercase(string: String) -> Bool {
    string == string.uppercased()
}

将字符串与它本身的大写版本进行比较。如果字符串已经完全大写,则不会有任何变化,两个字符串将完全相同,否则它们将不同,== 将返回 false。

如果想从函数中返回两个或多个值,可以使用数组。例如,这里有一个返回用户详细信息的数组:

func getUser() -> [String] {
    ["Taylor", "Swift"]
}

let user = getUser()
print("Name: \(user[0]) \(user[1])") 

这就有问题了,因为我们很难记住 user[0] 和 user[1] 是什么,而且如果我们调整了数组中的数据,那么 user[0] 和 user[1] 最终可能会变成别的东西,或者根本就不存在。

我们可以使用字典来代替,但这也有自己的问题:

func getUser() -> [String: String] {
    [
        "firstName": "Taylor",
        "lastName": "Swift"
    ]
}

let user = getUser()
print("Name: \(user["firstName", default: "Anonymous"]) \(user["lastName", default: "Anonymous"])")

是的,我们现在已经为用户数据的各个部分赋予了有意义的名称,但看看 print() 的调用——虽然我们知道 name 和 lastName 都会存在,但我们仍然需要提供默认值,以防情况与我们的预期不同。

这两种解决方案都很糟糕,但 Swift 有一种元组(Tuple)形式的解决方案。与数组、字典和集合一样,元组允许我们将多个数据放入一个变量中,但与其他选项不同的是,元组有固定的大小,并且可以有多种数据类型。

下面是我们的函数返回元组时的样子:

func getUser() -> (firstName: String, lastName: String) {
    (firstName: "Taylor", lastName: "Swift")
}

let user = getUser()
print("Name: \(user.firstName) \(user.lastName)")

让我们分析一下…

  1. 现在我们的返回类型是**(firstName: String, lastName: String)**,这是一个包含两个字符串的元组。
  2. 元组中的每个字符串都有一个名称。这些名称没有加引号:它们是元组中每个项目的特定名称,与字典中的任意键不同。
  3. 在函数中,我们会发回一个元组,其中包含我们承诺的所有元素,并附加了名称:firstName 将被设置为 “Taylor”,等等。
  4. 当我们调用 getUser() 时,我们可以使用键名读取元组的值:firstName、lastName 等。

我知道元组看起来与字典非常相似,但它们是不同的:

  1. 访问字典中的值时,Swift 无法提前知道它们是否存在。是的,我们知道 user[“firstName”] 会存在,但 Swift 无法确定,所以我们需要提供一个默认值。
  2. 当您访问元组中的值时,Swift 会提前知道它是否可用,因为元组表示它将可用。
  3. 我们使用 user.firstName 访问值:它不是字符串,因此也不会出现错别字。
  4. 除了 “firstName “之外,我们的字典可能还包含数百个其他值,但我们的元组不可能——我们必须列出它将包含的所有值,因此它保证包含所有值,而不包含其他值。

因此,与字典相比,元组有一个关键优势:我们可以精确指定哪些值会存在以及它们的类型,而字典可能包含也可能不包含我们所要求的值。

使用元组时,还有三件事必须知道。

首先,如果要从函数中返回一个元组,Swift 已经知道元组中每个项的名称,因此在使用 return 时不需要重复这些名称。因此,这段代码的作用与我们之前的元组相同:

func getUser() -> (firstName: String, lastName: String) {
    ("Taylor", "Swift")
}

其次,有时你会发现你得到的元组中的元素没有名称。遇到这种情况,你可以使用从 0 开始的数字索引访问元组的元素,就像这样:

func getUser() -> (String, String) {
    ("Taylor", "Swift")
}

let user = getUser()
print("Name: \(user.0) \(user.1)")

这些数字索引也适用于有命名元素的元组,但我一直认为使用命名更可取。

最后,如果函数返回的是一个元组,你可以根据需要将元组拆分成单个值。

要理解我的意思,请先看一下这段代码:

func getUser() -> (firstName: String, lastName: String) {
    (firstName: "Taylor", lastName: "Swift")
}

let user = getUser()
let firstName = user.firstName
let lastName = user.lastName

print("Name: \(firstName) \(lastName)")

这又回到了 getUser() 的命名版本,当元组出现时,我们会将其中的元素复制到单个内容中,然后再使用它们。这没有什么新意,我们只是稍微移动了一下数据而已。

不过,我们可以跳过第一步,将 getUser() 的返回值拆分成两个独立的常量,而不是先将元组赋值给 user,然后再从其中复制单个值:

let (firstName, lastName) = getUser()
print("Name: \(firstName) \(lastName)")

这种语法一开始可能会让你头疼,但它实际上只是我们之前所使用的语法的简写:将 getUser() 返回的两个元素的元组转换成两个独立的常量。

事实上,如果你不需要元组中的所有值,你可以进一步使用 _ 来告诉 Swift 忽略元组中的这一部分:

let (firstName, _) = getUser()
print("Name: \(firstName)")

在 Swift 中,什么时候应该使用数组、集合或元组?

由于数组、集合和元组的工作方式略有不同,因此必须确保选择正确的方式,以便正确、高效地存储数据。

请记住:数组保持顺序,可以有重复数据;集合是无序的,不能有重复数据;而元组内部有固定数量、固定类型的值。

所以:

  • 如果你想存储游戏字典中所有单词的列表,那么这个列表没有重复,顺序也不重要,所以你会选择集合。
  • 如果要存储用户阅读过的所有文章,如果顺序无关紧要(如果只关心用户是否阅读过),则使用集合;如果顺序重要,则使用数组。
  • 如果要存储一个视频游戏的高分列表,则顺序很重要,而且可能包含重复内容(如果两个玩家获得了相同的分数),因此要使用数组。
  • 如果要存储待办事项列表中的项目,如果顺序是可预测的,则效果最佳,因此应使用数组。
  • 如果要保存两个字符串,或两个字符串和一个整数,或三个布尔值,或类似的内容,则应使用元组。

有关元组的一些例子

由多种数据组成,可以是不同的类型的数据。更简单的处理复合型数据,省去建立变量和命名的痛苦。

let fruits = ("orange", 10)
fruits.0 //调用时
fruits.1

let fruits = (name: "orange", price: 10)
fruits.name
fruits.price

可以结合别名使用:

typealias Human = (name: String, height: Double, hairColor: String)
//typealias是声明类型别名

例子1:

//例子
let girl = ("Amy", 155, "Gold")
print("This girl's name is \(girl.0), height is \(girl.1), hair's color is \(girl.2)")

例子2:

//例子2
typealias Human = (name: String, height: Int, hairColor: String)
let girl: Human = ("Amy", 155, "Gold")
print("Girl's name is \(girl.name), height is \(girl.height), hair color is \(girl.hairColor)")

//还可以反过来调用Tuple中的资料
let (name, height, hairColor) = girl
print(name) //显示Amy
print(height) //显示155
print(hairColor) //显示Gold
//如果只是想调用其中一个资料,可以将name、height或hairColor换成下划线"_"

例子3:

//例子3
typealias student = (name: String, 数学: Int, 英文: Int, 历史: Int, 国文: Int)
let studentA: student = ("小鸭", 93, 68, 77, 72)
let studentB: student = ("贝贝", 84, 89, 59, 72)
let totalA = studentA.数学 + studentA.历史 + studentA.国文 + studentA.英文
let totalB = studentB.数学 + studentB.历史 + studentB.国文 + studentB.英文
let (result, average) = totalA > totalB ? (studentA.name, totalA / 4) : (studentB.name, totalB / 4)
print("\(result)的平均分数较高,平均分数为\(average)分。")

如何自定义参数标签

你一定见过 Swift 开发人员喜欢给函数参数命名,因为这样可以让人更容易记住函数被调用时参数的作用。例如,我们可以编写一个函数来掷一定次数的骰子,使用参数来控制骰子的面数和掷的次数:

func rollDice(sides: Int, count: Int) -> [Int] {
    // Start with an empty array
    var rolls = [Int]()

    // Roll as many dice as needed
    for _ in 1...count {
        // Add each result to our array
        let roll = Int.random(in: 1...sides)
        rolls.append(roll)
    }

    // Send back all the rolls
    return rolls
}

let rolls = rollDice(sides: 6, count: 4)

即使你在六个月后再来看这段代码,我也确信 rollDice(sides: 6, count: 4) 的意思不言自明。 这种为外部使用的参数命名的方法对 Swift 来说是如此重要,以至于在确定调用哪个方法时,它实际上会使用这些名称。这与许多其他语言完全不同,但在 Swift 中却是完全正确的:

func hireEmployee(name: String) { }
func hireEmployee(title: String) { }
func hireEmployee(location: String) { }

是的,这些函数都叫 hireEmployee(),但当你调用它们时,Swift 会根据你提供的参数名知道你指的是哪个函数。为了区分不同的选项,文档中经常会提到每个函数都包含其参数,例如:hireEmployee(name:) 或 hireEmployee(title:)。 不过,有时这些参数名称并不那么有用,我想从两个方面来说明。 首先,回想一下之前学习的 hasPrefix() 函数:

let lyric = "I see a red door and I want it painted black"
print(lyric.hasPrefix("I see"))

当我们调用 hasPrefix() 时,我们会直接传入要检查的前缀 —— 我们不会说 hasPrefix(string:),或者更糟的是,hasPrefix(prefix:)。怎么会这样呢? 当我们为函数定义参数时,实际上可以添加两个名称:一个用于函数被调用的地方,另一个用于函数内部。hasPrefix() 利用这一点将 _ 指定为参数的外部名称,这是 Swift 表示 “忽略此参数”的方式,并导致该参数没有外部标签。 如果你觉得这样读起来更好,我们也可以在自己的函数中使用同样的技巧。例如,我们之前有这样一个函数:

func isUppercase(string: String) -> Bool {
    string == string.uppercased()
}

let string = "HELLO, WORLD"
let result = isUppercase(string: string)

你可能看了之后会觉得完全正确,但你也可能看了 string: string 之后会觉得重复很烦人。毕竟,除了字符串,我们还能传递什么呢? 如果我们在参数名前加上下划线,就可以像这样去掉外部参数标签:

func isUppercase(_ string: String) -> Bool {
    string == string.uppercased()
}

let string = "HELLO, WORLD"
let result = isUppercase(string)

这在 Swift 中经常使用,例如 append() 用于将项目添加到数组中,contains() 用于检查项目是否在数组中,在这两种情况下,不需要标签也能很明显地看出参数是什么。 外部参数名的第二个问题是它们不合适——你想要参数名,所以 _ 并不是一个好主意,但它们在函数的调用位置读起来并不自然。 举个例子,这是我们之前看过的另一个函数:

func printTimesTables(number: Int) {
    for i in 1...12 {
        print("\(i) x \(number) is \(i * number)")
    }
}

printTimesTables(number: 5)

这段代码是有效的 Swift 代码,我们可以不加改动。但调用时读起来并不顺畅:printTimesTables(number: 5)。这样会更好:

func printTimesTables(for: Int) {
    for i in 1...12 {
        print("\(i) x \(for) is \(i * for)")
    }
}

printTimesTables(for: 5)

这在调用位置上读起来要好得多——你可以大声说 “打印 5 的乘法表”,这样就说得通了。但现在我们遇到了无效的 Swift:虽然 for 是允许的,而且在调用位置读起来也很好,但在函数内部却是不允许的。 我们已经看到了如何在参数名前加上 _,这样就不需要编写外部参数名了。那么,另一种方法就是在这里写第二个名称:一个用于外部,一个用于内部。 如下所示:

func printTimesTables(for number: Int) {
    for i in 1...12 {
        print("\(i) x \(number) is \(i * number)")
    }
}

printTimesTables(for: 5)

其中有三点需要仔细研究:

  1. 我们写 for number: Int:外部名称是 for,内部名称是 number,类型是 Int。
  2. 调用函数时,我们使用外部名称作为参数:printTimesTables(for: 5)。
  3. 在函数内部,我们使用参数的内部名称:print(“\(i) x \(number) is \(i * number)”)。

因此,Swift 提供了两种重要的方法来控制参数名:我们可以使用 _ 作为外部参数名,这样它就不会被使用;或者在这里添加第二个参数名,这样我们就同时拥有了外部和内部参数名。 提示:前面提到,从技术上讲,传递给函数的值称为 “参数”,而函数内部接收的值称为参数值。这就是事情变得有点混乱的地方,因为现在我们的参数标签和参数名称并列在函数定义中。正如前面所说,将对这两种情况都使用 “参数”一词,当需要区分时,你会看到我使用 “外部参数名 “和 “内部参数名 “来区分它们。

何时省略参数标签?

如果我们在函数参数的外部标签中使用下划线,Swift 就会允许我们不使用该参数的任何名称。这在 Swift 开发的某些部分是非常常见的做法,尤其是在构建不使用 SwiftUI 的应用程序时。 跳过参数名的主要原因是,当你的函数名是一个动词,而第一个参数是动词所作用的名词时。例如:

  • 启用闹钟应该是 enable(alarm),而不是 enable(alarm: alarm)
  • 唱一首歌应该是 sing(song),而不是 sing(song: song)
  • 查找客户应使用 find(customer),而不是 find(user: customer)

当参数标签很可能与输入的名称相同时,这一点尤为重要:

  • 购买产品应使用 buy(toothbrush) 而不是 buy(item: toothbrush)
  • 问候一个人应使用 greet(taylor) 而不是 greet(person: taylor)
  • 阅读一本书应该是 read(book),而不是 read(book: book)

在 SwiftUI 出现之前,应用程序是使用苹果的 UIKit、AppKit 和 WatchKit 框架构建的,而这些框架是使用一种名为 Objective-C 的旧语言设计的。在这种语言中,函数的第一个参数总是不命名的,因此当你在 Swift 中使用这些框架时,你会看到很多函数的第一个参数标签都是下划线,以保持与 Objective-C 的互操作性。

好了,今天对函数就暂时学到这里,下一章,我们继续!

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注