<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Technology &#8211; BeautyLife Studio</title>
	<atom:link href="https://beautylife-studio.top/category/tech/feed/" rel="self" type="application/rss+xml" />
	<link>https://beautylife-studio.top</link>
	<description>It&#039;s a BeautyLife!</description>
	<lastBuildDate>Tue, 22 Apr 2025 05:10:36 +0000</lastBuildDate>
	<language>zh-Hans</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9</generator>

<image>
	<url>https://beautylife-studio.top/wp-content/uploads/2024/03/cropped-new-icon-32x32.png</url>
	<title>Technology &#8211; BeautyLife Studio</title>
	<link>https://beautylife-studio.top</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Learning Swift &#038; SwiftUI Chapter 15</title>
		<link>https://beautylife-studio.top/2025/tech/1774/</link>
					<comments>https://beautylife-studio.top/2025/tech/1774/#respond</comments>
		
		<dc:creator><![CDATA[Alan C.]]></dc:creator>
		<pubDate>Tue, 22 Apr 2025 05:10:36 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[Learning Swift & SwiftUI]]></category>
		<guid isPermaLink="false">https://beautylife-studio.top/?p=1774</guid>

					<description><![CDATA[Swift 复习]]></description>
										<content:encoded><![CDATA[
<p>经过前面大约14章的学习，我们其实已经掌握了Swift的绝大部份基础知识。接下来我们就可以开始主攻SwiftUI了，但在这之前，我想我们有必要再把我们学习过的Swift内容复习一遍，这样我们才能更好的开始SwiftUI。那，现在就让我们开始吧！</p>



<h2 class="wp-block-heading">创建常量(constants)和变量(variables)</h2>



<p>Swift 可以创建常量和变量，但通常更倾向于创建常量。</p>



<p>用它来创建和更改变量字符串：</p>



<pre class="wp-block-code"><code>var name = "Ted"
name = "Rebecca"
</code></pre>



<p>如果不想改变数值，可以使用常量来代替：</p>



<pre class="wp-block-code"><code>let user = "Daphne"
</code></pre>



<p>print() 函数有助于学习和调试，并能显示变量的一些信息：</p>



<pre class="wp-block-code"><code>print(user)
</code></pre>



<h2 class="wp-block-heading">字符串 (Strings)</h2>



<p>Swift 字符串以双引号开始和结束：</p>



<pre class="wp-block-code"><code>let actor = "Tom Cruise"
</code></pre>



<p>它们与表情符号也配合得很好：</p>



<pre class="wp-block-code"><code>let actor = "Tom Cruise 🏃‍♂️"
</code></pre>



<p>如果你希望在字符串中使用双引号，请在它们前面放置一个反斜杠：</p>



<pre class="wp-block-code"><code>let quote = "He tapped a sign saying \"Believe\" and walked away."
</code></pre>



<p>如果你想要一个跨越多行的字符串，请以三个双引号开头和结尾，如下所示：</p>



<pre class="wp-block-code"><code>let movie = """
A day in
the life of an
Apple engineer
"""
</code></pre>



<p>Swift为字符串提供了许多有用的属性和方法，包括**<code>.count</code>**来读取它有多少个字母：</p>



<pre class="wp-block-code"><code>print(actor.count)
</code></pre>



<p>还有**<code>hasPrefix()</code><strong>和</strong><code>hasSuffix()</code>**让我们知道字符串是以特定字母开头还是结尾：</p>



<pre class="wp-block-code"><code>print(quote.hasPrefix("He"))
print(quote.hasSuffix("Away."))
</code></pre>



<p>**提示：**在Swift中，字符串区分大小写，因此第二次检查将返回false。</p>



<h2 class="wp-block-heading">整数 (Int)</h2>



<p>Swift使用**<code>Int</code>**类型存储整数，该类型支持一系列标准数学运算符：</p>



<pre class="wp-block-code"><code>let score = 10
let higherScore = score + 10
let halvedScore = score / 2
</code></pre>



<p>它还支持修改原位变量的复合赋值运算符：</p>



<pre class="wp-block-code"><code>var counter = 10
counter += 5
</code></pre>



<p>整数自己具有一些有用的功能，例如**<code>isMultiple(of:)</code>**方法：</p>



<pre class="wp-block-code"><code>let number = 120
print(number.isMultiple(of: 3))
</code></pre>



<p>你还可以在特定范围内生成随机整数，如下：</p>



<pre class="wp-block-code"><code>let id = Int.random(in: 1...1000)
</code></pre>



<h2 class="wp-block-heading">小数 (Decimals)</h2>



<p>如果你创建一个具有小数点的数字，Swift会将其视为**<code>Double</code>**：</p>



<pre class="wp-block-code"><code>let score = 3.0
</code></pre>



<p>Swift认为**<code>Double</code><strong>是与</strong><code>Int</code>**完全不同的数据类型，不会让你将它们混合在一起。</p>



<h2 class="wp-block-heading">布尔值 (Booleans)</h2>



<p>Swift使用**<code>Bool</code>**类型来存储真或假：</p>



<pre class="wp-block-code"><code>let goodDogs = true
let gameOver = false
</code></pre>



<p>你可以通过调用其**<code>toggle()</code>**方法将布尔值从true翻转到false：</p>



<pre class="wp-block-code"><code>var isSaved = false
isSaved.toggle()
</code></pre>



<h2 class="wp-block-heading">连接字符串</h2>



<p>你可以使用<em>字符串插值</em>从其他数据中创建字符串：在字符串中写一个反斜杠，然后将变量或常量的名称放在括号中，如下所示：</p>



<pre class="wp-block-code"><code>let name = "Taylor"
let age = 26
let message = "I'm \(name) and I'm \(age) years old."
print(message)
</code></pre>



<p>当该代码运行时，它会打印“I’m Taylor and I’m 26 years old.”</p>



<h2 class="wp-block-heading">数组 (Arrays)</h2>



<p>你可以将项目分组到这样的数组中：</p>



<pre class="wp-block-code"><code>var colors = &#91;"Red", "Green", "Blue"]
let numbers = &#91;4, 8, 15, 16]
var readings = &#91;0.1, 0.5, 0.8]
</code></pre>



<p>每一个都包含不同类型的数据：一个字符串、一个整数和一个小数。当我们从数组中读取数据时，我们将得到适当的类型-<strong><code>String</code></strong>、<strong><code>Int</code>或<code>Double</code></strong>：</p>



<pre class="wp-block-code"><code>print(colors&#91;0])
print(readings&#91;2])
</code></pre>



<p>**提示：**确保项目存在于你请求的索引中，否则您的代码将崩溃——你的应用程序将停止工作。</p>



<p>如果你的数组是可变的，您可以使用**<code>append()</code>**添加新项目：</p>



<pre class="wp-block-code"><code>colors.append("Tartan")
</code></pre>



<p>你添加的数据类型必须与已经存在的数据类型相匹配。</p>



<p>数组具有有用的功能，例如**<code>.count</code><strong>来读取数组中有多少个项目，或者</strong><code>remove(at:)</code>**来删除特定索引中的一个项目：</p>



<pre class="wp-block-code"><code>colors.remove(at: 0)
print(colors.count)
</code></pre>



<p>你可以使用**<code>contains()</code>**来检查数组是否包含特定项目，如下所在：</p>



<pre class="wp-block-code"><code>print(colors.contains("Octarine"))
</code></pre>



<h2 class="wp-block-heading">字典 (Dictionaries)</h2>



<p>字典根据我们指定的键存储多个值。例如，我们可以创建一个字典来存储有关一个人的信息：</p>



<pre class="wp-block-code"><code>let employee = &#91;
    "name": "Taylor",
    "job": "Singer"
]
</code></pre>



<p>要从字典中读取数据，请使用创建时使用的相同键：</p>



<pre class="wp-block-code"><code>print(employee&#91;"name", default: "Unknown"])
print(employee&#91;"job", default: "Unknown"])
</code></pre>



<p>如果我们请求的密钥不存在，将使用**<code>default</code>**值。</p>



<h2 class="wp-block-heading">集合 (Sets)</h2>



<p>集合类似于数组，只是你不能添加重复的项目，而且它们不会按照特定顺序存储项目。</p>



<p>这就成了一组数字：</p>



<pre class="wp-block-code"><code>var numbers = Set(&#91;1, 1, 3, 5, 7])
print(numbers)
</code></pre>



<p>请记住，该集合将忽略重复值，并且不会记住数组中使用的顺序。</p>



<p>将项目添加到集合中是通过调用其**<code>insert()</code>**方法完成的，如下所示：</p>



<pre class="wp-block-code"><code>numbers.insert(10)
</code></pre>



<p>与数组相比，集合有一个很大的优势：在集合上使用**<code>contains()</code>**实际上是即时的，无论集合包含多少项——即使是包含10,000,000项的集合也会立即响应。</p>



<h2 class="wp-block-heading">枚举 (Enums)</h2>



<p>枚举是一组命名值，我们可以创建并使用它来使我们的代码更高效、更安全。例如，我们可以像这样对工作日进行枚举：</p>



<pre class="wp-block-code"><code>enum Weekday {
    case monday, tuesday, wednesday, thursday, friday
}
</code></pre>



<p>这称为新的枚举**<code>Weekday</code>**，并提供五个案例来处理五个工作日。</p>



<p>我们现在可以制作该枚举的实例，然后为其分配其他可能的案例：</p>



<pre class="wp-block-code"><code>var day = Weekday.monday
day = .friday
</code></pre>



<h2 class="wp-block-heading">类型注释 (Type annotations)</h2>



<p>你可以通过使用如下<em>类型注释</em>来尝试为新变量或常量强制特定类型：</p>



<pre class="wp-block-code"><code>var score: Double = 0
</code></pre>



<p>如果没有**<code>: Double</code><strong>部分，Swift会推断这是一个</strong><code>Int</code><strong>，但我们覆盖了它，并说它是一个</strong><code>Double</code>**。</p>



<p>以下是一些基于迄今为止涵盖的类型的类型注释：</p>



<pre class="wp-block-code"><code>let player: String = "Roy"
var luckyNumber: Int = 13
let pi: Double = 3.141
var isEnabled: Bool = true
var albums: Array&lt;String&gt; = &#91;"Red", "Fearless"]
var user: Dictionary&lt;String, String&gt; = &#91;"id": "@twostraws"]
var books: Set&lt;String&gt; = Set(&#91;"The Bluest Eye", "Foundation"])
</code></pre>



<p>数组和字典非常普遍，它们具有更易于编写的特殊语法：</p>



<pre class="wp-block-code"><code>var albums: &#91;String] = &#91;"Red", "Fearless"]
var user: &#91;String: String] = &#91;"id": "@twostraws"]
</code></pre>



<p>知道确切的事物类型对于创建空集合很重要。例如，两者都创建空字符串数组：</p>



<pre class="wp-block-code"><code>var teams: &#91;String] = &#91;String]()
var clues = &#91;String]()
</code></pre>



<p>枚举的值与枚举本身的类型相同，因此我们可以这样写：</p>



<pre class="wp-block-code"><code>enum UIStyle {
    case light, dark, system
}

var style: UIStyle = .light
</code></pre>



<h2 class="wp-block-heading">条件 (Conditions)</h2>



<p>使用**<code>if</code><strong>、</strong><code>else</code><strong>和</strong><code>else if</code>**语句来检查条件并酌情运行一些代码：</p>



<pre class="wp-block-code"><code>let age = 16

if age &lt; 12 {
    print("You can't vote")
} else if age &lt; 18 {
    print("You can vote soon.")
} else {
    print("You can vote now.")
}
</code></pre>



<p>我们可以使用**<code>&amp;&amp;</code>**来组合两个条件，只有当里面的两个部分为真时，整个条件才会为真：</p>



<pre class="wp-block-code"><code>let temp = 26

if temp &gt; 20 &amp;&amp; temp &lt; 30 {
    print("It's a nice day.")
}
</code></pre>



<p>或者，如果<em>任一</em>子条件为真，**<code>||</code>**将使条件为真。</p>



<h2 class="wp-block-heading">switch语句</h2>



<p>Swift允许我们使用**<code>switch</code><strong>/</strong><code>case</code>**语法根据多个条件检查值，如下所示：</p>



<pre class="wp-block-code"><code>enum Weather {
    case sun, rain, wind
}

let forecast = Weather.sun

switch forecast {
case .sun:
    print("A nice day.")
case .rain:
    print("Pack an umbrella.")
default:
    print("Should be okay.")
}
</code></pre>



<p>**<code>switch</code>**语句<em>必须</em>详尽无遗：必须处理所有可能的值，这样你就不会意外错过一个。</p>



<h2 class="wp-block-heading"><strong>三元条件运算符 (The ternary conditional operator)</strong></h2>



<p>三元运算符允许我们检查一个条件并返回两个值之一：如果条件为真，则返回一个值，如果条件为假，则返回第二个值：</p>



<pre class="wp-block-code"><code>let age = 18
let canVote = age &gt;= 18 ? "Yes" : "No"
</code></pre>



<p>当该代码运行时，**<code>canVote</code><strong>将设置为“是”，因为</strong><code>age</code>**设置为18岁。</p>



<h2 class="wp-block-heading">循环 (Loops)</h2>



<p>Swift的**<code>for</code>**循环为集合或自定义范围内的每个项目运行一些代码。例如：</p>



<pre class="wp-block-code"><code>let platforms = &#91;"iOS", "macOS", "tvOS", "watchOS"]

for os in platforms {
    print("Swift works on \(os).")
}
</code></pre>



<p>你还可以循环一系列数字：</p>



<pre class="wp-block-code"><code>for i in 1...12 {
    print("5 x \(i) is \(5 * i)")
}
</code></pre>



<p>**<code>1...12</code><strong>包含1到12（含）的值。如果您想排除最终数字，请使用</strong><code>..&lt;</code>**代替：</p>



<pre class="wp-block-code"><code>for i in 1..&lt;13 {
    print("5 x \(i) is \(5 * i)")
}
</code></pre>



<p><strong>提示：如果你不需要循环变量，请使用<code>_</code></strong>：</p>



<pre class="wp-block-code"><code>var lyric = "Haters gonna"

for _ in 1...5 {
    lyric += " hate"
}

print(lyric)
</code></pre>



<p>还有**<code>while</code>**循环，执行循环主体，直到条件为false，如下：</p>



<pre class="wp-block-code"><code>var count = 10

while count &gt; 0 {
    print("\(count)…")
    count -= 1
}

print("Go!")
</code></pre>



<p>你可以使用**<code>continue</code>**”跳过当前循环迭代，然后继续执行以下操作：</p>



<pre class="wp-block-code"><code>let files = &#91;"me.jpg", "work.txt", "sophie.jpg"]

for file in files {
    if file.hasSuffix(".jpg") == false {
        continue
    }

    print("Found picture: \(file)")
}
</code></pre>



<p>或者，使用**<code>break</code>**退出循环并跳过所有剩余的迭代。</p>



<h2 class="wp-block-heading">函数 (Functions)</h2>



<p>要创建新函数，请写上**<code>func</code>**然后加上函数名称，然后在括号内添加参数：</p>



<pre class="wp-block-code"><code>func printTimesTables(number: Int) {
    for i in 1...12 {
        print("\(i) x \(number) is \(i * number)")
    }
}

printTimesTables(number: 5)
</code></pre>



<p>我们需要在调用站点上写出**<code>number: 5</code>**，因为参数名称是函数调用的一部分。</p>



<p>要从函数中返回数据，请告诉Swift它是什么类型，然后使用**<code>return</code>**关键字将其发送回。例如，这返回一个掷骰子的结果：</p>



<pre class="wp-block-code"><code>func rollDice() -&gt; Int {
    return Int.random(in: 1...6)
}

let result = rollDice()
print(result)
</code></pre>



<p>如果你的函数只包含一行代码，您可以删除**<code>return</code>**关键字：</p>



<pre class="wp-block-code"><code>func rollDice() -&gt; Int {
    Int.random(in: 1...6)
}
</code></pre>



<h2 class="wp-block-heading">从函数中返回多个值</h2>



<p>元组存储了固定数量的特定类型的值，这是一种从函数返回多个值的便捷方法：</p>



<pre class="wp-block-code"><code>func getUser() -&gt; (firstName: String, lastName: String) {
    (firstName: "Taylor", lastName: "Swift")
}

let user = getUser()
print("Name: \(user.firstName) \(user.lastName)")
</code></pre>



<p>如果你不需要元组中的所有值，你可以重组元组，将其拆分成单个值，然后用”_”告诉 Swift 忽略某些值：</p>



<pre class="wp-block-code"><code>let (firstName, _) = getUser()
print("Name: \(firstName)")
</code></pre>



<h2 class="wp-block-heading">自定义参数标签 (Customizing parameter labels)</h2>



<p>如果你在调用函数时不想传递参数的名称，请在它前面放置下划线：</p>



<pre class="wp-block-code"><code>func isUppercase(_ string: String) -&gt; Bool {
    string == string.uppercased()
}

let string = "HELLO, WORLD"
let result = isUppercase(string)
</code></pre>



<p>另一种选择是在名字之前给它起另外一个名字：一个用于外部，一个用于内部：</p>



<pre class="wp-block-code"><code>func printTimesTables(for number: Int) {
    for i in 1...12 {
        print("\(i) x \(number) is \(i * number)")
    }
}

printTimesTables(for: 5)
</code></pre>



<p>在该代码中，**<code>for</code><strong>外部使用，</strong><code>number</code>**在内部使用。</p>



<h2 class="wp-block-heading">为参数提供默认值</h2>



<p>我们可以通过在类型后写一个等值来提供默认参数值，然后提供一个值，如下所示：</p>



<pre class="wp-block-code"><code>func greet(_ person: String, formal: Bool = false) {
	if formal {
        print("Welcome, \(person)!")
	  } else {
        print("Hi, \(person)!")
    }
}
</code></pre>



<p>现在我们可以用两种方式调用**<code>greet()</code>**）：</p>



<pre class="wp-block-code"><code>greet("Tim", formal: true)
greet("Taylor")
</code></pre>



<h2 class="wp-block-heading">处理函数中的错误</h2>



<p>要处理函数中的错误，你需要告诉Swift哪些错误可能发生，编写一个可以抛出错误的函数，然后调用它并处理任何问题。</p>



<p>首先，定义可能出现的错误：</p>



<pre class="wp-block-code"><code>enum PasswordError: Error {
    case short, obvious
}
</code></pre>



<p>接下来，编写一个可以抛出错误的函数。这是通过将**<code>throws</code><strong>加入函数类型，然后使用</strong><code>throw</code>**来触发特定错误来完成的：</p>



<pre class="wp-block-code"><code>func checkPassword(_ password: String) throws -&gt; String {
    if password.count &lt; 5 {
        throw PasswordError.short
    }

    if password == "12345" {
        throw PasswordError.obvious
    }

    if password.count &lt; 10 {
        return "OK"
    } else {
        return "Good"
    }
}
</code></pre>



<p>现在通过启动**<code>do</code><strong>块来调用抛出函数，使用</strong><code>try</code>**调用函数，然后捕捉发生的错误：</p>



<pre class="wp-block-code"><code>let string = "12345"

do {
    let result = try checkPassword(string)
    print("Rating: \(result)")
} catch PasswordError.obvious {
    print("I have the same combination on my luggage!")
} catch {
    print("There was an error.")
}
</code></pre>



<p>当涉及到捕捉错误时，你必须始终有一个可以处理各种错误的**<code>catch</code>**块。</p>



<h2 class="wp-block-heading">闭包 (Closures)</h2>



<p>你可以将函数直接分配给像这样的常量或变量：</p>



<pre class="wp-block-code"><code>let sayHello = {
    print("Hi there!")
}

sayHello()
</code></pre>



<p>在该代码中，**<code>sayHello</code>**是一个闭包——一块代码，我们可以随时传递和调用。如果你希望闭包接受参数，它们必须写在大括号内：</p>



<pre class="wp-block-code"><code>let sayHello = { (name: String) -&gt; String in
    "Hi \(name)!"
}
</code></pre>



<p>**<code>in</code>**用于标记参数的结束并返回类型——之后的所有内容都是闭包本身的主体。</p>



<p>闭包在Swift中被广泛使用。例如，有一个名为**<code>filter()</code>**的数组方法，它通过测试运行数组的所有元素，任何返回测试为true的元素都会在新数组中返回。</p>



<p>我们可以使用闭包提供该测试，因此我们可以过滤数组，仅包含以T开头的名称：</p>



<pre class="wp-block-code"><code>let team = &#91;"Gloria", "Suzanne", "Tiffany", "Tasha"]

let onlyT = team.filter({ (name: String) -&gt; Bool in
    return name.hasPrefix("T")
})
</code></pre>



<p>在闭包中，我们列出了传递给我们的参数**<code>filter()</code><strong>），这是数组中的字符串。我们还说，我们的闭包返回一个布尔值，然后使用</strong><code>in</code>**来标记闭包代码的开头——之后，其他一切都是正常的函数代码。</p>



<h2 class="wp-block-heading">尾部闭包(Trailing closures)和速记语法(shorthand syntax)</h2>



<p>Swift有一些诀窍，使结尾更容易阅读。以下是一些过滤数组的代码，只包含以“T”开头的名称：</p>



<pre class="wp-block-code"><code>let team = &#91;"Gloria", "Suzanne", "Tiffany", "Tasha"]

let onlyT = team.filter({ (name: String) -&gt; Bool in
    return name.hasPrefix("T")
})

print(onlyT)
</code></pre>



<p>你可以看到，闭包的主体只有一行代码，因此我们可以去掉return：</p>



<pre class="wp-block-code"><code>let onlyT = team.filter({ (name: String) -&gt; Bool in
    name.hasPrefix("T")
})
</code></pre>



<p>**<code>filter()</code>**必须给出一个从其数组中接受一个项目的函数，如果它应该在返回的数组中，则返回true。</p>



<p>由于我们传递的函数<em>必须有</em>这样的行为，所以我们不需要在闭包中指定类型。所以，我们可以重写代码成这样：</p>



<pre class="wp-block-code"><code>let onlyT = team.filter({ name in
    name.hasPrefix("T")
})
</code></pre>



<p>我们可以进一步使用称为<em>尾随闭包语法</em>的特殊语法，如下所示：</p>



<pre class="wp-block-code"><code>let onlyT = team.filter { name in
    name.hasPrefix("T")
}
</code></pre>



<p>最后，Swift可以为我们提供简短的参数名称，因此我们甚至不再写入**<code>name in</code><strong>，而是依赖为我们提供的特殊命名值：</strong><code>$0</code>**：</p>



<pre class="wp-block-code"><code>let onlyT = team.filter {
    $0.hasPrefix("T")
}
</code></pre>



<h2 class="wp-block-heading">结构体 (Structs)</h2>



<p>结构允许我们创建自己的自定义数据类型，并完成它们自己的属性和方法：</p>



<pre class="wp-block-code"><code>struct Album {
    let title: String
    let artist: String
    var isReleased = true

    func printSummary() {
        print("\(title) by \(artist)")
    }
}

let red = Album(title: "Red", artist: "Taylor Swift")
print(red.title)
red.printSummary()
</code></pre>



<p>当我们创建结构实例时，我们使用<em>初始化器</em>来创建结构——Swift允许我们像函数一样对待结构，传递其每个属性的参数。它根据结构的属性无声地生成此<em>成员初始化器</em>。</p>



<p>如果你想让结构的方法更改其属性之一，请将其标记为<em>突变</em>：</p>



<pre class="wp-block-code"><code>mutating func removeFromSale() {
    isReleased = false
}
</code></pre>



<h2 class="wp-block-heading">计算属性 (Computed properties)</h2>



<p>计算属性在每次访问时都会计算其值。例如，我们可以编写一个**<code>Employee</code>**结构，跟踪该员工的剩余假期：</p>



<pre class="wp-block-code"><code>struct Employee {
    let name: String
    var vacationAllocated = 14
    var vacationTaken = 0

    var vacationRemaining: Int {
        vacationAllocated - vacationTaken
    }
}
</code></pre>



<p>为了能够写入给**<code>vacationRemaining</code>**，我们需要同时提供<em>获取</em>器(getter)和<em>设置器</em>(setter)：</p>



<pre class="wp-block-code"><code>var vacationRemaining: Int {
    get {
        vacationAllocated - vacationTaken
    }

    set {
        vacationAllocated = vacationTaken + newValue
    }
}
</code></pre>



<p>**<code>newValue</code>**由Swift提供，并存储用户分配给属性的任何值。</p>



<h2 class="wp-block-heading">属性观察器 (Property observers)</h2>



<p>属性观察器是属性更改时运行的代码片段：**<code>didSet</code><strong>在属性刚刚更改时运行，</strong><code>willSet</code>**在属性更改<em>之前</em>运行。</p>



<p>我们可以通过在分数更改时让**<code>Game</code><strong>结构打印消息来演示</strong><code>didSet</code>**：</p>



<pre class="wp-block-code"><code>struct Game {
    var score = 0 {
        didSet {
            print("Score is now \(score)")
        }
    }
}

var game = Game()
game.score += 10
game.score -= 3
</code></pre>



<h2 class="wp-block-heading">自定义初始化器 (Custom initializers)</h2>



<p>初始化器是一种特殊函数，用于准备要使用的新结构体实例，确保所有属性都有初始值。</p>



<p>Swift 会根据结构体的属性生成一个初始化器，但你也可以创建自己的初始化器：</p>



<pre class="wp-block-code"><code>struct Player {
    let name: String
    let number: Int

    init(name: String) {
        self.name = name
        number = Int.random(in: 1...99)
    }
}
</code></pre>



<p>重要：初始化程序前面没有&nbsp;<strong><code>func</code></strong>，也不会明确返回值。</p>



<h2 class="wp-block-heading">门禁控制（Access control)</h2>



<p>Swift 为结构体内部的访问控制提供了多种选项，但最常见的有四种：</p>



<ul class="wp-block-list">
<li>使用 private 表示 “不要让结构体之外的任何东西使用它”。</li>



<li>使用 private(set) 表示 “结构体之外的任何内容都可以读取此内容，但不要让它们更改”。</li>



<li>使用 fileprivate 表示 “不允许当前文件以外的任何内容使用此文件”。</li>



<li>使用 public 表示 “允许任何人在任何地方使用它”。</li>
</ul>



<p>例如：</p>



<pre class="wp-block-code"><code>struct BankAccount {
    private(set) var funds = 0

    mutating func deposit(amount: Int) {
        funds += amount
    }

    mutating func withdraw(amount: Int) -&gt; Bool {
        if funds &gt; amount {
            funds -= amount
            return true
        } else {
            return false
        }
    }
}
</code></pre>



<p>因为我们使用了&nbsp;<strong><code>private(set)</code></strong>，所以从结构体外部读取资金是可以的，但写入是不可能的。</p>



<h2 class="wp-block-heading">静态属性和方法（Static properties and methods)</h2>



<p>Swift 支持静态属性和方法，允许你将属性或方法直接添加到结构体本身，而不是结构体的某个实例：</p>



<pre class="wp-block-code"><code>struct AppData {
    static let version = "1.3 beta 2"
    static let settings = "settings.json"
}
</code></pre>



<p>使用这种方法，我们可以在任何需要检查或显示应用程序版本号的地方读取&nbsp;<strong><code>AppData.version</code></strong>。</p>



<h2 class="wp-block-heading">类（Classes）</h2>



<p>类允许我们创建自定义数据类型，与结构体有五个不同之处。</p>



<p>第一个不同点是，我们可以通过继承其他类的功能来创建类：</p>



<pre class="wp-block-code"><code>class Employee {
    let hours: Int

    init(hours: Int) {
        self.hours = hours
    }

    func printSummary() {
        print("I work \(hours) hours a day.")
    }
}

class Developer: Employee {
    func work() {
        print("I'm coding for \(hours) hours.")
    }
}

let novall = Developer(hours: 8)
novall.work()
novall.printSummary()
</code></pre>



<p>如果子类要更改父类的方法，必须使用覆盖（<strong><code>override</code></strong>）：</p>



<pre class="wp-block-code"><code>override func printSummary() {
    print("I spend \(hours) hours a day searching Stack Overflow.")
}
</code></pre>



<p>第二个不同点是，类的初始化器更加复杂。这里有很多复杂之处，但有三个关键点：</p>



<ol class="wp-block-list">
<li>Swift 不会为类生成成员初始化器。</li>



<li>如果子类有自定义初始化器，它必须在设置完自己的属性后调用父类的初始化器。</li>



<li>如果子类没有任何初始化器，它将自动继承父类的初始化器。</li>
</ol>



<p>例如：</p>



<pre class="wp-block-code"><code>class Vehicle {
    let isElectric: Bool

    init(isElectric: Bool) {
        self.isElectric = isElectric
    }
}

class Car: Vehicle {
    let isConvertible: Bool

    init(isElectric: Bool, isConvertible: Bool) {
        self.isConvertible = isConvertible
        super.init(isElectric: isElectric)
    }
}
</code></pre>



<p><strong><code>super</code></strong>&nbsp;允许我们调用属于父类的方法，例如父类的初始化器。</p>



<p>第三个区别是，类实例的所有副本共享数据，这意味着对其中一个副本所做的更改会自动更改其他副本。</p>



<p>例如：</p>



<pre class="wp-block-code"><code>class Singer {
    var name = "Adele"
}

var singer1 = Singer()
var singer2 = singer1
singer2.name = "Justin"
print(singer1.name)  
print(singer2.name)
</code></pre>



<p>这将打印出两个相同的 “Justin”——尽管我们只更改了其中一个，但另一个也发生了变化。相比之下，结构体副本不会共享数据。</p>



<p>第四个区别是，类可以有一个去初始化器，当对象的最后一个引用被销毁时，去初始化器会被调用。</p>



<p>因此，我们可以创建一个类，在创建和销毁时打印一条信息：</p>



<pre class="wp-block-code"><code>class User {
    let id: Int

    init(id: Int) {
        self.id = id
        print("User \(id): I'm alive!")
    }

    deinit {
        print("User \(id): I'm dead!")
    }
}

for i in 1...3 {
    let user = User(id: i)
    print("User \(user.id): I'm in control!")
}
</code></pre>



<p>最后一个区别是，即使类本身不变，我们也可以通过类来更改变量属性：</p>



<pre class="wp-block-code"><code>class User {
    var name = "Paul"
}

let user = User()
user.name = "Taylor"
print(user.name)
</code></pre>



<p>因此，类在使用改变数据的方法时不需要使用**<code>mutating</code>**关键字。</p>



<h2 class="wp-block-heading">协议（<strong>Protocols）</strong></h2>



<p>协议定义了我们期望数据类型支持的功能，Swift 确保我们的代码遵循这些规则。</p>



<p>例如，我们可以这样定义**<code>Vehicle</code>**协议：</p>



<pre class="wp-block-code"><code>protocol Vehicle {
    func estimateTime(for distance: Int) -&gt; Int
    func travel(distance: Int)
}
</code></pre>



<p>这列出了该协议工作所需的方法，但不包含任何代码——我们只指定了方法名称、参数和返回类型。</p>



<p>一旦有了协议，就可以通过实现所需的功能来使数据类型符合协议。例如，我们可以创建一个符合**<code>Vehicle</code>**协议的&nbsp;<code><strong>Car</strong></code>&nbsp;结构：</p>



<pre class="wp-block-code"><code>struct Car: Vehicle {
    func estimateTime(for distance: Int) -&gt; Int {
        distance / 50
    }

    func travel(distance: Int) {
        print("I'm driving \(distance)km.")
    }
}
</code></pre>



<p><strong><code>Vehicle</code></strong>&nbsp;中列出的所有方法必须完全存在于&nbsp;<strong><code>Car</code></strong>&nbsp;中，并且具有相同的名称、参数和返回类型。</p>



<p>现在，你可以编写一个接受任何符合&nbsp;<strong><code>Vehicle</code></strong>&nbsp;类型的函数，因为 Swift 知道它同时实现了&nbsp;<strong><code>estimateTime()</code></strong>&nbsp;和&nbsp;<strong><code>travel()</code></strong>：</p>



<pre class="wp-block-code"><code>func commute(distance: Int, using vehicle: Vehicle) {
    if vehicle.estimateTime(for: distance) &gt; 100 {
        print("Too slow!")
    } else {
        vehicle.travel(distance: distance)
    }
}

let car = Car()
commute(distance: 100, using: car)
</code></pre>



<p>协议也可以要求属性，因此我们可以要求车辆有多少个座位和目前有多少乘客的属性：</p>



<pre class="wp-block-code"><code>protocol Vehicle {
    var name: String { get }
    var currentPassengers: Int { get set }
    func estimateTime(for distance: Int) -&gt; Int
    func travel(distance: Int)
}
</code></pre>



<p>这就增加了两个属性：一个是标有 **<code>get</code>**的属性，可能是常量或计算属性；另一个是标有&nbsp;<code><strong>get set</strong></code>&nbsp;的属性，可能是变量或带有 getter 和 setter 的计算属性。</p>



<p>现在，所有符合要求的类型都必须添加这两个属性的实现，比如**<code>Car</code>**的实现：</p>



<pre class="wp-block-code"><code>let name = "Car"
var currentPassengers = 1
</code></pre>



<p>提示：你可以根据需要遵守任意多个协议，只需用逗号分隔列出即可。</p>



<h2 class="wp-block-heading">扩展（<strong>Extensions）</strong></h2>



<p>扩展可以让我们为任何类型添加功能。例如，Swift 的字符串有一个用于修剪空白和新行的方法，但它非常长，因此我们可以将它变成一个扩展：</p>



<pre class="wp-block-code"><code>extension String {
    func trimmed() -&gt; String {
        self.trimmingCharacters(in: .whitespacesAndNewlines)
    }
}

var quote = "   The truth is rarely pure and never simple   "
let trimmed = quote.trimmed()
</code></pre>



<p>如果你想直接更改一个值，而不是返回一个新值，请像这样将方法标记为突变：</p>



<pre class="wp-block-code"><code>extension String {
    mutating func trim() {
        self = self.trimmed()
    }
}

quote.trim()
</code></pre>



<p>扩展还可以为类型添加计算属性，比如这个：</p>



<pre class="wp-block-code"><code>extension String {
    var lines: &#91;String] {
        self.components(separatedBy: .newlines)
    }
}
</code></pre>



<p><strong><code>components(separatedBy:)</code></strong>&nbsp;方法使用我们选择的边界（在本例中是新行）将字符串分割成字符串数组。</p>



<p>现在，我们可以在所有字符串中使用该属性：</p>



<pre class="wp-block-code"><code>let lyrics = """
But I keep cruising
Can't stop, won't stop moving
"""

print(lyrics.lines.count)
</code></pre>



<h2 class="wp-block-heading">协议扩展（<strong>Protocol extensions）</strong></h2>



<p>协议扩展扩展了整个协议，添加了计算属性和方法实现，因此任何符合该协议的类型都能获得它们。</p>



<p>例如，数组（Array）、字典（Dictionary）和集合（Set）都符合**<code>Collection</code>**协议，因此我们可以像这样为它们添加计算属性：</p>



<pre class="wp-block-code"><code>extension Collection {
    var isNotEmpty: Bool {
        isEmpty == false
    }
}
</code></pre>



<p>现在我们可以派上用场了：</p>



<pre class="wp-block-code"><code>let guests = &#91;"Mario", "Luigi", "Peach"]

if guests.isNotEmpty {
    print("Guest count: \(guests.count)")
}
</code></pre>



<p>这种方法意味着我们可以在协议中列出所需的方法，然后在协议扩展中添加这些方法的默认实现。然后，所有符合要求的类型都可以使用这些默认实现，或根据需要提供自己的实现。</p>



<h2 class="wp-block-heading">可选项（<strong>Optionals）</strong></h2>



<p>可选项表示没有数据，例如，可选项可以区分数值为 0 的整数和没有数值的整数。</p>



<p>要了解可选项的作用，请看这段代码：</p>



<pre class="wp-block-code"><code>let opposites = &#91;
    "Mario": "Wario",
    "Luigi": "Waluigi"
]

let peachOpposite = opposites&#91;"Peach"]
</code></pre>



<p>它试图读取连接到键 “Peach”的值，而这个键并不存在，所以它不可能是一个普通字符串。Swift 的解决方案被称为可选项，即可能存在也可能不存在的数据。</p>



<p>一个可选字符串可能有一个字符串在里面等着我们，也可能什么都没有——一个叫做 nil 的特殊值，意思是 “无值”。任何类型的数据都可以是可选的，包括 Int、Double 和 Bool，以及枚举、结构体和类的实例。</p>



<p>Swift 不会让我们直接使用可选数据，因为它可能是空的。这意味着我们需要解包可选数据才能使用它——我们需要查看其内部是否存在值，如果存在，则取出并使用它。</p>



<p>Swift 提供了几种解包可选项的方法，但你最常见的方法是这样的：</p>



<pre class="wp-block-code"><code>if let marioOpposite = opposites&#91;"Mario"] {
    print("Mario's opposite is \(marioOpposite)")
}
</code></pre>



<p>这将从字典中读取可选值，如果可选值内有字符串，则将其解包——字符串将被放入&nbsp;<strong><code>marioOpposite</code></strong>&nbsp;常量中，不再是可选值。由于我们已经解开了可选值，因此条件成功，**<code>print()</code>**代码也就运行了。</p>



<h2 class="wp-block-heading">用 guard 解包可选项</h2>



<p>Swift 还有第二种解包可选项的方法，称为&nbsp;<strong><code>guard let</code></strong>，它与&nbsp;<strong><code>if let</code></strong>&nbsp;非常相似，只是相反：如果可选项有值，<strong><code>if let</code></strong>&nbsp;会运行括号内的代码；如果可选项没有值，<strong><code>guard let</code></strong>&nbsp;会运行代码。</p>



<p>它看起来像这样：</p>



<pre class="wp-block-code"><code>func printSquare(of number: Int?) {
    guard let number = number else {
        print("Missing input")
        return
    }

    print("\(number) x \(number) is \(number * number)")
}
</code></pre>



<p>如果使用&nbsp;<strong><code>guard</code></strong>&nbsp;检查函数的输入是否有效，Swift 要求在检查失败时使用&nbsp;<strong><code>return</code></strong>。但是，如果你正在解包的可选项内部有一个值，你可以在&nbsp;<strong><code>guard</code></strong>&nbsp;代码结束后使用它。</p>



<p>提示：你可以在任何条件下使用 guard，包括不解除可选项的条件。</p>



<h2 class="wp-block-heading">空聚合（<strong>Nil coalescing）</strong></h2>



<p>Swift 还有第三种解包可选项的方法，称为空聚合操作符（nil coalescing operator），它可以解包可选项，并在可选项为空时提供默认值：</p>



<pre class="wp-block-code"><code>let tvShows = &#91;"Archer", "Babylon 5", "Ted Lasso"]
let favorite = tvShows.randomElement() ?? "None"
</code></pre>



<p>在创建可选项的许多地方，空聚合操作符都很有用。例如，从字符串创建整数时会返回一个可选的&nbsp;<strong><code>Int?</code></strong>&nbsp;在这里，我们可以使用空聚合提供一个默认值：</p>



<pre class="wp-block-code"><code>let input = ""
let number = Int(input) ?? 0
print(number)
</code></pre>



<h2 class="wp-block-heading">可选项链（<strong>Optional chaining）</strong></h2>



<p>可选项链读取可选项内的可选项，就像这样：</p>



<pre class="wp-block-code"><code>let names = &#91;"Arya", "Bran", "Robb", "Sansa"]
let chosen = names.randomElement()?.uppercased()
print("Next in line: \(chosen ?? "No one")")
</code></pre>



<p>可选项链在第 2 行：一个问号，后面跟着更多代码。它允许我们说：“如果可选项里面有一个值，那么就把它拆开&#8230;&#8230; ”并添加更多代码。在我们的例子中，我们说的是 “如果我们从数组中得到了一个随机元素，就把它大写”。</p>



<h2 class="wp-block-heading">可选项try?（Optional try?）</h2>



<p>当调用一个可能会出错的函数时，我们可以使用&nbsp;<strong><code>try?</code></strong>&nbsp;将其结果转换为一个可选项，在成功时包含一个值，反之则为&nbsp;<strong><code>nil</code></strong>。</p>



<p>如下所示：</p>



<pre class="wp-block-code"><code>enum UserError: Error {
    case badID, networkFailed
}

func getUser(id: Int) throws -&gt; String {
    throw UserError.networkFailed
}

if let user = try? getUser(id: 23) {
    print("User: \(user)")
}
</code></pre>



<p><strong><code>getUser()</code></strong>&nbsp;函数总是会抛出&nbsp;<strong><code>networkFailed</code></strong>，但我们并不关心抛出的是什么，我们只关心调用是否发回了用户。</p>



<h2 class="wp-block-heading">总结</h2>



<p>至此，我们一起学习了 Swift 语言的大部分基础知识，但实际上我们只是触及了这门语言的皮毛。幸运的是，我们所学到的知识已经足以让你使用 Swift 和 SwiftUI 构建一些出色的软件。</p>



<p>接下来，我们将开始学习SwiftUI来创建一些简单的App，如果你有兴趣请继续关注我的教程。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://beautylife-studio.top/2025/tech/1774/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Learning Swift &#038; SwiftUI Chapter 14</title>
		<link>https://beautylife-studio.top/2024/tech/1538/</link>
					<comments>https://beautylife-studio.top/2024/tech/1538/#respond</comments>
		
		<dc:creator><![CDATA[Alan C.]]></dc:creator>
		<pubDate>Tue, 24 Sep 2024 10:05:57 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[Learning Swift & SwiftUI]]></category>
		<guid isPermaLink="false">https://beautylife-studio.top/?p=1538</guid>

					<description><![CDATA[可选项 Optionals]]></description>
										<content:encoded><![CDATA[
<p>也许大家知道空引用（Null references）这个概念，它表示变量没有值。今天我们将一起学习Swift中解决空引用的方法，即Optional &#8211; 可选项。可选项是一种非常重要的语言特性，但它可能会让你觉得非常困惑，如果第一遍没有完全明白，我想我们可能需要重复多学几遍，但请不要觉得沮丧，因为初学者对于这个内容都会如此。</p>



<p>本质上讲，可选项试图回答一个问题：“如果我们的变量没有值怎么办？”Swift希望确保我们所有的程序都尽可能安全，因此它有一些非常具体也非常重要的技术来处理这种情况。而可选项即是其中之一。</p>



<p>今天的内容有五大部份，可能有些多，但请务必仔细跟我一起来！</p>



<h1 class="wp-block-heading">如何使用可选项处理缺失数据</h1>



<p>Swift 喜欢可预测性，这意味着它尽可能鼓励我们编写安全的代码，并按照我们期望的方式运行。我们已经见过了抛掷函数（throwing function），但 Swift 还有另一种确保可预测性的重要方法，叫做可选项（optionals）——这个词的意思是 “这个东西可能有值，也可能没有”。</p>



<p>要了解可选项的作用，请看下面的代码：</p>



<pre class="wp-block-code"><code>let opposites = &#91;
    "Mario": "Wario",
    "Luigi": "Waluigi"
]

let peachOpposite = opposites&#91;"Peach"]
</code></pre>



<p>在这里，我们创建了一个有两个键的 [String: String] 字典： 马里奥和路易吉。然后，我们尝试读取连接到键 “Peach ”的值，但这个键并不存在，我们也没有提供一个默认值来发送回缺失的数据。</p>



<p>代码运行后，peachOpposite 会是什么呢？这是一个 [String: String] 字典，这意味着键是字符串，值也是字符串，但我们刚刚试图读取一个不存在的键——如果不存在一个值，那么返回一个字符串是没有意义的。</p>



<p>Swift 的解决方案称为可选项（optionals），即可能存在也可能不存在的数据。它们主要通过在数据类型后添加问号来表示，因此在本例中，peachOpposite 将是一个String?，而不是一个String。</p>



<p>可选项就像一个盒子，里面可能有东西，也可能没有。因此，String? 表示里面可能有一个字符串在等着我们，也可能什么都没有——一个叫做 nil 的特殊值，意思是 “无值”。任何类型的数据都可以是可选的，包括 Int、Double 和 Bool，以及枚举、结构和类的实例。</p>



<p>你可能会想 &#8220;那么&#8230;&#8230;这里到底发生了什么变化？以前我们有一个String，现在我们有一个String？，但这对我们编写的代码有什么实际改变呢？</p>



<p>这就是关键所在： Swift 希望我们的代码是可预测的，这意味着它不会让我们使用可能不存在的数据。对于可选项，这意味着我们需要解开可选项才能使用它——我们需要查看框内是否有值，如果有，就取出并使用它。</p>



<p>Swift 提供了两种主要的可选项解包方法，但你最常见的是下面这种：</p>



<pre class="wp-block-code"><code>if let marioOpposite = opposites&#91;"Mario"] {
    print("Mario's opposite is \(marioOpposite)")
}
</code></pre>



<p>if let 语法在 Swift 中非常常见，它将创建条件 (if) 和创建常量 (let) 结合在一起。结合起来，它可以做三件事：</p>



<ol class="wp-block-list">
<li>从字典中读取可选值。</li>



<li>如果可选值中包含字符串，它就会被解包——这意味着里面的字符串会被放入 marioOpposite 常量中。</li>



<li>条件成功执行——我们成功解包了可选值——因此条件的正文将被运行。</li>
</ol>



<p>只有在可选项中包含值时，才会运行条件的主体。当然，如果你想添加一个 else 代码块，也是可以的，因为这只是一个普通的条件，所以这样的代码也是可以的：</p>



<pre class="wp-block-code"><code>var username: String? = nil

if let unwrappedName = username {
    print("We got a user: \(unwrappedName)")
} else {
    print("The optional was empty.")
}
</code></pre>



<p>把可选项想象成薛定谔的数据类型：框内可能有一个值，也可能没有，但唯一的办法就是检查。</p>



<p>到目前为止，这可能看起来比较学术化，但可选项确实是帮助我们开发出更好软件的关键。你看，可选项意味着数据可能存在也可能不存在，同样，非可选项——常规字符串、整数等意味着数据必须可用。</p>



<p>想想看：如果我们有一个非可选项 Int，这就意味着里面肯定有一个数字，永远都有。它可能是 100 万或 0，但它仍然是一个数字，而且保证存在。相比之下，可选 Int 设置为 nil 则没有任何值——它不是 0 或任何其他数字，而是什么都没有。</p>



<p>同样，如果我们有一个非可选字符串，这意味着其中肯定有一个字符串：它可能是 “Hello ”或空字符串，但这两种字符串都不同于设置为 nil 的可选字符串。</p>



<p>如果需要，每种数据类型都可以是可选的，包括数组和字典等集合。同样，一个整数数组可能包含一个或多个数字，也可能根本不包含数字，但这两种情况都不同于设置为 nil 的可选数组。</p>



<p>需要明确的是，设置为 nil 的可选整数与持有 0 的非可选整数不同，设置为 nil 的可选字符串与当前设置为空字符串的非可选字符串不同，而设置为 nil 的可选数组与当前没有任何项的非可选数组不同——我们谈论的是没有任何数据，不管是空的还是其他的。</p>



<p>如果你尝试将一个可选整数传递到一个需要非可选整数的函数中，你就可以看到这一点，就像下面这样：</p>



<pre class="wp-block-code"><code>func square(number: Int) -&gt; Int {
    number * number
}

var number: Int? = nil
print(square(number: number))
</code></pre>



<p>Swift 会拒绝编译该代码，因为可选整数需要解包——我们不能在需要非可选值的地方使用可选值，因为如果里面没有值，我们就会遇到问题。</p>



<p>因此，要使用可选值，我们必须先像这样对其进行解包：</p>



<pre class="wp-block-code"><code>if let unwrappedNumber = number {
    print(square(number: unwrappedNumber))
}
</code></pre>



<p>在结束之前，我还想提最后一件事：在解包可选项时，将其解包为同名常量是很常见的做法。这在 Swift 中是完全允许的，这意味着我们不必一直将常量命名为 unwrappedNumber 或类似名称。</p>



<p>使用这种方法，我们可以将之前的代码重写成这样：</p>



<pre class="wp-block-code"><code>if let number = number {
    print(square(number: number))
}
</code></pre>



<p>刚读到这种风格时，你会感到有些困惑，因为现在感觉就像量子物理学——数字真的可以同时可选和不可选吗？当然不是。</p>



<p>这里发生的事情是，我们临时创建了第二个同名常量，它只能在条件的主体中使用。这就是所谓的阴影（shadow），主要用于可选的解包，如上图所示。</p>



<p>因此，在条件的主体中，我们可以使用一个未封装的值——一个真正的 Int 而不是一个可选的 Int？- 但在外部，我们仍然可以使用可选值。</p>



<h2 class="wp-block-heading">为什么Swift要有可选项？</h2>



<p>Swift 的可选项是其最强大的功能之一，同时也是最令人困惑的功能之一。可选项的核心功能很简单：它们允许我们表示某些数据的缺失——字符串不仅是空的，而且是不存在的。</p>



<p>在 Swift 中，任何数据类型都可以是可选的：</p>



<ul class="wp-block-list">
<li>一个整数可能是 0、-1、500 或任何其他范围的数字。</li>



<li>可选整数可能是所有常规整数值，但也可能是 nil —— 它可能不存在。</li>



<li>字符串可能是 “Hello”，可能是莎士比亚全集，也可能是“”——空字符串。</li>



<li>可选字符串可以是任何常规字符串值，也可以是 nil。</li>



<li>自定义 User 结构可以包含描述用户的各种属性。</li>



<li>可选的 User 结构可以包含所有这些相同的属性，也可以根本不存在。</li>
</ul>



<p>区分 “它可能是该类型的任何可能值”和 “它可能是 nil ”是理解可选项的关键，但有时这并不容易。</p>



<p>例如，想想布尔值：布尔值可以是 true 或 false。这意味着一个可选的布尔值可以是 true、false 或两者都不是——它可能什么都不是。这有点难以理解，因为在任何时候，某些东西肯定总是真或假的。</p>



<p>那么，请回答我：我喜欢巧克力吗？除非你是我的朋友，或者在微博上非常关注我，否则你无法确定——你不能肯定地说 “真（我喜欢巧克力）”或 “假（我不喜欢巧克力）”，因为你根本不知道。当然，你可以问我并找出答案，但在问我之前，唯一安全的答案就是 “我不知道”。</p>



<p>如果你想到空字符串“”，这也会让人有点困惑。该字符串不包含任何内容，但这与 nil 并不相同——空字符串仍然是一个字符串。</p>



<p>在学习过程中，可选项可能会让人感觉非常痛苦——你可能会认为 Swift 不需要它们，你可能会认为它们只会碍事，你可能会在每次不得不使用它们的时候磨牙。但请相信我：几个月后，它们就会变得非常有意义，你会怀疑没有它们自己是怎么活下来的！</p>



<h2 class="wp-block-heading">为什么Swift会让我们解包可选项？</h2>



<p>Swift 的可选项可以存储一个值，如 5 或 “Hello”，也可以什么都不存储。正如你可能想象的那样，只有当数字确实存在时，才有可能尝试将两个数字相加，这就是为什么 Swift 不允许我们尝试使用可选项的值，除非我们将它们解包——除非我们查看可选项内部，检查其中是否确实有一个值，然后将值取出来。</p>



<p>在 Swift 中，有几种方法可以做到这一点，但最常见的一种是 if let，就像下面这样：</p>



<pre class="wp-block-code"><code>func getUsername() -> String? {
    "Taylor"
}

if let username = getUsername() {
    print("Username is \(username)")
} else {
    print("No username")
}
</code></pre>



<p>getUsername() 函数返回一个可选字符串，这意味着它可能是一个字符串，也可能是 nil。为了便于理解，我让它始终返回一个值，但这并不改变 Swift 的想法——它仍然是一个可选字符串。</p>



<p>if let 这一行结合了很多功能：</p>



<ol class="wp-block-list">
<li>它调用了 getUsername() 函数。</li>



<li>从那里接收可选字符串。</li>



<li>它会查看可选字符串内部是否有值。</li>



<li>如果确实有一个值（是 “Taylor”），该值将从可选字符串中取出并放入一个新的用户名常量中。</li>



<li>然后，条件被视为 true，并打印出 “用户名是 Taylor”。</li>
</ol>



<p>因此，if let 是一种非常简洁的处理可选项的方法，可以一次性检查并提取所有值。</p>



<p>事实上，它是如此简洁，甚至为我们提供了一个小捷径。与其这样写：</p>



<pre class="wp-block-code"><code>if let number = number {
    print(square(number: number))
}
</code></pre>



<p>实际上我们可以这样写：</p>



<pre class="wp-block-code"><code>if let number {
    print(square(number: number))
}
</code></pre>



<p>它的作用完全相同——创建一个数字的阴影副本，只在该条件的正文中进行解包，只是减少了一些重复。</p>



<p>可选项最重要的一个特性是，如果不先解包，Swift 不会让我们使用它们。这为我们所有的应用程序提供了巨大的保护，因为它杜绝了不确定性：当你递给一个字符串时，你知道它是一个有效的字符串；当你调用一个返回整数的函数时，你知道它可以立即安全使用。当你在代码中使用可选项时，Swift 会确保你正确处理它们——检查并解包它们，而不是将不安全的值与已知的安全数据混合在一起。</p>



<h1 class="wp-block-heading">如何使用guard解包可选项</h1>



<p>我们已经见识过 Swift 如何使用 if let 来解包可选项，这也是使用可选项最常见的方法。但还有第二种方法可以做同样的事情，而且几乎同样常见：guard let。</p>



<p>它看起来像这样：</p>



<pre class="wp-block-code"><code>func printSquare(of number: Int?) {
    guard let number = number else {
        print("Missing input")
        return
    }

    print("\(number) x \(number) is \(number * number)")
}
</code></pre>



<p>与 if let 类似，guard let 也会检查可选项中是否有值，如果有，它就会获取该值并将其放入我们选择的常量中。</p>



<p>不过，它这样做的方式会把事情弄反：</p>



<pre class="wp-block-code"><code>var myVar: Int? = 3

if let unwrapped = myVar {
    print("Run if myVar has a value inside")
}

guard let unwrapped = myVar else {
    print("Run if myVar doesn't have a value inside")
}
</code></pre>



<p>因此，如果可选项有值，if let 运行括号内的代码；如果可选项没有值，guard let 运行括号内的代码。这就解释了代码中 else 的用法： “检查我们是否能解开可选项，如果不能，那么&#8230;&#8230;”</p>



<p>我知道这听起来差别不大，但却有着重要的影响。要知道，guard 提供的功能是检查我们的程序状态是否符合我们的预期，如果不符合，就跳出——例如，退出当前函数。</p>



<p>这有时被称为提前返回：我们在函数启动时检查函数的所有输入是否有效，如果有任何输入无效，我们就运行一些代码，然后直接退出。如果所有检查都通过了，我们的函数就能按预期运行。</p>



<p>guard 正是为这种编程风格而设计的，事实上它有两个帮助：</p>



<ol class="wp-block-list">
<li>如果使用 guard 来检查函数的输入是否有效，如果检查失败，Swift 总是会要求你使用 return。</li>



<li>如果检查通过，并且正在解包的可选项内部有一个值，则可以在 guard 代码结束后使用该值。</li>
</ol>



<p>如果你看看前面的 printSquare() 函数，就会明白这两点的作用：</p>



<pre class="wp-block-code"><code>func printSquare(of number: Int?) {
    guard let number = number else {
        print("Missing input")

        // 1: We *must* exit the function here
        return
    }

    // 2: `number` is still available outside of `guard`
    print("\(number) x \(number) is \(number * number)")
}
</code></pre>



<p>因此：使用 if let 来解包可选项，以便以某种方式对其进行处理；使用 guard let 来确保可选项中包含某些内容，否则就退出。</p>



<p>提示：你可以在任何条件下使用 guard，包括不解包可选项的条件。例如，你可以使用 guard someArray.isEmpty else { return }。</p>



<h2 class="wp-block-heading">何时使用 guard let 而不是 if let</h2>



<p>Swift 为我们提供了一种 if let 的替代方法，叫做 guard let，它也会在可选项包含值的情况下对其进行解包，但工作原理略有不同：guard let 的设计目的是在检查失败时退出当前函数、循环或条件，因此使用它解包的任何值都会在检查后继续存在。</p>



<p>为了说明两者的区别，下面是一个以可选整数形式返回生命意义的函数：</p>



<pre class="wp-block-code"><code>func getMeaningOfLife() -&gt; Int? {
    42
}
</code></pre>



<p>下面是该函数在另一个名为 printMeaningOfLife() 的函数中的应用：</p>



<pre class="wp-block-code"><code>func printMeaningOfLife() {
    if let name = getMeaningOfLife() {
        print(name)
    }
}
</code></pre>



<p>如果使用 if let，getMeaningOfLife() 的结果只有在返回整数而不是 nil 时才会被打印出来。</p>



<p>如果我们使用 guard let 来编写，结果会是这样的：</p>



<pre class="wp-block-code"><code>func printMeaningOfLife() {
    guard let name = getMeaningOfLife() else {
        return
    }

    print(name)
}
</code></pre>



<p>是的，有点长，但有两件重要的事情发生了变化：</p>



<ol class="wp-block-list">
<li>它让我们专注于 “幸福之路”——当一切都按计划进行时，函数的行为，也就是打印生命的意义。</li>



<li>guard 要求我们在使用函数时退出当前作用域，这意味着如果函数失败，我们必须从函数中返回。这不是可选的： 如果不返回，Swift 不会编译我们的代码。</li>
</ol>



<p>在方法的开头使用一个或多个 guard 是很常见的，因为它用于验证某些条件是否正确。这使得我们的代码更容易阅读，而不是先检查一个条件，然后运行一些代码，再检查另一个条件，然后运行一些不同的代码。</p>



<p>因此，如果只是想解开一些可选项，可以使用 if let，但如果是在继续之前专门检查条件是否正确，则更倾向于使用 guard let。</p>



<h1 class="wp-block-heading">如何使用 nil聚合（nil coalescing）解包可选项</h1>



<p>等等&#8230;&#8230;Swift 还有第三种解包可选项的方法？没错！它被称为nil聚合操作符，可以让我们解除对可选项的包裹，并在可选项为空的情况下提供一个默认值。</p>



<p>让我们倒退一下：</p>



<pre class="wp-block-code"><code>let captains = &#91;
    "Enterprise": "Picard",
    "Voyager": "Janeway",
    "Defiant": "Sisko"
]

let new = captains&#91;"Serenity"]
</code></pre>



<p>这将在我们的captains字典中读取一个不存在的键，这意味着 new 将是一个可选字符串，并将其设置为 nil。</p>



<p>通过 nil 聚合操作符（写成 ??），我们可以为任何可选项提供一个默认值，就像这样：</p>



<pre class="wp-block-code"><code>let new = captains&#91;"Serenity"] ?? "N/A"
</code></pre>



<p>这将从captains字典中读取值，并尝试将其解包。如果可选项中有一个值，它就会被送回并存储在 new 中，但如果没有，就会使用 “N/A ”来代替。</p>



<p>这就意味着，无论可选项包含什么（值或 nil），new 的最终结果都将是一个真正的字符串，而不是一个可选项。这可能是 captains 值中的字符串，也可能是 “N/A”。</p>



<p>我知道你在想什么：难道我们不能在从字典读取数据时指定一个默认值吗？如果你这么想，那就完全正确了：</p>



<pre class="wp-block-code"><code>let new = captains&#91;"Serenity", default: "N/A"]
</code></pre>



<p>结果完全相同，这可能会让人觉得 nil 聚合操作符毫无意义。然而，nil 聚合操作符不仅适用于字典，还适用于任何可选项。</p>



<p>例如，数组上的 randomElement() 方法会从数组中随机返回一个项目，但它返回的是一个可选项，因为你可能会在一个空数组上调用它。因此，我们可以使用 nil coalescing 来提供默认值：</p>



<pre class="wp-block-code"><code>let tvShows = &#91;"Archer", "Babylon 5", "Ted Lasso"]
let favorite = tvShows.randomElement() ?? "None"
</code></pre>



<p>或者，你有一个带有可选属性的结构，希望在缺少该属性时提供一个合理的默认值：</p>



<pre class="wp-block-code"><code>struct Book {
    let title: String
    let author: String?
}

let book = Book(title: "Beowulf", author: nil)
let author = book.author ?? "Anonymous"
print(author)
</code></pre>



<p>甚至在从字符串创建整数时也很有用，因为转换可能失败——你可能提供了一个无效的整数，如 “Hello”。在这里，我们可以使用 nil coalescing 来提供一个默认值，就像这样：</p>



<pre class="wp-block-code"><code>let input = ""
let number = Int(input) ?? 0
print(number)
</code></pre>



<p>正如你所看到的，nil 聚合操作符在任何情况下都很有用，只要你有一个可选项，并想使用其中的值，或者在缺少该值时提供一个默认值。</p>



<h2 class="wp-block-heading">什么时候应该在 Swift 中使用 nil 聚合？</h2>



<p>Nil coalescing 可以让我们尝试解开可选项，但如果可选项包含 nil，则会提供一个默认值。这在 Swift 中非常有用，因为虽然可选项是一个很棒的功能，但通常最好是非可选项，即一个真正的字符串，而不是一个 “可能是字符串，可能是 nil ”的字符串。</p>



<p>例如，如果你正在开发一个聊天应用程序，并希望加载用户保存的任何信息草稿，你可能会写这样的代码：</p>



<pre class="wp-block-code"><code>let savedData = loadSavedMessage() ?? ""
</code></pre>



<p>因此，如果 loadSavedMessage() 返回一个内部包含字符串的可选项，它就会被解包并放入 savedData。但如果可选项为 nil，那么 Swift 会将 savedData 设置为空字符串。无论如何，savedData 最终都将是String，而不是String?。</p>



<p>如果你愿意，可以使用链式 nil coalescing，不过我认为这种方法并不常见。因此，如果你愿意，这种代码是有效的：</p>



<pre class="wp-block-code"><code>let savedData = first() ?? second() ?? ""
</code></pre>



<p>这将尝试运行 first()，如果返回 nil，则尝试运行 second()，如果返回 nil，则使用空字符串。</p>



<p>请记住，读取字典键值总是会返回一个可选项，因此你可能需要在此处使用 nil coalescing 来确保返回一个非可选项：</p>



<pre class="wp-block-code"><code>let scores = &#91;"Picard": 800, "Data": 7000, "Troi": 900]
let crusherScore = scores&#91;"Crusher"] ?? 0
</code></pre>



<p>这绝对是一个品味问题，但字典提供了一种稍有不同的方法，让我们可以指定键未找到时的默认值：</p>



<pre class="wp-block-code"><code>let crusherScore = scores&#91;"Crusher", default: 0]
</code></pre>



<p>你可以选择自己喜欢的方式——在读取字典值时，两者并无本质区别。</p>



<h1 class="wp-block-heading">如何使用可选项链处理多个可选项</h1>



<p>可选链是一种简化的语法，用于读取可选项内部的可选项。这听起来可能是你很少会用到的东西，但如果我给你举个例子，你就会明白为什么它很有用了。</p>



<p>请看这段代码：</p>



<pre class="wp-block-code"><code>let names = &#91;"Arya", "Bran", "Robb", "Sansa"]

let chosen = names.randomElement()?.uppercased() ?? "No one"
print("Next in line: \(chosen)")
</code></pre>



<p>这同时使用了两个可选项：你已经看到了 nil 聚合运算符是如何在可选项为空时帮助提供默认值的，但在此之前，你看到了可选链，在这里我们有一个问号，后面跟着更多的代码。</p>



<p>通过可选链，我们可以说 “如果可选项内部有一个值，那么就把它拆开&#8230;&#8230;”，然后我们可以添加更多代码。在我们的例子中，我们说的是 “如果我们设法从数组中获取了一个随机元素，那么就把它大写”。请记住，randomElement() 返回的是一个可选项，因为数组可能是空的。</p>



<p>可选项链的神奇之处在于，如果可选项为空，它不会做任何事情——它只会返回之前的可选项，仍然是空的。这意味着可选链的返回值始终是一个可选项，这就是为什么我们仍然需要 nil coalescing 来提供一个默认值。</p>



<p>可选链的长度可以随心所欲，只要有任何部分返回 nil，代码的其余部分就会被忽略并返回 nil。</p>



<p>举个例子来说明可选链的作用：我们想根据作者姓名按字母顺序排列书籍。如果我们将其分解，那么：</p>



<ul class="wp-block-list">
<li>我们有一个图书结构体的可选实例——我们可能有一本书要排序，也可能没有。</li>



<li>这本书可能有作者，也可能是匿名的。</li>



<li>如果有作者字符串，它可能是空字符串，也可能是文本，因此我们不能总是依赖于第一个字母。</li>



<li>如果第一个字母存在，请确保它是大写字母，这样，名字小写的作者（如 bell hooks）就能正确排序。</li>
</ul>



<p>如下所示：</p>



<pre class="wp-block-code"><code>struct Book {
    let title: String
    let author: String?
}

var book: Book? = nil
let author = book?.author?.first?.uppercased() ?? "A"
print(author)
</code></pre>



<p>因此，它的意思是 “如果我们有一本书，这本书有一个作者，而作者有一个首字母，那么就把它大写并送回，否则就送回 A”。</p>



<p>诚然，你不可能在可选项上钻研那么深，但我希望你能看到语法的简短是多么令人赏心悦目！</p>



<h2 class="wp-block-heading">为什么可选项链如此重要？</h2>



<p>Swift 的可选项链让我们可以在一行代码中挖掘多层可选项，如果其中任何一层为空，那么整行代码都会变为空。</p>



<p>举个简单的例子，我们可能有一个姓名列表，并希望根据姓氏的第一个字母找到他们的位置：</p>



<pre class="wp-block-code"><code>let names = &#91;"Vincent": "van Gogh", "Pablo": "Picasso", "Claude": "Monet"]
let surnameLetter = names&#91;"Vincent"]?.first?.uppercased()
</code></pre>



<p>在这里，由于 names[“Vincent”] 可能不存在，我们对字典值使用了可选项链，而在读取姓氏的第一个字符时，由于字符串可能是空的，我们又使用了可选链。</p>



<p>可选链是 nil coalescing 的好帮手，因为它允许你挖掘层层可选项，同时还能在任何一个可选项为 nil 时提供合理的退路。</p>



<p>回到姓氏的例子，如果我们无法读取某人姓氏的第一个字母，我们可以自动返回“?”：</p>



<pre class="wp-block-code"><code>let surnameLetter = names&#91;"Vincent"]?.first?.uppercased() ?? "?"
</code></pre>



<p>有关可选链的更多信息，我向你推荐 Andy Bargh 的这篇文章：<a href="https://andybargh.com/optional-chaining/%E3%80%82"></a><a href="https://andybargh.com/optional-chaining/%E3%80%82">https://andybargh.com/optional-chaining/。</a></p>



<h1 class="wp-block-heading">如何使用可选项处理函数失效</h1>



<p>当我们调用一个可能会出错的函数时，要么使用 try 调用它并适当处理错误，要么如果我们确定函数不会失败，就使用 try! (剧透：你应该很少使用 try！）。</p>



<p>不过，还有另一种方法：如果我们只关心函数是成功还是失败，我们可以使用可选的 try，让函数返回一个可选值。如果函数运行时没有抛出任何错误，那么可选值将包含返回值，但如果抛出了任何错误，函数将返回 nil。这意味着我们无法确切知道抛出了什么错误，但通常这并无大碍——我们可能只关心函数是否正常运行。</p>



<p>如下所示：</p>



<pre class="wp-block-code"><code>enum UserError: Error {
    case badID, networkFailed
}

func getUser(id: Int) throws -> String {
    throw UserError.networkFailed
}

if let user = try? getUser(id: 23) {
    print("User: \(user)")
}
</code></pre>



<p>getUser() 函数总是会抛出 networkFailed 错误，这对我们的测试目的来说没有问题，但我们实际上并不关心抛出的是什么错误，我们关心的只是调用是否发回了用户。</p>



<p>这就是 try? 的作用所在：它使 getUser() 返回一个可选字符串，如果有任何错误抛出，该字符串将为零。如果你想知道到底发生了什么错误，那么这种方法就没用了，但很多时候我们并不关心。</p>



<p>如果你愿意，可以将 try? 与 nil coalescing 结合起来，这意味着 “尝试从该函数获取返回值，但如果失败，则使用该默认值”。</p>



<p>不过请注意：你需要在 nil coalescing 之前添加一些括号，以便 Swift 能准确理解您的意思。例如，你可以这样写：</p>



<pre class="wp-block-code"><code>let user = (try? getUser(id: 23)) ?? "Anonymous"
print(user)
</code></pre>



<p>你会发现 try? 主要用于三个地方：</p>



<ol class="wp-block-list">
<li>与 guard let 结合使用，可在 try? 调用返回 nil 时退出当前函数。</li>



<li>与 nil coalescing 结合使用，以尝试某些操作或在失败时提供一个默认值。</li>



<li>在调用任何没有返回值的抛出函数时，当你真的不在乎它是否成功时——例如，你可能正在向日志文件写入内容或向服务器发送分析结果。</li>
</ol>



<h2 class="wp-block-heading">什么时候应该使用可选项try?</h2>



<p>我们可以在 Swift 中使用 do、try 和 catch 运行抛出函数，但另一种方法是使用 try? 将抛出函数调用转换为可选项。如果函数成功执行，其返回值将是一个可选项，其中包含你通常会收到的返回值；如果函数失败，其返回值将是一个设置为 nil 的可选项。</p>



<p>使用可选 try 有好处也有坏处，但主要是看错误对你来说有多重要。如果你想运行一个函数，并且只关心它成功或失败——你不需要区分它可能失败的各种原因——那么使用可选 try 就非常合适，因为你可以把整件事归结为 &#8220;它成功了吗？”</p>



<p>因此，与其这样写：</p>



<pre class="wp-block-code"><code>do {
    let result = try runRiskyFunction()
    print(result)
} catch {
    // it failed!
}
</code></pre>



<p>你可以这样写：</p>



<pre class="wp-block-code"><code>if let result = try? runRiskyFunction() {
    print(result)
}
</code></pre>



<p>如果这是你想要做的，那么你可以让 runRiskyFunction() 返回一个可选项，而不是抛出错误，但抛出错误和使用可选 try 确实给了我们灵活性，让我们可以在以后改变主意。也就是说，如果我们编写了一个会抛出错误的函数，那么在调用现场，我们可以根据当时的需要使用 try/catch 或使用可选 try。</p>



<p>无论如何，可选 try能让你更专注于手头的问题。</p>



<h1 class="wp-block-heading">总结</h1>



<p>在这几章中，我们学习了 Swift 最重要的功能之一，虽然大多数人一开始都觉得可选项难以理解，但几乎每个人都认为它们在实践中非常有用。</p>



<p>让我们回顾一下所学内容：</p>



<ul class="wp-block-list">
<li>可选项让我们可以表示没有数据，这意味着我们可以说 “这个整数没有值”——这与 0 这样的固定数字不同。</li>



<li>因此，所有不是可选项的东西内部肯定都有一个值，即使那只是一个空字符串。</li>



<li>拆开可选项的过程就像在一个盒子里查看里面有什么：如果里面有一个值，它就会被送回去使用，否则里面就是 nil。</li>



<li>如果可选项有值，我们可以使用 if let 来运行代码；如果可选项没有值，我们可以使用 guard let 来运行代码，但使用 guard 时，我们必须在之后退出函数。</li>



<li>nil 聚合操作符??可以解包并返回可选项的值，或使用默认值。</li>



<li>通过可选项链，我们可以用方便的语法读取另一个可选项内部的可选项。</li>



<li>如果一个函数可能会出错，可以使用 try? ——返回函数的返回值，或者在抛出错误时返回 nil。</li>
</ul>



<p>说到语言特性，可选项是仅次于闭包的让人难以掌握的特性，但我保证，几个月后你就会发现，没有可选项你将不知如何编写程序！</p>
]]></content:encoded>
					
					<wfw:commentRss>https://beautylife-studio.top/2024/tech/1538/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Learning Swift &#038; SwiftUI Chapter 13</title>
		<link>https://beautylife-studio.top/2024/tech/1511/</link>
					<comments>https://beautylife-studio.top/2024/tech/1511/#respond</comments>
		
		<dc:creator><![CDATA[Alan C.]]></dc:creator>
		<pubDate>Wed, 21 Aug 2024 05:26:15 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[Learning Swift & SwiftUI]]></category>
		<guid isPermaLink="false">https://beautylife-studio.top/?p=1511</guid>

					<description><![CDATA[协议与扩展]]></description>
										<content:encoded><![CDATA[
<p>今天我们将一起学习一些真正的Swift功能：协议、扩展和协议扩展。</p>



<p>协议扩展允许我们摒弃庞大、复杂的继承层次结构，取而代之的是更小、更简单的协议，这些协议可以组合在一起。</p>



<p>从你的第一个 SwiftUI 项目开始，你就会用到协议，它们在你的整个 Swift 编码生涯中都将是无价之宝，值得你花时间去熟悉它们。</p>



<p>本章节有四个部分，让你了解协议、扩展等内容。现在就让我们开始吧！</p>



<h1 class="wp-block-heading">如何创建和使用协议</h1>



<p>协议有点像 Swift 中的合约：它让我们定义我们期望数据类型支持哪些功能，而 Swift 则确保我们代码的其余部分遵循这些规则。</p>



<p>想想我们如何编写代码来模拟一个人从家到办公室的通勤过程。我们可以创建一个小的 Car 结构，然后编写这样一个函数：</p>



<pre class="wp-block-code"><code>func commute(distance: Int, using vehicle: Car) {
    // lots of code here
}
</code></pre>



<p>当然，他们也可能坐火车上下班，所以我们也会这样写：</p>



<pre class="wp-block-code"><code>func commute(distance: Int, using vehicle: Train) {
    // lots of code here
}
</code></pre>



<p>他们也可能乘坐公共汽车：</p>



<pre class="wp-block-code"><code>func commute(distance: Int, using vehicle: Bus) {
    // lots of code here
}
</code></pre>



<p>或者，他们可能会使用自行车、电动摩托车、共享单车或其他交通工具。</p>



<p>事实上，在这个层面上，我们并不关心基本的出行方式。我们关心的是更广泛的问题：用户使用每种选择通勤可能需要多长时间，以及如何执行移动到新地点的实际行动。</p>



<p>这就是协议的作用所在：协议让我们定义了一系列我们想要使用的属性和方法。它们并不实现这些属性和方法——实际上并不在其背后添加任何代码——它们只是说这些属性和方法必须存在，有点像蓝图。</p>



<p>例如，我们可以这样定义一个新的车辆协议：</p>



<pre class="wp-block-code"><code>protocol Vehicle {
    func estimateTime(for distance: Int) -&gt; Int
    func travel(distance: Int)
}
</code></pre>



<p>让我们来分析一下：</p>



<ul class="wp-block-list">
<li>要创建一个新协议，我们要写 protocol，然后是协议名称。这是一个新类型，因此我们需要使用大写字母开头的驼峰式大小写。</li>



<li>在协议中，我们要列出所有需要使用的方法，以使该协议按照我们期望的方式运行。</li>



<li>这些方法内部不包含任何代码——这里不提供函数体。相反，我们指定了方法名称、参数和返回类型。如果需要，还可以将方法标记为抛出（throwing）或突变（mutating）。</li>
</ul>



<p>那么，我们已经制定了协议——这对我们有什么帮助呢？</p>



<p>现在，我们可以设计与该协议配合使用的类型。这意味着要创建新的结构体、类或枚举来实现协议的要求，我们把这个过程称为 “采用协议（adopting）”或 “符合协议（conforming）”。</p>



<p>协议并没有规定必须存在的全部功能，只规定了最基本的功能。这意味着当你创建符合协议的新类型时，你可以根据需要添加各种其他属性和方法。</p>



<p>例如，我们可以创建一个符合车辆协议的 Car 结构，就像下面这样：</p>



<pre class="wp-block-code"><code>struct Car: Vehicle {
    func estimateTime(for distance: Int) -> Int {
        distance / 50
    }

    func travel(distance: Int) {
        print("I'm driving \(distance)km.")
    }

    func openSunroof() {
        print("It's a nice day!")
    }
}
</code></pre>



<p>在这段代码中，我有几处需要特别注意：</p>



<ol class="wp-block-list">
<li>我们在 Car 名称后面使用冒号来告诉 Swift：Car 与 Vehicle 相符，就像我们标记子类一样。</li>



<li>我们在 Vehicle 中列出的所有方法必须完全存在于 Car 中。如果它们的名称略有不同、接受的参数不同、返回类型不同等，那么 Swift 就会说我们没有遵守协议。</li>



<li>Car 中的方法提供了我们在协议中定义的方法的实际实现。在本例中，这意味着我们的结构体提供了一个粗略估计，即行驶一定距离需要多少分钟，并在调用 travel() 时打印一条信息。</li>



<li>openSunroof()方法并非来自车辆协议，而且在该协议中也没有实际意义，因为许多车辆类型都没有天窗。不过没关系，因为协议只描述了符合要求的类型必须具备的最低功能，它们可以根据需要添加自己的功能。</li>
</ol>



<p>现在，我们创建了一个协议，并制作了一个符合协议的汽车结构体。</p>



<p>最后，让我们更新之前的 commute() 函数，使其使用我们为 Car 添加的新方法：</p>



<pre class="wp-block-code"><code>func commute(distance: Int, using vehicle: Car) {
    if vehicle.estimateTime(for: distance) &gt; 100 {
        print("That's too slow! I'll try a different vehicle.")
    } else {
        vehicle.travel(distance: distance)
    }
}

let car = Car()
commute(distance: 100, using: car)
</code></pre>



<p>这些代码都能正常工作，但在这里，协议实际上并没有增加任何价值。是的，它让我们在 Car 内部实现了两个非常特殊的方法，但我们本可以在不添加协议的情况下做到这一点，那又何必呢？</p>



<p>聪明的地方来了： Swift 知道，任何符合 Vehicle 的类型都必须实现 estimateTime() 和 travel() 方法，因此它允许我们使用 Vehicle 作为参数类型，而不是 Car。我们可以这样重写函数：</p>



<pre class="wp-block-code"><code>func commute(distance: Int, using vehicle: Vehicle) {
</code></pre>



<p>现在我们是说，只要数据类型符合车辆协议，就可以使用任何类型的数据调用此函数。函数的主体无需更改，因为 Swift 可以确定 estimateTime() 和 travel() 方法已经存在。</p>



<p>如果你还想知道为什么这很有用，请参考下面的结构：</p>



<pre class="wp-block-code"><code>struct Bicycle: Vehicle {
    func estimateTime(for distance: Int) -> Int {
        distance / 10
    }

    func travel(distance: Int) {
        print("I'm cycling \(distance)km.")
    }
}

let bike = Bicycle()
commute(distance: 50, using: bike)
</code></pre>



<p>现在我们有了第二个同样符合 Vehicle 标准的结构体，这就是协议的威力所在：我们现在既可以向 commute() 函数传递汽车，也可以传递自行车。在内部，该函数可以拥有它想要的所有逻辑，当它调用 estimateTime() 或 travel() 时，Swift 会自动使用相应的逻辑——如果我们传入一辆汽车，它就会说 “我在开车”，但如果我们传入一辆自行车，它就会说 “我在骑自行车”。</p>



<p>因此，协议让我们讨论的是我们想要使用的功能类型，而不是确切的类型。与其说 “这个参数必须是一辆汽车”，我们倒不如说 “这个参数可以是任何东西，只要它能估算旅行时间并移动到新的位置”。</p>



<p>除了方法外，你还可以编写协议来描述符合要求的类型中必须存在的属性。要做到这一点，先写入 var，然后写入一个属性名称，再列出它是否应该可读和/或可写。</p>



<p>例如，我们可以规定所有符合 Vehicle 的类型都必须指定其名称和当前拥有的乘客数量，就像这样：</p>



<pre class="wp-block-code"><code>protocol Vehicle {
    var name: String { get }
    var currentPassengers: Int { get set }
    func estimateTime(for distance: Int) -&gt; Int
    func travel(distance: Int)
}
</code></pre>



<p>这就增加了两个属性：</p>



<ol class="wp-block-list">
<li>一个名为 name 的字符串，它必须是可读的。这可能意味着它是一个常量，但也可能是一个带有 getter 的计算属性。</li>



<li>一个名为 currentPassengers 的整数，必须是可读写的。这可能意味着它是一个变量，但也可能是一个带有 getter 和 setter 的计算属性。</li>
</ol>



<p>这两个属性都需要类型注解，因为我们不能在协议中提供默认值，就像协议不能为方法提供实现一样。</p>



<p>有了这两个额外的要求，Swift 就会警告我们，Car 和 Bicycle 都不再符合协议，因为它们缺少了属性。为了解决这个问题，我们可以为 Car 添加以下属性：</p>



<pre class="wp-block-code"><code>let name = "Car"
var currentPassengers = 1
</code></pre>



<p>这些给自行车：</p>



<pre class="wp-block-code"><code>let name = "Bicycle"
var currentPassengers = 1
</code></pre>



<p>同样，只要你遵守规则，你也可以用计算属性来代替它们——如果你使用 { get set }，那么你就不能使用常量属性来遵守协议。</p>



<p>因此，现在我们的协议需要两个方法和两个属性，这意味着所有符合协议的类型都必须实现这四点，我们的代码才能正常工作。这反过来又意味着 Swift 可以确定这些功能是存在的，因此我们可以编写依赖于这些功能的代码。</p>



<p>例如，我们可以编写一个接受车辆数组的方法，并使用它来计算一系列选项的估算值：</p>



<pre class="wp-block-code"><code>func getTravelEstimates(using vehicles: &#91;Vehicle], distance: Int) {
    for vehicle in vehicles {
        let estimate = vehicle.estimateTime(for: distance)
        print("\(vehicle.name): \(estimate) hours to travel \(distance)km")
    }
}
</code></pre>



<p>希望这能向你展示协议的真正威力——我们接受一整套 “车辆 ”协议，这意味着我们可以传入一辆汽车、一辆自行车或任何其他符合 “车辆 ”协议的结构，它就会自动运行：</p>



<pre class="wp-block-code"><code>getTravelEstimates(using: &#91;car, bike], distance: 150)
</code></pre>



<p>除了接受协议作为参数外，你还可以根据需要从函数中返回协议。</p>



<p><strong>提示</strong>：您可以根据需要遵从任意多个协议，只需用逗号将它们一个一个地分隔开来即可。如果你需要对某事物进行子类化并符合某个协议，您应该先列出父类的名称，然后再编写您的协议。</p>



<h2 class="wp-block-heading">Swift 为什么需要协议？</h2>



<p>协议可以让我们定义结构体、类和枚举应该如何工作：它们应该具有哪些方法和属性。Swift 会为我们强制执行这些规则，因此当我们说某个类型符合某个协议时，Swift 会确保它拥有该协议要求的所有方法和属性。</p>



<p>在实践中，协议允许我们以更通用的方式处理数据。因此，与其说 “这个 buy() 方法必须接受一个 Book 对象”，我们可以说 &#8220;这个方法可以接受任何符合 Purchaseable 协议的对象。这可能是一本书，但也可能是一部电影、一辆汽车、一些咖啡等等——这让我们的简单方法更加灵活，同时确保 Swift 为我们执行规则。</p>



<p>在代码中，我们只适用于书籍的简单 buy() 方法将如下所示：</p>



<pre class="wp-block-code"><code>struct Book {
    var name: String
}

func buy(_ book: Book) {
    print("I'm buying \(book.name)")
}
</code></pre>



<p>要创建一种更灵活、基于协议的方法，我们首先要创建一个协议，声明我们需要的基本功能。这可能包括许多方法和属性，但在这里我们只需要一个名称字符串：</p>



<pre class="wp-block-code"><code>protocol Purchaseable {
    var name: String { get set }
}
</code></pre>



<p>现在，我们可以根据需要定义任意多个结构体，每个结构体都有一个名称字符串，以符合该协议：</p>



<pre class="wp-block-code"><code>struct Book: Purchaseable {
    var name: String
    var author: String
}

struct Movie: Purchaseable {
    var name: String
    var actors: &#91;String]
}

struct Car: Purchaseable {
    var name: String
    var manufacturer: String
}

struct Coffee: Purchaseable {
    var name: String
    var strength: Int
}
</code></pre>



<p>你会发现这些类型中的每一种都有一个不同的属性，而协议中并没有声明这些属性，没关系——协议决定了所需的最低功能，但我们可以随时添加更多的功能。</p>



<p>最后，我们可以重写 buy() 函数，使它可以接受任何类型的 Purchaseable 项目：</p>



<pre class="wp-block-code"><code>func buy(_ item: Purchaseable) {
    print("I'm buying \(item.name)")
}
</code></pre>



<p>在该方法中，我们可以安全地使用项目的 name 属性，因为 Swift 会保证每个 Purchaseable 项目都有 name 属性。但它并不保证我们定义的任何其他属性都会存在，只保证协议中特别声明的属性。</p>



<p>因此，协议可以让我们创建类型共享功能的蓝图，然后在函数中使用这些蓝图，让它们在更广泛的数据上运行。</p>



<h1 class="wp-block-heading">如何使用不透明返回类型</h1>



<p>Swift 提供了一个非常晦涩、复杂但又非常重要的功能，叫做不透明返回类型（opaque return types），它可以让我们消除代码中的复杂性。老实说，如果不是因为一个非常重要的事实，我们是不会在初学者课程中介绍它的：当你创建第一个 SwiftUI 项目时，你就会立即看到它。</p>



<p><strong>重要提示</strong>：你不需要详细了解不透明返回类型的工作原理，只需要知道它们的存在并执行一项非常特殊的工作。在学习的过程中，你可能会开始怀疑这个功能为什么有用，但请相信我：它很重要，也很有用，所以请努力学习！</p>



<p>让我们实现两个简单的功能：</p>



<pre class="wp-block-code"><code>func getRandomNumber() -&gt; Int {
    Int.random(in: 1...6)
}

func getRandomBool() -&gt; Bool {
    Bool.random()
}
</code></pre>



<p><strong>提示</strong>：Bool.random() 返回 true 或 false。与随机整数和小数不同，我们不需要指定任何参数，因为没有自定义选项。</p>



<p>因此，getRandomNumber() 返回一个随机整数，而 getRandomBool() 返回一个随机布尔值。</p>



<p>Int 和 Bool 都符合 Swift 的通用协议 Equatable，意思是 “可以进行等价比较”。通过 Equatable 协议，我们可以像下面这样使用 ==：</p>



<pre class="wp-block-code"><code>print(getRandomNumber() == getRandomNumber())
</code></pre>



<p>因为这两种类型都符合 Equatable，所以我们可以尝试修改函数，返回一个 Equatable 值，就像这样：</p>



<pre class="wp-block-code"><code>func getRandomNumber() -&gt; Equatable {
    Int.random(in: 1...6)
}

func getRandomBool() -&gt; Equatable {
    Bool.random()
}
</code></pre>



<p>但是，这段代码将无法工作，Swift 会弹出一条错误信息，在你的 Swift 职业生涯中，这条错误信息不太可能有什么帮助：“protocol &#8216;Equatable&#8217; can only be used as a generic constraint because it has Self or associated type requirements（协议&#8217;Equatable&#8217;只能用作通用约束，因为它有 Self 或相关类型要求）”。Swift 的错误意味着返回 Equatable 没有意义，而理解为什么没有意义是理解不透明返回类型的关键。</p>



<p>首先：是的，你可以从函数中返回协议，而且通常这样做非常有用。例如，你可能有一个为用户寻找租车服务的函数：它接受需要承载的乘客人数，以及他们需要多少行李，但它可能会返回几个结构体中的一个： 紧凑型车、SUV、小型货车等。</p>



<p>我们可以通过返回一个被所有这些结构体采用的车辆协议来处理这个问题，这样无论谁调用函数，都会得到与他们的请求相匹配的某种汽车，而不必编写 10 个不同的函数来处理所有的汽车类型。每种汽车类型都将实现 Vehicle 的所有方法和属性，这意味着它们是可以互换的——从编码的角度来看，我们并不关心返回的是哪种选项。</p>



<p>现在想想发送回 Int 或 Bool。是的，两者都符合 Equatable 协议，但它们不能互换——我们不能使用 == 来比较 Int 和 Bool，因为无论它们符合什么协议，Swift 都不会允许我们这样做。</p>



<p>从函数中返回协议非常有用，因为它可以让我们隐藏信息：我们不必说明返回的确切类型，而是专注于返回的功能。就车辆协议而言，这可能意味着返回座位数、大约的燃料使用量和价格。这意味着我们可以在以后修改代码，而不会造成任何破坏：我们可以返回 RaceCar 或 PickUpTruck 等，只要它们实现了 Vehicle 所需的属性和方法即可。</p>



<p>以这种方式隐藏信息非常有用，但 Equatable 却无法做到这一点，因为无法比较两个不同的 Equatable 事物。即使我们调用 getRandomNumber() 两次得到两个整数，我们也无法对它们进行比较，因为我们隐藏了它们的确切数据类型——我们隐藏了它们是两个可以比较的整数这一事实。</p>



<p>这就是不透明返回类型的作用所在：它允许我们在代码中隐藏信息，但不会被 Swift 编译器发现。这意味着我们保留了在内部灵活运用代码的权利，以便将来可以返回不同的内容，但 Swift 总是能理解实际返回的数据类型，并进行适当的检查。</p>



<p>要将我们的两个函数升级为不透明返回类型，请在其返回类型前添加关键字 some，如下所示：</p>



<pre class="wp-block-code"><code>func getRandomNumber() -&gt; some Equatable {
    Int.random(in: 1...6)
}

func getRandomBool() -&gt; some Equatable {
    Bool.random()
}
</code></pre>



<p>现在我们可以调用 getRandomNumber() 两次，并使用 == 比较结果。 从我们的角度来看，我们仍然只有一些 Equatable 数据，但 Swift 知道它们在幕后实际上是两个整数。</p>



<p>返回不透明的返回类型意味着我们仍然可以专注于我们想要返回的功能，而不是具体的类型，这反过来又允许我们在将来改变主意，而不会破坏代码的其余部分。例如，getRandomNumber() 可以改用 Double.random(in:)，代码仍然可以正常运行。</p>



<p>但这样做的好处是，Swift 总是知道真正的底层数据类型。这是一个微妙的区别，但返回 Vehicle 意味着 “任何类型的车辆，但我们不知道是什么”，而返回某种 Vehicle 意味着 “特定类型的车辆，但我们不想说是哪一种”。</p>



<p>说到这里，我估计你已经头晕目眩了，让我举个真实的例子来说明为什么这在 SwiftUI 中很重要。SwiftUI 需要确切知道你想在屏幕上显示什么样的布局，因此我们要编写代码来描述它。</p>



<p>我们可以这样说 “有一个屏幕，顶部是工具栏，底部是标签栏，中间是彩色图标的滚动网格，每个图标下面都有一个标签，用粗体字写明图标的含义，当你点击一个图标时，就会出现一条信息&#8221;。</p>



<p>当 SwiftUI 询问我们的布局时，整个描述就会成为布局的返回类型。我们需要明确说明我们希望在屏幕上显示的每一件事物，包括位置、颜色、字体大小等等。你能想象把这些作为返回类型输入吗？这会有一公里长！而且每次更改代码来生成布局时，都需要更改返回类型来匹配。</p>



<p>这时，不透明的返回类型就派上用场了：我们可以返回某个 View 类型，这意味着将返回某种视图屏幕，但我们不必写出它的长达一公里的类型。同时，Swift 知道真正的底层类型，因为这就是不透明返回类型的工作原理： Swift 总是知道返回数据的确切类型，SwiftUI 将使用它来创建布局。</p>



<p>正如在开头所说，不透明返回类型是一个非常晦涩、复杂但又非常重要的功能，如果不是因为它们在 SwiftUI 中被广泛使用，我们也不会在初学者课程中学习它们。</p>



<p>因此，当你在 SwiftUI 代码中看到一些 View 时，这实际上是我们在告诉 Swift：&#8221;这将发送回某种视图来布局，但我不想写出具体的内容——你自己去想吧！”</p>



<h1 class="wp-block-heading">如何创建和使用扩展</h1>



<p>扩展可以让我们为任何类型添加功能，不管是我们创建的还是别人创建的，甚至是苹果自己的类型。</p>



<p>为了演示这一点，我想向大家介绍一种有用的字符串方法，名为 trimmingCharacters(in:)。它可以从字符串的开头或结尾删除某些类型的字符，例如字母数字、小数位，或者最常见的空白和新行。</p>



<p>空白是空格字符、制表符以及这两种字符的其他变体的总称。新行是文本中的换行符，听起来似乎很简单，但实际上并没有单一的换行方式，所以当我们要求修剪新行时，它会自动为我们处理所有的变体。</p>



<p>例如，下面是一个两边都有空白的字符串：</p>



<pre class="wp-block-code"><code>var quote = "   The truth is rarely pure and never simple   "
</code></pre>



<p>如果我们想修剪两边的空白和换行符，可以这样做：</p>



<pre class="wp-block-code"><code>let trimmed = quote.trimmingCharacters(in: .whitespacesAndNewlines)
</code></pre>



<p>.whitespacesAndNewlines 值来自 Apple 的 Foundation API，实际上 trimmingCharacters(in:) 也是如此——正如我们在本课程开始时所学的，Foundation 确实包含了很多有用的代码！</p>



<p>每次都要调用 trimmingCharacters(in:)有点啰嗦，所以让我们编写一个扩展来缩短它：</p>



<pre class="wp-block-code"><code>extension String {
    func trimmed() -&gt; String {
        self.trimmingCharacters(in: .whitespacesAndNewlines)
    }
}
</code></pre>



<p>让我们来分析一下&#8230;</p>



<ol class="wp-block-list">
<li>我们从扩展关键字（extension）开始，它告诉 Swift 我们要为现有类型添加功能。</li>



<li>哪种类型？接下来就知道了：我们要为字符串（String）添加功能。</li>



<li>现在我们打开一个括号，在最后的收尾括号之前的所有代码都将添加到字符串中。</li>



<li>我们要添加一个名为 trimmed() 的新方法，它将返回一个新的字符串。</li>



<li>在该方法中，我们调用与之前相同的方法：trimmingCharacters(in:)，并将结果发送回去。</li>



<li>请注意我们如何在这里使用 self——它自动指向当前字符串。这是因为我们当前处于字符串扩展中。</li>
</ol>



<p>现在，只要我们想删除空白和换行符，就可以写出下面的代码：</p>



<pre class="wp-block-code"><code>let trimmed = quote.trimmed()
</code></pre>



<p>简单多了！</p>



<p>虽然省了一些键入，但它比普通函数好得多吗？</p>



<p>事实上，我们完全可以写一个这样的函数：</p>



<pre class="wp-block-code"><code>func trim(_ string: String) -&gt; String {
    string.trimmingCharacters(in: .whitespacesAndNewlines)
}
</code></pre>



<p>然后这样使用：</p>



<pre class="wp-block-code"><code>let trimmed2 = trim(quote)
</code></pre>



<p>从函数的制作和使用两方面来看，这都比使用扩展程序代码少。这种函数被称为全局函数，因为它在我们的项目中随处可用。</p>



<p>不过，与全局函数相比，扩展功能有很多优点，包括：</p>



<ol class="wp-block-list">
<li>当你键入 quote 时。Xcode 会弹出字符串上的方法列表，包括我们在扩展中添加的所有方法。这使得我们的额外功能很容易被找到。</li>



<li>编写全局函数会让你的代码变得相当凌乱——它们很难组织，也很难跟踪。另一方面，扩展会自然地按其扩展的数据类型分组。</li>



<li>由于扩展方法是原始类型的完整组成部分，因此它们可以完全访问该类型的内部数据。例如，这意味着它们可以使用标有私有访问控制的属性和方法。</li>
</ol>



<p>此外，扩展还可以更方便地就地修改值，即直接修改值，而不是返回一个新值。</p>



<p>例如，我们之前编写了一个 trimmed() 方法，该方法会返回一个去除了空白和换行符的新字符串，但如果我们想直接修改字符串，可以将其添加到扩展中：</p>



<pre class="wp-block-code"><code>mutating func trim() {
    self = self.trimmed()
}
</code></pre>



<p>因为引号字符串是作为变量创建的，所以我们可以这样修剪它：</p>



<pre class="wp-block-code"><code>quote.trim()
</code></pre>



<p>请注意该方法现在的命名略有不同：返回新值时，我们使用 trimmed()，而直接修改字符串时，我们使用 trim()。这是有意为之，也是 Swift 设计指南的一部分：如果要返回一个新值，而不是就地修改，那么就应该使用 ed 或 ing 这样的词尾，就像 reversed()。</p>



<p>小提示：之前我们学习了数组的 sorted() 方法。现在你知道了这条规则，就会意识到如果创建了一个变量数组，就可以使用 sort() 方法对数组进行就地排序，而不是返回一个新的副本。</p>



<p>你也可以使用扩展为类型添加属性，但有一条规则：这些属性必须是计算属性，而不是存储属性。这样做的原因是，添加新的存储属性会影响数据类型的实际大小——如果我们给一个整数添加了大量存储属性，那么每个整数都需要占用更多内存空间，这会带来各种问题。</p>



<p>幸运的是，我们仍然可以使用计算属性完成很多工作。例如，我喜欢为字符串添加一个名为 lines 的属性，它可以将字符串拆分成一个个单独的行数组。它封装了另一个名为 components(separatedBy:) 的字符串方法，该方法通过在我们选择的边界上分割字符串，将字符串分割成一个字符串数组。在本例中，我们希望这个边界是新的行，因此我们要在字符串扩展中添加这个方法：</p>



<pre class="wp-block-code"><code>var lines: &#91;String] {
    self.components(separatedBy: .newlines)
}
</code></pre>



<p>这样，我们就可以读取任何字符串的行属性了：</p>



<pre class="wp-block-code"><code>let lyrics = """
But I keep cruising
Can't stop, won't stop moving
It's like I got this music in my mind
Saying it's gonna be alright
"""

print(lyrics.lines.count)
</code></pre>



<p>无论是单行代码还是复杂的功能片段，扩展的目标始终是一致的：使代码更易于编写、阅读和长期维护。</p>



<p>在我们结束之前，我想向大家展示一个在使用扩展时非常有用的技巧。你之前已经看到 Swift 如何为结构体自动生成一个成员初始化器，就像这样：</p>



<pre class="wp-block-code"><code>struct Book {
    let title: String
    let pageCount: Int
    let readingHours: Int
}

let lotr = Book(title: "Lord of the Rings", pageCount: 1178, readingHours: 24)
</code></pre>



<p>我还提到，创建自己的初始化器意味着 Swift 不再为我们提供成员式初始化器。这是故意的，因为自定义初始化器意味着我们要根据一些自定义逻辑来分配数据，就像这样：</p>



<pre class="wp-block-code"><code>struct Book {
    let title: String
    let pageCount: Int
    let readingHours: Int

    init(title: String, pageCount: Int) {
        self.title = title
        self.pageCount = pageCount
        self.readingHours = pageCount / 50
    }
}
</code></pre>



<p>如果 Swift 在这种情况下保留成员初始化器，就会跳过我们计算大致读取时间的逻辑。</p>



<p>不过，有时你会希望两者兼得——既能使用自定义初始化器，又能保留 Swift 的自动成员初始化器。在这种情况下，了解 Swift 的具体做法很有必要：如果我们在结构体内部实现了自定义初始化器，那么 Swift 就会禁用自动成员初始化器。</p>



<p>这个额外的小细节可能会给你接下来的提示：如果我们在扩展中实现了自定义初始化程序，那么 Swift 不会禁用自动成员初始化程序。仔细想想，这就说得通了：如果在扩展中添加一个新的初始化器，同时也禁用默认初始化器，那么我们的一个小改动就可能会破坏其他各种 Swift 代码。</p>



<p>因此，如果我们希望我们的 Book 结构既有默认的成员初始化器，又有自定义的初始化器，我们就会将自定义的初始化器放在扩展中，就像这样：</p>



<pre class="wp-block-code"><code>extension Book {
    init(title: String, pageCount: Int) {
        self.title = title
        self.pageCount = pageCount
        self.readingHours = pageCount / 50
    }
}
</code></pre>



<h2 class="wp-block-heading">什么时候应该在 Swift 中使用扩展？</h2>



<p>扩展可以让我们为类、结构体等添加功能，这对于修改我们不拥有的类型（例如 Apple 或其他人编写的类型）很有帮助。使用扩展添加的方法与原本属于该类型的方法没有区别，但在属性方面有所不同：扩展不能添加新的存储属性，只能添加计算属性。</p>



<p>扩展对于组织我们自己的代码也很有用，虽然有多种方法可以做到这一点，但我想在这里重点介绍两种：一致性分组和目的分组。</p>



<p>一致性分组是指将协议一致性作为扩展添加到类型中，并在扩展中添加所有需要的方法。这样，开发人员在阅读扩展时就更容易理解需要在脑中保留多少代码——如果当前扩展增加了对打印的支持，他们就不会发现打印方法与其他无关协议的方法混在一起。</p>



<p>另一方面，目的分组意味着创建扩展来完成特定任务，这使得处理大型类型变得更容易。例如，你可以创建一个扩展来专门处理该类型的加载和保存。</p>



<p>这里值得补充的是，许多人意识到他们有一个大类，并试图通过将其拆分成扩展来使其变小。需要明确的是：该类型的大小与以前完全一样，只是被整齐地分割开来。这确实意味着它可能更容易理解，但并不意味着类变小了。</p>



<h1 class="wp-block-heading">如何创建和使用协议扩展</h1>



<p>协议可以让我们定义符合要求的类型必须遵守的契约，而扩展可以让我们为现有类型添加功能。但是，如果我们可以在协议上编写扩展会怎样呢？</p>



<p>不用再担心了，Swift 支持协议扩展：我们可以扩展整个协议来添加方法实现，这意味着任何符合该协议的类型都能获得这些方法。</p>



<p>让我们从一个小的例子开始。写一个条件来检查数组中是否有任何值是很常见的，就像下面这样：</p>



<pre class="wp-block-code"><code>let guests = &#91;"Mario", "Luigi", "Peach"]

if guests.isEmpty == false {
    print("Guest count: \(guests.count)")
}
</code></pre>



<p>有些人喜欢使用布尔 ! 运算符，就像这样：</p>



<pre class="wp-block-code"><code>if !guests.isEmpty {
    print("Guest count: \(guests.count)")
}
</code></pre>



<p>我并不太喜欢这两种方法，因为对我来说，“如果某个数组不是空的 ”读起来并不自然。</p>



<p>我们可以通过一个非常简单的 Array 扩展来解决这个问题，就像下面这样：</p>



<pre class="wp-block-code"><code>extension Array {
    var isNotEmpty: Bool {
        isEmpty == false
    }
}
</code></pre>



<p>提示：Xcode 的 playgrounds 从上到下运行代码，因此请确保将扩展名放在用到它的地方之前。</p>



<p>现在，我们可以编写我认为更容易理解的代码了：</p>



<pre class="wp-block-code"><code>if guests.isNotEmpty {
    print("Guest count: \(guests.count)")
}
</code></pre>



<p>但我们可以做得更好。我们刚刚为数组添加了 isNotEmpty，但集合和字典呢？当然，我们可以重复自己的工作，将代码复制到这些扩展中，但有一个更好的解决方案： 数组、集合和字典都符合名为 Collection 的内置协议，通过该协议，它们可以获得 contains()、sorted()、reversed() 等功能。</p>



<p>这一点很重要，因为 Collection 也是 isEmpty 属性存在的必要条件。因此，如果我们在 Collection 上编写扩展，我们仍然可以访问 isEmpty，因为它是必需的。这意味着我们可以在代码中将 Array 更改为 Collection，从而获得此属性：</p>



<pre class="wp-block-code"><code>extension Collection {
    var isNotEmpty: Bool {
        isEmpty == false
    }
}
</code></pre>



<p>有了这一个词的改动，我们现在就可以在数组、集合和字典以及任何其他符合 Collection 的类型上使用 isNotEmpty。信不信由你，这个小小的扩展存在于成千上万的 Swift 项目中，因为很多人觉得它更容易阅读。</p>



<p>更重要的是，通过扩展协议，我们添加了原本需要在单个结构体内部完成的功能。这真的很强大，它带来了 Apple 称为面向协议编程的技术——我们可以在协议中列出一些必要的方法，然后在协议扩展中添加这些方法的默认实现。然后，所有符合要求的类型都可以使用这些默认实现，或根据需要提供自己的实现。</p>



<p>例如，如果我们有这样一个协议：</p>



<pre class="wp-block-code"><code>protocol Person {
    var name: String { get }
    func sayHello()
}
</code></pre>



<p>这意味着所有符合要求的类型都必须添加 sayHello() 方法，但我们也可以像这样添加一个默认实现作为扩展：</p>



<pre class="wp-block-code"><code>extension Person {
    func sayHello() {
        print("Hi, I'm \(name)")
    }
}
</code></pre>



<p>现在，符合要求的类型可以添加自己的 sayHello() 方法（如果它们愿意），但它们并不需要这样做——它们始终可以依赖我们协议扩展中提供的方法。</p>



<p>因此，我们可以创建一个没有 sayHello() 方法的雇员：</p>



<pre class="wp-block-code"><code>struct Employee: Person {
    let name: String
}
</code></pre>



<p>但由于它符合 Person 标准，我们可以使用扩展中提供的默认实现：</p>



<pre class="wp-block-code"><code>let taylor = Employee(name: "Taylor Swift")
taylor.sayHello()
</code></pre>



<p>Swift 经常使用协议扩展，但老实说，你还不需要非常详细地了解它们——你完全可以在不使用协议扩展的情况下构建出色的应用程序。现在你知道它们的存在就足够了！</p>



<h2 class="wp-block-heading">协议扩展在 Swift 中什么时候有用？</h2>



<p>协议扩展在 Swift 中随处可见，因此你经常会看到它被描述为 “面向协议的编程语言”。我们使用它们直接为协议添加功能，这意味着我们不需要在许多结构体和类中复制该功能。</p>



<p>例如，Swift 的数组有一个 allSatisfy() 方法，如果数组中的所有项都通过了测试，该方法就会返回 true。因此，我们可以创建一个数字数组，然后检查它们是否都是偶数：</p>



<pre class="wp-block-code"><code>let numbers = &#91;4, 8, 15, 16]
let allEven = numbers.allSatisfy { $0.isMultiple(of: 2) }
</code></pre>



<p>这确实很有用，但如果它也能在Sets中使用，是不是会更有用？当然有用，这也是它能发挥作用的原因：</p>



<pre class="wp-block-code"><code>let numbers2 = Set(&#91;4, 8, 15, 16])
let allEven2 = numbers2.allSatisfy { $0.isMultiple(of: 2) }
</code></pre>



<p>其基本原理是相同的：将数组或集合中的每个项通过你提供的测试，如果所有项都返回 true，那么方法的结果就是 true。</p>



<p>字典也能使用这种方法吗？当然可以，而且工作原理完全相同：每个键/值对都会传入闭包，而你需要返回 true 或 false。看起来是这样的：</p>



<pre class="wp-block-code"><code>let numbers3 = &#91;"four": 4, "eight": 8, "fifteen": 15, "sixteen": 16]
let allEven3 = numbers3.allSatisfy { $0.value.isMultiple(of: 2) }
</code></pre>



<p>当然，Swift 开发人员并不想重复编写相同的代码，因此他们使用了协议扩展：他们编写了单个 allSatisfy() 方法，该方法在名为 Sequence 的协议上运行，所有数组、集合和字典都符合该协议。这意味着 allSatisfy() 方法可以立即用于所有这些类型，共享完全相同的代码。</p>



<h1 class="wp-block-heading">总结</h1>



<p>在这几章中，我们学习了 Swift 一些复杂但强大的功能，但如果你感到有些吃力，也不要难过——这些功能一开始确实很难掌握，只有当你有时间在自己的代码中进行尝试后，才能真正理解它们。</p>



<p>让我们回顾一下所学到的知识：</p>



<ul class="wp-block-list">
<li>协议就像代码的合同：我们指定了所需的函数和方法，符合协议的类型必须实现这些函数和方法。</li>



<li>不透明返回类型可以让我们在代码中隐藏一些信息。这可能意味着我们希望保留将来更改的灵活性，但也意味着我们不需要写出庞大的返回类型。</li>



<li>扩展可以让我们为自己的自定义类型或 Swift 内置类型添加功能。这可能意味着添加一个方法，但我们也可以添加计算属性。</li>



<li>协议扩展让我们可以同时为多种类型添加功能——我们可以为协议添加属性和方法，所有符合协议的类型都可以访问它们。</li>
</ul>



<p>归纳起来，这些功能看似简单，其实不然。你需要了解它们，知道它们的存在，但你只需要在表面上使用它们，以便继续你的学习之旅。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://beautylife-studio.top/2024/tech/1511/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Learning Swift &#038; SwiftUI Chapter 12</title>
		<link>https://beautylife-studio.top/2024/tech/1508/</link>
					<comments>https://beautylife-studio.top/2024/tech/1508/#respond</comments>
		
		<dc:creator><![CDATA[Alan C.]]></dc:creator>
		<pubDate>Mon, 12 Aug 2024 04:20:16 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[Learning Swift & SwiftUI]]></category>
		<guid isPermaLink="false">https://beautylife-studio.top/?p=1508</guid>

					<description><![CDATA[类 Class]]></description>
										<content:encoded><![CDATA[
<p>类与结构体非常相似，因为我们使用它们来创建带有属性和方法的新数据类型。然而，类引入了一个新的、重要的、复杂的特性，即继承——使一个类建立在另一个类的基础上的能力。</p>



<p>毫无疑问，这是一个强大的功能，当你开始构建真正的 iOS 应用程序时，你将无法避免使用类。但请记住，一定要保持代码的简洁：一项功能的存在并不意味着你一定需要使用它。</p>



<p>SwiftUI 在用户界面设计中广泛使用了结构体。那么，它在数据方面也广泛使用了类：当你在屏幕上显示来自某个对象的数据时，或者当你在布局之间传递数据时，你通常会使用类。</p>



<p>今天我们一起来学习类的内容，有六大部份，内容有点多，让我们一一来看看。</p>



<h2 class="wp-block-heading">如何创建自己的类</h2>



<p>Swift 使用结构体来存储大多数数据类型，包括字符串、Int、Double 和数组，但还有一种创建自定义数据类型的方法，称为类。这些类与结构体有许多共同点，但在关键的地方有所不同。</p>



<p>首先，类和结构体的共同点包括：</p>



<ul class="wp-block-list">
<li>你可以创建并命名它们。</li>



<li>可以添加属性和方法，包括属性观察者和访问控制。</li>



<li>你可以创建自定义初始化器，以便随心所欲地配置新实例。</li>
</ul>



<p>然而，类与结构体在五个关键点上有所不同：</p>



<ol class="wp-block-list">
<li>你可以让一个类建立在另一个类的功能基础上，获得它的所有属性和方法作为起点。如果你想有选择性地覆盖某些方法，也可以这样做。</li>



<li>基于第一点，Swift 不会自动为类生成成员初始化器。这意味着你要么需要编写自己的初始化器，要么为所有属性分配默认值。</li>



<li>当你复制一个类的实例时，两个副本共享相同的数据——如果你更改其中一个副本，另一个副本也会随之更改。</li>



<li>在销毁类实例的最终副本时，Swift 可以选择运行一个名为去初始化器(deinitializer)的特殊函数。</li>



<li>即使你将一个类设为常量，只要其属性是变量，你仍然可以改变它们。</li>
</ol>



<p>从表面上看，这些可能看起来相当随意，而且你很可能会想，既然我们已经有了结构体，为什么还需要类呢？</p>



<p>其实，SwiftUI 广泛使用类，主要是为了第 3 点：类的所有副本共享相同的数据。这意味着应用程序的许多部分都可以共享相同的信息，因此，如果用户在一个屏幕上更改了姓名，所有其他屏幕都会自动更新以反映该更改。</p>



<p>其他要点也很重要，但用途各不相同：</p>



<ol class="wp-block-list">
<li>在苹果较早的 UI 框架 UIKit 中，基于一个类构建另一个类的能力非常重要，但在 SwiftUI 应用程序中却不常见。在 UIKit 中，长类层次结构很常见，类 A 基于类 B 构建，类 B 基于类 C 构建，类 C 基于类 D 构建，等等。</li>



<li>缺乏成员初始化器是一件令人讨厌的事，但希望你能明白，由于一个类可以基于另一个类，因此实现起来会很棘手——如果类 C 增加了一个额外的属性，就会破坏 C、B 和 A 的所有初始化器。</li>



<li>改变常量类的变量与类的多重拷贝行为有关：常量类意味着我们不能改变拷贝指向的容器，但如果属性是可变的，我们仍然可以改变容器内的数据。这一点与结构体不同，在结构体中，每个副本都是唯一的，并拥有自己的数据。</li>



<li>由于一个类的实例可以在多个地方被引用，因此知道最终副本何时被销毁就变得非常重要。这就是去初始化器的作用所在：它允许我们在最后一个副本消失时清理我们分配的任何特殊资源。</li>
</ol>



<p>在结束之前，让我们来看看创建和使用类的一小段代码：</p>



<pre class="wp-block-code"><code>class Game {
    var score = 0 {
        didSet {
            print("Score is now \(score)")
        }
    }
}

var newGame = Game()
newGame.score += 10
</code></pre>



<p>是的，它与结构体的唯一区别是，它是用 class 而不是 struct 创建的，其他都是一样的。这可能会让类显得多余，但请相信我：它们的所有五个不同点都很重要。</p>



<p>在接下来的内容中，我们将详细学习类和结构体的五种不同之处，但现在最重要的是：结构体很重要，类也很重要——在使用 SwiftUI 时，你将同时需要这两种类型。</p>



<h3 class="wp-block-heading">为什么 Swift 有类和结构体？</h3>



<p>类和结构体使 Swift 开发人员能够创建带有属性和方法的自定义复杂类型，但它们有五个重要的不同点：</p>



<ul class="wp-block-list">
<li>类没有合成的成员初始化器。</li>



<li>一个类可以建立在另一个类之上（&#8221;继承自&#8221;另一个类），从而获得其属性和方法。</li>



<li>结构体的副本总是唯一的，而类的副本实际上指向相同的共享数据。 类有去初始化器，即在类的实例被销毁时调用的方法，但结构体没有。</li>



<li>常量类中的变量属性可以自由修改，但常量结构体中的变量属性不能修改。</li>
</ul>



<p>我们很快会详细了解这些差异，但重点是它们的存在和重要性。在可能的情况下，大多数 Swift 开发人员更倾向于使用结构体而不是类，这意味着当你选择类而不是结构体时，是因为你希望获得上述行为之一。</p>



<h3 class="wp-block-heading">为什么 Swift 类没有成员初始化器？</h3>



<p>Swift 的结构体有很多有用的功能，其中之一就是它们自带一个合成的成员初始化器，让我们只需指定结构体的属性就能创建新的结构体实例。然而，Swift 的类却没有这个功能，这让人很恼火，但它们为什么没有呢？</p>



<p>主要原因是类具有继承性，这将使成员初始化器难以使用。想想看：如果我创建了一个类，而你继承了这个类，然后在我的类中添加了一些属性，那么你的代码就会崩溃——你依赖于我的成员初始化器的所有地方都会突然失效。</p>



<p>因此，Swift 有一个简单的解决方案：类的作者必须手工编写自己的初始化器，而不是自动生成成员初始化器。这样，你就可以随心所欲地添加属性，而不会影响你的类的初始化器，也不会影响继承自你的类的其他人。而且，当你决定更改初始化器时，也是你自己的决定，因此你完全知道这样做对任何继承类的影响。</p>



<h2 class="wp-block-heading">如何让一个类继承另一个类</h2>



<p>Swift 允许我们在现有类的基础上创建类，这个过程被称为继承(inheritance)。当一个类从另一个类（它的 &#8220;父类 &#8220;或 &#8220;超类&#8221;）继承功能时，Swift 会让新类（&#8221;子类&#8221;）访问父类的属性和方法，这样我们就可以对新类的行为方式进行微小的添加或修改。</p>



<p>要使一个类继承自另一个类，请在子类名称后面写一个冒号，然后添加父类的名称。例如，下面是一个只有一个属性和一个初始化器的 Employee 类：</p>



<pre class="wp-block-code"><code>class Employee {
    let hours: Int

    init(hours: Int) {
        self.hours = hours
    }
}
</code></pre>



<p>我们可以创建 Employee 的两个子类，每个子类都将获得 hours 属性和初始化器：</p>



<pre class="wp-block-code"><code>class Developer: Employee {
    func work() {
        print("I'm writing code for \(hours) hours.")
    }
}

class Manager: Employee {
    func work() {
        print("I'm going to meetings for \(hours) hours.")
    }
}
</code></pre>



<p>请注意，这两个子类可以直接引用hours——就好像它们自己添加了该属性，只是我们不必一直重复自己的工作。</p>



<p>这些类都继承自 Employee，但每个类都添加了自己的自定义属性。因此，如果我们分别创建一个实例并调用 work()，就会得到不同的结果：</p>



<pre class="wp-block-code"><code>let robert = Developer(hours: 8)
let joseph = Manager(hours: 10)
robert.work()
joseph.work()
</code></pre>



<p>除了共享属性，还可以共享方法，然后可以从子类中调用这些方法。举个例子，在 Employee 类中添加以下内容：</p>



<pre class="wp-block-code"><code>func printSummary() {
    print("I work \(hours) hours a day.")
}
</code></pre>



<p>因为 Developer 继承自 Employee，所以我们可以立即在 Developer 实例上调用 printSummary()，就像这样：</p>



<pre class="wp-block-code"><code>let novall = Developer(hours: 8)
novall.printSummary()
</code></pre>



<p>当你想改变一个继承的方法时，情况就会变得复杂一些。例如，我们刚把 printSummary() 放进 Employee 中，但可能其中一个子类想要稍微不同的行为。</p>



<p>这时，Swift 会强制执行一条简单的规则：如果子类想要更改父类的方法，就必须在子类的版本中使用覆盖。这样做有两个目的：</p>



<ol class="wp-block-list">
<li>如果你试图在不使用覆盖的情况下更改方法，Swift 将拒绝构建你的代码。这可以防止你不小心覆盖了一个方法。</li>



<li>如果你使用了覆盖，但你的方法实际上并没有覆盖父类中的某些内容，Swift 将拒绝构建你的代码，因为你可能犯了一个错误。</li>
</ol>



<p>因此，如果我们希望开发人员拥有一个独一无二的 printSummary() 方法，我们可以将其添加到Developer类中：</p>



<pre class="wp-block-code"><code>override func printSummary() {
    print("I'm a developer who will sometimes work \(hours) hours a day, but other times spend hours arguing about whether code should be indented using tabs or spaces.")
}
</code></pre>



<p>Swift 对于方法覆盖的工作方式很聪明：如果父类有一个不带参数的 work() 方法，而子类有一个接受字符串来指定工作位置的 work() 方法，这就不需要override，因为你并没有替换父类的方法。</p>



<p>提示：如果确定类不支持继承，可以将其标记为 final。这意味着类本身可以从其他事物继承，但不能被用来继承——任何子类都不能使用最终类作为父类。</p>



<h3 class="wp-block-heading">什么时候需要重写方法？</h3>



<p>在 Swift 中，任何继承自父类的子类都可以覆盖父类的方法，有时还可以覆盖父类的属性，这意味着它们可以用自己的方法替换父类的方法实现。</p>



<p>这种程度的自定义非常重要，让我们的开发生活变得更轻松。想想看：如果有人设计了一个出色的类，而你又想使用它，但它又不太合适，那么你只需重写它的一部分行为，而不必自己重写整个类，这不是很好吗？</p>



<p>当然，这正是方法覆盖的用武之地：你可以保留你想要的所有行为，只需在自定义子类中更改一两个小部分即可。</p>



<p>在苹果公司最初的 iOS 用户界面框架 UIKit 中，这种方法被大量使用。例如，考虑一些内置应用程序，如 &#8220;设置 &#8220;和 &#8220;信息&#8221;。这两个应用程序都以行的形式显示信息： “设置”有&#8221;常规&#8221;、&#8221;控制中心&#8221;、&#8221;显示和亮度 &#8220;等行，而 &#8220;信息 &#8220;则有单独的行来显示你与不同人的每次对话。在 UIKit 中，这些行被称为tables，它们有一些共同的行为：你可以滚动浏览所有行，可以点击行选择其中一行，行的右侧有灰色的小箭头，等等。</p>



<p>这些行列表非常常见，因此苹果为我们提供了使用它们的现有代码，其中包含了所有这些内置的标准行为。当然，有些部分实际上需要更改，例如列表有多少行以及列表中包含哪些内容。因此，Swift 开发人员会创建 Apple 表格的子类，并覆盖他们想要更改的部分，这样他们就能获得所有内置功能以及大量的灵活性和控制权。</p>



<p>Swift 让我们在覆盖函数之前使用override关键字，这确实很有帮助：</p>



<ul class="wp-block-list">
<li>如果在不需要时使用（因为父类没有声明相同的方法），就会出现错误。这可以防止你输入错误的内容，如参数名或类型，还可以在父类更改方法而你没有跟上时防止覆盖失败。</li>



<li>如果你在需要的时候没有使用它，那么你也会得到一个错误信息。这样可以避免意外更改父类的行为。</li>
</ul>



<h3 class="wp-block-heading">哪些类应声明为 final？</h3>



<p>最终类是不能被继承的类，这意味着你的代码的用户不可能添加功能或更改已有的功能。这不是默认情况：你必须在类中添加 final 关键字，以选择这种行为。</p>



<p>请记住，任何子类化你的类的人都可以覆盖你的属性，也许还可以覆盖你的方法，这将为他们提供难以置信的力量。如果你做了他们不喜欢的事情，他们可以直接替换掉。他们可能仍然会调用你原来的方法，但也可能不会。</p>



<p>这可能会带来一些问题：也许你的类做了一些非常重要的事情，不能被替换掉；也许你有客户签订了支持合同，你不想让他们破坏你的代码工作方式。</p>



<p>在 Swift 出现之前，苹果自己的大部分代码都是用早期的 Objective-C 语言编写的。Objective-C 对最终类的支持并不完善，因此苹果通常会在其网站上发布大型警告。例如，有一个非常重要的类叫做 AVPlayerViewController，它是用来播放电影的，其文档页面上有一个大大的黄色警告，上面写着：&#8221;不支持子类化 AVPlayerViewController 并重写其方法，否则会导致未定义的行为&#8221;。我们不知道为什么，只知道不应该这么做。还有一个类叫 Timer，用于处理定时事件（如闹钟），那里的警告更简单：&#8221;请勿子类化 Timer&#8221;。</p>



<p>在 Swift 中，final 类曾经比非 final 类性能更强，因为我们提供了更多关于代码如何运行的信息，Swift 会利用这些信息进行优化。</p>



<p>这种情况已经不是第一次出现了，但即使在今天，我想很多人还是会本能地将自己的类声明为 final，意思是 &#8220;除非我特别允许，否则你不应该从这个类中进行子类化&#8221;。</p>



<h2 class="wp-block-heading">如何为类添加初始化器</h2>



<p>Swift 中的类初始化器比结构体初始化器复杂得多，但只要稍加挑选，我们就能专注于真正重要的部分：如果子类有任何自定义初始化器，那么它必须在设置完自己的属性（如果有的话）后始终调用父类的初始化器。</p>



<p>正如之前所说，Swift 不会自动为类生成成员初始化器。无论是否发生继承，这一点都适用——它永远不会为你自动生成成员初始化器。因此，你要么需要编写自己的初始化器，要么为类的所有属性提供默认值。</p>



<p>让我们从定义一个新类开始：</p>



<pre class="wp-block-code"><code>class Vehicle {
    let isElectric: Bool

    init(isElectric: Bool) {
        self.isElectric = isElectric
    }
}
</code></pre>



<p>它有一个布尔属性，还有一个初始化器，用于设置该属性的值。记住，这里使用 self 表明我们将 isElectric 参数赋值给了同名的属性。</p>



<p>现在，假设我们想创建一个继承自 Vehicle 的 Car 类，一开始可以这样写：</p>



<pre class="wp-block-code"><code>class Car: Vehicle {
    let isConvertible: Bool

    init(isConvertible: Bool) {
        self.isConvertible = isConvertible
    }
}
</code></pre>



<p>但 Swift 会拒绝构建该代码：我们已经说过，车辆类需要知道它是否是电动车，但我们并没有为此提供一个值。 Swift 希望我们做的是为 Car 提供一个包含 isElectric 和 isConvertible 的初始化器，但我们不能自己存储 isElectric，而是需要将其传递给超类——我们需要让超类运行自己的初始化器。 下面就是这个过程：</p>



<pre class="wp-block-code"><code>class Car: Vehicle {
    let isConvertible: Bool

    init(isElectric: Bool, isConvertible: Bool) {
        self.isConvertible = isConvertible
        super.init(isElectric: isElectric)
    }
}
</code></pre>



<p>super 是 Swift 自动提供给我们的另一个值，与 self 类似：它允许我们调用属于父类的方法，例如父类的初始化器。如果你愿意，也可以在其他方法中使用它；它并不局限于初始化器。 现在，我们在两个类中都有了一个有效的初始化器，我们可以像这样创建 Car 的实例：</p>



<pre class="wp-block-code"><code>let teslaX = Car(isElectric: true, isConvertible: false)
</code></pre>



<p>提示：如果子类没有自己的初始化程序，它将自动继承父类的初始化程序。</p>



<h2 class="wp-block-heading">如何复制类</h2>



<p>在 Swift 中，类实例的所有副本共享相同的数据，这意味着你对其中一个副本所做的任何更改都会自动更改其他副本。出现这种情况是因为类在 Swift 中是引用类型，这意味着类的所有副本都会引用相同的底层数据。</p>



<p>要了解这一点，请试试这个简单的类：</p>



<pre class="wp-block-code"><code>class User {
    var username = "Anonymous"
}
</code></pre>



<p>该类只有一个属性，但由于它存储在一个类中，因此将在该类的所有副本中共享。</p>



<p>因此，我们可以创建该类的一个实例：</p>



<pre class="wp-block-code"><code>var user1 = User()
</code></pre>



<p>然后，我们可以复制 user1 并更改用户名值：</p>



<pre class="wp-block-code"><code>var user2 = user1
user2.username = "Taylor"
</code></pre>



<p>希望你能明白这一点！现在，我们已经更改了副本的用户名属性，然后可以打印出每个不同副本的相同属性：</p>



<pre class="wp-block-code"><code>print(user1.username)  
print(user2.username)
</code></pre>



<p>&#8230;&#8230;而这两个实例都将打印 &#8220;Taylor&#8221;（泰勒），即使我们只更改了其中一个实例，另一个实例也发生了变化。</p>



<p>这看似是一个错误，但实际上是一项功能，而且是一项非常重要的功能，因为它允许我们在应用程序的各个部分共享通用数据。正如你所看到的，SwiftUI 在很大程度上依赖于类来获取数据，特别是因为它们可以非常容易地共享。</p>



<p>相比之下，结构体不能在副本中共享数据，这意味着如果我们在代码中将类 User 更改为结构体 User，就会得到不同的结果：会打印出 &#8220;Anonymous&#8221;（匿名），然后是 &#8220;Taylor&#8221;（泰勒），因为更改副本并不会同时调整原始副本。</p>



<p>如果要创建一个类实例的唯一副本（有时称为深度副本），就需要安全地创建一个新实例并复制所有数据。</p>



<p>在我们的例子中，这很简单：</p>



<pre class="wp-block-code"><code>class User {
    var username = "Anonymous"

    func copy() -&gt; User {
        let user = User()
        user.username = username
        return user
    }
}
</code></pre>



<p>现在，我们可以安全地调用 copy() 来获取一个具有相同起始数据的对象，但今后的任何更改都不会影响原始数据。</p>



<h3 class="wp-block-heading">为什么一个类的副本要共享数据？</h3>



<p>Swift 有一个特点一开始确实让人困惑，那就是类和结构体在复制时的行为有何不同：同一个类的副本共享它们的底层数据，这意味着改变一个副本会改变所有副本；而结构体始终有自己独特的数据，改变一个副本不会影响其他副本。</p>



<p>这种区别的专业术语是 &#8220;值类型（value types）与引用类型（reference types）&#8221;。结构体是值类型，这意味着它们可以保存简单的值，如数字 5 或字符串 &#8220;hello&#8221;。不管你的结构体有多少属性或方法，它仍然被认为是一个简单的值，比如一个数字。另一方面，类是引用类型，这意味着它们引用其他地方的值。</p>



<p>对于值类型来说，这一点很容易理解，不言自明。例如，请看这段代码：</p>



<pre class="wp-block-code"><code>var message = "Welcome"
var greeting = message
greeting = "Hello"
</code></pre>



<p>代码运行时，message仍将设置为 &#8220;Welcome&#8221;，但greeting将设置为 &#8220;Hello&#8221;。这似乎显而易见且符合一般逻辑，这就是结构体的行为方式：它们的值完全包含在变量中，不会以某种方式与其他值共享。这意味着它们的所有数据都直接存储在每个变量中，所以当你复制它时，你会得到所有数据的深度拷贝。</p>



<p>相比之下，引用类型就像指向某些数据的路标。如果我们创建了一个类的实例，它会占用 iPhone 上的一些内存，而存储实例的变量实际上只是指向对象所在实际内存的路标。如果你复制了一个对象，就会得到一个新的路标，但它仍然指向原始对象所在的内存。这就是为什么改变一个类的一个实例会改变所有实例的原因：对象的所有副本都是指向同一块内存的路标。</p>



<p>在 Swift 开发中，这种差异的重要性怎么估计都不为过。之前我们提到 Swift 开发人员更喜欢使用结构体来创建自定义类型，而这种复制行为正是其中一个重要原因。想象一下，如果你有一个大型应用程序，并希望在不同地方共享一个 User 对象，那么如果其中一个地方更改了你的用户，会发生什么情况？如果你使用的是类，那么使用你的用户的所有其他地方的数据都会在不知不觉中发生变化，最终可能会出现问题。但如果使用的是结构体，应用程序的每个部分都有自己的数据副本，不会被意外更改。</p>



<p>与编程中的许多事情一样，你所做的选择应有助于传达你的一些推理。在本例中，使用类而不是结构体传递了一个强烈的信息，即你希望以某种方式共享数据，而不是拥有大量不同的副本。</p>



<h2 class="wp-block-heading">如何为类创建去初始化程序</h2>



<p>Swift 的类可以选择使用去初始化器（deinitializer），它有点像初始化器的反义词，在对象被销毁时调用，而不是在创建时调用。</p>



<p>这有几个小问题：</p>



<ol class="wp-block-list">
<li>与初始化器一样，去初始化器也不能使用 func，因为它们比较特殊。</li>



<li>去初始化器从不接受参数或返回数据，因此在编写时甚至不使用括号。</li>



<li>当类实例的最终副本被销毁时，去初始化器将自动被调用。例如，这可能意味着它是在一个函数内部创建的，而该函数现在正在结束。</li>



<li>我们从不直接调用去初始化器，而是由系统自动处理。</li>



<li>结构体没有去初始化器，因为无法复制它们。</li>
</ol>



<p>究竟何时调用去初始化器取决于你正在做什么，但实际上它归结于一个叫做 scope 的概念。作用域或多或少意味着 &#8220;信息可用的上下文&#8221;，你已经看过很多例子了：</p>



<ol class="wp-block-list">
<li>如果你在函数内部创建了一个变量，你就不能从函数外部访问它。</li>



<li>如果在 if 条件中创建变量，那么在条件之外就无法使用该变量。</li>



<li>如果在 for 循环（包括循环变量本身）中创建变量，则无法在循环之外使用该变量。</li>
</ol>



<p>纵观全局，你会发现每一个变量都使用大括号来创建它们的作用域：条件、循环和函数都创建了局部作用域。</p>



<p>当一个值退出作用域时，我们指的是它创建时的上下文正在消失。对于结构体来说，这意味着数据被销毁，但对于类来说，这意味着只有底层数据的一个副本会消失——其他地方可能还有其他副本。但是，当最后一份副本消失时，也就是指向类实例的最后一个常量或变量被销毁时，底层数据也会被销毁，它所使用的内存会返回系统。</p>



<p>为了演示这一点，我们可以创建一个类，使用初始化器和去初始化器在创建和销毁时打印一条信息：</p>



<pre class="wp-block-code"><code>class User {
    let id: Int

    init(id: Int) {
        self.id = id
        print("User \(id): I'm alive!")
    }

    deinit {
        print("User \(id): I'm dead!")
    }
}
</code></pre>



<p>现在，我们可以使用循环快速创建和销毁用户实例——如果我们在循环内创建了一个用户实例，当循环迭代结束时，它将被销毁：</p>



<pre class="wp-block-code"><code>for i in 1...3 {
    let user = User(id: i)
    print("User \(user.id): I'm in control!")
}
</code></pre>



<p>当代码运行时，你会看到它分别创建和销毁了每个用户，其中一个用户甚至在另一个用户创建之前就被完全销毁了。</p>



<p>请记住，只有在类实例的最后一个引用被销毁时，才会调用去初始化器。这可能是你藏起来的变量或常量，也可能是你存储在数组中的东西。</p>



<p>例如，如果我们在创建 User 实例时添加了它们，那么只有在数组被清除时它们才会被销毁：</p>



<pre class="wp-block-code"><code>var users = &#91;User]()

for i in 1...3 {
    let user = User(id: i)
    print("User \(user.id): I'm in control!")
    users.append(user)
}

print("Loop is finished!")
users.removeAll()
print("Array is clear!")
</code></pre>



<h3 class="wp-block-heading">为什么类有去初始化器，而结构体没有？</h3>



<p>类的一个微小但重要的特性是，它们可以有一个去初始化函数——与 init() 相对应，在类实例被销毁时运行。结构体没有去初始化函数，这意味着我们无法知道结构体何时被销毁。</p>



<p>去初始化器的工作就是告诉我们类实例何时被销毁。对于结构体来说，这一点相当简单：当结构体的所有者不再存在时，结构体就会被销毁。因此，如果我们在一个方法中创建了一个结构体，并且该方法结束了，那么结构体就被销毁了。</p>



<p>然而，类具有复杂的复制行为，这意味着程序的不同部分可能存在多个类的副本。所有副本都指向相同的底层数据，因此现在很难判断实际的类实例何时销毁，即指向它的最终变量何时消失。</p>



<p>在幕后，Swift 会执行一种称为自动引用计数（ARC）的功能。ARC 会跟踪每个类实例的副本数量：每复制一个类实例，Swift 就会在其引用计数上加 1；每销毁一个副本，Swift 就会在其引用计数上减 1。当引用计数为 0 时，意味着没有人再引用该类，Swift 将调用去初始化器并销毁对象。</p>



<p>因此，结构体没有去初始化器的原因很简单，因为它们不需要去初始化器：每个结构体都有自己的数据副本，因此在销毁时不需要发生任何特殊情况。</p>



<p>你可以将去初始化器放在代码的任何地方，但代码应该读起来像句子，这让我觉得我的类应该读起来像章节。因此，去初始化器应该放在最后，它是类的 &#8220;结尾&#8221;。</p>



<h2 class="wp-block-heading">如何在类内使用变量</h2>



<p>Swift 类的工作原理有点像路标：我们拥有的每个类实例副本实际上都是指向相同底层数据的路标。这主要是因为改变一个副本会改变所有其他副本，但这也与类如何处理变量属性有关。</p>



<p>这一小段代码演示了事情是如何运作的：</p>



<pre class="wp-block-code"><code>class User {
    var name = "Paul"
}

let user = User()
user.name = "Taylor"
print(user.name)

</code></pre>



<p>这样就创建了一个常量 User 实例，但随后又对其进行了更改，即更改了常量值。这很糟糕，对吗？</p>



<p>但它根本没有改变常量值。是的，类中的数据发生了变化，但类实例本身——我们创建的对象——并没有变化，事实上也无法改变，因为我们将其设置为常量。</p>



<p>试想一下：我们创建了一个指向用户的恒定标识点，但我们删除了该用户的姓名标签，并写入了一个不同的姓名。这个用户并没有改变——他仍然存在——但其内部数据的一部分发生了改变。</p>



<p>现在，如果我们使用 let 将 name 属性设为常量，那么它就不会被更改——我们有了一个指向用户的常量路标，但我们用永久性墨水写下了他们的名字，这样它就不会被擦除。</p>



<p>相比之下，如果我们把用户实例和用户名都设为属性变量，会发生什么呢？现在，我们可以更改属性，但如果我们愿意，也可以改成一个全新的用户实例。继续用路标做比喻，这就好比把路标转向完全不同的人。</p>



<p>用这段代码试试看：</p>



<pre class="wp-block-code"><code>class User {
    var name = "Paul"
}

var user = User()
user.name = "Taylor"
user = User()
print(user.name)
</code></pre>



<p>这最终会打印出 &#8220;Paul&#8221;，因为即使我们将名字改为 &#8220;Taylor&#8221;，我们也会用一个新的用户对象覆盖整个用户对象，将其重置为 &#8220;Paul&#8221;。</p>



<p>最后一种变体是拥有一个可变实例和常量属性，这意味着我们可以创建一个新的用户，但一旦创建完成，我们就无法更改其属性。</p>



<p>因此，我们有四种选择：</p>



<ol class="wp-block-list">
<li>恒定实例，恒定属性——指向同一个用户的路标，用户名始终不变。</li>



<li>恒定实例，可变属性——指路牌始终指向同一个用户，但其名称可以改变。</li>



<li>可变实例，常量属性——指路牌可以指向不同的用户，但其名称永远不会改变。</li>



<li>可变实例，可变属性——指路牌可以指向不同的用户，而且这些用户的名称也可以改变。</li>
</ol>



<p>这看起来可能非常令人困惑，甚至有些迂腐。不过，由于类实例的共享方式，它的作用非常重要。</p>



<p>假设你得到了一个 User 实例。你的实例是常量，但其中的属性被声明为变量。这不仅告诉你，如果你想改变该属性，你就可以改变它，更重要的是告诉你，该属性有可能在其他地方被改变——你的这个类可能是从其他地方复制过来的，由于该属性是变量，这意味着其他代码部分可能会出其不意地改变它。</p>



<p>当你看到常量属性时，这意味着你可以确保当前代码或程序的任何其他部分都不会改变它，但一旦你要处理的是变量属性（无论类实例本身是否常量），就意味着数据有可能在你的脚下发生变化。</p>



<p>这一点与结构体不同，因为恒定的结构体即使属性是可变的，也不能改变其属性。希望你现在能明白为什么会发生这种情况：结构体没有路标，它们直接保存数据。这就意味着，如果你试图改变结构体内部的值，你也会隐式地改变结构体本身，而这是不可能的，因为结构体是常量。</p>



<p>这样做的一个好处是，类在使用改变数据的方法时不需要使用 mutating 关键字。这个关键字对于结构体来说非常重要，因为常量结构体无论如何创建，其属性都不能改变，所以当 Swift 看到我们在一个常量结构体实例上调用一个突变方法时，它就知道这是不允许的。</p>



<p>对于类来说，实例本身是如何创建的不再重要——决定属性是否可以修改的唯一因素是属性本身是否创建为常量。Swift 可以通过查看创建属性的方式来了解这一点，因此不再需要对方法进行特殊标记。</p>



<h3 class="wp-block-heading">为什么常量类中的变量属性可以更改？</h3>



<p>结构体和类之间一个微小但重要的区别是它们处理属性可变性的方式：</p>



<ul class="wp-block-list">
<li>可变类可以更改可变属性</li>



<li>常量类可更改变量属性</li>



<li>可变结构体可以更改可变属性</li>



<li>常量结构体不能更改变量属性</li>
</ul>



<p>原因在于类和结构体之间的根本区别：一个指向内存中的某些数据，而另一个是一个值，如数字 5。</p>



<p>请看下面的代码：</p>



<pre class="wp-block-code"><code>var number = 5
number = 6
</code></pre>



<p>我们不能简单地将数字 5 定义为 6，因为这样做没有意义，会破坏我们对数学的一切认知。相反，这段代码删除了分配给数字的现有值，取而代之的是数字 6。</p>



<p>这就是 Swift 中结构体的工作原理：当我们改变结构体的某个属性时，实际上是在改变整个结构体。当然，Swift 可以在幕后进行一些优化，这样每次我们只更改其中一部分时，它就不会真正丢弃整个值，但从我们的角度来看，它就是这样处理的。</p>



<p>因此，如果更改结构体的一部分实际上意味着销毁并重新创建整个结构体，那么希望你能明白为什么常量结构体不允许更改其变量属性——这意味着销毁并重新创建本应是常量的东西，而这是不可能的。</p>



<p>类不是这样工作的：你可以改变其属性的任何部分，而不必销毁和重新创建值。因此，常量类可以自由更改其可变属性。</p>



<h2 class="wp-block-heading">总结</h2>



<p>类并不像结构体那样常用，但它们在共享数据方面有着不可估量的作用，如果你选择学习苹果较早的 UIKit 框架，你会发现自己会大量使用它们。</p>



<p>让我们回顾一下所学内容：</p>



<ul class="wp-block-list">
<li>类与结构体有很多共同点，包括都能拥有属性和方法，但类与结构体有五大不同之处。</li>



<li>首先，类可以从其他类继承，这意味着类可以访问父类的属性和方法。如果需要，你可以选择覆盖子类中的方法，或者将一个类标记为最终类，以阻止其他人子类化它。</li>



<li>其次，Swift 不会为类生成成员初始化器，因此你需要自己动手。如果一个子类有自己的初始化器，它必须在某个时刻调用父类的初始化器。</li>



<li>第三，如果你创建了一个类实例，然后复制了它，那么所有这些副本都会指向同一个实例。这意味着改变其中一个副本中的某些数据会改变所有副本。</li>



<li>第四，类可以有去初始化器，当一个实例的最后一个副本被销毁时，去初始化器就会运行。</li>



<li>最后，无论实例本身是否作为变量创建，类实例内部的变量属性都可以更改。</li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://beautylife-studio.top/2024/tech/1508/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Learning Swift &#038; SwiftUI Chapter 11</title>
		<link>https://beautylife-studio.top/2024/tech/1399/</link>
					<comments>https://beautylife-studio.top/2024/tech/1399/#respond</comments>
		
		<dc:creator><![CDATA[Alan C.]]></dc:creator>
		<pubDate>Sat, 18 May 2024 04:38:33 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[Learning Swift & SwiftUI]]></category>
		<guid isPermaLink="false">https://beautylife-studio.top/?p=1399</guid>

					<description><![CDATA[结构体 Part 2]]></description>
										<content:encoded><![CDATA[
<p>正如你所看到的，结构体让我们可以将单个数据片段组合成新的东西，然后附加方法以便我们可以操作这些数据。</p>



<p>今天，我们将学习结构体的一些更高级的特性，包括静态属性和访问控制，它们使结构体变得更加强大。</p>



<p>在 Swift 中隐藏对某些属性和方法的访问实际上可以让我们的代码变得更好，因为能够访问它的地方更少了。</p>



<p>需要提醒大家的是，这两样东西在 SwiftUI 中都会被广泛使用，所以现在就花时间掌握它们是值得的，因为从你的第一个项目开始就会用到它们。</p>



<p>今天我们将学习两部分内容，在这两部分内容中，你将了解多级访问控制以及创建静态属性和方法的能力。现在让我们开始吧！</p>



<h2 class="wp-block-heading">如何使用访问控制限制对内部数据的访问</h2>



<p>默认情况下，Swift 的结构体允许我们自由访问其属性和方法，但这往往不是你想要的——有时你想要隐藏某些数据，防止外部访问。例如，你可能需要在访问属性之前应用某些逻辑，或者你知道某些方法需要以特定的方式或顺序调用，因此不应该被外部访问。</p>



<p>我们可以用一个结构示例来说明这个问题：</p>



<pre class="wp-block-code"><code>struct BankAccount {
    var funds = 0

    mutating func deposit(amount: Int) {
        funds += amount
    }

    mutating func withdraw(amount: Int) -&gt; Bool {
        if funds &gt;= amount {
            funds -= amount
            return true
        } else {
            return false
        }
    }
}
</code></pre>



<p>它有从银行账户存款和取款的方法，应该这样使用：</p>



<pre class="wp-block-code"><code>var account = BankAccount()
account.deposit(amount: 100)
let success = account.withdraw(amount: 200)

if success {
    print("Withdrew money successfully")
} else {
    print("Failed to get the money")
}
</code></pre>



<p>但属性funds只是从外部暴露给我们的，那么是什么阻止了我们直接接触它呢？答案是完全没有——这种代码是允许的：</p>



<pre class="wp-block-code"><code>account.funds -= 1000
</code></pre>



<p>这就完全绕过了我们为阻止人们取出比他们拥有的更多资金而设置的逻辑，现在我们的程序可能会以奇怪的方式运行。</p>



<p>为了解决这个问题，我们可以告诉 Swift，资金只能在结构体内部访问——通过属于结构体的方法以及任何计算属性、属性观察器等。</p>



<p>这只需要一个额外的单词 private：</p>



<pre class="wp-block-code"><code>private var funds = 0
</code></pre>



<p>现在，从结构体外部访问资金是不可能的，但在 deposit() 和 withdraw() 内却可以。如果你尝试从结构体外部读取或写入资金，Swift 将拒绝编译您的代码。</p>



<p>这就是所谓的访问控制，因为它控制了如何从结构体外部访问结构体的属性和方法。</p>



<p>Swift 为我们提供了多个选项，但在学习过程中，你只需要几个选项：</p>



<ul class="wp-block-list">
<li>使用 private 表示 &#8220;不要让结构体之外的任何东西使用它&#8221;。</li>



<li>使用 fileprivate 表示 &#8220;不允许当前文件以外的任何内容使用此结构&#8221;。</li>



<li>使用 public 表示 &#8220;允许任何人在任何地方使用它&#8221;。</li>
</ul>



<p>还有一个额外的选项有时对学习者很有用，那就是：private(set)。这意味着 &#8220;允许任何人读取此属性，但只允许我的方法写入它&#8221;。如果我们在 BankAccount 中使用了这种方法，就意味着我们可以在结构体之外打印 account.funds，但只有 deposit() 和 withdraw() 可以实际更改该值。</p>



<p>在这种情况下，我认为 private(set) 是资金的最佳选择：你可以随时读取当前的银行账户余额，但如果不通过我的逻辑，你就无法改变它。</p>



<p>仔细想想，访问控制其实就是限制你和你团队中其他开发人员的行为，这是明智之举！如果我们能让 Swift 本身阻止我们犯错，这总是明智之举。</p>



<p>重要提示：如果你对一个或多个属性使用私有访问控制，你很可能需要创建自己的初始化器。</p>



<h3 class="wp-block-heading">访问控制的意义何在？</h3>



<p>Swift 的访问控制关键字让我们可以限制如何访问代码的不同部分，但很多时候它只是在遵守我们制定的规则——如果我们愿意，我们可以移除这些规则，绕过限制，那又有什么意义呢？</p>



<p>答案有几个，但有一个特别简单，所以我就从这里说起：有时访问控制会用到你不拥有的代码中，所以你无法取消限制。例如，当你使用苹果公司的 API 构建应用程序时，这种情况就很常见：他们会限制你能做什么，不能做什么，你需要遵守这些限制。</p>



<p>当然，在你自己的代码中，你可以删除任何你设置的访问控制限制，但这并不意味着它毫无意义。访问控制可以让我们确定如何使用某个值，因此，如果需要非常小心地访问某些东西，那么你就必须遵守规则。</p>



<p>这里再举个例子，如下：</p>



<pre class="wp-block-code"><code>private var learnedSections = Set&lt;String&gt;()
</code></pre>



<p>它是私有的，这意味着没有人可以直接读取或写入它。于是需要提供用于读取或写入值的公共方法。这是有意为之，因为这个例子中学习章节需要做的不仅仅是向该集合中插入一个字符串，它还需要更新用户界面以反映变化，并需要保存新信息以便应用程序记住它已被学习。</p>



<p>如果没有将 learnedSections 属性设置为私有，就有可能忘记并直接向其中写入内容。这将导致用户界面与其数据不一致，而且也无法保存更改——总之是很糟糕的！</p>



<p>因此，通过使用私有属性，这里需要要求 Swift 为用户执行规则：不要从 User 结构之外的任何地方读取或写入该属性。</p>



<p>访问控制的另一个好处是，它可以让我们控制其他人如何查看我们的代码，也就是所谓的 &#8220;表面区域（surface area）&#8221;。试想一下：如果我给你一个结构体，其中有 30 个公共属性和方法，你可能无法确定哪些是你可以使用的，哪些是内部使用的。另一方面，如果我将其中的 25 个属性和方法标记为私有，那么你就会立刻明白，你不应该在外部使用它们。</p>



<p>访问控制可能是一个相当棘手的问题，尤其是当你考虑到外部代码时。因此，苹果公司自己的相关文档相当长也就不足为奇了——你可以在这里找到：<a href="https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html%E3%80%82"></a><a href="https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html%E3%80%82">https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html。</a></p>



<h2 class="wp-block-heading">静态属性和方法</h2>



<p>你已经看到了我们如何将属性和方法附加到结构体上，以及每个结构体如何拥有自己唯一的属性副本，这样调用结构体上的方法就不会读取同一类型中不同结构体的属性。</p>



<p>有时——只是有时——你想将属性或方法添加到结构体本身，而不是结构体的某个特定实例，这样就可以直接使用它们。我们会在 SwiftUI 中经常使用这种技术来做两件事：创建示例数据和存储需要在不同地方访问的固定数据。</p>



<p>首先，我们来看一个如何创建和使用静态属性和方法的简化示例：</p>



<pre class="wp-block-code"><code>struct School {
    static var studentCount = 0

    static func add(student: String) {
        print("\(student) joined the school.")
        studentCount += 1
    }
}
</code></pre>



<p>注意其中的关键字 static，这意味着 studentCount 属性和 add() 方法都属于 School 结构本身，而不是结构的单个实例。</p>



<p>要使用这段代码，我们可以写如下代码：</p>



<pre class="wp-block-code"><code>School.add(student: "Taylor Swift")
print(School.studentCount)
</code></pre>



<p>我没有创建学校的实例——我们可以直接在结构体上使用 add() 和 studentCount。这是因为它们都是静态的，这意味着它们并不唯一地存在于结构体的实例中。</p>



<p>这也解释了为什么我们可以修改 studentCount 属性而不将方法标记为mutating——只有在结构体的实例作为常量创建时才需要使用普通的结构体函数，而调用 add() 时并不存在实例。</p>



<p>如果要混合使用静态和非静态属性和方法，有两条规则：</p>



<ol class="wp-block-list">
<li>要从静态代码访问非静态代码&#8230;&#8230;你就不走运了：静态属性和方法不能引用非静态属性和方法，因为这根本说不通——你要引用的是school的哪个实例？</li>



<li>要从非静态代码访问静态代码，请始终使用你的类型名称，如 School.studentCount。你也可以使用 Self 来引用当前类型。</li>
</ol>



<p>现在我们有了 self 和 Self，它们的含义不同：self 指的是结构体的当前值，而 Self 指的是当前类型。</p>



<p>提示：我们很容易忘记 self 和 Self 之间的区别，但仔细想想，这就像 Swift 的其他命名方式一样——我们用大写字母（Int、Double、Bool 等）开始所有数据类型，所以 Self 也用大写字母开始也是合理的。</p>



<p>现在，你可以听到无数其他学习者在说：“这到底是为什么？”我明白&#8211;起初，这似乎是一个相当多余的功能。因此，我想向你展示使用静态数据的两种主要方式。</p>



<p>首先，使用静态属性来组织应用程序中的常用数据。例如，可能会使用 AppData 这样的结构来存储在许多地方使用的大量共享值：</p>



<pre class="wp-block-code"><code>struct AppData {
    static let version = "1.3 beta 2"
    static let saveFilename = "settings.json"
    static let homeURL = "https://beautylife-studio.top"
}
</code></pre>



<p>使用这种方法，我可以在任何需要检查或显示应用程序版本号的地方（关于屏幕、调试输出、日志信息、支持邮件等）读取 AppData.version。</p>



<p>常用静态数据的第二个原因是为了创建结构体的示例。正如你稍后将看到的，SwiftUI 的最佳工作方式是在开发过程中显示应用程序的预览，而这些预览通常需要示例数据。例如，如果你要在屏幕上显示一名员工的数据，你就需要在预览屏幕上显示一名员工的示例，这样你就可以在工作时检查所有数据是否正确。</p>



<p>最好使用结构体上的静态示例属性来实现这一点，如下所示：</p>



<pre class="wp-block-code"><code>struct Employee {
    let username: String
    let password: String

    static let example = Employee(username: "cfederighi", password: "hairforceone")
}
</code></pre>



<p>现在，当你需要在设计预览中使用 Employee 实例时，只需使用 Employee.example 就可以了。</p>



<p>正如我在开头所说，静态属性或静态方法只有在极少数情况下才有意义，但它们仍然是非常有用的工具。</p>



<h3 class="wp-block-heading">Swift 中的静态属性和方法有什么用？</h3>



<p>大多数学习 Swift 的人一眼就能看出常规属性和方法的价值，但却很难理解为什么静态属性和方法会有用。当然，它们确实不如常规属性和方法有用，但它们在 Swift 代码中仍然相当常见。</p>



<p>静态属性和方法的一个常见用途是存储你在整个应用程序中使用的常用功能。例如，我制作了一款名为 UI HandBook 的应用程序，这是一款供学习 SwiftUI 的人使用的 iOS 应用程序，用户可以通过该应用程序自定义参数来获取预览效果和生成代码的内容。我想在应用程序中存储一些常用信息，例如应用程序在 App Store 上的 URL，这样我就可以在应用程序需要的地方引用这些信息。因此，我用这样的代码来存储数据：</p>



<pre class="wp-block-code"><code>struct UIHandBook {
    static let appURL = "https://apps.apple.com/app/ui-handbook/id6463360470"
}
</code></pre>



<p>这样，当有人分享应用中的内容时，我就可以写 UIHandBook.appURL，帮助其他人发现应用。如果没有 static 关键字，我就需要创建一个新的 UIHandBook 结构实例来读取固定的应用程序 URL，而这其实是没有必要的。</p>



<p>另一个例子是使用静态属性和静态方法在同一个结构体中存储一些随机熵，就像这样：</p>



<pre class="wp-block-code"><code>struct MathSimply {
    static var entropy = Int.random(in: 1...1000)

    static func getEntropy() -> Int {
        entropy += 1
        return entropy
    }
}
</code></pre>



<p>随机熵是软件为使随机数生成更有效而收集的一些随机性，但我在应用程序中做了一点手脚，因为我不想要真正随机的数据。该应用程序的设计是按照随机顺序为你提供各种 Swift 测试，但如果真的是随机的，那么你很可能有时会连续看到相同的问题。我不希望出现这种情况，所以我的熵实际上是让随机性变得不那么随机，这样我们就能得到更公平的问题分布。因此，这里的代码所做的就是存储一个熵整数，它一开始是随机的，但每次调用 getEntropy() 时都会递增 1。</p>



<p>这个 &#8220;公平随机 &#8220;的熵在整个应用程序中都会使用，这样就不会出现重复的问题，所以它们也是由 MathSimply 结构静态共享的，因此任何地方都可以访问它们。</p>



<p>在继续之前，我还想提两件大家可能感兴趣的事情。</p>



<p>首先，我的 MathSimply 结构其实根本不需要是结构体——我可以也应该将它声明为枚举而不是结构体。这是因为枚举没有任何实例，所以在这里它比结构体更好，因为我不想创建这个类型的实例——没有理由这样做。使用枚举可以避免这种情况的发生，从而有助于明确我的意图。</p>



<p>其次，因为我有一个专用的 getEntropy() 方法，所以我实际上要求 Swift 限制对entrogy的访问，这样我就不能从任何地方访问它。这就是所谓的访问控制，在 Swift 中看起来是这样的：</p>



<pre class="wp-block-code"><code>private static var entropy = Int.random(in: 1...1000)
</code></pre>



<p>我们很快就会详细学习访问控制。</p>



<h2 class="wp-block-heading">总结</h2>



<p>结构体在 Swift 中几乎随处可见： 字符串、Int、Double、数组甚至 Bool 都是以结构体的形式实现的，现在你可以识别出 isMultiple(of:) 这样的函数实际上是属于 Int 的方法。</p>



<p>让我们回顾一下我们还学到了什么：</p>



<ul class="wp-block-list">
<li>你可以通过编写 struct，为其命名，然后将结构体的代码放在大括号内来创建自己的结构体。</li>



<li>结构体可以有变量、常量（称为属性）和函数（称为方法）</li>



<li>如果方法试图修改结构体的属性，必须将其标记为mutating。</li>



<li>你可以将属性存储在内存中，也可以创建计算属性，在每次访问时计算一个值。</li>



<li>我们可以为结构体内部的属性附加 didSet 和 willSet 属性观察器，当我们需要确保某些代码总是在属性发生变化时被执行时，这就很有用了。</li>



<li>初始化器有点像专用函数，Swift 会使用所有结构体的属性名称为其生成一个初始化器。</li>



<li>你可以根据需要创建自己的自定义初始化器，但必须确保在初始化器结束时，结构体中的所有属性都有一个值，然后再调用其他方法。</li>



<li>我们可以根据需要，使用访问权限从外部将任何属性和方法标记为可用或不可用。</li>



<li>我们可以将属性或方法直接附加到结构体上，这样就可以在不创建结构体实例的情况下使用它们。</li>
</ul>



<h2 class="wp-block-heading">思考</h2>



<p>结构体是SwiftUI应用程序的核心，所以我们必须花费大量的时间来确保了解它的作用和工作方式。</p>



<p>所以这里设置一个思考题，我们一起来想一下：创建一个结构体来存储汽车的信息，包括汽车的品牌、型号、座位数、当前档位，然后添加一个方法来升档或降档。考虑一下变量和访问控制：哪些数据应该是变量而不是常量，哪些数据应该公开？换挡方法是否应该以某种方式验证其输入？</p>



<p>这里有一些提示：</p>



<ol class="wp-block-list">
<li>汽车的品牌和型号以及座位数，一旦生成就不会改变了，因此可以保持不变（常量）。</li>



<li>但当前档位显然可以发生变化，因此可以将其作为变量。增减档位应确保这种变化是可能的——例如：汽车没有0档，但是有N档和R档，且最大档位一般也不会超过10。</li>



<li>如果使用私有访问控制，可能还需要创建自己的初始化程序。</li>



<li>记住在改变属性的方法中使用mutating关键字。</li>
</ol>



<p>此问题没有标准答案，所以大家只要是可行的代码就是正确的。大家试试看吧！</p>
]]></content:encoded>
					
					<wfw:commentRss>https://beautylife-studio.top/2024/tech/1399/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Learning Swift &#038; SwiftUI Chapter 10</title>
		<link>https://beautylife-studio.top/2024/tech/1375/</link>
					<comments>https://beautylife-studio.top/2024/tech/1375/#respond</comments>
		
		<dc:creator><![CDATA[Alan C.]]></dc:creator>
		<pubDate>Mon, 29 Apr 2024 01:17:29 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[Learning Swift & SwiftUI]]></category>
		<guid isPermaLink="false">https://beautylife-studio.top/?p=1375</guid>

					<description><![CDATA[结构体 Part 1]]></description>
										<content:encoded><![CDATA[
<p>上一章我们刚刚学习了闭包，这是一个很难的内容，然而你还能回来继续学习Swift。说真的，这值得尊敬！</p>



<p>我有一些好消息要告诉你。首先，在接下来的几章里，你不仅可以不去想闭包，而且一旦你休息了一段时间，我们就会开始在真正的 iOS 项目中实践它们。因此，即使你还不完全清楚它们是如何工作的，或者为什么需要它们，一切都会变得明朗起来——坚持下去！</p>



<p>总之，今天的主题是结构体。结构体是 Swift 让我们从多个小类型中创建自己的数据类型的方法之一。例如，你可以把三个字符串和一个布尔值放在一起，然后说这代表了应用程序中的一个用户。事实上，Swift 本身的大多数类型都是以结构体的形式实现的，包括字符串、Int、布尔、数组等。</p>



<p>更重要的是，结构体在 SwiftUI 中极为常见，因为我们设计的每一块 UI 都是基于结构体构建的，内部有大量的结构体。结构体并不难学，但公平地说，在闭包之后，几乎所有东西都变得简单了！</p>



<p>今天我们将学习四个大内容，在这四个大内容中，你将认识自定义结构体、计算属性、属性观察器等。现在就让我们开始吧！</p>



<h2 class="wp-block-heading">1. 如何创建你自己的结构体</h2>



<p>通过 Swift 的结构体，我们可以创建自己的自定义复杂数据类型，并拥有自己的变量和函数。</p>



<p>一个简单的结构体是这样的：</p>



<pre class="wp-block-code"><code>struct Album {
    let title: String
    let artist: String
    let year: Int

    func printSummary() {
        print("\(title) (\(year)) by \(artist)")
    }
}
</code></pre>



<p>这样就创建了一个名为 &#8220;专辑 &#8220;的新类型，其中包含两个名为标题和艺术家的字符串常量，以及一个名为年份的整数常量。我还添加了一个简单的函数，用于总结三个常量的值。</p>



<p>注意到 Album 是如何以大写字母 A 开头的吗？这是 Swift 的标准，我们一直在使用它——想想 String、Int、Bool、Set 等等。在引用数据类型时，我们使用驼峰式大小写，即第一个字母大写；但在引用类型内部的内容时，例如变量或函数，我们使用驼峰式大小写，即第一个字母小写。请记住，在大多数情况下，这只是一个惯例，而不是规则，但坚持使用很有帮助。</p>



<p>此时，Album 就像 String 或 Int 一样——我们可以创建它们、赋值、复制它们等等。例如，我们可以创建几个相册，然后打印它们的一些值并调用它们的函数：</p>



<pre class="wp-block-code"><code>let red = Album(title: "Red", artist: "Taylor Swift", year: 2012)
let wings = Album(title: "Wings", artist: "BTS", year: 2016)

print(red.title)
print(wings.artist)

red.printSummary()
wings.printSummary()
</code></pre>



<p>请注意我们是如何像调用函数一样创建新相册的——我们只需按照定义的顺序为每个常量提供值即可。</p>



<p>正如你所看到的，red和wings来自同一个 Album 结构，但一旦我们创建了它们，它们就会像创建两个字符串一样分开。</p>



<p>当我们在每个结构体上调用 printSummary() 时，就可以看到这一点，因为该函数会引用标题、艺术家和年份。在这两种情况下，每个结构体都会打印出正确的值：red 打印出 &#8220;Red (2012) by Taylor Swift&#8221;，wings 打印出 &#8220;Wings (2016) by BTS&#8221;——Swift明白，当对 red 调用 printSummary() 时，它应该使用同样属于 red 的标题、艺术家和年份常量。</p>



<p>更有趣的地方在于，当你想让值发生变化时。例如，我们可以创建一个可以根据需要休假的 Employee 结构：</p>



<pre class="wp-block-code"><code>struct Employee {
    let name: String
    var vacationRemaining: Int

    func takeVacation(days: Int) {
        if vacationRemaining > days {
            vacationRemaining -= days
            print("I'm going on vacation!")
            print("Days remaining: \(vacationRemaining)")
        } else {
            print("Oops! There aren't enough days remaining.")
        }
    }
}
</code></pre>



<p>然而，这实际上是行不通的——Swift 会拒绝构建代码。</p>



<p>要知道，如果我们使用 let 将雇员创建为常量，Swift 就会将雇员及其所有数据设为常量——我们可以正常调用函数，但这些函数不允许更改结构体的数据，因为我们将其设为常量了。</p>



<p>因此，Swift 让我们多做了一步：任何只读取数据的函数都没有问题，但任何改变结构体数据的函数都必须使用特殊的 mutating 关键字标记，就像下面这样：</p>



<pre class="wp-block-code"><code>mutating func takeVacation(days: Int) {
</code></pre>



<p>现在，我们的代码可以正常构建，但 Swift 会阻止我们在常量结构体中调用 takeVacation()。</p>



<p>在代码中，这是允许的：</p>



<pre class="wp-block-code"><code>var archer = Employee(name: "Sterling Archer", vacationRemaining: 14)
archer.takeVacation(days: 5)
print(archer.vacationRemaining)
</code></pre>



<p>但如果将 var archer 改为 let archer，你会发现 Swift 拒绝再次构建你的代码——我们试图在一个常量结构体上调用一个mutating函数，这是不允许的。</p>



<p>我们将在接下来的几章中详细探讨结构体，但首先我想给一些东西命名。</p>



<ul class="wp-block-list">
<li>属于结构体的变量和常量称为属性（properties）。</li>



<li>属于结构体的函数称为方法（methods）。</li>



<li>当我们从结构体中创建常量或变量时，我们称其为实例（instance）——例如，你可能会为 Album 结构体创建十几个独特的实例。</li>



<li>当我们创建结构体实例时，我们会使用初始化器（initializer），如下所示： Album(title: &#8220;Wings&#8221;, artist: &#8220;BTS&#8221;, year: 2016)。</li>
</ul>



<p>最后一个初始化器乍看起来可能有点奇怪，因为我们是把结构体当作函数来处理并传递参数。这就是所谓的 &#8220;语法糖（syntactic sugar）&#8221;&#8211;Swift 会在结构体内部默默创建一个名为 init() 的特殊函数，并使用结构体的所有属性作为参数。然后，它会自动将这两段代码视为相同的代码：</p>



<pre class="wp-block-code"><code>var archer1 = Employee(name: "Sterling Archer", vacationRemaining: 14)
var archer2 = Employee.init(name: "Sterling Archer", vacationRemaining: 14)
</code></pre>



<p>实际上，我们以前就依赖于这种行为。早在我们第一次学习 Double 时，我就解释过不能将 Int 和 Double 相加，而需要写这样的代码：</p>



<pre class="wp-block-code"><code>let a = 1
let b = 2.0
let c = Double(a) + b
</code></pre>



<p>现在你可以看到这里到底发生了什么： Swift 自己的 Double 类型是作为结构体实现的，它有一个接受整数的初始化函数。</p>



<p>Swift 生成初始化函数的方式非常智能，如果我们为属性赋值，它甚至会插入默认值。</p>



<p>例如，如果我们的结构体有以下两个属性：</p>



<pre class="wp-block-code"><code>let name: String
var vacationRemaining = 14
</code></pre>



<p>然后，Swift 会静默地生成一个初始化程序，将 vacationRemaining 的默认值设为 14，从而使这两个值都有效：</p>



<pre class="wp-block-code"><code>let kane = Employee(name: "Lana Kane")
let poovey = Employee(name: "Pam Poovey", vacationRemaining: 35)
</code></pre>



<p>提示：如果为常量属性指定默认值，该值将从初始化器中完全删除。如果要指定默认值，但又留有在需要时覆盖它的可能性，可以使用变量属性。</p>



<h3 class="wp-block-heading">结构体与元组之间有何区别？</h3>



<p>Swift 的 tuples 可以让我们在单个变量中存储多个不同名称的值，而 struct 也有类似的功能，那么它们有什么区别，什么时候应该选择其中一个而不是另一个呢？</p>



<p>刚开始学习时，区别很简单：tuple 实际上就是一个没有名字的结构体，就像匿名结构体一样。这意味着你可以将它定义为（name: String, age: Int, city: String），它将做与下面结构体相同的事情：</p>



<pre class="wp-block-code"><code>struct User {
    var name: String
    var age: Int
    var city: String
}
</code></pre>



<p>但是，元组有一个问题：虽然元组非常适合一次性使用，尤其是当你想从一个函数中返回多个数据时，但反复使用元组会很烦人。</p>



<p>试想一下：如果你有几个处理用户信息的函数，你会愿意这样写吗？</p>



<pre class="wp-block-code"><code>func authenticate(_ user: User) { ... }
func showProfile(for user: User) { ... }
func signOut(_ user: User) { ... }
</code></pre>



<p>或者这样：</p>



<pre class="wp-block-code"><code>func authenticate(_ user: (name: String, age: Int, city: String)) { ... }
func showProfile(for user: (name: String, age: Int, city: String)) { ... }
func signOut(_ user: (name: String, age: Int, city: String)) { ... }
</code></pre>



<p>想一想在 User 结构体中添加一个新属性有多难（确实非常容易），而在元组中添加一个值又有多难？(非常困难，而且容易出错！）。</p>



<p>因此，当你想从函数中返回两个或多个任意值时，请使用元组，而当您想多次发送或接收某些固定数据时，请使用结构体。</p>



<h3 class="wp-block-heading">函数与方法之间有何区别？</h3>



<p>Swift 的函数可以让我们命名一段功能并重复运行它，而 Swift 的方法做的也是同样的事情，那么它们有什么区别呢？</p>



<p>老实说，唯一真正的区别在于，方法属于结构体、枚举和类等类型，而函数不属于。仅此而已，这就是唯一的区别。两者都可以接受任意数量的参数，包括可变参数，并且都可以返回值。它们如此相似，以至于 Swift 仍然使用 func 关键字来定义方法。</p>



<p>当然，与结构体等特定类型相关联意味着方法获得了一个重要的超级能力：它们可以引用该类型中的其他属性和方法，这意味着您可以为 User 类型编写一个 describe() 方法，打印用户的姓名、年龄和城市。</p>



<p>方法还有一个非常微妙的优点：方法可以避免命名空间污染。每当我们创建一个函数，该函数的名称就开始在我们的代码中产生意义——我们可以编写 wakeUp() 并让它做一些事情。因此，如果编写 100 个函数，就会有 100 个保留名；如果编写 1000 个函数，就会有 1000 个保留名。</p>



<p>这种情况很快就会变得一团糟，但通过将功能放到方法中，我们就限制了这些名称的使用范围——除非我们特别编写 someUser.wakeUp()，否则 wakeUp() 就不再是保留名称了。这就减少了所谓的污染，因为如果我们的大部分代码都放在方法中，就不会意外出现名称冲突。</p>



<h3 class="wp-block-heading">为什么我们需要为某些方法标记mutating？</h3>



<p>修改结构体的属性是可能的，但前提是该结构体是作为变量创建的。当然，在你的结构体内部，我们无法判断你使用的是可变结构体还是常量结构体，因此 Swift 提供了一个简单的解决方案：当结构体的方法试图更改任何属性时，你必须将其标记为突变（mutating）。</p>



<p>除了将方法标记为突变外，你不需要做其他任何事情，但这样做会给 Swift 足够的信息来阻止该方法用于常量结构体实例。</p>



<p>有两个重要细节你会发现很有用：</p>



<ul class="wp-block-list">
<li>将方法标记为突变将阻止该方法在常量结构体上被调用，即使该方法本身实际上没有改变任何属性。如果你说它改变了什么，Swift 就会相信你！</li>



<li>未标记为突变的方法不能调用突变函数——你必须将它们都标记为突变。</li>
</ul>



<h2 class="wp-block-heading">2. 如何动态地计算属性值</h2>



<p>结构体可以有两种属性：存储属性是一个变量或常量，用于保存结构体实例中的数据；计算属性则在每次访问时动态计算属性值。这意味着计算属性是存储属性和函数的混合体：它们的访问方式类似于存储属性，但工作方式类似于函数。</p>



<p>举个例子，以前我们有一个 Employee 结构，可以跟踪该员工还剩多少天假期。下面是一个简化版本：</p>



<pre class="wp-block-code"><code>struct Employee {
    let name: String
    var vacationRemaining: Int
}

var archer = Employee(name: "Sterling Archer", vacationRemaining: 14)
archer.vacationRemaining -= 5
print(archer.vacationRemaining)
archer.vacationRemaining -= 3
print(archer.vacationRemaining)
</code></pre>



<p>这虽然是一个微不足道的结构，但我们却失去了宝贵的信息——我们给这名员工分配了 14 天的假期，然后在他休假时减去这些天数，但这样我们就失去了他们最初获得的假期天数。</p>



<p>我们可以将其调整为使用计算属性，例如</p>



<pre class="wp-block-code"><code>struct Employee {
    let name: String
    var vacationAllocated = 14
    var vacationTaken = 0

    var vacationRemaining: Int {
        vacationAllocated - vacationTaken
    }
}
</code></pre>



<p>现在，我们不再直接为 vacationRemaining 赋值，而是通过从分配的假期中减去已使用的假期来计算它。</p>



<p>当我们读取 vacationRemaining 时，它看起来就像一个普通的存储属性：</p>



<pre class="wp-block-code"><code>var archer = Employee(name: "Sterling Archer", vacationAllocated: 14)
archer.vacationTaken += 4
print(archer.vacationRemaining)
archer.vacationTaken += 4
print(archer.vacationRemaining)
</code></pre>



<p>这是一个非常强大的功能：我们读取的是一个看起来像属性的东西，但在幕后，Swift 正在运行一些代码来计算它每次的值。</p>



<p>但我们无法写入它，因为我们还没有告诉 Swift 应该如何处理。为了解决这个问题，我们需要同时提供一个 getter 和一个 setter，它们分别是 &#8220;读取代码 &#8220;和 &#8220;写入代码 &#8220;的别致名称。</p>



<p>在本例中，getter 非常简单，因为它只是我们现有的代码。但 setter 就比较有趣了——如果你设置了某个员工的 vacationRemaining，那么你是希望他的 vacationAllocated 值增加还是减少呢？</p>



<p>我假定这两种情况中的第一种是正确的，在这种情况下，属性会是这样的：</p>



<pre class="wp-block-code"><code>var vacationRemaining: Int {
    get {
        vacationAllocated - vacationTaken
    }

    set {
        vacationAllocated = vacationTaken + newValue
    }
}
</code></pre>



<p>请注意 get 和 set 如何在读取或写入值时标记要运行的单个代码片段。更重要的是，请注意 newValue——这是 Swift 自动提供给我们的，它存储了用户试图为属性赋值的任何值。</p>



<p>有了 getter 和 setter，我们现在就可以修改 vacationRemaining 了：</p>



<pre class="wp-block-code"><code>var archer = Employee(name: "Sterling Archer", vacationAllocated: 14)
archer.vacationTaken += 4
archer.vacationRemaining = 5
print(archer.vacationAllocated)
</code></pre>



<p>SwiftUI 广泛使用计算属性——你将在创建的第一个项目中就看到它们！</p>



<h3 class="wp-block-heading">什么时候应该使用计算属性或存储属性？</h3>



<p>属性可以让我们将信息附加到结构体上，Swift 提供了两种不同的属性：存储属性和计算属性，前者是将值存储在内存中，以备日后使用，而后者则是每次调用时都会重新计算值。在幕后，计算属性实际上只是一个函数调用，碰巧属于你的结构体。</p>



<p>决定使用哪种属性，部分取决于属性值是否依赖于其他数据，部分也取决于性能。性能部分很简单：如果你经常在属性值未发生变化时读取属性，那么使用存储属性要比使用计算属性快得多。另一方面，如果属性很少被读取，甚至根本不会被读取，那么使用计算属性就可以省去计算其值并将其存储在某个地方的麻烦。</p>



<p>当涉及到依赖关系（即属性值是否依赖于其他属性的值）时，情况就会发生逆转：这正是计算属性的用武之地，因为你可以确保它们返回的值总是考虑到最新的程序状态。</p>



<p>懒惰属性（Lazy properties）有助于缓解很少读取存储属性的性能问题，而属性观察者（property observers）则可以缓解存储属性的依赖性问题——我们很快就会学习它们。</p>



<h2 class="wp-block-heading">3. 如何在属性发生变化时采取行动？</h2>



<p>Swift 允许我们创建属性观察者，它们是在属性发生变化时运行的特殊代码。它们有两种形式：一种是在属性刚发生变化时运行的 didSet 观察器，另一种是在属性发生变化之前运行的 willSet 观察器。</p>



<p>要了解为什么需要属性观察者，请想想这样的代码：</p>



<pre class="wp-block-code"><code>struct Game {
    var score = 0
}

var game = Game()
game.score += 10
print("Score is now \(game.score)")
game.score -= 3
print("Score is now \(game.score)")
game.score += 1
</code></pre>



<p>这将创建一个 Game 结构，并修改其分数若干次。每次分数发生变化时，都会有一行 print() 跟随，这样我们就能跟踪分数的变化。但有一个错误：在最后，分数改变了，但没有被打印出来，这是一个错误。</p>



<p>有了属性观察器，我们就可以解决这个问题，使用 didSet 将 print() 调用直接附加到属性上，这样无论何时何地，只要它发生变化，我们就会运行一些代码。</p>



<p>下面是同样的示例，现在使用了属性观察器：</p>



<pre class="wp-block-code"><code>struct Game {
    var score = 0 {
        didSet {
            print("Score is now \(score)")
        }
    }
}

var game = Game()
game.score += 10
game.score -= 3
game.score += 1
</code></pre>



<p>如果你需要，Swift 会在 didSet 中自动提供常量 oldValue，以防你需要根据改变的内容自定义功能。还有一个 willSet 变体，可以在属性发生变化之前运行一些代码，进而提供将分配的新值，以备你根据该值采取不同的操作。</p>



<p>我们可以使用一个代码示例来演示所有这些功能，示例将在值发生变化时打印信息，以便你可以看到代码运行时的流程：</p>



<pre class="wp-block-code"><code>struct App {
    var contacts = &#91;String]() {
        willSet {
            print("Current value is: \(contacts)")
            print("New value will be: \(newValue)")
        }

        didSet {
            print("There are now \(contacts.count) contacts.")
            print("Old value was \(oldValue)")
        }
    }
}

var app = App()
app.contacts.append("Adrian E")
app.contacts.append("Allen W")
app.contacts.append("Ish S")
</code></pre>



<p>是的，向数组追加会同时触发 willSet 和 didSet，因此运行该代码时会打印大量文本。</p>



<p>在实践中，willSet 比 didSet 用得少得多，但你可能还是会时不时地看到它，所以知道它的存在很重要。无论你选择哪种方法，都请尽量避免在属性观察器中投入过多的工作——如果 game.score += 1 这样看似微不足道的事情触发了高强度的工作，就会经常让你措手不及，并导致各种性能问题。</p>



<h3 class="wp-block-heading">什么时候你应该使用属性观察器？</h3>



<p>Swift 的属性观察器让我们可以分别使用 willSet 和 didSet 来附加功能，以便在属性更改之前或之后运行。大多数情况下，属性观察器并不是必需的，只是很好用——我们可以正常更新属性，然后在代码中调用函数。那什么时候会用到属性观察器呢？</p>



<p>最重要的原因是方便：使用属性观察器意味着只要属性发生变化，你的功能就会被执行。当然，你也可以使用函数来做到这一点，但你会记得在改变属性的每一个地方都使用函数吗？</p>



<p>这就是函数方法的问题所在：每当属性发生变化时，你都要记得调用该函数，如果你忘记了，你的代码中就会出现神秘的错误。另一方面，如果你使用 didSet 将功能直接附加到属性上，它就会一直发生，而且你可以将确保这一点的工作移交给 Swift，这样你的大脑就可以专注于更有趣的问题。</p>



<p>有一个地方使用属性观察者是个坏主意，那就是如果你在其中添加了缓慢的工作。如果你有一个年龄为整数的 User 结构，你会希望年龄的变化立即发生，毕竟这只是一个数字。如果你附加了一个 didSet 属性观察器，该观察器会执行各种缓慢的工作，那么突然改变一个整数所需的时间可能会比你预期的要长，这会给你带来各种问题。</p>



<h3 class="wp-block-heading">什么情况下应该使用 willSet 而不是 didSet？</h3>



<p>willSet 和 didSet 都允许我们将观察器附加到属性上，这意味着当属性发生变化时，Swift 会运行一些代码，以便我们有机会对变化做出响应。问题是：你是想在属性发生变化之前知道，还是在属性发生变化之后知道？</p>



<p>答案很简单：大多数情况下，你都会使用 didSet，因为我们希望在发生变化后采取行动，以便更新用户界面、保存更改等。这并不意味着 willSet 没有用处，只是在实际应用中，它远不如同类产品受欢迎。</p>



<p>最常使用 willSet 的情况是，当你需要在更改之前知道程序的状态时。例如，SwiftUI 会在某些地方使用 willSet 来处理动画，这样它就能在更改前获取用户界面的快照。当它同时拥有 &#8220;更改前 &#8220;和 &#8220;更改后 &#8220;的快照时，就可以将两者进行比较，从而查看用户界面中需要更新的所有部分。</p>



<h2 class="wp-block-heading">4. 如何创建自定义初始化器</h2>



<p>初始化器是专门用于准备新结构体实例的方法。你已经看到 Swift 如何根据我们在结构体中放置的属性为我们默默生成初始化器，但你也可以创建自己的初始化器，只要遵循一条黄金法则：初始化器结束时，所有属性都必须有一个值。</p>



<p>让我们从 Swift 默认的结构体初始化器开始：</p>



<pre class="wp-block-code"><code>struct Player {
    let name: String
    let number: Int
}

let player = Player(name: "Megan R", number: 15)
</code></pre>



<p>它通过为两个属性提供值来创建一个新的Player实例。Swift 将此称为成员初始化器（memberwise initializer），也就是按照定义的顺序接受每个属性的初始化器。</p>



<p>正如我所说，这种代码是可行的，因为 Swift 会默默生成一个接受这两个值的初始化器，但我们也可以编写自己的初始化器来做同样的事情。这里唯一的问题是，你必须小心区分输入的参数名和分配的属性名。</p>



<p>如下所示：</p>



<pre class="wp-block-code"><code>struct Player {
    let name: String
    let number: Int

    init(name: String, number: Int) {
        self.name = name
        self.number = number
    }
}
</code></pre>



<p>这与我们之前的代码工作原理相同，只是现在初始化器归我们所有，因此我们可以根据需要在这里添加额外的功能。</p>



<p>不过，有几件事希望大家注意：</p>



<ol class="wp-block-list">
<li>没有 func 关键字。没错，就语法而言，这看起来像一个函数，但 Swift 对初始化器的处理方式比较特殊。</li>



<li>尽管这会创建一个新的Player实例，但初始化器从来没有明确的返回类型——它们总是返回其所属的数据类型。</li>



<li>我使用 self 将参数赋值给属性，以明确我们的意思是 &#8220;将 name 参数赋值给我的 name 属性&#8221;。</li>
</ol>



<p>最后一点尤为重要，因为如果没有 self，我们就会有 name = name，而这是说不通的——我们是将属性赋值给参数，还是将参数赋值给它自己，还是其他什么？通过写&nbsp;<a href="http://self.name/">self.name</a>，我们可以明确我们指的是 &#8220;属于我当前实例的 name 属性&#8221;，而不是其他任何东西。</p>



<p>当然，我们的自定义初始化器并不需要像 Swift 提供的默认成员初始化器那样工作。例如，我们可以说你必须提供球员姓名，但球衣号码是随机的：</p>



<pre class="wp-block-code"><code>struct Player {
    let name: String
    let number: Int

    init(name: String) {
        self.name = name
        number = Int.random(in: 1...99)
    }
}

let player = Player(name: "Megan R")
print(player.number)
</code></pre>



<p>请记住一条黄金法则：初始化器结束时，所有属性都必须有一个值。如果我们没有在初始化器中为 number 赋值，Swift 就会拒绝构建我们的代码。</p>



<p>重要提示：虽然你可以在初始化器中调用结构体的其他方法，但不能在为所有属性赋值之前这样做——Swift 需要在确定一切安全后再做其他事情。</p>



<p>如果需要，你可以为结构体添加多个初始化器，还可以利用外部参数名称和默认值等功能。但是，一旦尼实现了自己的自定义初始化器，你就将无法访问 Swift 生成的成员初始化器，除非你采取额外措施保留它。这并非偶然：如果你使用了自定义初始化器，Swift 会有效地认为这是因为你使用了某种特殊方法来初始化属性，这意味着默认初始化器不再可用。</p>



<h3 class="wp-block-heading">Swift 的成员初始化器如何工作？</h3>



<p>默认情况下，所有 Swift 结构都会获得一个合成的成员初始化器，这意味着我们会自动获得一个初始化器，接受结构的每个属性值。这个初始化器让结构体变得易于使用，但 Swift 还做了两件让它变得特别聪明的事情。</p>



<p>首先，如果你的任何属性都有默认值，那么它们就会作为默认参数值加入初始化器中。因此，如果我创建了这样一个结构体：</p>



<pre class="wp-block-code"><code>struct User {
    var name: String
    var yearsActive = 0
}
</code></pre>



<p>然后，我可以通过以下两种方式之一来创建它：</p>



<pre class="wp-block-code"><code>struct Employee {
    var name: String
    var yearsActive = 0
}

let roslin = Employee(name: "Laura Roslin")
let adama = Employee(name: "William Adama", yearsActive: 45)
</code></pre>



<p>这使得它们更容易创建，因为你可以只填写需要的部分。</p>



<p>Swift 的第二个聪明之处在于，如果你创建了自己的初始化器，就可以移除成员初始化器。</p>



<p>例如，如果我有一个创建匿名雇员的自定义初始化器，它看起来就会像这样：</p>



<pre class="wp-block-code"><code>struct Employee {
    var name: String
    var yearsActive = 0

    init() {
        self.name = "Anonymous"
        print("Creating an anonymous employee…")
    }
}
</code></pre>



<p>这样一来，我就不能再依赖成员初始化器了，因此也就不能再使用这种方法了：</p>



<pre class="wp-block-code"><code>let roslin = Employee(name: "Laura Roslin")
</code></pre>



<p>这并非偶然，而是我们有意为之的：我们创建了自己的初始化器，如果 Swift 保留其成员初始化器，那么它可能会丢失我们在初始化器中所做的重要工作。</p>



<p>因此，一旦你为结构体添加了自定义初始化器，默认的成员初始化器就会消失。如果你想保留它，请将自定义初始化器移至扩展中，就像这样：</p>



<pre class="wp-block-code"><code>struct Employee {
    var name: String
    var yearsActive = 0
}

extension Employee {
    init() {
        self.name = "Anonymous"
        print("Creating an anonymous employee…")
    }
}

// creating a named employee now works
let roslin = Employee(name: "Laura Roslin")

// as does creating an anonymous employee
let anon = Employee()
</code></pre>



<h3 class="wp-block-heading">什么时候在方法中使用self？</h3>



<p>在方法内部，Swift 允许我们使用 self 来引用结构体的当前实例，但一般来说，除非你特别需要区分你的意思，否则你不希望使用 self。</p>



<p>到目前为止，使用 self 的最常见原因是在初始化器中，在初始化器中，你可能希望参数名称与类型的属性名称相匹配，就像下面这样：</p>



<pre class="wp-block-code"><code>struct Student {
    var name: String
    var bestFriend: String

    init(name: String, bestFriend: String) {
        print("Enrolling \(name) in class…")
        self.name = name
        self.bestFriend = bestFriend
    }
}
</code></pre>



<p>当然，你不一定非要这样做，但在参数名上添加某种前缀会显得有点笨拙：</p>



<pre class="wp-block-code"><code>struct Student {
    var name: String
    var bestFriend: String

    init(name studentName: String, bestFriend studentBestFriend: String) {
        print("Enrolling \(studentName) in class…")
        name = studentName
        bestFriend = studentBestFriend
    }
}
</code></pre>



<p>除了初始化器之外，使用 self 的主要原因是我们在闭包中，Swift 要求使用 self，这样我们就能清楚地了解正在发生什么。只有在从属于类的闭包内部访问 self 时才需要这样做，如果不添加，Swift 将拒绝构建你的代码。</p>



<p>好了，结构体的Part 1到此为止，下一章我们继续。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://beautylife-studio.top/2024/tech/1375/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Learning Swift &#038; SwiftUI Chapter 9</title>
		<link>https://beautylife-studio.top/2024/tech/1353/</link>
					<comments>https://beautylife-studio.top/2024/tech/1353/#respond</comments>
		
		<dc:creator><![CDATA[Alan C.]]></dc:creator>
		<pubDate>Sat, 20 Apr 2024 06:52:51 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[Learning Swift & SwiftUI]]></category>
		<guid isPermaLink="false">https://beautylife-studio.top/?p=1353</guid>

					<description><![CDATA[闭包]]></description>
										<content:encoded><![CDATA[
<p>今天我们一起来学习闭包（Closure），闭包对于初学Swift的人来说是难以理解的第一件事，但是如果不去尝试我们可能永远也不会理解它的具体用法。所以无论如何，我们都要尝试去使用它，毕竟它很有用。</p>



<p>闭包有点像匿名的函数——我们可以创建函数并直接将其赋值给变量，或者将其传递给其他函数以自定义其工作方式。是的，你没看错：将一个函数作为参数传递到另一个函数中。</p>



<p>闭包真的很难。我这么说并不是想让你放弃，只是想让你提前知道，如果你觉得闭包难理解或难记忆，没关系，我们都经历过！</p>



<p>有时，闭包的语法会让你的眼睛有点吃不消，这一点在你学习今天的课程时会非常明显。如果你觉得有点吃力——如果你盯着一些代码却不能百分百确定它的意思—只需返回去再看一遍，给你的记忆一点提示。</p>



<p>SwiftUI 广泛使用闭包，因此值得花时间了解一下。是的，闭包可能是 Swift 中最复杂的功能，但这有点像骑自行车上山，一旦你到达山顶，一旦你掌握了闭包，一切都会变得容易得多。</p>



<p>现在就让我们开始吧！</p>



<h2 class="wp-block-heading">如何创建和使用闭包</h2>



<p>函数在 Swift 中非常强大。是的，您已经看到了如何调用函数、传入值和返回数据，但您还可以将函数赋值给变量、将函数传入函数，甚至从函数返回函数。</p>



<p>例如：</p>



<pre class="wp-block-code"><code>func greetUser() {
    print("Hi there!")
}

greetUser()

var greetCopy = greetUser
greetCopy()</code></pre>



<p>它创建了一个很简单的函数并调用了它，但随后又创建了该函数的副本并调用了副本。因此，它会打印两次相同的信息。</p>



<p>重要提示：复制函数时，不要在函数后面写括号<br>——是 var greetCopy = greetUser，而不是 var greetCopy = greetUser()。如果在这里加上括号，就等于调用函数并将其返回值赋回给其他东西。</p>



<p>但如果你想跳过创建一个单独的函数，直接将功能赋值给一个常量或变量呢？事实证明，你也可以这么做：</p>



<pre class="wp-block-code"><code>let sayHello = {
    print("Hi there!")
}

sayHello()</code></pre>



<p>Swift 给它起了一个高大上的名字——闭包表达式（closure expression），这是一种花哨的说法，表示我们刚刚创建了一个闭包——我们可以随时传递和调用的代码块。这个闭包没有名字，但它实际上是一个不带参数、不返回值的函数。</p>



<p>如果你想让闭包接受参数，就需要用一种特殊的方式来编写。你看，闭包以大括号开始和结束，这意味着我们不能在大括号外写代码来控制参数或返回值。因此，Swift 有一个巧妙的变通方法：我们可以将相同的信息放在大括号内，就像这样：</p>



<pre class="wp-block-code"><code>let sayHello = { (name: String) -&gt; String in
    "Hi \(name)!"
}</code></pre>



<p>我在这里多加了一个关键字，你发现了吗？它是 in 关键字，直接出现在闭包的参数和返回类型之后。同样，如果是普通函数，参数和返回类型会出现在大括号外，但闭包不能这样做。因此，in 用于标记参数和返回类型的结束——之后的所有内容都是闭包的主体。这样做是有原因的，你很快就会明白。</p>



<p>与此同时，你可能会有一个更基本的问题： &#8220;我为什么需要这些东西？&#8221; 我知道，闭包看起来确实非常晦涩难懂。更糟糕的是，它们看起来既晦涩又复杂——很多很多人在初次接触闭包时都会感到非常吃力，因为它们是复杂的怪兽，看起来好像永远都用不上。</p>



<p>然而，正如你将看到的那样，闭包在 Swift 中被广泛使用，几乎遍布 SwiftUI 的每一个角落。说真的，在你编写的每个 SwiftUI 应用程序中都会用到它们，有时甚至会用到上百次——也许不一定是你上面看到的这种形式，但你会经常用到它。</p>



<p>要了解闭包为何如此有用，我首先要向你介绍函数类型。我们已经看到整数有 Int 类型，小数有 Double 类型等，现在我想让你思考函数也有类型。</p>



<p>以我们之前写的 greetUser() 函数为例：它不接受任何参数，不返回任何值，也不会出错。如果我们把它写成 greetCopy 的类型注解，我们会这样写：</p>



<pre class="wp-block-code"><code>var greetCopy: () -&gt; Void = greetUser</code></pre>



<p>让我们来分析一下…</p>



<ol class="wp-block-list">
<li>空括号表示函数不带参数。</li>



<li>箭头的含义与创建函数时的含义相同：我们即将声明函数的返回类型。</li>



<li>Void 表示 &#8220;无&#8221;&#8211;该函数不返回任何内容。有时你可能会看到它被写成（），但我们通常会避免这种写法，因为它会与空参数列表相混淆。</li>
</ol>



<p>每个函数的类型都取决于它接收和发送的数据。这听起来很简单，但却隐藏着一个重要的问题：接收到的数据的名称并不是函数类型的一部分。</p>



<p>我们可以用更多代码来证明这一点：</p>



<pre class="wp-block-code"><code>func getUserData(for id: Int) -&gt; String {
    if id == 1989 {
        return "Taylor Swift"
    } else {
        return "Anonymous"
    }
}

let data: (Int) -&gt; String = getUserData
let user = data(1989)
print(user)</code></pre>



<p>一开始很简单：这是一个接受整数并返回字符串的函数。但当我们复制这个函数时，函数的类型并不包括 for 外部参数的名称，因此在调用复制的函数时，我们使用的是 data(1989) 而不是 data(for:1989)。</p>



<p>巧妙的是，这条规则同样适用于所有闭包——你可能已经注意到，我实际上并没有使用我们之前写的 sayHello 闭包，这是因为我不想让你对调用时缺少参数名产生疑问。现在就调用它：</p>



<pre class="wp-block-code"><code>sayHello("Taylor")</code></pre>



<p>这就像我们复制函数一样，不使用参数名。所以，还是那句话：外部参数名只有在我们直接调用函数时才重要，而不是在我们创建闭包或首先拷贝函数时。</p>



<p>你可能还在想为什么这些都重要，这一切都将变得清晰起来。还记得我说过我们可以在数组中使用 sorted() 来对元素进行排序吗？</p>



<p>这意味着我们可以写这样的代码：</p>



<pre class="wp-block-code"><code>let team = &#91;"Gloria", "Suzanne", "Piper", "Tiffany", "Tasha"]
let sortedTeam = team.sorted()
print(sortedTeam)</code></pre>



<p>这确实很棒，但如果我们想控制排序呢？如果我们总是想让一个人排在第一位，因为他是队长，而其他人则按字母顺序排序呢？</p>



<p>实际上，sorted() 允许我们传递一个自定义排序函数来控制排序。这个函数必须接受两个字符串，如果第一个字符串排序在第二个字符串之前，则返回 true；如果第一个字符串排序在第二个字符串之后，则返回 false。</p>



<p>如果Suzanne是队长，函数将如下所示：</p>



<pre class="wp-block-code"><code>func captainFirstSorted(name1: String, name2: String) -&gt; Bool {
    if name1 == "Suzanne" {
        return true
    } else if name2 == "Suzanne" {
        return false
    }

    return name1 &lt; name2
}</code></pre>



<p>因此，如果第一个名字是 Suzanne，则返回 true，这样 name1 就会排序在 name2 之前。另一方面，如果 name2 是 Suzanne，则返回 false，这样 name1 就会排序在 name2 之后。如果两个名字都不是 Suzanne，只需使用 &lt; 按字母顺序排序即可。</p>



<p>正如我所说，sorted() 可以通过一个函数来创建自定义排序顺序，只要该函数 a) 接受两个字符串，并且 b) 返回一个布尔值，sorted() 就可以使用它。</p>



<p>这正是我们的新函数 captainFirstSorted() 所做的，所以我们可以直接使用它：</p>



<pre class="wp-block-code"><code>let captainFirstTeam = team.sorted(by: captainFirstSorted)
print(captainFirstTeam)</code></pre>



<p>运行后，它将打印出[&#8220;Suzanne&#8221;、&#8221;Gloria&#8221;、&#8221;Piper&#8221;、&#8221;Tasha&#8221;、&#8221;Tiffany&#8221;]，完全符合我们的要求。</p>



<p>现在，我们已经完成了两个看似完全不同的任务。首先，我们可以将闭包创建为匿名函数，并将其存储在常量和变量中：</p>



<pre class="wp-block-code"><code>let sayHello = {
    print("Hi there!")
}

sayHello()</code></pre>



<p>我们还可以将函数传递给其他函数，就像我们将 captainFirstSorted() 传递给 sorted() 一样：</p>



<pre class="wp-block-code"><code>let captainFirstTeam = team.sorted(by: captainFirstSorted)</code></pre>



<p>闭包的强大之处在于我们可以将这两者结合起来：sorted() 需要一个接受两个字符串并返回布尔值的函数，它并不关心该函数是使用 func 正式创建的，还是使用闭包提供的。</p>



<p>因此，我们可以再次调用 sorted()，但不是传入 captainFirstTeam() 函数，而是启动一个新的闭包：写一个开放括号，列出它的参数和返回类型，写入，然后写入我们的标准函数代码。</p>



<p>一开始你会觉得很费劲。这并不是因为你不够聪明，不懂闭包，或者不适合 Swift 编程，而是闭包真的很难。别担心，我们将研究如何让它变得更简单！</p>



<p>好了，让我们编写一些新代码，使用闭包调用 sorted()：</p>



<pre class="wp-block-code"><code>let captainFirstTeam = team.sorted(by: { (name1: String, name2: String) -&gt; Bool in
    if name1 == "Suzanne" {
        return true
    } else if name2 == "Suzanne" {
        return false
    }

    return name1 &lt; name2
})</code></pre>



<p>这一下子就包含了一大段语法，我想再说一遍，这会变得更容易——在下一章中，我们将研究如何减少代码量，以便更容易看清发生了什么。</p>



<p>但首先，我想分析一下这里发生了什么：</p>



<ol class="wp-block-list">
<li>我们像以前一样调用 sorted() 函数。</li>



<li>我们传递的不是一个函数，而是一个闭包——从 by: 后面的开头括号到最后一行的结尾括号都是闭包的一部分。</li>



<li>在闭包内部，我们列出了 sorted() 将传递给我们的两个参数，即两个字符串。我们还指出闭包将返回一个布尔值，然后用 in 标记闭包代码的开始。</li>



<li>其他都是正常的函数代码。<br>再次强调，这里有很多语法，如果你感到头疼，没人会责怪你，但我希望你至少能看到闭包的一点好处：像 sorted() 这样的函数可以让我们传递自定义代码来调整它们的工作方式，而且是直接传递——我们不需要为了这一种用法而编写一个新函数。</li>
</ol>



<p>现在你了解了什么是闭包，让我们看看能否让它们更容易阅读…</p>



<h2 class="wp-block-heading">什么是闭包？为什么Swift那么喜欢使用闭包？</h2>



<p>我相信你一定问过自己这个问题。如果你没有，我会很惊讶，因为闭包是 Swift 最强大的功能之一，但也很容易让人感到困惑。</p>



<p>所以，如果你坐在这里想 &#8220;哇，闭包真难！&#8221;，请不要沮丧——它们确实很难，你觉得它们难，说明你的大脑在正确地运行。</p>



<p>在 Swift 中使用闭包的一个最常见的原因是为了存储功能——可以说 &#8220;我想让你在某个时候做一些工作，但不一定是现在&#8221;。举几个例子：</p>



<ol class="wp-block-list">
<li>在延迟后运行某些代码。</li>



<li>在动画完成后运行某些代码。</li>



<li>下载完成后运行一些代码。</li>



<li>当用户从菜单中选择一个选项时运行一些代码。</li>
</ol>



<p>闭包让我们可以将某些功能封装在一个变量中，然后存储在某个地方。我们也可以从函数中返回，然后将闭包存储在其他地方。</p>



<p>在学习过程中，闭包有点难读，尤其是当它们接受和/或返回自己的参数时。不过没关系：小步前进，遇到困难时再回头看，这样就不会有问题了。</p>



<h2 class="wp-block-heading">为什么 Swift 的闭包参数在大括号内？</h2>



<p>闭包和函数都可以接受参数，但它们接受参数的方式截然不同。下面是一个接受字符串和整数的函数：</p>



<pre class="wp-block-code"><code>func pay(user: String, amount: Int) {
    // code
}</code></pre>



<p>这里写的是一模一样的闭包：</p>



<pre class="wp-block-code"><code>let payment = { (user: String, amount: Int) in
    // code
}</code></pre>



<p>正如你所看到的，参数被移到了大括号内，而 in 关键字则用来标记参数列表的结束和闭包主体的开始。</p>



<p>闭包将参数放在括号内是为了避免混淆 Swift：如果我们写的是 let payment = (user: String, amount: Int)，那么看起来就像是在创建一个元组，而不是一个闭包，这就很奇怪了。</p>



<p>仔细想想，将参数放在大括号内还能巧妙地捕捉到整个过程是存储在变量中的一个数据块——参数列表和闭包主体都是同一段代码的一部分，并存储在我们的变量中。</p>



<p>将参数列表放在大括号内说明了 in 关键字的重要性——如果没有这个关键字，Swift 就很难知道闭包主体究竟从哪里开始，因为没有第二组大括号。</p>



<h2 class="wp-block-heading">如何从不带参数的闭包返回值？</h2>



<p>Swift 中的闭包拥有与简单函数截然不同的语法，其中一个容易引起混淆的地方是我们如何接受和返回参数。</p>



<p>首先，下面是一个只接受一个参数且不返回任何参数的闭包：</p>



<pre class="wp-block-code"><code>let payment = { (user: String) in
    print("Paying \(user)…")
}</code></pre>



<p>这是一个接受一个参数并返回布尔值的闭包：</p>



<pre class="wp-block-code"><code>let payment = { (user: String) -&gt; Bool in
    print("Paying \(user)…")
    return true
}</code></pre>



<p>如果你想返回一个不接受任何参数的值，就不能直接写入 -&gt; Bool，Swift 不会明白你的意思。相反，你应该在参数列表中使用空括号，就像这样：</p>



<pre class="wp-block-code"><code>let payment = { () -&gt; Bool in
    print("Paying an anonymous person…")
    return true
}</code></pre>



<p>如果你仔细想想，这和写 func payment() -&gt; Bool 的标准函数是一样的。</p>



<h2 class="wp-block-heading">如何使用尾部闭包和速记语法</h2>



<p>Swift 有一些小技巧可以减少闭包带来的语法量，但首先让我们提醒自己问题所在。下面是上一章结尾处的代码：</p>



<pre class="wp-block-code"><code>let team = &#91;"Gloria", "Suzanne", "Piper", "Tiffany", "Tasha"]

let captainFirstTeam = team.sorted(by: { (name1: String, name2: String) -&gt; Bool in
    if name1 == "Suzanne" {
        return true
    } else if name2 == "Suzanne" {
        return false
    }

    return name1 &lt; name2
})

print(captainFirstTeam)</code></pre>



<p>如果你还记得，sorted() 可以接受任何类型的函数来进行自定义排序，但有一条规则：该函数必须接受相关数组中的两个项目（这里是两个字符串），如果第一个字符串应在第二个字符串之前排序，则返回一个设置为 true 的布尔值。</p>



<p>说白了，函数必须这样运行——如果它什么都不返回，或者只接受一个字符串，那么 Swift 就会拒绝构建我们的代码。</p>



<p>仔细想想：在这段代码中，我们为 sorted() 提供的函数必须提供两个字符串并返回一个布尔值，那么我们为什么还要在闭包中重复自己的操作呢？</p>



<p>答案是：不需要。我们不需要指定两个参数的类型，因为它们必须是字符串；我们也不需要指定返回类型，因为它必须是布尔型。因此，我们可以这样重写代码：</p>



<pre class="wp-block-code"><code>let captainFirstTeam = team.sorted(by: { name1, name2 in</code></pre>



<p>这已经减少了代码中的杂乱无章，但我们还可以更进一步：当一个函数接受另一个函数作为参数时（如 sorted()），Swift 允许使用特殊的语法，即尾部闭包语法。它看起来像这样：</p>



<pre class="wp-block-code"><code>let captainFirstTeam = team.sorted { name1, name2 in
    if name1 == "Suzanne" {
        return true
    } else if name2 == "Suzanne" {
        return false
    }

    return name1 &lt; name2
}</code></pre>



<p>我们不把闭包作为参数传递进去，而是直接开始闭包，这样就去掉了开头的 by: 和结尾的括号。希望你现在能明白为什么参数列表和 in 会出现在闭包内部，因为如果它们出现在外部，看起来就更奇怪了！</p>



<p>Swift 还有最后一种方法可以让闭包变得不那么杂乱： Swift 可以使用速记语法自动为我们提供参数名称。有了这种语法，我们甚至不用再写 name1、name2，而是依赖 Swift 为我们提供的特殊命名值：$0 和 $1，分别代表第一个和第二个字符串。</p>



<p>使用这种语法，我们的代码会变得更加简短：</p>



<pre class="wp-block-code"><code>let captainFirstTeam = team.sorted {
    if $0 == "Suzanne" {
        return true
    } else if $1 == "Suzanne" {
        return false
    }

    return $0 &lt; $1
}</code></pre>



<p>我把这个放在最后，是因为它不像其他语法那样一目了然——有些人甚至看到这个语法就讨厌它，因为它不太清楚，这没关系。</p>



<p>就我个人而言，我不会在这里使用它，因为我们对每个值都使用了不止一次，但如果我们的 sorted() 调用比较简单，比如我们只想进行反向排序，那么我会使用它：</p>



<pre class="wp-block-code"><code>let reverseTeam = team.sorted {
    return $0 &gt; $1
}</code></pre>



<p>因此，in 用于标记参数和返回类型的结束，之后的所有内容都是闭包的主体。这样做是有原因的，你很快就会明白。</p>



<p>在这里，我将比较从 &lt; 翻转到了 &gt;，因此我们得到了一个反向排序，但现在我们只需一行代码，因此我们可以删除返回值，将其缩减到几乎没有：</p>



<pre class="wp-block-code"><code>let reverseTeam = team.sorted { $0 &gt; $1 }</code></pre>



<p>关于何时使用速记语法，何时不使用速记语法，并没有固定的规则，但为了以防万一，除非出现以下情况，否则我会使用速记语法：</p>



<ol class="wp-block-list">
<li>闭包代码较长。</li>



<li>$0 和其系列参数的使用次数超过一次。</li>



<li>有三个或三个以上的参数（例如：$2、$3 等）。</li>
</ol>



<p>如果你还不相信闭包的威力，让我们再来看两个例子。</p>



<p>首先，filter() 函数可以让我们在数组中的每个项上运行一些代码，并返回一个新数组，其中包含函数返回 true 的每个项。因此，我们可以像这样找到所有名字以 T 开头的队员：</p>



<pre class="wp-block-code"><code>let tOnly = team.filter { $0.hasPrefix("T") }
print(tOnly)</code></pre>



<p>这将打印出[&#8220;Tiffany&#8221;, &#8220;Tasha&#8221;]，因为只有这两位队员的名字是以 T 开头的。</p>



<p>其次，map() 函数可以让我们使用自己选择的代码转换数组中的每个项，并将转换后的所有项发送回一个新数组：</p>



<pre class="wp-block-code"><code>let uppercaseTeam = team.map { $0.uppercased() }
print(uppercaseTeam)</code></pre>



<p>这将打印出[&#8220;GLORIA&#8221;, &#8220;SUZANNE&#8221;, &#8220;PIPER&#8221;, &#8220;TIFFANY&#8221;, &#8220;TASHA&#8221;]，因为它将每个名字都大写了，并从结果中产生了一个新数组。</p>



<p>提示：使用 map() 时，返回的类型不一定与开始使用的类型相同，例如，你可以将整数数组转换为字符串数组。</p>



<p>就像我说的，在 SwiftUI 中你会经常使用闭包：</p>



<ol class="wp-block-list">
<li>当你在屏幕上创建一个数据列表时，SwiftUI 会要求你提供一个函数，从列表中接受一个项目并将其转换为可以在屏幕上显示的内容。</li>



<li>当你创建一个按钮时，SwiftUI 会要求您提供一个在按下按钮时执行的函数，以及另一个生成按钮内容的函数——一张图片或一些文本，等等。</li>



<li>即使只是垂直堆叠文本，也可以使用闭包来完成。</li>
</ol>



<p>是的，你可以在 SwiftUI 的每次操作中都创建单独的函数，但相信我：你不会这样做。闭包让这种代码变得完全自然，我想你会惊讶于 SwiftUI 是如何使用闭包编写出非常简单、整洁的代码的。</p>



<h2 class="wp-block-heading">Swift 为什么使用尾部闭包语法？</h2>



<p>尾部闭包语法是为了让 Swift 代码更易于阅读而设计的，尽管有些人喜欢避免使用它。</p>



<p>我们先来看一个简单的例子。下面是一个函数，它接受一个 Double，然后接受一个包含要做的更改的闭包：</p>



<pre class="wp-block-code"><code>func animate(duration: Double, animations: () -&gt; Void) {
    print("Starting a \(duration) second animation…")
    animations()
}</code></pre>



<p>(如果你想知道，这是一个真实的、非常常见的 UIKit 函数的简化版本！）。</p>



<p>我们可以像这样调用该函数，而不使用尾部闭包：</p>



<pre class="wp-block-code"><code>animate(duration: 3, animations: {
    print("Fade out the image")
})</code></pre>



<p>这很常见。很多人不使用尾部闭包，这没关系。但更多的 Swift 开发人员看到结尾的 }) 会有点畏缩——这并不令人愉快。</p>



<p>尾部闭包让我们可以清理干净，同时还能移除动画参数标签。同样的函数调用会变成这样：</p>



<pre class="wp-block-code"><code>animate(duration: 3) {
    print("Fade out the image")
}</code></pre>



<p>当尾部闭包的含义直接与函数名称相连时，尾部闭包的效果最好——因为函数名为 animate()，所以你可以看到闭包在做什么。</p>



<p>如果你不确定是否要使用尾部闭包，我的建议是开始在所有地方使用它们。一旦你使用了一两个月，你就会有足够的经验来回顾并做出更明确的决定，但希望你能习惯它们，因为它们在 Swift 中真的很常见！</p>



<h2 class="wp-block-heading">什么情况下应该使用速记参数名？</h2>



<p>在使用闭包时，Swift 为我们提供了一种特殊的参数速记语法，让我们可以非常简洁地编写闭包。这种语法会自动将参数名称编号为 $0、$1、$2 等，我们无法在自己的代码中使用这些名称，所以当你看到它们时，就会立即明白这是闭包的速记语法。</p>



<p>至于什么时候应该使用它们，那就 &#8220;看情况 &#8220;了：</p>



<ol class="wp-block-list">
<li>参数多吗？如果是这样，速记语法就不再有用，事实上会适得其反——是$3还是$4，你需要与$0进行比较，给它们起个实际的名字，它们的含义就会更清楚。</li>



<li>函数是否常用？随着 Swift 技能的提高，你会开始意识到有少数（可能有 10 个左右）极其常用的函数会使用闭包，因此其他人在阅读你的代码时会很容易理解 $0 的含义。</li>



<li>速记名称在你的方法中是否多次使用？如果你需要引用 $0 的次数超过两三次，你也许应该给它一个真正的名字。</li>
</ol>



<p>重要的是，你的代码要易于阅读和理解。有时，这意味着代码要简短，但并不总是如此——根据具体情况选择速记语法。</p>



<h2 class="wp-block-heading">如何接受函数作为参数</h2>



<p>我们还想了解最后一个与闭包相关的话题，那就是如何编写接受其他函数作为参数的函数。这一点对于闭包尤为重要，因为它使用了尾部闭包语法，但无论如何，这都是一项有用的技能。</p>



<p>前面我们学习了这段代码：</p>



<pre class="wp-block-code"><code>func greetUser() {
    print("Hi there!")
}

greetUser()

var greetCopy: () -&gt; Void = greetUser
greetCopy()</code></pre>



<p>我故意在这里添加了类型注解，因为这正是我们在指定函数作为参数时使用的注解：我们告诉 Swift 函数接受哪些参数，以及它的返回类型。</p>



<p>请再次做好心理准备：这种语法一开始有点难看！下面是一个函数，它通过重复一定次数的函数来生成一个整数数组：</p>



<pre class="wp-block-code"><code>func makeArray(size: Int, using generator: () -&gt; Int) -&gt; &#91;Int] {
    var numbers = &#91;Int]()

    for _ in 0..&lt;size {
        let newNumber = generator()
        numbers.append(newNumber)
    }

    return numbers
}</code></pre>



<p>让我们来分析一下…</p>



<ol class="wp-block-list">
<li>这个函数叫做 makeArray()。它接受两个参数，其中一个是我们想要的整数个数，同时返回一个整数数组。</li>



<li>第二个参数是一个函数。它本身不接受任何参数，但每次调用都会返回一个整数。</li>



<li>在 makeArray() 中，我们创建一个新的空整数数组，然后按照要求循环多少次。</li>



<li>每次循环时，我们都会调用作为参数传递进来的生成器函数。这会返回一个新的整数，因此我们将其放入数字数组中。</li>



<li>最后返回完成的数组。</li>
</ol>



<p>makeArray() 的主体部分非常简单：重复调用函数生成整数，将每个值添加到数组中，然后全部返回。</p>



<p>复杂的部分是第一行：</p>



<pre class="wp-block-code"><code>func makeArray(size: Int, using generator: () -&gt; Int) -&gt; &#91;Int] {</code></pre>



<p>在这里，我们有两组括号和两组返回类型，所以一开始可能会有点混乱。如果把它拆分开来，就可以线性阅读了：</p>



<ol class="wp-block-list">
<li>我们正在创建一个新函数。</li>



<li>函数名为 makeArray()。</li>



<li>第一个参数是一个名为 size 的整数。</li>



<li>第二个参数是一个名为 generator 的函数，它本身不接受任何参数，并返回一个整数。</li>



<li>整个 makeArray() 函数返回一个整数数组。</li>
</ol>



<p>因此，我们现在可以制作任意大小的整数数组，只需传入一个用于生成每个数字的函数即可：</p>



<pre class="wp-block-code"><code>let rolls = makeArray(size: 50) {
    Int.random(in: 1...20)
}

print(rolls)</code></pre>



<p>请记住，同样的功能也适用于专用函数，所以我们可以这样写：</p>



<pre class="wp-block-code"><code>func generateNumber() -&gt; Int {
    Int.random(in: 1...20)
}

let newRolls = makeArray(size: 50, using: generateNumber)
print(newRolls)</code></pre>



<p>这将调用 generateNumber() 50 次来填充数组。</p>



<p>在你学习 Swift 和 SwiftUI 的过程中，需要了解如何接受函数作为参数的次数屈指可数，但至少现在您已经了解了它的工作原理和重要原因。</p>



<p>在我们继续之前还有最后一件事：如果需要，你可以让函数接受多个函数参数，在这种情况下，你可以指定多个尾部闭包。这里的语法在 SwiftUI 中非常常见，所以在这里至少要向大家展示一下。</p>



<p>为了演示这一点，下面是一个接受三个函数参数的函数，每个函数都不接受任何参数，也不返回任何内容：</p>



<pre class="wp-block-code"><code>func doImportantWork(first: () -&gt; Void, second: () -&gt; Void, third: () -&gt; Void) {
    print("About to start first work")
    first()
    print("About to start second work")
    second()
    print("About to start third work")
    third()
    print("Done!")
}</code></pre>



<p>我在其中添加了额外的 print() 调用，以模拟在调用第一、第二和第三个闭包之间进行的特定工作。</p>



<p>在调用时，第一个尾部闭包与我们已经使用过的完全相同，但第二个和第三个闭包的格式不同：你要结束前一个闭包的括号，然后写入外部参数名称和一个冒号，然后开始另一个括号。</p>



<p>如下所示：</p>



<pre class="wp-block-code"><code>doImportantWork {
    print("This is the first work")
} second: {
    print("This is the second work")
} third: {
    print("This is the third work")
}</code></pre>



<p>使用三个尾部闭包并不像你想象的那样罕见。例如，在 SwiftUI 中制作一个内容部分需要使用三个尾部闭包：一个用于内容本身，一个用于放在上方的页眉，一个用于放在下方的页脚。</p>



<h2 class="wp-block-heading">为什么要使用闭包作为参数？</h2>



<p>Swift 的闭包可以像使用其他类型的数据一样使用，这意味着你可以将它们传递到函数中，获取它们的副本，等等。但是，当你刚刚学习时，你可能会觉得这很像 &#8220;你可以这样做，但并不意味着你应该这样做&#8221;——你很难看到这样做的好处。</p>



<p>我能举出的最好的例子之一就是 Siri 与应用程序的集成方式。Siri 是一个系统服务，可以在 iOS 设备上的任何地方运行，但它能与应用进行交流，你可以用 Lyft 预约乘车，用 Carrot Weather 查看天气，等等。在幕后，Siri 会在后台启动应用程序的一小部分来传递我们的语音请求，然后将应用程序的响应作为 Siri 用户界面的一部分显示出来。</p>



<p>现在想想：如果我的应用表现不佳，需要 10 秒钟才能响应 Siri 呢？请记住，用户实际上看不到我的应用，只能看到 Siri，因此从他们的角度看，Siri 似乎完全冻结了。</p>



<p>这样的用户体验会很糟糕，因此苹果使用闭包来代替：它会在后台启动我们的应用，并传递给我们一个闭包，我们可以在完成后调用它。这样，我们的应用就可以花尽可能长的时间来找出需要完成的工作，而当我们完成工作后，我们就可以调用闭包向 Siri 发回数据。使用闭包发回数据而不是从函数返回值，意味着 Siri 无需等待函数完成，因此它可以保持用户界面的交互性——不会冻结。</p>



<p>另一个常见的例子是发出网络请求。iPhone 平均每秒可以处理几十亿件事情，但连接到日本的服务器却需要半秒甚至更长的时间，与设备上的运行速度相比，这几乎是冰川般的速度。因此，当我们从互联网上请求数据时，我们都是以闭包的方式进行的： &#8220;请获取这些数据，完成后运行这个闭包&#8221;。同样，这意味着我们不会在一些缓慢的工作正在进行时强制冻结我们的用户界面。</p>



<h2 class="wp-block-heading">总结：闭包</h2>



<p>在前面，我们已经介绍了很多关于闭包的知识，现在让我们来回顾一下：</p>



<ul class="wp-block-list">
<li>你可以在 Swift 中复制函数，除了会丢失外部参数名称外，它们的工作方式与原始函数相同。</li>



<li>所有函数都有类型，就像其他数据类型一样。这包括它们接收的参数和返回类型，返回类型可能是 Void，也称为 &#8220;无&#8221;。</li>



<li>通过赋值给常量或变量，可以直接创建闭包。</li>



<li>接受参数或返回值的闭包必须在括号内声明，并在后面加上关键字 in。</li>



<li>函数可以接受其他函数作为参数。它们必须事先声明这些函数必须使用哪些数据，Swift 会确保这些规则得到遵守。</li>



<li>在这种情况下，你也可以传递一个闭包，而不是传递一个专用函数——你可以直接创建一个闭包。Swift 允许这两种方法同时使用。</li>



<li>在传递闭包作为函数参数时，如果 Swift 可以自动计算出闭包内部的类型，那么您就不需要明确写出闭包内部的类型。返回值的情况也是如此，如果 Swift 可以自动计算出返回值，你就不需要指定返回值。</li>



<li>如果函数的一个或多个最终参数是函数，你可以使用尾部闭包语法。</li>



<li>你也可以使用诸如 $0 和 $1之类的速记参数名，但我建议只有在某些情况下才这样做。</li>



<li>你可以自己创建接受函数作为参数的函数，不过在实践中，知道如何使用这些函数要比知道如何创建它们重要得多。</li>
</ul>



<p>在 Swift 语言的各个部分中，我认为闭包是最难学习的部分。一开始，不仅语法有点难看，而且将函数传递到函数的概念本身也需要一点时间才能理解。</p>



<p>所以，如果你读完了本章，感觉头都要炸了，那就太好了——这意味着你对闭包的理解已经成功了一半！</p>
]]></content:encoded>
					
					<wfw:commentRss>https://beautylife-studio.top/2024/tech/1353/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Learning Swift &#038; SwiftUI Chapter 8</title>
		<link>https://beautylife-studio.top/2024/tech/1295/</link>
					<comments>https://beautylife-studio.top/2024/tech/1295/#respond</comments>
		
		<dc:creator><![CDATA[Alan C.]]></dc:creator>
		<pubDate>Fri, 12 Apr 2024 04:22:35 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[Learning Swift & SwiftUI]]></category>
		<guid isPermaLink="false">https://beautylife-studio.top/?p=1295</guid>

					<description><![CDATA[函数 Part 2]]></description>
										<content:encoded><![CDATA[
<p>今天是我们学习函数的下半部分，这一部分我们先了解一下如何给函数的参数提供默认值，然后再一起学习如何处理函数中的错误，最后是总结。</p>



<h2 class="wp-block-heading">如何为参数提供默认值</h2>



<p>为函数添加参数可以让我们添加自定义点，这样函数就可以根据我们的需要对不同的数据进行操作。有时我们希望提供这些自定义点，以保持代码的灵活性，但有时你并不想考虑这些。</p>



<p>例如，我们之前看过这样一个函数：</p>



<pre class="wp-block-code"><code>func printTimesTables(for number: Int, end: Int) {
    for i in 1...end {
        print("\(i) x \(number) is \(i * number)")
    }
}

printTimesTables(for: 5, end: 20)
</code></pre>



<p>它可以打印任何乘法表，从数字的 1 倍开始，直到任意终点。根据我们想要的乘法表，这个数字总是会发生变化，但终点似乎是提供一个合理默认值的绝佳位置——我们可能希望在大多数情况下都数到 10 或 12，但同时仍有可能在某些情况下数到不同的值。</p>



<p>为了解决这个问题，Swift 允许我们为任何或所有参数指定默认值。在本例中，我们可以将 end 设置为默认值 12，也就是说，如果我们不指定，12 将自动被使用。</p>



<p>代码如下：</p>



<pre class="wp-block-code"><code>func printTimesTables(for number: Int, end: Int = 12) {
    for i in 1...end {
        print("\(i) x \(number) is \(i * number)")
    }
}

printTimesTables(for: 5, end: 20)
printTimesTables(for: 8)
</code></pre>



<p>请注意，我们现在可以用两种不同的方式调用 printTimesTables()：当我们需要时，我们可以同时提供两个时间参数，但如果我们不这样做——如果我们只写 printTimesTables(for: 8) ——那么默认值 12 将被用于结束。</p>



<p>实际上，我们已经在之前使用过的代码中看到过默认参数的实际应用：</p>



<pre class="wp-block-code"><code>var characters = &#91;"Lana", "Pam", "Ray", "Sterling"]
print(characters.count)
characters.removeAll()
print(characters.count)
</code></pre>



<p>这会将一些字符串添加到数组中，并打印数组的计数，然后删除所有字符串并再次打印计数。</p>



<p>作为一种性能优化，Swift 为数组提供的内存只够容纳数组中的项目，再加上一点额外的内存，这样数组就能随着时间的推移一点点增长。如果数组中添加了更多项目，Swift 会自动分配越来越多的内存，从而尽可能减少浪费。</p>



<p>当我们调用 removeAll() 时，Swift 会自动删除数组中的所有项，然后释放分配给数组的所有内存。这通常是你想要的，因为毕竟你删除对象是有原因的。但有时——只是有时——你可能会向数组中添加大量新项目，因此这个函数还有第二种形式，即在删除项目的同时保留之前的容量：</p>



<pre class="wp-block-code"><code>characters.removeAll(keepingCapacity: true)
</code></pre>



<p>这可以通过使用默认参数值来实现：keepingCapacity 是一个布尔值，默认值为 false，这样它在默认情况下就会做明智的事情，同时也允许我们在需要保持数组现有容量时输入 true。</p>



<p>正如你所看到的，默认参数值可以让我们在函数中保持灵活性，而不会让函数在大多数情况下调用起来令人讨厌——只有当你需要一些不寻常的东西时，才需要发送一些参数。</p>



<h2 class="wp-block-heading">何时使用函数的默认参数</h2>



<p>默认参数通过为参数提供常用的默认值，让函数的调用变得更简单。因此，当我们想使用这些默认值调用函数时，我们可以完全忽略这些参数，就像它们不存在一样，我们的函数就会做正确的事情。当然，当我们需要自定义参数时，也可以对其进行更改。</p>



<p>Swift 开发人员经常使用默认参数，因为它们可以让我们专注于需要定期更改的重要部分。这确实有助于简化复杂的函数，使代码更易于编写。</p>



<p>例如，想象一下这样的寻路代码：</p>



<pre class="wp-block-code"><code>func findDirections(from: String, to: String, route: String = "fastest", avoidHighways: Bool = false) {
    // code here
}
</code></pre>



<p>这样做的前提是，大多数情况下，人们会选择最快的路线来往于两个地点之间，而不会避开高速公路——这种合理的默认值在大多数情况下都可能有效，同时也为我们提供了在需要时提供自定义值的空间。</p>



<p>因此，我们可以通过以下三种方式调用同一个函数：</p>



<pre class="wp-block-code"><code>findDirections(from: "Shanghai", to: "Hangzhou")
findDirections(from: "Shanghai", to: "Hangzhou", route: "scenic")
findDirections(from: "Shanghai", to: "Hangzhou", route: "scenic", avoidHighways: true)
</code></pre>



<p>在大多数情况下，代码更短、更简单，但在我们需要的时候又能灵活运用——完美。</p>



<h2 class="wp-block-heading">如何处理函数中的错误</h2>



<p>出错是家常便饭，比如你想读取的文件不存在，或者你想下载的数据因为网络故障而下载失败。如果我们不能优雅地处理错误，那么我们的代码就会崩溃，所以 Swift 让我们处理错误，或者至少承认错误可能发生。</p>



<p>这需要三个步骤：</p>



<ol class="wp-block-list">
<li>告诉 Swift 可能发生的错误。</li>



<li>编写一个可以在发生错误时标记错误的函数。</li>



<li>调用该函数，并处理任何可能发生的错误。</li>
</ol>



<p>让我们举一个完整的例子：如果用户要求我们检查其密码的强度，如果密码太短或太明显，我们就会提示严重错误。</p>



<p>因此，我们需要先定义可能发生的错误。这意味着要在 Swift 现有的 Error 类型基础上创建一个新的枚举，就像下面这样：</p>



<pre class="wp-block-code"><code>enum PasswordError: Error {
    case short, obvious
}
</code></pre>



<p>这就是说，密码有两种可能的错误：简短和明显。它并没有定义这两种错误的含义，只是说它们存在。</p>



<p>第二步是编写一个函数，触发其中一个错误。在 Swift 中，当一个错误被触发或抛出时，我们就意味着函数出了致命的问题，它不会像正常情况下那样继续运行，而是会立即终止，不会返回任何值。</p>



<p>在我们的例子中，我们要编写一个检查密码强度的函数：如果密码强度很差（少于 5 个字符或非常有名），我们就会立即抛出错误，但对于所有其他字符串，我们会返回 &#8220;OK&#8221;、&#8221;Good &#8220;或 &#8220;Excellent &#8220;等级。</p>



<p>下面是 Swift 中的效果：</p>



<pre class="wp-block-code"><code>func checkPassword(_ password: String) throws -&gt; String {
    if password.count &lt; 5 {
        throw PasswordError.short
    }

    if password == "12345" {
        throw PasswordError.obvious
    }

    if password.count &lt; 8 {
        return "OK"
    } else if password.count &lt; 10 {
        return "Good"
    } else {
        return "Excellent"
    }
}
</code></pre>



<p>让我们来分析一下&#8230;</p>



<ol class="wp-block-list">
<li>如果函数可以抛出错误而不自行处理，则必须在返回类型前将函数标记为抛出（throws）。</li>



<li>我们并没有明确说明函数会抛出哪种错误，只是说明它可以抛出错误。</li>



<li>标记为 throws 并不意味着函数必须抛出错误，只是意味着它可能会抛出错误。</li>



<li>当需要抛出错误时，我们在写 throw 时，在后面跟上我们的 PasswordError 案例之一。这会立即退出函数，这意味着它不会返回字符串。</li>



<li>如果没有抛出错误，函数的行为必须与正常情况一样——它需要返回一个字符串。</li>
</ol>



<p>这就完成了抛出错误的第二步：我们定义了可能发生的错误，然后编写了一个使用这些错误的函数。</p>



<p>最后一步是运行函数并处理可能发生的错误。Swift Playgrounds 在错误处理方面相当宽松，因为它们主要是用来学习的，但在实际的 Swift 项目中，你会发现有三个步骤：</p>



<ol class="wp-block-list">
<li>使用 do 开始一个可能会出错的工作块。</li>



<li>使用 try 调用一个或多个抛出函数。</li>



<li>使用 catch 处理任何抛出的错误。</li>
</ol>



<p>在伪代码中，它看起来像这样：</p>



<pre class="wp-block-code"><code>do {
    try someRiskyWork()
} catch {
    print("Handle errors here")
}
</code></pre>



<p>如果我们想使用当前的 checkPassword() 函数编写 try，可以这样写：</p>



<pre class="wp-block-code"><code>let string = "12345"

do {
    let result = try checkPassword(string)
    print("Password rating: \(result)")
} catch {
    print("There was an error.")
}
</code></pre>



<p>如果 checkPassword() 函数工作正常，它将返回一个值到 result 中，然后打印出来。但如果出现任何错误（本例中就会出现错误），则永远不会打印密码评级信息，执行过程会立即跳转到 catch 块。</p>



<p>这段代码中有几个不同的部分值得讨论，但我想重点谈谈其中最重要的部分：try。这段代码必须写在调用所有可能出错的函数之前，它是给开发人员的一个可视化信号，表明如果发生错误，正常的代码执行将被中断。</p>



<p>使用 try 时，需要在 do 代码块中，并确保有一个或多个 catch 代码块可以处理任何错误。在某些情况下，您可以使用另一种写法 try！，它不需要 do 和 catch，但如果出现错误，您的代码就会崩溃。</p>



<p>说到捕获错误，你必须始终有一个可以处理各种错误的 catch 块。不过，如果您愿意，也可以捕获特定的错误：</p>



<pre class="wp-block-code"><code>let string = "12345"

do {
    let result = try checkPassword(string)
    print("Password rating: \(result)")
} catch PasswordError.short {
    print("Please use a longer password.")
} catch PasswordError.obvious {
    print("I have the same combination on my luggage!")
} catch {
    print("There was an error.")
}
</code></pre>



<p>随着学习的深入，你会发现抛出函数是如何嵌入到许多苹果自己的框架中的，因此，即使你可能不会自己创建太多的抛出函数，你至少需要知道如何安全地使用它们。</p>



<p>小提示：苹果抛出的大多数错误都会提供一条有意义的信息，你可以根据需要将其呈现给用户。Swift 通过在 catch 代码块中自动提供的错误值来提供这些信息，通常可以通过阅读 error.localizedDescription 来了解具体发生了什么。</p>



<h2 class="wp-block-heading">何时应该编写抛出函数？</h2>



<p>Swift 中的抛出函数是指那些遇到无法或不愿处理的错误的函数。这并不意味着它们会抛出错误，只是说它们有可能会抛出错误。因此，Swift 会确保我们在使用它们时小心谨慎，以应对可能发生的错误。</p>



<p>但在编写代码时，你很可能会想：&#8221;这个函数是否应该抛出遇到的错误，还是应该自己处理？这种情况很常见，老实说并没有唯一的答案——你可以在函数内部处理错误（从而使其不再是一个抛出函数），也可以将错误全部发送回调用函数的对象（称为 &#8220;错误传播(error propagation)&#8221;，有时也称为 &#8220;错误冒泡(bubbling up errors)&#8221;），甚至还可以在函数中处理一些错误，再将一些错误发送回去。所有这些都是有效的解决方案，你都会在某些时候用到。</p>



<p>在刚开始使用时，我建议你在大多数情况下避免使用抛出函数。一开始，它们会让人感觉有点笨拙，因为无论你在哪里使用函数，都需要确保所有的错误都得到了处理，所以这几乎有点 &#8220;传染&#8221;的感觉——突然间，你的代码中有好几个地方都需要处理错误，如果这些错误进一步扩散，那么 &#8220;传染 &#8220;就会蔓延开来。</p>



<p>因此，在学习过程中要从小处入手：减少抛出函数的数量，然后再向外扩展。随着时间的推移，你会更好地掌握管理错误的方法，从而保持程序的流畅，你也会对添加抛出函数更有信心。</p>



<h2 class="wp-block-heading">为什么 Swift 让我们在每个抛出函数前都使用 try？</h2>



<p>使用 Swift 的抛出函数依赖于三个独特的关键字：do、try 和 catch。我们需要同时使用这三个关键字才能调用一个抛出函数，这很不寻常——大多数其他语言只使用两个关键字，因为他们不需要在每个抛出函数前都写 try。</p>



<p>Swift 不同于其他语言的原因很简单：通过强制我们在每个抛出函数之前使用 try，我们明确承认了代码的哪些部分可能导致错误。如果在一个 do 代码块中有多个抛出函数，这一点就特别有用，就像下面这样：</p>



<pre class="wp-block-code"><code>do {
    try throwingFunction1()
    nonThrowingFunction1()
    try throwingFunction2()
    nonThrowingFunction2()
    try throwingFunction3()
} catch {
    // handle errors
}
</code></pre>



<p>正如你所看到的，使用 try 可以清楚地表明，第一、第三和第五次函数调用可以抛出错误，但第二和第四次不能。</p>



<h2 class="wp-block-heading">总结：函数</h2>



<p>在前面的章节中，我们已经介绍了很多关于函数的知识，现在让我们来回顾一下：</p>



<ul class="wp-block-list">
<li>函数通过分割代码块并为其命名，让我们可以轻松重复使用代码。</li>



<li>所有函数都以单词 func 开头，然后是函数名称。函数的主体包含在大括号中。</li>



<li>我们可以添加参数，使函数更加灵活——用逗号分隔，逐个列出参数：参数名称，然后是冒号，然后是参数类型。</li>



<li>你可以控制外部如何使用这些参数名，可以使用自定义的外部参数名，也可以使用下划线禁用该参数的外部名称。</li>



<li>如果你认为某些参数值会重复使用，你可以为它们设置一个默认值，这样你的函数就可以减少代码编写量，并在默认情况下做明智的事情。</li>



<li>如果需要，函数可以返回一个值，但如果要从函数中返回多个数据，则应使用元组。元组可以容纳多个命名的元素，但它的限制与字典不同——你需要具体列出每个元素及其类型。</li>



<li>函数可以抛出错误：你可以创建一个枚举，定义可能会发生的错误，根据需要在函数内部抛出这些错误，然后使用 do、try 和 catch 在调用位置处理这些错误。</li>
</ul>



<h2 class="wp-block-heading">思考题</h2>



<p>编写一个函数，使它能接收一个从1到10000的整数，并返回这个整数的整数平方根。要求：</p>



<ul class="wp-block-list">
<li>不能使用sqrt()函数或类似的Swift内置函数</li>



<li>如果数字小于1或者大于10000，则使函数抛出“超限”错误。</li>



<li>只考虑整数平方根，不考虑小数。</li>



<li>如果没有整数平方根，则抛出“无根”错误。</li>
</ul>



<p>答案可能有很多种，不用拘泥于我提供的代码，只要功能实现即可：</p>



<pre class="wp-block-code"><code>enum NumberError: Error {
    case noRoot, outOfBounds
}

func sqrtOf(_ number: Int) throws -&gt; Int {
    if number &gt; 10000 || number &lt; 1 {
        throw NumberError.outOfBounds
    }
    for i in 1...100 {
        let x = i * i
        if number == x {
            return i
        }
    }
    throw NumberError.noRoot
}

do {
    let number = 100000
    let result = try sqrtOf(number)
    print("\(number)'s root is \(result).")
} catch NumberError.noRoot {
    print("This number has no root.")
} catch NumberError.outOfBounds {
    print("This number is out of bounds.")
}
</code></pre>



<p>好了，函数的内容就到此为止了，虽然看似很简单，但函数的用处非常大，希望大家能多多练习。</p>



<p>下一章，我们会开始学习闭包(Closure)。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://beautylife-studio.top/2024/tech/1295/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Learning Swift &#038; SwiftUI Chapter 7</title>
		<link>https://beautylife-studio.top/2024/tech/1293/</link>
					<comments>https://beautylife-studio.top/2024/tech/1293/#respond</comments>
		
		<dc:creator><![CDATA[Alan C.]]></dc:creator>
		<pubDate>Fri, 12 Apr 2024 04:20:16 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[Learning Swift & SwiftUI]]></category>
		<guid isPermaLink="false">https://beautylife-studio.top/?p=1293</guid>

					<description><![CDATA[函数 Part 1]]></description>
										<content:encoded><![CDATA[
<p>今天我们开始一起学习函数 Function。</p>



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



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



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



<p>让我们开始吧！</p>



<h2 class="wp-block-heading">如何使用函数重复使用代码</h2>



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



<p>例如，假设我们有这样一段简单漂亮的代码：</p>



<pre class="wp-block-code"><code>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.")
</code></pre>



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



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



<p>这就是函数的作用所在：我们可以调出代码，给它一个名字，然后随时随地运行它。这意味着所有的&nbsp;<strong><code>print()</code></strong>行都会留在一个地方，并在其他地方重复使用。</p>



<p>如下所示：</p>



<pre class="wp-block-code"><code>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.")
}
</code></pre>



<p>让我们来分析一下&#8230;</p>



<ol class="wp-block-list">
<li>首先是 <strong><code>func</code></strong> 关键字，它标志着一个函数的开始。</li>



<li>我们将函数命名为 <strong><code>showWelcome</code></strong>。这个名字可以是你想要的任何名字，但尽量让人记住——<strong><code>printInstructions()</code></strong>、<strong><code>displayHelp()</code></strong> 等都是不错的选择。</li>



<li>函数的主体包含在打开和关闭的大括号中，就像循环的主体和条件的主体一样。</li>
</ol>



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



<p>现在你知道为什么了：这些&nbsp;<strong><code>()</code></strong>&nbsp;是在函数中使用的。它们不仅在创建函数时使用（如上图所示），还在调用函数时使用（即要求 Swift 运行其代码时）。在我们的例子中，我们可以这样调用函数：</p>



<pre class="wp-block-code"><code>showWelcome()
</code></pre>



<p>这就是函数的调用位置，这是一个花哨的名字，意思是 &#8220;函数被调用的地方&#8221;。</p>



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



<p>举个例子，我们已经使用过这样的代码：</p>



<pre class="wp-block-code"><code>let number = 139

if number.isMultiple(of: 2) {
    print("Even")
} else {
    print("Odd")
}
</code></pre>



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



<p>同样，当我们掷虚拟骰子时，我们使用了这样的代码：</p>



<pre class="wp-block-code"><code>let roll = Int.random(in: 1...20)
</code></pre>



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



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



<p>代码如下：</p>



<pre class="wp-block-code"><code>func printTimesTables(number: Int) {
    for i in 1...12 {
        print("\(i) x \(number) is \(i * number)")
    }
}

printTimesTables(number: 5)
</code></pre>



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



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



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



<pre class="wp-block-code"><code>func printTimesTables(number: Int, end: Int) {
    for i in 1...end {
        print("\(i) x \(number) is \(i * number)")
    }
}

printTimesTables(number: 5, end: 20)
</code></pre>



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



<pre class="wp-block-code"><code>printTimesTables(5, 20)
</code></pre>



<p>你还记得哪个号码是哪个吗？可能记得 但六个月后你还记得吗？可能记不住。</p>



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



<p>请看这段代码：</p>



<pre class="wp-block-code"><code>func printTimesTables(number: Int, end: Int) {
</code></pre>



<p>这里的&nbsp;<strong><code>number</code></strong>&nbsp;和&nbsp;<strong><code>end</code></strong>&nbsp;都是参数：它们是占位符名称，在调用函数时会被填入值，这样我们就有了函数内部这些值的名称。</p>



<p>现在看看这段代码：</p>



<pre class="wp-block-code"><code>printTimesTables(number: 5, end: 20)
</code></pre>



<p>在这里，5 和 20 是参数：它们是发送到函数中的实际值，用于填充数字和结尾。</p>



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



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



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



<p>因此，对于这段代码：</p>



<pre class="wp-block-code"><code>func printTimesTables(number: Int, end: Int) {
</code></pre>



<p>这不是有效代码，因为它将结束置于数字之前：</p>



<pre class="wp-block-code"><code>printTimesTables(end: 20, number: 5)
</code></pre>



<p>提示：在函数内创建的任何数据都会在函数结束时自动销毁。</p>



<h2 class="wp-block-heading">函数中应包含哪些代码？</h2>



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



<p>有三种情况你会想要创建自己的函数：</p>



<ol class="wp-block-list">
<li>最常见的情况是，你需要在多个地方使用相同的功能。在这种情况下，使用函数意味着你可以修改一段代码，然后更新所有使用你的函数的地方。</li>



<li>函数对于分割代码也很有用。如果你有一个很长的函数，就很难跟上所有的内容，但如果你把它分成三到四个较小的函数，就会变得更容易跟上。</li>



<li>最后一个原因更为先进： Swift 允许我们在现有函数的基础上构建新函数，这是一种称为函数组合的技术。通过将工作分割成多个小函数，函数组合让我们可以通过以各种方式组合这些小函数来构建大函数，这有点像乐高积木。</li>
</ol>



<h2 class="wp-block-heading">函数应接收多少个参数？</h2>



<p>乍一看，这个问题就像是 &#8220;一段字符串有多长？&#8221;。也就是说，这个问题没有真正的、硬性的答案——一个函数可以不带参数，也可以带 20 个参数。</p>



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



<ul class="wp-block-list">
<li>它需要所有六个参数吗？</li>



<li>能否将该函数拆分成参数更少的小函数？</li>



<li>是否应该以某种方式对这些参数进行分组？</li>
</ul>



<p>我们稍后将讨论解决这个问题的一些技巧，但这里有一个重要的教训：这就是所谓的 &#8220;代码气味&#8221;——代码中的某些东西暗示着我们构建程序的方式存在潜在问题。</p>



<h2 class="wp-block-heading">如何从函数中返回值</h2>



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



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



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



<p>例如，我们可以这样写：</p>



<pre class="wp-block-code"><code>let root = sqrt(169)
print(root)
</code></pre>



<p>如果想从函数中返回自己的值，需要做两件事：</p>



<ol class="wp-block-list">
<li>在函数的开头括号前写一个箭头，然后写一个数据类型，告诉 Swift 将返回哪种数据。</li>



<li>使用 <strong><code>return</code></strong> 关键字发回数据。</li>
</ol>



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



<pre class="wp-block-code"><code>func rollDice() -&gt; Int {
    return Int.random(in: 1...6)
}

let result = rollDice()
print(result)
</code></pre>



<p>因此，函数必须返回一个整数，而实际值会通过**<code>return</code>**关键字发送回来。</p>



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



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



<p>让我们举一个更复杂的例子：两个字符串是否包含相同的字母，无论它们的顺序如何？这个函数应该接受两个字符串参数，如果它们的字母相同，则返回&nbsp;<strong><code>true</code></strong>&nbsp;&#8211; 因此，&#8221;abc &#8220;和 &#8220;cab &#8220;应该返回&nbsp;<strong><code>true</code></strong>，因为它们都包含一个 &#8220;a&#8221;、一个 &#8220;b&#8221;和一个 &#8220;c&#8221;。</p>



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



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



<p>这里有一个示例解决方案：</p>



<pre class="wp-block-code"><code>func areLettersIdentical(string1: String, string2: String) -&gt; Bool {
    let first = string1.sorted()
    let second = string2.sorted()
    return first == second
}
</code></pre>



<p>让我们来分析一下：</p>



<ol class="wp-block-list">
<li>它创建了一个名为 <strong><code>areLettersIdentical()</code></strong> 的新函数。</li>



<li>该函数接受两个字符串参数：<strong><code>string1</code></strong> 和 <strong><code>string2</code></strong>。</li>



<li>函数说它返回一个 <strong><code>Bool</code></strong>，所以在某些时候我们必须总是返回 <strong><code>true</code></strong> 或 <strong><code>false</code></strong>。</li>



<li>在函数体中，我们对两个字符串进行排序，然后使用 <strong><code>==</code></strong> 对字符串进行比较——如果相同，则返回 <strong><code>true</code></strong>，否则返回 <strong><code>false</code></strong>。</li>
</ol>



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



<pre class="wp-block-code"><code>func areLettersIdentical(string1: String, string2: String) -&gt; Bool {
    return string1.sorted() == string2.sorted()
}
</code></pre>



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



<pre class="wp-block-code"><code>func areLettersIdentical(string1: String, string2: String) -&gt; Bool {
    string1.sorted() == string2.sorted()
}
</code></pre>



<p>我们还可以对&nbsp;<strong><code>rollDice()</code></strong>&nbsp;函数进行同样的处理：</p>



<pre class="wp-block-code"><code>func rollDice() -&gt; Int {
    Int.random(in: 1...6)
}
</code></pre>



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



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



<p>你已经学会了如何使用&nbsp;<strong><code>sqrt()</code></strong>，所以我们可以建立一个&nbsp;<strong><code>pythagoras()</code></strong>&nbsp;函数，接受两个Double数并返回另一个Double数：</p>



<pre class="wp-block-code"><code>func pythagoras(a: Double, b: Double) -&gt; Double {
    let input = a * a + b * b
    let root = sqrt(input)
    return root
}

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

</code></pre>



<p>因此，这是一个名为&nbsp;<strong><code>pythagoras()</code></strong>&nbsp;的函数，它接受两个 Double 参数，并返回另一个 Double。该函数将 a 和 b 相乘，然后将其传入&nbsp;<strong><code>sqrt()</code></strong>，并返回结果。</p>



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



<p>这就是解决方案：</p>



<pre class="wp-block-code"><code>func pythagoras(a: Double, b: Double) -&gt; Double {
    sqrt(a * a + b * b)
}
</code></pre>



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



<h2 class="wp-block-heading">Swift 函数中何时不需要返回关键字？</h2>



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



<p>现在，&#8221;表达式 &#8220;并不是经常使用的一个词，但在这里理解它很重要。当我们编写程序时，我们会这样做：</p>



<pre class="wp-block-code"><code>5 + 8
</code></pre>



<p>或这样：</p>



<pre class="wp-block-code"><code>greet("Alan")
</code></pre>



<p>这些代码行会被解析为单个值：5 + 8 会被解析为 13，而 greet(&#8220;Alan&#8221;) 可能会返回字符串 &#8220;Hi, Alan!&#8221;。</p>



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



<pre class="wp-block-code"><code>let isAdmin = true 
let isOwner = false
let isEditingEnabled = false
</code></pre>



<p>那么这行代码就会解析为一个单一的值：</p>



<pre class="wp-block-code"><code>isOwner == true &amp;&amp; isEditingEnabled || isAdmin == true
</code></pre>



<p>这将变为 &#8220;true&#8221;，因为即使 isOwner 为假，isAdmin 为真，所以整个表达式变为 true。</p>



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



<pre class="wp-block-code"><code>let name = "Otis"
</code></pre>



<p>是的，这创建了一个常量，但它本身并不成为一个值——我们不能写&nbsp;<strong><code>return let name = "Otis"</code></strong>。</p>



<p>同样，我们也可以这样写条件：</p>



<pre class="wp-block-code"><code>if name == "Maeve" {
    print("Hello, Maeve!")
}
</code></pre>



<p>这也不能成为一个单一的值，因为其中有一个条件。</p>



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



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



<pre class="wp-block-code"><code>func doMath() -&gt; Int {
    return 5 + 5
}

func doMoreMath() -&gt; Int {
    5 + 5
}
</code></pre>



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



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



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



<p>为了演示这一点，请看下面的函数：</p>



<pre class="wp-block-code"><code>func greet(name: String) -&gt; String {
    if name == "Taylor Swift" {
        return "Oh wow!"
    } else {
        return "Hello, \(name)"
    }
}
</code></pre>



<p>如果我们想删除其中的返回语句，就无法写出这样的内容：</p>



<pre class="wp-block-code"><code>func greet(name: String) -&gt; String {
    if name == "Taylor Swift" {
        "Oh wow!"
    } else {
        "Hello, \(name)"
    }
}
</code></pre>



<p>这是不允许的，因为我们有实际的语句 —— if 和 else。</p>



<p>不过，我们可以这样使用三元运算符：</p>



<pre class="wp-block-code"><code>func greet(name: String) -&gt; String {
    name == "Taylor Swift" ? "Oh wow!" : "Hello, \(name)"
}
</code></pre>



<p>这是一个单一表达式。如果 name 等于 &#8220;Taylor Swift&#8221;，那么它的解析结果是这样的：</p>



<ul class="wp-block-list">
<li>Swift 将检查 name 是否为 Taylor Swift。</li>



<li>是，所以 name ==&#8221;Taylor Swift &#8220;为真。</li>



<li>三元运算符会意识到它的条件现在为真，所以它会选择 &#8220;哦哇&#8221;，而不是 &#8220;你好，（名字）&#8221;。</li>
</ul>



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



<h2 class="wp-block-heading">如何从函数中返回多个值</h2>



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



<pre class="wp-block-code"><code>func isUppercase(string: String) -&gt; Bool {
    string == string.uppercased()
}
</code></pre>



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



<p>如果想从函数中返回两个或多个值，可以使用数组。例如，这里有一个返回用户详细信息的数组：</p>



<pre class="wp-block-code"><code>func getUser() -&gt; &#91;String] {
    &#91;"Taylor", "Swift"]
}

let user = getUser()
print("Name: \(user&#91;0]) \(user&#91;1])") 
</code></pre>



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



<p>我们可以使用字典来代替，但这也有自己的问题：</p>



<pre class="wp-block-code"><code>func getUser() -&gt; &#91;String: String] {
    &#91;
        "firstName": "Taylor",
        "lastName": "Swift"
    ]
}

let user = getUser()
print("Name: \(user&#91;"firstName", default: "Anonymous"]) \(user&#91;"lastName", default: "Anonymous"])")
</code></pre>



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



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



<p>下面是我们的函数返回元组时的样子：</p>



<pre class="wp-block-code"><code>func getUser() -&gt; (firstName: String, lastName: String) {
    (firstName: "Taylor", lastName: "Swift")
}

let user = getUser()
print("Name: \(user.firstName) \(user.lastName)")
</code></pre>



<p>让我们分析一下&#8230;</p>



<ol class="wp-block-list">
<li>现在我们的返回类型是**<code>（firstName: String, lastName: String）</code>**，这是一个包含两个字符串的元组。</li>



<li>元组中的每个字符串都有一个名称。这些名称没有加引号：它们是元组中每个项目的特定名称，与字典中的任意键不同。</li>



<li>在函数中，我们会发回一个元组，其中包含我们承诺的所有元素，并附加了名称：firstName 将被设置为 &#8220;Taylor&#8221;，等等。</li>



<li>当我们调用 getUser() 时，我们可以使用键名读取元组的值：firstName、lastName 等。</li>
</ol>



<p>我知道元组看起来与字典非常相似，但它们是不同的：</p>



<ol class="wp-block-list">
<li>访问字典中的值时，Swift 无法提前知道它们是否存在。是的，我们知道 user[&#8220;firstName&#8221;] 会存在，但 Swift 无法确定，所以我们需要提供一个默认值。</li>



<li>当您访问元组中的值时，Swift 会提前知道它是否可用，因为元组表示它将可用。</li>



<li>我们使用 user.firstName 访问值：它不是字符串，因此也不会出现错别字。</li>



<li>除了 &#8220;firstName &#8220;之外，我们的字典可能还包含数百个其他值，但我们的元组不可能——我们必须列出它将包含的所有值，因此它保证包含所有值，而不包含其他值。</li>
</ol>



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



<p>使用元组时，还有三件事必须知道。</p>



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



<pre class="wp-block-code"><code>func getUser() -&gt; (firstName: String, lastName: String) {
    ("Taylor", "Swift")
}
</code></pre>



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



<pre class="wp-block-code"><code>func getUser() -&gt; (String, String) {
    ("Taylor", "Swift")
}

let user = getUser()
print("Name: \(user.0) \(user.1)")
</code></pre>



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



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



<p>要理解我的意思，请先看一下这段代码：</p>



<pre class="wp-block-code"><code>func getUser() -&gt; (firstName: String, lastName: String) {
    (firstName: "Taylor", lastName: "Swift")
}

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

print("Name: \(firstName) \(lastName)")
</code></pre>



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



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



<pre class="wp-block-code"><code>let (firstName, lastName) = getUser()
print("Name: \(firstName) \(lastName)")
</code></pre>



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



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



<pre class="wp-block-code"><code>let (firstName, _) = getUser()
print("Name: \(firstName)")
</code></pre>



<h2 class="wp-block-heading">在 Swift 中，什么时候应该使用数组、集合或元组？</h2>



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



<p>请记住：数组保持顺序，可以有重复数据；集合是无序的，不能有重复数据；而元组内部有固定数量、固定类型的值。</p>



<p>所以：</p>



<ul class="wp-block-list">
<li>如果你想存储游戏字典中所有单词的列表，那么这个列表没有重复，顺序也不重要，所以你会选择集合。</li>



<li>如果要存储用户阅读过的所有文章，如果顺序无关紧要（如果只关心用户是否阅读过），则使用集合；如果顺序重要，则使用数组。</li>



<li>如果要存储一个视频游戏的高分列表，则顺序很重要，而且可能包含重复内容（如果两个玩家获得了相同的分数），因此要使用数组。</li>



<li>如果要存储待办事项列表中的项目，如果顺序是可预测的，则效果最佳，因此应使用数组。</li>



<li>如果要保存两个字符串，或两个字符串和一个整数，或三个布尔值，或类似的内容，则应使用元组。</li>
</ul>



<h2 class="wp-block-heading">有关元组的一些例子</h2>



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



<pre class="wp-block-code"><code>let fruits = ("orange", 10)
fruits.0 //调用时
fruits.1

let fruits = (name: "orange", price: 10)
fruits.name
fruits.price
</code></pre>



<p>可以结合别名使用：</p>



<pre class="wp-block-code"><code>typealias Human = (name: String, height: Double, hairColor: String)
//typealias是声明类型别名
</code></pre>



<p>例子1：</p>



<pre class="wp-block-code"><code>//例子
let girl = ("Amy", 155, "Gold")
print("This girl's name is \(girl.0), height is \(girl.1), hair's color is \(girl.2)")
</code></pre>



<p>例子2：</p>



<pre class="wp-block-code"><code>//例子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换成下划线"_"
</code></pre>



<p>例子3：</p>



<pre class="wp-block-code"><code>//例子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 &gt; totalB ? (studentA.name, totalA / 4) : (studentB.name, totalB / 4)
print("\(result)的平均分数较高，平均分数为\(average)分。")
</code></pre>



<h2 class="wp-block-heading">如何自定义参数标签</h2>



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



<pre class="wp-block-code"><code>func rollDice(sides: Int, count: Int) -&gt; &#91;Int] {
    // Start with an empty array
    var rolls = &#91;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)
</code></pre>



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



<pre class="wp-block-code"><code>func hireEmployee(name: String) { }
func hireEmployee(title: String) { }
func hireEmployee(location: String) { }
</code></pre>



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



<pre class="wp-block-code"><code>let lyric = "I see a red door and I want it painted black"
print(lyric.hasPrefix("I see"))
</code></pre>



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



<pre class="wp-block-code"><code>func isUppercase(string: String) -&gt; Bool {
    string == string.uppercased()
}

let string = "HELLO, WORLD"
let result = isUppercase(string: string)
</code></pre>



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



<pre class="wp-block-code"><code>func isUppercase(_ string: String) -&gt; Bool {
    string == string.uppercased()
}

let string = "HELLO, WORLD"
let result = isUppercase(string)
</code></pre>



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



<pre class="wp-block-code"><code>func printTimesTables(number: Int) {
    for i in 1...12 {
        print("\(i) x \(number) is \(i * number)")
    }
}

printTimesTables(number: 5)
</code></pre>



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



<pre class="wp-block-code"><code>func printTimesTables(for: Int) {
    for i in 1...12 {
        print("\(i) x \(for) is \(i * for)")
    }
}

printTimesTables(for: 5)
</code></pre>



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



<pre class="wp-block-code"><code>func printTimesTables(for number: Int) {
    for i in 1...12 {
        print("\(i) x \(number) is \(i * number)")
    }
}

printTimesTables(for: 5)
</code></pre>



<p>其中有三点需要仔细研究：</p>



<ol class="wp-block-list">
<li>我们写 for number: Int：外部名称是 for，内部名称是 number，类型是 Int。</li>



<li>调用函数时，我们使用外部名称作为参数：printTimesTables(for: 5)。</li>



<li>在函数内部，我们使用参数的内部名称：print(&#8220;\(i) x \(number) is \(i * number)&#8221;)。</li>
</ol>



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



<h2 class="wp-block-heading">何时省略参数标签？</h2>



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



<ul class="wp-block-list">
<li>启用闹钟应该是 enable(alarm)，而不是 enable(alarm: alarm)</li>



<li>唱一首歌应该是 sing(song)，而不是 sing(song: song)</li>



<li>查找客户应使用 find(customer)，而不是 find(user: customer)</li>
</ul>



<p>当参数标签很可能与输入的名称相同时，这一点尤为重要：</p>



<ul class="wp-block-list">
<li>购买产品应使用 buy(toothbrush) 而不是 buy(item: toothbrush)</li>



<li>问候一个人应使用 greet(taylor) 而不是 greet(person: taylor)</li>



<li>阅读一本书应该是 read(book)，而不是 read(book: book)</li>
</ul>



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



<p>好了，今天对函数就暂时学到这里，下一章，我们继续！</p>
]]></content:encoded>
					
					<wfw:commentRss>https://beautylife-studio.top/2024/tech/1293/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Learning Swift &#038; SwiftUI Chapter 6</title>
		<link>https://beautylife-studio.top/2024/tech/1291/</link>
					<comments>https://beautylife-studio.top/2024/tech/1291/#respond</comments>
		
		<dc:creator><![CDATA[Alan C.]]></dc:creator>
		<pubDate>Fri, 12 Apr 2024 04:17:05 +0000</pubDate>
				<category><![CDATA[Technology]]></category>
		<category><![CDATA[Learning Swift & SwiftUI]]></category>
		<guid isPermaLink="false">https://beautylife-studio.top/?p=1291</guid>

					<description><![CDATA[循环]]></description>
										<content:encoded><![CDATA[
<p>今天我们一起来学习循环。循环是一个好东西，它使得计算机称为如此出色的工具，它利用了计算机的速度每秒重复运行着简单的任务数十亿次而不知疲惫。</p>



<p>不过，循环虽然是一个好东西，它很有用，但如果处理不当，它也是造成计算机死机的罪魁祸首。这到底是怎么回事，让我们今天一起来一探究竟。</p>



<h2 class="wp-block-heading">如何使用 for 循环重复工作</h2>



<p>计算机非常擅长做重复性工作，而 Swift 可以轻松地重复某些代码固定的次数，或者对数组、字典或集合中的每个项目重复一次。</p>



<p>让我们从简单的事情开始：如果我们有一个字符串数组，我们可以像这样打印每个字符串：</p>



<pre class="wp-block-code"><code>let platforms = &#91;"iOS", "macOS", "tvOS", "watchOS"]

for os in platforms {
    print("Swift works great on \(os).")
}
</code></pre>



<p>这将循环遍历&nbsp;<strong><code>platforms</code></strong>&nbsp;中的所有项目，并将它们逐一放入&nbsp;<strong><code>os</code></strong>&nbsp;中。我们没有在其他地方创建&nbsp;<strong><code>os</code></strong>；它是作为循环的一部分为我们创建的，并且只在开头和结尾的大括号内可用。</p>



<p>括号内是我们要为数组中的每个项运行的代码，因此上面的代码将打印四行，每个循环项一行。首先，它将 &#8220;iOS&#8221;放在其中并调用&nbsp;<strong><code>print()</code></strong>，然后将 &#8220;macOS&#8221;放在其中并调用&nbsp;<strong><code>print()</code></strong>，接着是 &#8220;tvOS&#8221;，最后是 &#8220;watchOS&#8221;。</p>



<p>为了便于理解，我们给这些东西起了共同的名字：</p>



<ul class="wp-block-list">
<li>我们将大括号内的代码称为循环体</li>



<li>我们称循环体的一次循环为循环迭代。</li>



<li>我们称 <strong><code>os</code></strong> 为循环变量。它只存在于循环体中，并将在下一次循环迭代中改变为新值。</li>
</ul>



<p>应该说，<strong><code>os</code></strong>&nbsp;这个名字并不特别，我们可以这样写：</p>



<pre class="wp-block-code"><code>for name in platforms {
    print("Swift works great on \(name).")
}
</code></pre>



<p>甚至这样写：</p>



<pre class="wp-block-code"><code>for androidOS in platforms {
    print("Swift works great on \(androidOS).")
}
</code></pre>



<p>代码仍然可以完全一样地工作。</p>



<p>事实上，Xcode 在这方面真的很聪明：如果你写的是&nbsp;<strong><code>for plat</code></strong>，它会识别出有一个名为&nbsp;<strong><code>platforms</code></strong>&nbsp;的数组，并自动完成&nbsp;<strong><code>for platform in platforms</code></strong>&nbsp;——它会识别出 platforms 是复数，并建议循环变量使用单数名称。当看到 Xcode 的建议出现时，按 Return 键选择它。</p>



<p>你也可以在一个固定的数字范围内循环，而不是在一个数组（或集合，或字典——语法相同！）上循环。例如，我们可以像这样打印出从 1 到 12 的 5 倍表：</p>



<pre class="wp-block-code"><code>for i in 1...12 {
    print("5 x \(i) is \(5 * i)")
}
</code></pre>



<p>其中有几处是新知识，让我们暂停一下，仔细研究一下：</p>



<ul class="wp-block-list">
<li>我使用了循环变量 <strong><code>i</code></strong>，这是 &#8220;正在计数的数字 &#8220;的常用编码约定。如果你在数第二个数字，你会使用 <strong><code>j</code></strong>，如果你在数第三个数字，你会使用 <strong><code>k</code></strong>，但如果你在数第四个数字，也许你应该选择更好的变量名。</li>



<li><strong><code>1...12</code></strong> 部分是一个范围，意思是 &#8220;介于 1 和 12 之间的所有整数，以及 1 和 12 本身&#8221;。在 Swift 中，范围是一种独特的数据类型。</li>
</ul>



<p>因此，当循环第一次运行时，i 将是 1，然后是 2、3 等，一直到 12，之后循环结束。</p>



<p>你还可以将循环放在循环内部，称为嵌套循环，就像下面这样：</p>



<pre class="wp-block-code"><code>for i in 1...12 {
    print("The \(i) times table:")

    for j in 1...12 {
        print("  \(j) x \(i) is \(j * i)")
    }

    print()
}
</code></pre>



<p>这展示了其他一些新东西，让我们再次暂停并仔细观察一下：</p>



<ul class="wp-block-list">
<li>现在有一个嵌套循环：我们从 1 数到 12，里面的每个数字我们再从 1 数到 12。</li>



<li>单独使用 <strong><code>print()</code></strong>，在没有输入文本或数值的情况下，只会开始一行新的内容。这有助于分割我们的输出，使其在屏幕上看起来更美观。</li>
</ul>



<p>因此，当你看到&nbsp;<strong><code>x...y</code></strong>&nbsp;时，你就会知道它创建了一个范围，从&nbsp;<strong><code>x</code></strong>&nbsp;开始，一直数到&nbsp;<strong><code>y</code></strong>。</p>



<p>Swift 有一种类似但不同的范围类型，它可以数到但不包括最后一个数字：<strong><code>..&lt;</code></strong>。这在代码中表现得淋漓尽致：</p>



<pre class="wp-block-code"><code>for i in 1...5 {
    print("Counting from 1 through 5: \(i)")
}

print()

for i in 1..&lt;5 {
    print("Counting 1 up to 5: \(i)")
}
</code></pre>



<p>运行时，在第一个循环中会打印数字 1、2、3、4、5，但在第二个循环中只会打印数字 1、2、3 和 4。我将&nbsp;<strong><code>1...5</code></strong>&nbsp;发音为 &#8220;一到包含五&#8221;，将&nbsp;<strong><code>1..&lt;5</code></strong>&nbsp;发音为 &#8220;一到不包含五&#8221;，在 Swift 的其他地方你也会看到类似的措辞。</p>



<p><strong>提示</strong>：**<code>..&lt;</code>**对于处理数组很有帮助，在数组中，我们从 0 开始计数，并经常希望数到但不包括数组中的项数。</p>



<p>在说完&nbsp;<strong><code>for</code></strong>&nbsp;循环之前，我还想说一件事：有时候，你想使用一个范围运行一定次数的代码，但实际上你并不想要循环变量——你不想要&nbsp;<strong><code>i</code></strong>&nbsp;或**<code>j</code>**，因为你并不使用它。</p>



<p>在这种情况下，你可以用下划线代替循环变量，就像这样：</p>



<pre class="wp-block-code"><code>var lyric = "Haters gonna"

for _ in 1...5 {
    lyric += " hate"
}

print(lyric)
</code></pre>



<h2 class="wp-block-heading">Swift 为什么在循环中使用下划线？</h2>



<p>如果要循环遍历数组中的项目，可以编写这样的代码：</p>



<pre class="wp-block-code"><code>let names = &#91;"Sterling", "Cyril", "Lana", "Ray", "Pam"]

for name in names {
    print("\(name) is a secret agent")
}
</code></pre>



<p>每次循环时，Swift 都会从&nbsp;<strong><code>names</code></strong>&nbsp;数组中读取一项，将其放入&nbsp;<strong><code>name</code></strong>&nbsp;常量，然后执行循环的正文——这就是&nbsp;<strong><code>print()</code></strong>&nbsp;方法。</p>



<p>不过，有时你实际上并不需要当前读取的值，这就是下划线的作用所在： Swift 会识别出你实际上并不需要这个变量，因此不会为你创建临时常量。</p>



<p>当然，Swift 也能看到这一点——它能看到你是否在循环中使用了&nbsp;<strong><code>name</code></strong>，所以不用下划线也能完成同样的工作。不过，使用下划线对我们的大脑也有类似的作用：我们可以查看代码，并立即发现循环变量没有被使用，无论循环体内部有多少行代码。</p>



<p>因此，如果你没有在循环体中使用循环变量，Swift 会警告你像这样重写：</p>



<pre class="wp-block-code"><code>let names = &#91;"Sterling", "Cyril", "Lana", "Ray", "Pam"]

for _ in names {
    print("&#91;CENSORED] is a secret agent!")
}
</code></pre>



<h2 class="wp-block-heading">为什么Swift有两个范围操作？</h2>



<p>当我们考虑数值的范围时，语言是相当令人困惑的。如果我说 &#8220;给我截至昨天的销售数据&#8221;，是指包括昨天还是不包括昨天？两者都有各自的用处，因此 Swift 提供了一种同时表示两者的方法：<strong><code>..&lt;</code></strong>&nbsp;是半开放范围，表示 &#8220;截至但不包括&#8221;；**<code>...</code>**是封闭范围操作符，表示 &#8220;截至并包括&#8221;。</p>



<p>为了在交谈时更容易区分，Swift 经常使用非常具体的语言： &#8220;1 to 5 &#8220;表示 1、2、3 和 4，而 &#8220;1 through 5 &#8220;表示 1、2、3、4 和 5。如果你还记得，Swift 的数组从索引 0 开始，这意味着一个包含三个项的数组的索引分别为 0、1 和 2，这是半开范围操作符的完美用例。</p>



<p>当你只想要某个范围的一部分时，情况就变得更有趣了，比如 &#8220;从 0 开始的任何内容 &#8220;或 &#8220;索引 5 到数组的末尾&#8221;。你看，这些在编程中相当有用，所以 Swift 让我们只指定范围的一部分，从而使它们更容易创建。</p>



<p>例如，如果我们有这样一个名称数组：</p>



<pre class="wp-block-code"><code>let names = &#91;"Piper", "Alex", "Suzanne", "Gloria"]
</code></pre>



<p>我们可以这样读出一个人的名字：</p>



<pre class="wp-block-code"><code>print(names&#91;0])
</code></pre>



<p>通过范围，我们还可以打印出类似这样的数值范围：</p>



<pre class="wp-block-code"><code>print(names&#91;1...3])
</code></pre>



<p>不过，这样做有一个小风险：如果我们的数组并不不包含四个项目，那么 1&#8230;3 就会失败。幸运的是，我们可以使用单边范围来表示 &#8220;给我 1 到数组的末尾&#8221;，就像这样：</p>



<pre class="wp-block-code"><code>print(names&#91;1...])
</code></pre>



<p>因此，范围非常适合对特定值进行计数，但也有助于从数组中读取项目组。</p>



<h2 class="wp-block-heading">如何使用 while 循环重复工作</h2>



<p>Swift 还有一种名为&nbsp;<strong><code>while</code></strong>&nbsp;的循环：提供一个条件，<strong><code>while</code></strong>&nbsp;循环就会持续执行循环体，直到条件为假。</p>



<p>虽然你仍会不时看到&nbsp;<strong><code>while</code></strong>&nbsp;循环，但它们并不像&nbsp;<strong><code>for</code></strong>&nbsp;循环那样常见。因此，我想介绍一下它们，让你知道它们的存在，但我们不要在它们身上花费太久时间。</p>



<p>下面是一个基本的&nbsp;<strong><code>while</code></strong>&nbsp;循环，让我们开始学习：</p>



<pre class="wp-block-code"><code>var countdown = 10

while countdown &gt; 0 {
    print("\(countdown)…")
    countdown -= 1
}

print("Blast off!")
</code></pre>



<p>这样，循环体——打印数字并减去 1——将持续运行，直到倒计数等于或小于 0，此时循环结束并打印最终信息。</p>



<p>当你不知道循环将运行多少次时，<strong><code>while</code></strong>&nbsp;循环就非常有用。为了演示这一点，我想向你介绍&nbsp;<strong><code>Int</code></strong>&nbsp;和&nbsp;<strong><code>Double</code></strong>&nbsp;都有的一个非常有用的功能：<strong><code>random(in:)</code></strong>。给它一个数字范围，它就会在这个范围内的某个地方随机返回一个&nbsp;<strong><code>Int</code></strong>&nbsp;或&nbsp;<strong><code>Double</code></strong>。</p>



<p>例如，以下代码会创建一个介于 1 和 1000 之间的新整数：</p>



<pre class="wp-block-code"><code>let id = Int.random(in: 1...1000)
</code></pre>



<p>这样就会随机产生一个介于 0 和 1 之间的小数：</p>



<pre class="wp-block-code"><code>let amount = Double.random(in: 0...1)
</code></pre>



<p>我们可以将此功能与&nbsp;<strong><code>while</code></strong>&nbsp;循环结合使用，反复掷一些虚拟的 20 面骰子，只有当掷出 20 时才结束循环。</p>



<p>下面是实现这一功能的代码：</p>



<pre class="wp-block-code"><code>// create an integer to store our roll
var roll = 0

// carry on looping until we reach 20
while roll != 20 {
    // roll a new dice and print what it was
    roll = Int.random(in: 1...20)
    print("I rolled a \(roll)")
}

// if we're here it means the loop ended – we got a 20!    
print("Critical hit!")
</code></pre>



<p>当你不知道在一个循环中应该使用**<code>for</code><strong>还是</strong><code>while</code><strong>时，你可以这样判断：当你知道循环的范围或次数时，一般都是使用</strong><code>for</code><strong>循环；当你不知道循环需要进行多少次，只知道它何时（什么条件）会停下来时，你就应该使用</strong><code>while</code>**。</p>



<h2 class="wp-block-heading">何时使用 while 循环？</h2>



<p>Swift 为我们提供了&nbsp;<strong><code>for</code></strong>&nbsp;循环和&nbsp;<strong><code>while</code></strong>&nbsp;循环，这两种循环都很常用。然而，在刚开始学习时，有两种常用的循环方式似乎很奇怪——你应该使用哪一种，为什么？</p>



<p>主要区别在于，<strong><code>for</code></strong>&nbsp;循环一般用于有限序列：例如，我们循环数字 1 到 10，或者循环数组中的项目。另一方面，<strong><code>while</code></strong>&nbsp;循环可以循环到任意条件变为 false 为止，直到我们告诉它们停止为止。</p>



<p>这意味着我们可以重复相同的代码，直到&#8230;</p>



<ul class="wp-block-list">
<li>&#8230;用户要求我们停止</li>



<li>&#8230;服务器要求我们停止</li>



<li>&#8230;我们找到了想要的答案</li>



<li>&#8230;我们生成了足够多的数据</li>
</ul>



<p>以上只是一些例子。想想看：如果我让你掷 10 次骰子并打印结果，你可以用一个简单的 for 循环从 1 数到 10 来完成。但如果我让你掷 10 个骰子并打印结果，同时在前一个骰子结果相同时自动再掷另一个骰子，那么你就无法提前知道需要掷多少个骰子。也许你会很幸运，只需要掷 10 次骰子，但也许你会重复掷几次，需要掷 15 次骰子。又或者你会遇到很多重复掷骰子的情况，需要掷 30 次——谁知道呢？</p>



<p>这时&nbsp;<strong><code>while</code></strong>&nbsp;循环就派上用场了：我们可以一直循环下去，直到准备好退出。</p>



<h2 class="wp-block-heading">如何使用 break 和 continue 跳过循环项</h2>



<p>Swift 提供了两种跳过循环中一个或多个项目的方法：<strong><code>continue</code></strong>&nbsp;跳过当前循环迭代，<strong><code>break</code></strong>&nbsp;跳过所有剩余迭代。与&nbsp;<strong><code>while</code></strong>&nbsp;循环一样，这两种方法有时也会用到，但在实际应用中却比想象的要少得多。</p>



<p>让我们从&nbsp;<strong><code>continue</code></strong>&nbsp;开始逐一了解它们。在对数据数组进行循环时，Swift 会从数组中取出一个项目，并使用它执行循环体。如果在循环体中调用&nbsp;<strong><code>continue</code></strong>，Swift 会立即停止执行当前的循环迭代，并跳转到循环中的下一个项目，然后继续正常执行。这通常用在循环的起始位置，在这里你可以消除那些没有通过你选择的测试的循环变量。</p>



<p>下面是一个例子：</p>



<pre class="wp-block-code"><code>let filenames = &#91;"me.jpg", "work.txt", "sophie.jpg", "logo.psd"]

for filename in filenames {
    if filename.hasSuffix(".jpg") == false {
        continue
    }

    print("Found picture: \(filename)")
}
</code></pre>



<p>它会创建一个文件名字符串数组，然后循环遍历每一个文件名字符串，并检查其后缀是否为&#8221;.jpg&#8221;——即是否是一张图片。继续用于所有未通过该测试的文件名，这样就跳过了循环体的其余部分。</p>



<p>至于&nbsp;<strong><code>break</code></strong>，它会立即退出循环，并跳过所有剩余的迭代。为了演示这一点，我们可以编写一些代码来计算两个数字的 10 个公倍数：</p>



<pre class="wp-block-code"><code>let number1 = 4
let number2 = 14
var multiples = &#91;Int]()

for i in 1...100_000 {
    if i.isMultiple(of: number1) &amp;&amp; i.isMultiple(of: number2) {
        multiples.append(i)

        if multiples.count == 10 {
            break
        }
    }
}

print(multiples)
</code></pre>



<p>这样就可以做很多事情了：</p>



<ol class="wp-block-list">
<li>创建两个常量来保存两个数字。</li>



<li>创建一个整数数组变量，用于存储两个数字的公倍数。</li>



<li>从 1 数到 100000，将每个循环变量赋值给 i。</li>



<li>如果 i 是第一个和第二个数字的倍数，则将其追加到整数数组中。</li>



<li>一旦数到 10 个倍数，就调用 break 退出循环。</li>



<li>打印出结果数组。</li>
</ol>



<p>因此，当你想跳过当前循环迭代的剩余部分时，请使用&nbsp;<strong><code>continue</code></strong>；当你想跳过所有剩余的循环迭代时，请使用&nbsp;<strong><code>break</code></strong>。</p>



<h2 class="wp-block-heading">为什么要退出循环？</h2>



<p>Swift 的 break 关键字可以让我们立即退出循环，无论我们在讨论哪种循环。很多时候你并不需要这样做，因为你正在循环数组中的项，并希望处理所有这些项，或者因为你正在从 1 数到 10，并希望处理所有这些值。</p>



<p>不过，有时你确实希望提前结束循环。例如，如果您有一个分数数组，并想知道玩家取得了多少分数而没有得到 0，你可以这样写：</p>



<pre class="wp-block-code"><code>let scores = &#91;1, 8, 4, 3, 0, 5, 2]
var count = 0

for score in scores {
    if score == 0 {
        break
    }

    count += 1
}

print("You had \(count) scores before you got 0.")
</code></pre>



<p>如果不中断，即使找到了第一个 0，我们也需要继续循环分数，这是一种浪费。</p>



<h2 class="wp-block-heading">Swift为什么会有贴标签的声明？</h2>



<p>Swift 的标签语句允许我们为代码的某些部分命名，最常用于打破嵌套循环。</p>



<p>为了演示标签语句，让我们来看一个例子：我们试图找出打开保险箱的正确动作组合。我们可以先定义一个包含所有可能动作的数组：</p>



<pre class="wp-block-code"><code>let options = &#91;"up", "down", "left", "right"]
</code></pre>



<p>为便于测试，以下是我们要猜测的秘密组合：</p>



<pre class="wp-block-code"><code>let secretCombination = &#91;"up", "up", "right"]
</code></pre>



<ul class="wp-block-list">
<li>要找到这种组合，我们需要制作包含所有可能的三色变量的数组：</li>



<li>上、上、上</li>



<li>上、上、下</li>



<li>上、上、左</li>



<li>上、上、右</li>



<li>上、下、左</li>



<li>上、下、右</li>
</ul>



<p>&#8230;&#8230;你明白了吧。</p>



<p>要做到这一点，我们可以写三个循环，一个嵌套在另一个里面，就像这样：</p>



<pre class="wp-block-code"><code>for option1 in options {
    for option2 in options {
        for option3 in options {
            print("In loop")
            let attempt = &#91;option1, option2, option3]

            if attempt == secretCombination {
                print("The combination is \(attempt)!")
            }
        }
    }
}
</code></pre>



<p>这样就可以多次重复相同的项目，从而创建一个尝试数组，如果尝试结果与秘密组合匹配，就会打印出一条信息。</p>



<p>但这段代码有一个问题：一旦我们找到了组合，循环就结束了，为什么循环还要继续运行呢？我们真正想说的是 &#8220;一旦找到组合，立即退出所有循环&#8221;——这就是标签语句的作用所在。我们可以用标签语句来写：</p>



<pre class="wp-block-code"><code>outerLoop: for option1 in options {
    for option2 in options {
        for option3 in options {
            print("In loop")
            let attempt = &#91;option1, option2, option3]

            if attempt == secretCombination {
                print("The combination is \(attempt)!")
                break outerLoop
            }
        }
    }
}
</code></pre>



<p>有了这个小改动，一旦找到组合，这三个循环就会停止运行。在这种微不足道的情况下，性能差别不大，但如果你的项目有数百甚至数千个呢？保存这样的工作是个好主意，值得你为自己的代码牢记。</p>



<h2 class="wp-block-heading">何时使用break，何时使用continue</h2>



<p>有时，Swift 学习者很难理解什么时候使用 break 关键字是正确的，什么时候使用 continue 关键字是正确的，因为它们都会改变我们的循环流程。</p>



<p>当我们使用 continue 关键字时，我们是在说 &#8220;我已经完成了这个循环的当前运行&#8221;——Swift 会跳过循环体的其余部分，转到循环中的下一个项目。但当我们说 break 时，我们是在说 &#8220;我已经完全结束了这个循环，所以请完全退出&#8221;。这意味着 Swift 将跳过循环体的剩余部分，同时也跳过任何其他循环项。</p>



<p>就像 break 一样，如果你愿意，也可以在带标签的语句中使用 continue，但老实说，我不记得见过有人这么做！</p>



<h2 class="wp-block-heading">总结：条件和循环</h2>



<p>在前面的章节中，我们已经介绍了很多关于条件和循环的内容，现在让我们来回顾一下：</p>



<ul class="wp-block-list">
<li>我们使用 if 语句来检查条件是否为真。你可以输入任何你想要的条件，但最终必须归结为布尔值。</li>



<li>如果需要，可以添加 else 块和/或多个 else if 块来检查其他条件。Swift 会按顺序执行这些代码。</li>



<li>你可以使用 || 组合条件，这意味着如果其中一个子条件为真，则整个条件为真；或者使用 &amp;&amp;，这意味着如果两个子条件都为真，则整个条件为真。</li>



<li>如果经常重复同类检查，可以使用 switch 语句来代替。这些语句必须始终穷举，这可能意味着要添加默认情况。</li>



<li>如果其中一个 switch 用到了 fallthrough，这意味着 Swift 会在之后执行下面的 case。这种情况并不常用。</li>



<li>三元条件操作符让我们可以检查 WTF： 什么、真、假。虽然一开始读起来有点困难，但在 SwiftUI 中你会看到它被频繁使用。</li>



<li>for 循环让我们可以对数组、集合、字典和范围进行循环。你可以将项目分配给循环变量并在循环中使用，也可以使用下划线 _ 忽略循环变量。</li>



<li>while 循环让我们可以制作自定义循环，持续运行直到某个条件变为 false。</li>



<li>我们可以分别使用 continue 或 break 跳过部分或全部循环项。</li>
</ul>



<p>这又是一大段新内容，但有了条件和循环，我们现在已经掌握了足够的知识来构建一些非常有用的软件——让我们试试看吧！</p>



<h2 class="wp-block-heading">思考题</h2>



<p>编写一个playground小程序，筛选1到100，对于每个数字：</p>



<ul class="wp-block-list">
<li>如果是3的倍数，请打印“3的倍数”</li>



<li>如果是5的倍数，请打印“5的倍数”</li>



<li>如果是3和5的公倍数，请打印“3和5的公倍数”</li>



<li>其他的数字只需打印数字即可</li>
</ul>



<p>代码的解法可能不止一种，大家不用纠结正确答案，只要结果正确即可。</p>



<p>下一章，我们开始学习函数。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://beautylife-studio.top/2024/tech/1291/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
