已经完成了所有关于Swift基础的部分,感觉东西说多不多,说少也确实有一些部分理解起来有一些困难。从昨天开始的三天为快速回顾(因为是快速回顾名,我只是浏览一遍唤醒记忆,直接摘录机器翻译,以后再人工校正):

函数

函数使您可以定义执行特定功能的可重用代码段。通常,函数能够接收一些值来修改其工作方式,但这不是必需的。

让我们从一个简单的函数开始:

1
2
3
func favoriteAlbum() {
print("My favorite is Fearless")
}

如果您将该代码放到操场上,将不会打印任何内容。是的,这是正确的。没有打印任何内容的原因是,我们已将“我最喜欢的是Fearless”消息放入名为的函数中favoriteAlbum(),并且直到我们要求Swift运行该favoriteAlbum()函数时,该代码才会被调用。为此,请添加以下代码行:

1
favoriteAlbum()

该函数运行(或“调用”)该函数,因此现在您将看到“我的最爱是Fearless”打印出来。

如您所见,您可以通过编写func,然后是您的函数名,然后打开和关闭括号,然后是用打开和关闭花括号标记的代码块来定义函数。然后,您可以通过写入函数名称以及其后的右括号和右括号来调用该函数。

当然,这是一个愚蠢的示例–该函数无论做什么都执行相同的操作,因此它没有任何意义。但是,如果我们想每次打印不同的专辑怎么办?在这种情况下,我们可以告诉Swift我们希望函数在调用时接受一个值,然后在其中使用该值。

让我们现在开始:

1
2
3
func favoriteAlbum(name: String) {
print("My favorite is \(name)")
}

这告诉Swift我们希望函数接受一个名为“ name”的值(称为“参数”),该值应该是字符串。然后,我们使用字符串插值法将喜欢的专辑名称直接写入输出消息中。要立即调用该函数,请编写以下代码:

1
favoriteAlbum(name: "Fearless")

鉴于它仍然只是一行代码,您可能仍然想知道这是什么意思。好吧,想象一下我们在一个大型应用程序的20个不同地方使用了该功能,然后您的首席设计师出现并告诉您将消息更改为“我非常喜欢Fearless –这是我的最爱!” 您是否真的要查找并更改代码中的所有20个实例?可能不会。使用功能,只需更改一次,一切都会更新。

您可以使函数接受任意数量的参数,因此让它接受名称和年份:

1
2
3
4
5
6
7
func printAlbumRelease(name: String, year: Int) {
print("\(name) was released in \(year)")
}

printAlbumRelease(name: "Fearless", year: 2008)
printAlbumRelease(name: "Speak Now", year: 2010)
printAlbumRelease(name: "Red", year: 2012)

这些函数参数名称很重要,实际上构成了函数本身的一部分。有时,您会看到几个具有相同名称的函数,例如handle(),但具有不同的参数名称以区分不同的动作。

外部和内部参数名称

有时,您希望在调用函数时以一种方式命名参数,而在函数本身内部以另一种方式命名。这意味着当您调用一个函数时,它几乎使用自然的英语,但是在函数内部,参数具有合理的名称。此技术在Swift中经常使用,因此现在值得理解。

为了演示这一点,让我们编写一个函数,该函数打印字符串中的字母数。使用count字符串的属性可以使用它,因此我们可以这样编写:

1
2
3
func countLettersInString(string: String) {
print("The string \(string) has \(string.count) letters.")
}

有了该功能,我们可以这样称呼它:

1
countLettersInString(string: "Hello")

虽然确实可以,但是有点罗word。另外,这不是您要大声说的那种话:“在字符串中打个招呼字母”。

Swift的解决方案是让您在调用参数时为其指定一个名称,并在方法内部指定另一个名称。要使用此功能,只需两次写入参数名称-一次用于外部,一次用于内部。

例如,我们可以在调用参数myString时str在方法内部命名该参数,如下所示:

1
2
3
4
5
func countLettersInString(myString str: String) {
print("The string \(str) has \(str.count) letters.")
}

countLettersInString(myString: "Hello")

您还可以指定一个下划线_作为外部参数名称,以告诉Swift它根本不应该具有任何外部名称。例如:

1
2
3
4
5
func countLettersInString(_ str: String) {
print("The string \(str) has \(str.count) letters.")
}

countLettersInString("Hello")

如您所见,这使代码行看起来像一个英语句子:“在字符串hello中计数字母”。

尽管在很多情况下使用_都是正确的选择,但是Swift程序员通常更喜欢命名其所有参数。仔细想想:为什么我们在函数中需要“ String”一词–我们还想在字母上加上什么?

因此,您通常会看到的是外部参数名称,例如“ in”,“ for”和“ with”,以及更有意义的内部名称。因此,编写此函数的“快捷”方式如下所示:

1
2
3
func countLetters(in string: String) {
print("The string \(string) has \(string.count) letters.")
}

这意味着您使用参数名称“ in”调用该函数,这在函数内部将毫无意义。但是,在函数内部,相同的参数称为“字符串”,这很有用。因此,该函数可以这样调用:

1
countLetters(in: "Hello")

而这是真正SWIFTY代码:“算在你好字母”读起来像是自然的英语,但代码也清晰,简洁。

返回值

Swift函数可以通过->在参数列表之后写入数据类型来返回值。完成此操作后,Swift将确保您的函数无论如何都将返回一个值,因此这又是您对代码的作用作出的保证。

例如,让我们编写一个函数,如果专辑是Taylor Swift的专辑之一,则返回true,否则返回false。这需要接受一个参数(要检查的相册的名称),并将返回一个布尔值。这是代码:

1
2
3
4
5
6
7
8
9
func albumIsTaylor(name: String) -> Bool {
if name == "Taylor Swift" { return true }
if name == "Fearless" { return true }
if name == "Speak Now" { return true }
if name == "Red" { return true }
if name == "1989" { return true }

return false
}

如果您想尝试新switch/case知识,此功能将是一个很好的地方。

现在,您可以通过传递相册名称并根据结果执行操作来调用它:

1
2
3
4
5
6
7
8
9
10
11
if albumIsTaylor(name: "Red") {
print("That's one of hers!")
} else {
print("Who made that?!")
}

if albumIsTaylor(name: "Blue") {
print("That's one of hers!")
} else {
print("Who made that?!")
}

可选值

Swift是一种非常安全的语言,我的意思是它会努力确保您的代码不会以令人惊讶的方式失败。

代码失败的最常见方式之一是尝试使用错误或丢失的数据。例如,想象一个像这样的函数:

1
2
3
func getHaterStatus() -> String {
return "Hate"
}

该函数不接受任何参数,它返回一个字符串:“ Hate”。但是,如果今天是一个特别阳光灿烂的日子,而那些仇恨者不觉得自己讨厌,该怎么办?好吧,也许我们什么都不愿回报:这个仇恨者今天没有仇恨。

现在,当涉及到字符串时,您可能会认为空字符串是不进行任何通信的一种很好的方式,有时可能是正确的。但是数字如何– 0是“空数字”吗?还是-1?

在您开始尝试为自己创建虚构规则之前,Swift有一个解决方案:可选。可选值是可能具有值或可能没有值的值。大多数人都难以理解可选内容,这没关系–我将尝试以几种方式对其进行解释,因此希望可以使用。

现在,想象一下一项调查,您问某人:“以1到5的比例,泰勒·斯威夫特有多棒?” –如果某人从未听说过她,会回答什么?当他们不知道泰勒·斯威夫特是谁时,1会不公平地提名她,5会称赞她。解决方案是可选的:“我根本不想提供电话号码。”

当我们使用-> String它时,它的意思是“这肯定会返回一个字符串”,这意味着该函数不能返回任何值,因此可以被称为安全函数,因为您将始终获得可以用作字符串的值。如果我们想告诉Swift这个函数可能返回一个值或可能不返回一个值,我们需要改用它:

1
2
3
func getHaterStatus() -> String? {
return "Hate"
}

请注意额外的问号:String?表示“可选字符串”。现在,在本例中,无论如何我们仍将返回“恨”,但让我们继续进行进一步的修改:如果天气晴朗,仇恨者将一片新叶子,并放弃了他们的仇恨生活,因此我们不希望返回任何价值。在Swift中,“无值”有一个特殊的名称:nil。

将此功能更改为:

1
2
3
4
5
6
7
func getHaterStatus(weather: String) -> String? {
if weather == "sunny" {
return nil
} else {
return "Hate"
}
}

它接受一个字符串参数(天气)并返回一个字符串(讨厌状态),但是该返回值可能存在或可能不存在-它为nil。在这种情况下,这意味着我们可能会得到一个字符串,或者我们可能会得到nil。

现在介绍重要内容:Swift希望您的代码真正安全,而尝试使用nil值是一个坏主意。它可能会使您的代码崩溃,可能使您的应用程序逻辑混乱,或者可能使您的用户界面显示错误的内容。结果,当您将值声明为可选值时,Swift将确保您安全地处理它。

现在让我们尝试一下:将这些代码行添加到您的游乐场:

1
2
var status: String
status = getHaterStatus(weather: "rainy")

第一行创建一个字符串变量,第二行为其分配来自的值,getHaterStatus()而今天的天气阴雨绵绵,所以那些讨厌的人肯定会讨厌。

该代码将不会运行,因为我们说的status是类型String,它需要一个值,但getHaterStatus()可能不会提供,因为它返回了一个可选字符串。也就是说,我们说过肯定会有一个字符串status,但getHaterStatus()可能根本不返回任何内容。Swift绝对不会让您犯此错误,这非常有用,因为它可以有效地阻止死掉所有常见的错误。

为了解决这个问题,我们需要将status变量设为a String?,或者只是完全删除类型注释,然后让Swift使用类型推断。第一个选项如下所示:

1
2
var status: String?
status = getHaterStatus(weather: "rainy")

第二个是这样的:

var status = getHaterStatus(weather: “rainy”)
无论选择哪种方法,该值都可能存在或不存在,默认情况下,Swift不会让您危险地使用它。举个例子,想象一下这样的函数:

1
2
3
4
5
func takeHaterAction(status: String) {
if status == "Hate" {
print("Hating")
}
}

那需要一个字符串并根据其内容打印一条消息。该函数需要一个String值,而不是一个String?值–您不能在此处传递可选参数,它需要一个真实的字符串,这意味着我们不能使用该status变量来调用它。

Swift有两种解决方案。两者都被使用,但是绝对比另一个更可取。第一个解决方案称为可选解包,它是使用特殊语法在条件语句中完成的。它同时执行两件事:检查可选参数是否具有值,如果有,将其解包装为非可选类型,然后运行代码块。

语法如下所示:

1
2
3
4
5
if let unwrappedStatus = status {
// unwrappedStatus contains a non-optional value!
} else {
// in case you want an else block, here you go…
}

这些if let语句在一行简洁的代码中进行检查和解包,这使它们非常常见。使用此方法,我们可以安全地解包的返回值,getHaterStatus()并确保仅takeHaterAction()使用有效的非可选字符串进行调用。这是完整的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func getHaterStatus(weather: String) -> String? {
if weather == "sunny" {
return nil
} else {
return "Hate"
}
}

func takeHaterAction(status: String) {
if status == "Hate" {
print("Hating")
}
}

if let haterStatus = getHaterStatus(weather: "rainy") {
takeHaterAction(status: haterStatus)
}

如果您了解此概念,欢迎跳到标题为“强制展开可选内容”的标题。如果您仍然不太了解可选内容,请继续阅读。

好的,如果您仍然在这里,则意味着上面的解释没有任何意义,或者您已经理解了,但可能需要进行一些说明。可选在Swift中广泛使用,因此您确实需要了解它们。我将尝试以另一种方式再次说明,希望这会有所帮助!

这是一个新功能:

1
2
3
4
5
6
7
8
9
func yearAlbumReleased(name: String) -> Int {
if name == "Taylor Swift" { return 2006 }
if name == "Fearless" { return 2008 }
if name == "Speak Now" { return 2010 }
if name == "Red" { return 2012 }
if name == "1989" { return 2014 }

return 0
}

该名称以Taylor Swift专辑的名称命名,并返回其发行年份。但是如果我们用专辑名称“ Lantern”来称呼它是因为我们将Taylor Swift和Hudson Mohawke混在一起(容易犯错,对吗?),则它返回0,因为它不是Taylor的专辑之一。

但是0在这里有意义吗?当然,如果这张专辑是在凯撒·奥古斯都(Caesar Augustus)成为罗马皇帝时于0 AD发行的,那么0也许是有意义的,但这只是令人困惑-人们需要提前知道0表示“未被识别”。

更好的主意是重写该函数,以便它返回一个整数(当发现一年时)或nil(当什么都没发现时),这要归功于可选函数。这是新功能:

1
2
3
4
5
6
7
8
9
func yearAlbumReleased(name: String) -> Int? {
if name == "Taylor Swift" { return 2006 }
if name == "Fearless" { return 2008 }
if name == "Speak Now" { return 2010 }
if name == "Red" { return 2012 }
if name == "1989" { return 2014 }

return nil
}

现在它返回nil,我们需要使用来包装结果,if let因为我们需要检查值是否存在。

如果您现在理解了这个概念,欢迎您跳到标题为“强制展开可选内容”的标题。如果您仍然不太了解可选内容,请继续阅读。

好吧,如果您仍然在这里,则意味着您确实在为可选项而苦苦挣扎,所以我将在最后解释它们。

这是一组名称:

1
var items = ["James", "John", "Sally"]

如果我们想编写一个在该数组中查找并告诉我们特定名称索引的函数,则可以编写如下代码:

1
2
3
4
5
6
7
8
9
func position(of string: String, in array: [String]) -> Int {
for i in 0 ..< array.count {
if array[i] == string {
return i
}
}

return 0
}

它将遍历数组中的所有项目,如果找到匹配项,则返回其位置,否则返回0。

现在尝试运行以下四行代码:

1
2
3
4
let jamesPosition = position(of: "James", in: items)
let johnPosition = position(of: "John", in: items)
let sallyPosition = position(of: "Sally", in: items)
let bobPosition = position(of: "Bob", in: items)

这将输出0、1、2、0-James和Bob的位置相同,即使一个存在而一个不存在。这是因为我用0表示“未找到”。简单的解决方法可能是使-1找不到,但是无论是0还是-1还是有问题,因为您必须记住该特定数字表示“未找到”。

解决方案是可选的:如果找到匹配项,则返回整数,否则返回nil。事实上,这正是该方法的内置“发现阵列”方法使用:someArray.firstIndex(of: someValue)。

当使用这些“可能存在,可能不存在”的值时,Swift会强制您在使用它们之前先将它们拆开,从而确认可能没有值。这就是if let语法的作用:如果可选参数有一个值,则将其解包并使用它,否则根本不要使用它。您不能偶然使用可能为空的值,因为Swift不会允许您使用。

如果您仍然不确定可选选项如何工作,那么最好的办法是在Twitter上问我,我将尽力提供帮助:您可以找到我@twostraws。

强制展开可选

Swift可让您使用感叹号字符来覆盖其安全性!。如果您知道某个可选项确实具有值,则可以通过在其后放置感叹号来强制将其拆开。

不过请小心:如果对没有值的变量尝试此操作,则代码将崩溃。

为了汇总一个可行的示例,下面是一些基础代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func yearAlbumReleased(name: String) -> Int? {
if name == "Taylor Swift" { return 2006 }
if name == "Fearless" { return 2008 }
if name == "Speak Now" { return 2010 }
if name == "Red" { return 2012 }
if name == "1989" { return 2014 }

return nil
}

var year = yearAlbumReleased(name: "Red")

if year == nil {
print("There was an error")
} else {
print("It was released in \(year)")
}

那是专辑发行的年份。如果找不到该专辑,year则将其设置为nil,并显示一条错误消息。否则,将打印年份。

还是会?好吧,yearAlbumReleased()返回一个可选的整数,并且此代码不if let用于解开该可选的整数。结果,它将打印出以下内容:“它是在Optional(2012)中发布的” –可能不是我们想要的!

在代码的这一点上,我们已经检查了我们是否有一个有效的值,因此if let在其中安全地展开可选内容是没有意义的。因此,Swift提供了一个解决方案–将第二个print()调用更改为:

print(“It was released in (year!)”)
请注意感叹号:它的意思是“我确定它包含一个值,所以请立即将其打开。”

隐式展开的可选

您还可以使用这种感叹号语法创建隐式解包的可选内容,这在某些人中确实开始感到困惑。所以,请仔细阅读!

  • 一个常规变量必须包含一个值。示例:String即使该字符串为空,也必须包含一个字符串,即””。它不能是零。
  • 一个可选的变量可能包含一个值,也可能不会。使用前必须先将其解开。示例:String?可能包含一个字符串,或者可能包含nil。找出答案的唯一方法是解开包装。
  • 一个隐式解包的可选内容可能包含一个值,也可能没有。但它并不需要在使用前要解开。Swift不会帮您检查,因此您需要格外小心。示例:String!可能包含一个字符串,或者可能包含nil-取决于您是否适当使用它。就像常规的可选内容一样,但是Swift可以让您直接访问该值,而无需展开安全性。如果您尝试这样做,则意味着您知道其中有一个价值–但是,如果您错了,则应用程序将崩溃。

遇到隐式展开的可选对象的主要时间是在iOS上的UIKit或macOS上的AppKit中使用用户界面元素时。这些需要预先声明,但是在创建它们之前不能使用它们-Apple喜欢在最后可能的时刻创建用户界面元素,以避免任何不必要的工作。必须不断解开值,您肯定知道会很烦人,因此将这些值隐式解开。

如果发现隐式解开的可选内容有些难处理,请不要担心-使用该语言时,它会变得很清楚。

可选链接

有时使用可选控件会感到有些笨拙,并且所有展开和检查工作都变得如此繁重,以至于您可能会抛出一些感叹号来强制展开内容,因此您可以继续工作。但是请注意:如果强制打开没有值的可选选项,则代码将崩溃。

Swift有两种技术可以帮助您减少代码的复杂度。第一个称为可选链接,它使您仅在可选具有值时才运行代码。将以下代码放入您的游乐场以开始我们的工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
func albumReleased(year: Int) -> String? {
switch year {
case 2006: return "Taylor Swift"
case 2008: return "Fearless"
case 2010: return "Speak Now"
case 2012: return "Red"
case 2014: return "1989"
default: return nil
}
}

let album = albumReleased(year: 2006)
print("The album is \(album)")

这将在结果窗格中输出“专辑是Optional(“ Taylor Swift”)“。

如果我们想将返回值转换albumReleased()为大写字母(即“ TAYLOR SWIFT”而不是“ Taylor Swift”),则可以调用该uppercased()字符串的方法。例如:

let str = “Hello world”
print(str.uppercased())
问题是,albumReleased()返回一个可选字符串:它可能返回一个字符串,或者可能什么都不返回。因此,我们真正的意思是,“如果返回字符串,则将其变为大写,否则不执行任何操作。” 这就是可选链接出现的地方,因为它恰好提供了该行为。

尝试将最后两行代码更改为此:

1
2
let album = albumReleased(year: 2006)?.uppercased()
print("The album is \(album)")

请注意,那里有一个问号,它是可选的链接:只有当问号之前的所有内容都具有值时,问号之后的所有内容才会运行。这不会影响的基础数据类型album,因为该行代码现在将返回nil或将返回大写专辑名称-它仍然是可选字符串。

您的可选链可以根据您的需要而定,例如:

1
let album = albumReleased(year: 2006)?.someOptionalValue?.someOtherOptionalValue?.whatever

Swift将从左到右检查它们,直到找到nil,然后停止。

无合并运算符

这个简单的Swift功能使您的代码更简单,更安全,但名字如此夸张,以至于很多人对此感到恐惧。真可惜,因为如果您花时间弄清楚nil合并运算符,将使您的生活更轻松!

它的作用是让您说“如果可以,请使用值A,但如果值A为零,则请使用值B。” 而已。这对于可选项特别有用,因为它可以有效地阻止它们成为可选项,因为您提供了非可选值B。因此,如果A是可选项并且具有值,则将使用它(我们有一个值。)没有值,B被使用(所以我们仍然有一个值)。无论哪种方式,我们绝对都有价值。

为了给您一个真实的上下文,请尝试在操场上使用以下代码:

1
2
let album = albumReleased(year: 2006) ?? "unknown"
print("The album is \(album)")

这个双重问号是nil合并运算符,在这种情况下,它的意思是“如果albumReleased()返回值,则将其放入album变量中,但如果albumReleased()返回nil,则使用’unknown’。

如果您现在在结果窗格中查看,您会看到其中印有“专辑是Taylor Swift”的标签-没有更多的可选内容。这是因为Swift现在可以确定它会返回真实值,或者是因为该函数返回了一个值,或者是因为您提供的是“未知”。反过来,这意味着您不需要拆开任何包装或发生崩溃的风险–确保可以使用真实数据,这使您的代码更安全,更容易使用。

枚举

枚举通常称为“枚举”,发音为“ ee-num”,是一种在Swift中定义自己的值的方法。在某些编程语言中,它们是简单的小事,但是如果您想超越基础知识,Swift会为它们添加大量功能。

让我们从前面的一个简单示例开始:

1
2
3
4
5
6
7
func getHaterStatus(weather: String) -> String? {
if weather == "sunny" {
return nil
} else {
return "Hate"
}
}

该函数接受定义当前天气的字符串。问题是,对于这种类型的数据,字符串是一个不好的选择–是“ rain”,“ rainy”还是“ raining”?还是“阵雨”,“发呆”或“暴风雨”?更糟糕的是,如果一个人用大写R写下“ Rain”,而其他人却不在乎键入的内容,而又写下“ Ran”怎么办?

枚举通过定义一个新的数据类型,然后定义它可以容纳的可能值来解决这个问题。例如,我们可以说有五种天气:太阳,云,雨,风和雪。如果我们将其设为枚举,则意味着Swift将仅接受这五个值-其他任何内容都会触发错误。在幕后,枚举通常只是简单的数字,比计算机使用的字符串要快得多。

让我们将其放入代码中:

1
2
3
4
5
6
7
8
9
10
11
12
13
enum WeatherType {
case sun, cloud, rain, wind, snow
}

func getHaterStatus(weather: WeatherType) -> String? {
if weather == WeatherType.sun {
return nil
} else {
return "Hate"
}
}

getHaterStatus(weather: WeatherType.cloud)

看一下前三行:第1行为我们的类型命名为WeatherType。这就是您要用来代替代码String或Int在代码中使用的东西。如我已经概述的那样,第2行定义了我们的枚举可能出现的五种情况。约定以小写字母开头,因此“ sun”,“ cloud”等。第3行只是一个大括号,结束了枚举。

现在看一下它的用法:我修改了getHaterStatus()它,使其具有一个WeatherType值。条件语句也被重写以与进行比较WeatherType.sun,这是我们的价值。请记住,此检查只是幕后的数字,闪电般快速。

现在,返回并再次阅读该代码,因为我将通过两个重要的更改来重写它。可以了,好了?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum WeatherType {
case sun
case cloud
case rain
case wind
case snow
}

func getHaterStatus(weather: WeatherType) -> String? {
if weather == .sun {
return nil
} else {
return "Hate"
}
}

getHaterStatus(weather: .cloud)

我在那里做了两个不同。首先,每种天气类型现在都各自不同。这似乎是一个很小的变化,在这个示例中确实如此,但是很快就变得很重要。第二个变化是我写的if weather == .sun–我不需要说出我的意思,WeatherType.sun因为Swift知道我正在与一个WeatherType变量进行比较,因此它使用类型推断。

枚举在switch/case块内部特别有用,特别是因为Swift知道您的枚举可以拥有的所有值,因此可以确保您覆盖所有这些值。例如,我们可能尝试将getHaterStatus()方法重写为此:

1
2
3
4
5
6
7
8
9
10
func getHaterStatus(weather: WeatherType) -> String? {
switch weather {
case .sun:
return nil
case .cloud, .wind:
return "dislike"
case .rain:
return "hate"
}
}

是的,我意识到“讨厌的人”并不是一个好主意,但是无论如何它都是学术性的,因为该代码无法构建:它无法处理这种.snow情况,Swift希望涵盖所有情况。您要么为此添加一个案例,要么添加一个默认案例。

带有附加值的枚举
Swift最强大的功能之一是枚举可以具有您定义的附加值。为了进一步扩展这个日益令人怀疑的示例,我将为该.wind案例添加一个值,以便我们可以说风速有多快。修改您的代码为此:

1
2
3
4
5
6
7
enum WeatherType {
case sun
case cloud
case rain
case wind(speed: Int)
case snow
}

如您所见,其他情况不需要速度值–我只是将其输入wind。现在是真正的魔力:Swift让我们在switch/case块中添加了附加条件,以便仅当这些条件为true时,案例才匹配。这使用let关键字访问案例中的值,然后使用where关键字进行模式匹配。

这是新功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func getHaterStatus(weather: WeatherType) -> String? {
switch weather {
case .sun:
return nil
case .wind(let speed) where speed < 10:
return "meh"
case .cloud, .wind:
return "dislike"
case .rain, .snow:
return "hate"
}
}

getHaterStatus(weather: WeatherType.wind(speed: 5))

您可以看到.wind其中两次出现,但只有在风速低于每小时10公里的情况下才第一次出现。如果风等于或大于10,则不会匹配。关键是您可以let用来获取枚举内的值(即声明可以引用的常量名称),然后使用where条件进行检查。

Swift switch/case从上到下进行评估,并在找到匹配项后立即停止。这意味着,如果case .cloud, .wind:在此之前出现,case .wind(let speed) where speed < 10:它将改为执行–并且输出更改。

因此,请仔细考虑如何订购案例!

提示: Swift的可选参数实际上是使用带有关联值的枚举来实现的。有两种情况:none和some,并且some可选值内包含任何值。

结构

结构是复杂的数据类型,这意味着它们由多个值组成。然后,您创建该结构的实例并填写其值,然后可以将其作为单个值传递给您的代码。例如,我们可以定义一个Person包含两个属性的struct类型:clothes和shoes:

1
2
3
4
struct Person {
var clothes: String
var shoes: String
}

定义结构时,Swift使它们的创建非常容易,因为它会自动生成所谓的成员初始化器。简而言之,这意味着您通过传递结构的两个属性的初始值来创建结构,如下所示:

1
2
let taylor = Person(clothes: "T-shirts", shoes: "sneakers")
let other = Person(clothes: "short skirts", shoes: "high heels")

创建结构实例后,只需编写结构名称,句点和要读取的属性即可读取其属性:

1
2
print(taylor.clothes)
print(other.shoes)

如果将一个结构分配给另一个结构,Swift会将其复制到幕后,以便它是原始结构的完整独立副本。好吧,这并非完全正确:Swift使用一种称为“写入时复制”的技术,这意味着它仅在您尝试更改数据时才实际复制您的数据。

为了帮助您了解结构副本的工作方式,请将其放入您的游乐场:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Person {
var clothes: String
var shoes: String
}

let taylor = Person(clothes: "T-shirts", shoes: "sneakers")
let other = Person(clothes: "short skirts", shoes: "high heels")

var taylorCopy = taylor
taylorCopy.shoes = "flip flops"

print(taylor)
print(taylorCopy)

这将创建两个Person结构,然后创建第三个结构,称为taylorCopy的副本taylor。接下来发生的是有趣的部分:代码更改taylorCopy,并打印和taylor。如果您在结果窗格中查看(可能需要调整其大小以适合其大小),您会看到该副本与原始副本具有不同的值:更改一个副本不会更改另一个副本。

结构内部的功能

您可以将函数放置在结构中,实际上,对于所有读取或更改结构中数据的函数来说,这样做都是一个好主意。例如,我们可以在Person结构中添加一个函数来描述它们的穿着,如下所示:

1
2
3
4
5
6
7
8
struct Person {
var clothes: String
var shoes: String

func describe() {
print("I like wearing \(clothes) with \(shoes)")
}
}

您还应该了解一件事,但是在该代码中看不到:当您在结构体中编写函数时,它被称为方法。在Swift中,您写func的是函数还是方法,但是当您谈论它们时,区别就保留了下来。

Swift还有另一种构建复杂数据类型的方法,称为类。它们看起来与结构相似,但是有许多重要的区别,包括:

您没有为类提供自动的成员初始化程序;您需要自己编写。
您可以将一个类定义为基于另一个类,并添加所需的任何新内容。
创建类的实例时,它称为对象。如果复制该对象,则默认情况下,两个副本都指向同一数据–更改一个,副本也更改。
所有这三个都是巨大的差异,因此在继续之前,我将更深入地介绍它们。

初始化对象

如果要将Person结构转换为Person类,Swift不允许我们这样写:

1
2
3
4
class Person {
var clothes: String
var shoes: String
}

这是因为我们将两个属性声明为String,如果您还记得的话,则意味着它们绝对必须具有值。这在结构中很好,因为Swift会为我们自动生成一个成员初始化程序,强制我们为这两个属性提供值,但是类不会发生这种情况,因此Swift不能确定它们将被赋予值。

有三种解决方案:使两个值成为可选字符串,为它们提供默认值,或编写我们自己的初始化程序。第一个选项很笨拙,因为它在我们的代码中不需要的地方引入了可选内容。第二个选项有效,但是除非实际使用这些默认值,否则这会有些浪费。这留下了第三个选项,实际上是正确的选择:编写我们自己的初始化程序。

为此,请在名为的类中创建一个方法,该方法init()采用我们关心的两个参数:

1
2
3
4
5
6
7
8
9
class Person {
var clothes: String
var shoes: String

init(clothes: String, shoes: String) {
self.clothes = clothes
self.shoes = shoes
}
}

在该代码中有两件事可能会让您失望。

首先,您不要func在init()方法之前写代码,因为它很特殊。其次,由于传递的参数名称与我们要分配的属性的名称相同,因此您self.可以使您的含义更清楚-“ clothes此对象的属性应设置clothes为传递的参数。” 您可以根据需要为他们指定唯一的名称-这取决于您。

重要说明: Swift要求在初始化程序结束时或在初始化程序调用任何其他方法时(以先到者为准),所有非可选属性都必须具有一个值。

类继承

类和结构之间的第二个区别是,类可以彼此构建以产生更大的东西,称为类继承。即使在最基本的程序中,这也是Cocoa Touch中广泛使用的一项技术,因此您应该牢记这一点。

让我们从简单的事情开始:一个Singer具有属性的类,即属性的名称和年龄。至于方法,将有一个简单的初始化程序来处理属性设置,还有一个sing()输出一些单词的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Singer {
var name: String
var age: Int

init(name: String, age: Int) {
self.name = name
self.age = age
}

func sing() {
print("La la la la")
}
}

现在,我们可以通过调用该初始化程序来创建该对象的实例,然后读出其属性并调用其方法:

1
2
3
4
var taylor = Singer(name: "Taylor", age: 25)
taylor.name
taylor.age
taylor.sing()

那是我们的基础课,但是我们将在此基础上继续:我想定义一个CountrySinger包含该Singer课所有内容的课,但是当我在课上打电话sing()时,我想打印“卡车,吉他和酒”。

当然,您可以将原始内容复制并粘贴Singer到一个称为的新类中,CountrySinger但这是一种懒惰的编程方式,如果您以后进行更改Singer并忘记将其复制过来,它将再次困扰您。相反,Swift有一个更聪明的解决方案:我们可以定义CountrySinger为基于Singer,它将获取其所有属性和方法供我们在以下基础上构建:

1
2
3
class CountrySinger: Singer {

}

那个冒号就是魔术的作用:它意味着“ CountrySinger延伸” Singer。现在,该新CountrySinger类(称为子类)尚未添加任何内容Singer(称为父类或超类)。我们希望它具有自己的sing()方法,但是在Swift中,您需要学习一个新的关键字:override。这意味着“我知道此方法是由我的父类实现的,但是我想为该子类更改它。”

拥有override关键字会有所帮助,因为它可以使您的意图明确。它还允许Swift检查您的代码:如果您不使用overrideSwift,则不允许您更改从超类获得的方法,或者如果使用override且没有任何要覆盖的内容,Swift会指出您的错误。

因此,我们需要override func像这样使用:

1
2
3
4
5
class CountrySinger: Singer {
override func sing() {
print("Trucks, guitars, and liquor")
}
}

现在修改taylor对象的创建方式:

1
2
var taylor = CountrySinger(name: "Taylor", age: 25)
taylor.sing()

如果更改CountrySinger为,Singer您应该可以看到结果窗格中出现的不同消息。

现在,为了使事情变得更复杂,我们将定义一个名为的新类HeavyMetalSinger。但是这一次,我们将存储一个新属性,noiseLevel该属性定义了这位特殊的重金属歌手喜欢大声尖叫麦克风的声音。

这会引起问题,这是需要以非常特殊的方式解决的问题:

Swift希望所有非可选属性都具有一个值。
我们的Singer班级没有noiseLevel财产。
因此,我们需要为其创建一个HeavyMetalSinger接受噪声水平的自定义初始化程序。
这种新的初始化还需要知道name和age重金属歌手,所以它可以传递到超类Singer。
将数据传递给超类是通过方法调用完成的,在给定所有属性值之前,您无法在初始化程序中进行方法调用。
因此,我们需要先设置自己的属性(noiseLevel),然后再传递其他参数供超类使用。
这听起来可能非常复杂,但是在代码中却很简单。这是HeavyMetalSinger带有自己的sing()方法的类:

1
2
3
4
5
6
7
8
9
10
11
12
class HeavyMetalSinger: Singer {
var noiseLevel: Int

init(name: String, age: Int, noiseLevel: Int) {
self.noiseLevel = noiseLevel
super.init(name: name, age: age)
}

override func sing() {
print("Grrrrr rargh rargh rarrrrgh!")
}
}

注意其初始化程序如何接受三个参数,然后调用super.init()以传递name并传递age给Singer超类-但仅在设置其自身的属性之后。您会看到super在使用对象时使用了很多东西,它的意思是“在我继承的类上调用一个方法。”通常用来表示“让我的父类先执行它需要做的所有事情,然后再执行多做点事。”

类继承是一个大话题,因此,如果尚不清楚,请不要担心。但是,您还需要了解一件事:类继承通常跨越多个级别。例如,A可以从B继承,B可以从C继承,C可以从D继承,依此类推。这使您可以构建功能并在多个类中重复使用,从而有助于使代码保持模块化并易于理解。

使用Objective-C代码

如果要让Apple操作系统的某些部分调用Swift类的方法,则需要使用特殊属性标记它@objc。这是“ Objective-C”的缩写,并且该属性有效地将该方法标记为可用于运行较旧的Objective-C代码的方法-几乎是所有iOS,macOS,watchOS和tvOS。例如,如果您要求系统在一秒钟后调用您的方法,则需要用标记@objc。

现在不必担心太多@objc-稍后我不仅会在上下文中进行解释,而且Xcode会始终告诉您何时需要它。另外,如果您不想使用@objc单个方法,可以将其放在@objcMembers类之前,以自动将其所有方法提供给Objective-C。

价值观与参考

复制结构时,整个对象(包括其所有值)都会被复制。这意味着更改结构的一个副本不会更改其他副本–它们都是单独的。对于类,一个对象的每个副本都指向同一个原始对象,因此,如果更改一个对象,它们都将更改。Swift调用结构“值类型”是因为它们只是指向一个值,而类调用“引用类型”是因为对象只是对实际值的共享引用。

这是一个重要的区别,这意味着在结构和类之间进行选择是重要的:

  • 如果您希望有一个共享状态可以被传递和修改,那么您正在寻找类。您可以将它们传递给函数或将它们存储在数组中,在其中进行修改,并将更改反映在程序的其余部分中。
  • 如果要避免一个副本不能影响所有其他副本的共享状态,那么您正在寻找结构。您可以将它们传递给函数或将它们存储在数组中,在其中进行修改,并且在引用它们的任何地方都不会改变。

如果我要总结结构和类之间的主要区别,我会这样说:类提供更多的灵活性,而结构提供更多的安全性。作为基本规则,除非有特殊原因要使用类,否则应始终使用结构。

参考资料

查看下一天的SwiftUI学习笔记

关于100days英文课程