
test

Moriarty's Blog

接下来我们将看见如何在 Python 中运行一个传统的“Hello World”程序。这里将会教你如何编写、保存与运行 Python 程序。
通过 Python 来运行的你的程序有两种方法——使用交互式解释器提示符或直接运行一个源代码文件。我们将了解如何使用他们二者的功能。
使用解释器提示符
在你的操作系统中打开终端(Terminal)程序。
然后通过输入 python3 并按下 enter 键来打开 Python 提示符(Python Prompt)。 当你启动 Python 后,你会看见在你能开始输入内容的地方出现了 >>> 。这个被称作 Python解释器提示符(Python Interpreter Prompt) 。 在 Python 解释器提示符,输入:
print ("HELLO WORLD")
在输入完成后按下 enter 键。你将会看到屏幕上打印出 HELLO WORLD 字样。
下面是一个在 Mac OS X 电脑上你能够看见的结果的示例。有关 Python 软件的细节将会因为 你使用的电脑而有所不同,但是从提示符(如 >>> )开始部分应该是相同的,而不会受到操 作系统的影响。
$ python3
Python 3.7.3 (v3.7.3:ef4ec6ed12, Mar 25 2019, 16:52:21)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> print ("HELLO WORLD")
HELLO WORLD
你自然会注意到,Python 会立即给你输出了一行结果!你刚才所输入的便是一句独立的 Python 语句 。我们使用 print (不必太过惊讶)命令来打印你所提供的信息。在这里,我 们提供了文本 Hello World ,然后它便被迅速地打印到了屏幕上。
如何退出解释器提示符
如果你正在使用一款 GNU/Linux 或 OS X 上的 Shell 程序,你可以通过按下 ctrl + d 组 合键或是输入 exit() (注意:要记住要包含括号 () )并敲下 enter 来退出解释器提示 符。
如果你使用的是 Windows 命令提示符,可以按下 ctrl + z 组合键并敲击 enter 键来 退出。
选择一款编辑器
使用一份源代码文件
现在让我们回到编程中来。在你学习一门新的编程语言时有一项传统,你所编写并运行的第 一个程序应该是 “Hello World” 程序——它所做的全部工作便是宣言你所运行的“Hello World”这句话。
启动你所选择的编辑器,输入如下程序并将它保存为 hello.py 。
打开一个新文件名将其命名为 hello.py ,然后输入如下内容:
print("hello world")
你应当将文件保存到哪里?保存到任何你知道其位置与路径的文件夹。如果你不了解这句话 是什么意思,那就创建一个新文件夹并用这一路径来保存并运行你所有的 Python 程序
你可以在终端上使用 mkdir 命令,如 mkdir /tmp/py 。
提示:你需要经常确认并确保你为文件赋予了 .py 扩展名,例如 foo.py 。
要想运行你的 Python 程序:
$ python3 hello.py
hello world
如果你得到了与类似的输出结果,那么恭喜你!——你已经成功运行了你的第一个 Python 程序。你亦已经成功穿过了学习编程的最困难的部分,也就是,开始编写你的第一个程序
如果你遭遇了什么错误,请确认是否已经正确地输入了上面所列出的内容,并尝试重新运行 程序。要注意 Python 是区分大小写的,如 print 和 Print 是不同的——注意前者的 p 是 小写的,而后者的 P 是大写的。此外,你需要确保每一行的第一个字符前面都没有任何空格 或制表格——我们会在后面了解 为什么这件事如此重要。
它是如何工作的
一款 Python 程序是由 语句 所构成的。在我们的第一个程序中,我们只有一条语句。在这条
语句中,我们调用 print 语句 来搭配我们提供的文本”hello world“。
获取帮助
如果你需要获得 Python 中有关任何函数或语句的快速信息,你可以使用其内置的 help 功 能。这在使用解释器提示符时十分有用。例如,运行 help(‘len’) 命令——这将显示出有关len 函数的帮助,了解其是用来计算项目数量的。
按下 q 键可以退出帮助。
类似地,你可以通过此方式获得几乎所有有关 Python 的信息。使用 help() 命令来了解有关help 它本身的更多信息吧!
如果你需要获得有关 return 这类运算符的帮助,你需要做的就是将它们放在引号中,就像help(‘return’) 这般,这样 Python 就不会混淆我们正在试图做的事情。
只是打印出 hello world 肯定是不够的,是吗?你会希望做得比这还要多——你想要输入一 些内容,操纵它,然后从中得到一些输出出来的内容。我们可以在 Python 中通过使用变量与 常量来实现这一目标,在本节中我们还会学习其它的一些概念。
注释
注释 是任何存在于 # 号右侧的文字,其主要用作写给程序读者看的笔记。 举个例子:
print ("Hello World") # print 是一个函数
或者:
#print 是一个函数
print ("Hello World")
你应该在你的程序中尽可能多地使用有用的注释:
代码会告诉你怎么做,注释会告诉你为何如此。
这样做对你的程序的读者来说非常有用,他们可以很容易地理解你的程序是做什么的。
字面量常量
一个字面常量(Literal Constants)2的例子是诸如 5 、 1.23 这样的数字,或者是如 这是一
串文本 或 This is a string 这样的文本。用这样的称呼是因为它们是 字面上的 3——你用的就是它字面意义上的值或是内容。数字 2 总是表示它本身而非其他含义——它是一个 常量,因为它的值不能被改变。因此,所有的这 些都被称作字面常量。
数字
数字主要分为两种类型——整数(Integers)与浮点数(Floats)。
有关整数的例子即 2 ,它只是一个整数。
有关浮点数(Floating Point Numbers,在英文中也会简写为 floats )的例子是 52.3E-4 。其中, E 表示 10 的幂。在这里, 52.3E-4 表示 52.3 * 10^-4 。
针对有经验的程序员的提示
没有单独的 long 类型。 int 类型可以指任何大小的整数。
字符串
3.23 或52.3E-4 。其中, E 表示 10 的幂。在这里, 52.3E-4 表示 52.3 * 10^-4 。
字符串
一串字符串(String)是 字符(Characters) 的 序列(Sequence)。基本上,字符串就是一 串词汇。
你将会在几乎所有你撰写的 Python 程序中使用字符串,所以对下面的部分你要多上点心。
你可以使用单引号来指定字符串,例如 ‘将我这样框进来’ 或 ‘Quote me on this’ 。 所有引号内的空间,诸如空格与制表符,都将按原样保留。
被双引号包括的字符串和被单引号括起的字符串其工作机制完全相同。例如 “你的名字是?” 或
“What’s your name?” 。
你可以通过使用三个引号—— “”” 或 ”’ 来指定多行字符串。你可以在三引号之间自由地 使用单引号与双引号。来看看这个例子:
'''这是一段多行字符串。这是它的第一行。
This is the second line.
"What's your name?," I asked.
He said "Bond, James Bond."
'''
字符串是不可变的
这意味着一旦你创造了一串字符串,你就不能再改变它。尽管这看起来像是一件坏事,但实 际上并非如此。我们将会在稍后展现的多个程序中看到为何这一点不是一个限制。
针对 C/C++ 程序员的提示
Python 中没有单独的 char 数据类型。它并非切实必要,并且我相信你不会想念它的。 针对 Perl/PHP 程序员的提示 记住单引号括起的字符串和双引号括起的字符串是一样的——它们不存在任何区别。
格式化方法
有时候我们会想要从其他信息中构建字符串。这正是 format() 方法大有用武之地的地方。
将以下内容保存为文件 str_format.py :
age = 20
name = 'Swaroop'
print('{0} was {1} years old when he wrote this book'.format(name, age)) print('Why is {0} playing with that python?'.format(name))
输出:
$ python3 str_format.py
Swaroop was 20 years old when he wrote this book
Why is Swaroop playing with that python?
它是如何工作的
一个字符串可以使用某些特定的格式(Specification),随后, format 方法将被调用,使用 这一方法中与之相应的参数替换这些格式。
在这里要注意我们第一次应用这一方法的地方,此处 {0} 对应的是变量 name ,它是该格式 化方法中的第一个参数。与之类似,第二个格式 {1} 对应的是变量 age ,它是格式化方法 中的第二个参数。请注意,Python 从 0 开始计数,这意味着索引中的第一位是 0,第二位是1,以此类推。
我们可以通过联立字符串来达到相同的效果:
name + 'is' +str(age) + 'years old'
但这样实现是很丑陋的,而且也容易出错。其次,转换至字符串的工作将由 format 方法自 动完成,而不是如这般需要明确转换至字符串。再次,当时用 format 方法时,我们可以直 接改动文字而不必与变量打交道,反之亦然。
同时还应注意数字只是一个可选选项,所以你同样可以写成:
age = 20
name = 'Swaroop'
print('{} was {} years old when he wrote this book'.format(name, age)) print('Why is {} playing with that python?'.format(name))
这样做同样能得到与前面的程序一样的输出结果。
Python 中 format 方法所做的事情便是将每个参数值替换至格式所在的位置。这之中可以有 更详细的格式,例如:
# 对于浮点数 '0.333' 保留小数点(.)后三位
print('{0:.3f}'.format(1.0/3))
# 使用下划线填充文本,并保持文字处于中间位置
# 使用 (^) 定义 '___hello___'字符串长度为 11
print('{0:_^11}'.format('hello'))
# 基于关键词输出 'Swaroop wrote A Byte of Python'
print('{name} wrote {book}'.format(name='Swaroop', book='A Byte of Python'))
输出:
0.333
___hello___
Swaroop wrote A Byte of Python
由于我们正在讨论格式问题,就要注意 print 总是会以一个不可见的“新一行”字符( n ) 结尾,因此重复调用 print 将会在相互独立的一行中分别打印。为防止打印过程中出现这一 换行符,你可以通过 end 指定其应以空白结尾:
print('a', end='')
print('b', end='')
输出结果如下:
ab
或者你通过 end 指定以空格结尾:
print('a', end=' ')
print('b', end=' ')
print('c')
输出结果如下:
a b c
转义序列
想象一下,如果你希望生成一串包含单引号( ‘ )的字符串,你应该如何指定这串字符串? 例如,你想要的字符串是 “What’s your name?” 。你不能指定 ‘What’s your name?’ ,因为这 会使 Python 对于何处是字符串的开始、何处又是结束而感到困惑。所以,你必须指定这个单 引号不代表这串字符串的结尾。这可以通过 转义序列(Escape Sequence) 来实现。你通过
\ 来指定单引号:要注意它可是反斜杠。现在,你可以将字符串指定为 ‘What\’s your name?’ 。
另一种指定这一特别的字符串的方式是这样的: “What’s your name?” ,如这个例子般使用 双引号。类似地, 你必须在使用双引号括起的字符串中对字符串内的双引号使用转义序列。 同样,你必须使用转义序列 \\ 来指定反斜杠本身。
如果你想指定一串双行字符串该怎么办?一种方式即使用如前所述的三引号字符串,或者你 可以使用一个表示新一行的转义序列—— \n 来表示新一行的开始。下面是一个例子:
'This is the first line\n
This is the second line'
另一个你应该知道的大有用处的转义序列是制表符: \t 。实际上还有很多的转义序列,但 我必须只在此展示最重要的一些。
还有一件需要的事情,在一个字符串中,一个放置在末尾的反斜杠表示字符串将在下一行继 续,但不会添加新的一行。来看看例子:
"This is the first sentence. \
This is the second sentence."
相当于
"This is the first sentence. This is the second sentence."
原始字符串
如果你需要指定一些未经过特殊处理的字符串,比如转义序列,那么你需要在字符串前增加
r 或 R 来指定一个 原始(Raw) 字符串。下面是一个例子:
r"Newlines are indicated by \n"
针对正则表达式用户的提示 在处理正则表达式时应全程使用原始字符串。否则,将会有大量 Backwhacking 需要处
理。举例说明的话,反向引用可以通过 ‘\\1′ 或 r’\1’ 来实现。
变量
如果只使用字面常量很快就会让人感到无聊——我们需要一些能够存储任何信息并且也能操 纵它们的方式。这便是 变量(Veriables) 登场的时刻。正如其名字所述那般,变量的值是可 以变化的,也就是说,你可以用变量来存储任何东西。变量只是你的计算机内存中用以存储 信息的一部分。与文字常量不同,你需要通过一些方式来访问这些变量,因此,你需要为它 们命名。
标识符命名
变量是标识符的一个例子。标识符(Identifiers) 是为 某些东西 提供的给定名称。在你命名 标识符时,你需要遵守以下规则:
数据类型
变量可以将各种形式的值保存为不同的数据类型(Data Type)。基本的类型是我们已经讨论 过的数字与字符串。在后面的章节中,我们会了解如何通过 类(Classes) 类创建我们自己 的类型。
对象
需要记住的是,Python 将程序中的任何内容统称为 对象(Object)。这是一般意义上的说法。我们以“某某对象(object)”相称,而非“某某东西(something)”。
针对面向对象编程语言用户的提示:Python 是强(Strongly)面向对象的,因为所有的一切都是对象, 包括数字、字符串与 函数。
接下来我们将看见如何使用变量与字面常量。
案例:使用变量与字面常量
输入并运行以下程序:
# 文件名:var.py
i=5
print(i)
i=i+1
print(i)
s = '''This is a multi-line string.
This is the second line.'''
print(s)
输出
5
6
This is a multi-line string.
This is the second line
它是如何工作的
下面是这一程序的工作原理。首先,我们使用赋值运算符( = )将字面常量数值 5 赋值给 变量 i 。这一行被称之为声明语句(Statement)因为其工作正是声明一些在这一情况下应 当完成的事情:我们将变量名 i 与值 5 相连接。然后,我们通过 print 语句来打印变量i 所声明的内容,这并不奇怪,只是将变量的值打印到屏幕上。
接着,我们将 1 加到 i 变量所存储的值中,并将得出的结果重新存储进这一变量。尔后我们将这一变量打印出来,并期望得到的值应为 6 。 类似地,我们将字面文本赋值给变量 s ,并将其打印出来。
针对静态编程语言程序员的提示:变量只需被赋予某一值。不需要声明或定义数据类型。
逻辑行与物理行
所谓物理行(Physical Line)是你在编写程序时 你所看到 的内容。所谓逻辑行(Logical
Line)是 Python 所看到 的单个语句。Python 会假定每一 物理行 会对应一个 逻辑行。 有关逻辑行的一个例子是诸如 print(‘hello world’) 这样一句语句——如果其本身是一行
(正如你在编辑器里所看到的那样),那么它也对应着一行物理行。
Python 之中暗含这样一种期望:Python 鼓励每一行使用一句独立语句从而使得代码更加可 读。
如果你希望在一行物理行中指定多行逻辑行,那么你必须通过使用分号( ; )来明确表明逻辑 行或语句的结束。下面是一个例子:
i = 5
print (i)
实际上等于:
i = 5;
print (i);
同样也可以是:
i = 5; print (i);
也可以这样写:
i = 5 ; print (i)
然而,我强烈建议对于每一行物理行最多只写入一行逻辑行。这个观点就是说你不应该使 用分号。实际上,我从未在 Python 程序中使用过一个分号。
在一类情况下这一方法会颇为有用:如果你有一行非常长的代码,你可以通过使用反斜杠将 其拆分成多个物理行。这被称作显式行连接(Explicit Line Joining):
s = 'This is a string. \
This continues the string.'
print(s)
输出:
This is a string. This continues the string.
类似的还有:
i = \
5
在某些情况下,会存在一个隐含的假设,允许你不使用反斜杠。这一情况即逻辑行以括号开 始,它可以是方括号或花括号,但不能是结束括号。这被称作 隐式行连接(Implicit Line Joining)。在后面当讨论列表(List)的章节时就会了解这一点。
缩进
空白区在 Python 中十分重要。实际上,空白区在各行的开头非常重要。这被称作 缩进 (Indentation)。在逻辑行的开头留下空白区(使用空格或制表符)用以确定各逻辑行的缩 进级别,而后者又可用于确定语句的分组。
这意味着放置在一起的语句必须拥有相同的缩进。每一组这样的语句被称为 块(block)。在后文章节的案例中了解块这一概念是多么重要。
有一件事你需要记住:错误的缩进可能会导致错误。下面是一个例子:
i =5
#注意下面一行开始有一个空格
print ('Value is ',i)
print ('Value is {}'.format(i))
当你运行这个程序时,你会得到这样的报错:
line 2
print ('Value is ',i)
^
IndentationError: unexpected indent
Python 指出的错误信息告诉我们程序的语法是无效的, 意即,程序没有被正确地写入。这一信息对你的意义是 你不能任意开始一个新的语句块(当然,除非你一直在使用默认的主代码块)。
如何缩进: 使用四个空格来缩进。这是来自 Python 语言官方的建议。好的编辑器会自动为你完成这 一工作。请确保你在缩进中使用数量一致的空格,否则你的程序将不会运行,或引发不 期望的行为。
我们所编写的大多数语句(逻辑行)都包含了表达式(Expressions)。一个表达式的简单例子
便是 2+3 。表达式可以拆分成运算符(Operators)与操作数(Operands)。
运算符(Operators)是进行某些操作,并且可以用诸如 + 等符号或特殊关键词加以表达的 功能。运算符需要一些数据来进行操作,这些数据就被称作操作数(Operands)。在上面的 例子中 2 和 3 就是操作数。
运算符
接下来我们将简要了解各类运算符及它们的用法。 要记得你可以随时在解释器中对给出的案例里的表达式进行求值。例如要想测试表达式
2+3 ,则可以使用交互式 Python 解释器提示符:
>>> 2 + 3
5
>>> 3 * 5
15
>>>
下面是可用运算符:
数字运算与赋值的快捷方式
一种比较常见的操作是对一个变量进行一项数学运算并将运算得出的结果返回给这个变量, 因此对于这类运算通常有如下的快捷表达方式:
a = 2
a = a * 3
同样也可以写成:
a = 2
a *= 3
要注意到变量=变量 运算 表达式 会演变成变量 运算 = 表达式。
求值顺序
如果你有一个诸如 2 + 3 * 4 的表达式,是优先完成加法还是优先完成乘法呢?数学知识会告诉我们应该先完成乘法。这意味着乘法运算符的优先级要高于加法运算符。
下面将给出 Python 中从最低优先级(最少绑定)到最高优先级(最多绑定)的优先级表。这意味着,在给定的表达式中,Python 将优先计算表中位列于后的较高优先级的运算符与表达式。
在上表中位列同一行的运算符具有相同优先级。例如 + 和 – 就具有相同的优先级。
改变运算顺序
为了使表达式更加易读,我们可以使用括号。举个例子, 2 + (3 * 4) 自是要比 2 + 3 * 4要更加容易理解,因为后者还要求你要了解运算符的优先级。和其它的一切一样,使用括号同样也要适度(而不要过度),同时亦应不要像 (2 + (3 * 4)) 这般冗余。
使用括号还有一个额外的优点——它能帮助我们改变运算的顺序。同样举个例子,如果你希望在表达式中计算乘法之前应先计算加法,那么你可以将表达式写作 (2 + 3) * 4 。
结合性
运算符通常由左至右结合。这意味着具有相同优先级的运算符将从左至右的方式依次进行求
值。如 2 + 3 + 4 将会以 (2 + 3) +4 的形式加以计算
表达式
# 文件名为 expression.py
length = 5
breadth = 2
area = length * breadth
print('Area is ', area)
print('Perimeter is ', 2 * (length + breadth))
输出:
$ python3 newone.py
Area is 10
Perimeter is 14
它是如何工作的
矩形的长度(Length)与宽度(Breadth)存储在以各自名称命名的变量中。我们使用它们并借助表达式来计算矩形的面积(Area)与周长(Perimeter)。我们将表达式 length *breadth 的结果存储在变量 area 中并将其通过使用 print 函数打印出来。在第二种情况中,我们直接在 print 函数中使用了表达式 2 * (length + breadth) 的值。
同时,你需要注意到 Python是如何漂亮地打印出 输出结果的。尽管我们没有特别在 Area is 和变量 area 之间指定空格,Python 会帮我们加上所以我们就能得到一个整洁的输出结果,同时程序也因为这样的处理方式而变得更加易读(因为我们不需要在用以输出的字符串中考虑空格问题)。这便是一个 Python 是如何让程序员的生活变得更加便捷美好的范例。
函数是一段完成特定任务的独立代码片段。你可以通过给函数命名来标识某个函数的功能,这个名字可以被用来在需要的时候“调用”这个函数来完成它的任务。
Swift 统一的函数语法非常的灵活,可以用来表示任何函数,包括从最简单的没有参数名字的 C 风格函数,到复杂的带局部和外部参数名的 Objective-C 风格函数。参数可以提供默认值,以简化函数调用。参数也可以既当做传入参数,也当做传出参数,也就是说,一旦函数执行结束,传入的参数值将被修改。
在 Swift 中,每个函数都有一个由函数的参数值类型和返回值类型组成的类型。你可以把函数类型当做任何其他普通变量类型一样处理,这样就可以更简单地把函数当做别的函数的参数,也可以从其他函数中返回函数。函数的定义可以写在其他函数定义中,这样可以在嵌套函数范围内实现功能封装。
当你定义一个函数时,你可以定义一个或多个有名字和类型的值,作为函数的输入,称为参数,也可以定义某种类型的值作为函数执行结束时的输出,称为返回类型。
每个函数有个函数名,用来描述函数执行的任务。要使用一个函数时,用函数名来“调用”这个函数,并传给它匹配的输入值(称作实参)。函数的实参必须与函数参数表里参数的顺序一致。
下面例子中的函数的名字是 greet(person:),之所以叫这个名字,是因为这个函数用一个人的名字当做输入,并返回向这个人问候的语句。为了完成这个任务,你需要定义一个输入参数——一个叫做 person 的 String 值,和一个包含给这个人问候语的 String 类型的返回值:
func greet(person: String) -> String {
let greeting = "Hello, " + person + "!"
return greeting
}
所有的这些信息汇总起来成为函数的定义,并以 func 作为前缀。指定函数返回类型时,用返回箭头 ->(一个连字符后跟一个右尖括号)后跟返回类型的名称的方式来表示。
该定义描述了函数的功能,它期望接收什么作为参数和执行结束时它返回的结果是什么类型。这样的定义使得函数可以在别的地方以一种清晰的方式被调用:
print(greet(person:"Anna"))
print(greet(person:"Brian"))
调用greet(person:)函数是,在圆括号内给它传一个String类型的实参,例如greet(person:"Anna")。正如上面所示,因为这个函数返回一个String类型的值,所以greet可以被包含在print(_:separator:terminator)的调用中,用来输出这个函数的返回值。
注意:print(_:separator:terminator:) 函数的第一个参数并没有设置一个标签,而其他的参数因为已经有了默认值,因此是可选的。关于这些函数语法上的变化详见下方关于 函数参数标签和参数名以及默认参数值。
在 greet(person:) 的函数体中,先定义了一个新的名为 greeting 的 String 常量,同时,把对 personName 的问候消息赋值给了 greeting 。然后用 return 关键字把这个问候返回出去。一旦 return greeting 被调用,该函数结束它的执行并返回 greeting 的当前值。
你可以用不同的输入值多次调用 greet(person:)。上面的例子展示的是用 “Anna” 和 “Brian” 调用的结果,该函数分别返回了不同的结果。
为了简化这个函数的定义,可以将问候消息的创建和返回写成一句:
func greetAgain(person: String) -> String {
return "Hello again, " + person + "!"
}
print(greetAgain(person: "Anna"))
函数参数与返回值在 Swift 中非常的灵活。你可以定义任何类型的函数,包括从只带一个未名参数的简单函数到复杂的带有表达性参数名和不同参数选项的复杂函数。
无参函数
函数可以没有参数。下面这个函数就是一个无参数函数,当被调用时,它返回固定的 String 消息:
func sayHelloWorld() -> String {
return "hello, world"
}
print(sayHelloWorld())
尽管这个函数没有参数,但是定义中在函数名后还是需要一对圆括号。当被调用时,也需要在函数名后写一对圆括号。
多参数函数
函数可以有多种输入参数,这些参数被包含在函数的括号之中,以逗号分隔。
下面这个函数用一个人名和是否已经打过招呼作为输入,并返回对这个人的适当问候语:
func greet(person: String, alreadyGreeted: Bool) -> String {
if alreadyGreeted {
return greetAgain(person: person)
} else {
return greet(person: person)
}
}
print(greet(person: "Tim", alreadyGreeted: true))
你可以通过在括号内使用逗号分隔来传递一个 String 参数值和一个标识为 alreadyGreeted 的 Bool 值,来调用 greet(person:alreadyGreeted:) 函数。注意这个函数和上面 greet(person:) 是不同的。虽然它们都有着同样的名字 greet,但是 greet(person:alreadyGreeted:) 函数需要两个参数,而 greet(person:) 只需要一个参数。
无返回值函数
函数可以没有返回值。下面是 greet(person:) 函数的另一个版本,这个函数直接打印一个 String 值,而不是返回它:
func greet(person: String) {
print("Hello, \(person)!")
}
greet(person: "Dave")
因为这个函数不需要返回值,所以这个函数的定义中没有返回箭头(->)和返回类型。
注意:严格地说,即使没有明确定义返回值,该 greet(Person:) 函数仍然返回一个值。没有明确定义返回类型的函数的返回一个 Void 类型特殊值,该值为一个空元组,写成 ()。
调用函数时,可以忽略该函数的返回值:
func printAndCount(string: String) -> Int {
print(string)
return string.count
}
func printWithoutCounting(string: String) {
let _ = printAndCount(string: string)
}
printAndCount(string: "hello, world")
printWithoutCounting(string: "hello, world")
第一个函数 printAndCount(string:),输出一个字符串并返回 Int 类型的字符数。第二个函数 printWithoutCounting(string:) 调用了第一个函数,但是忽略了它的返回值。当第二个函数被调用时,消息依然会由第一个函数输出,但是返回值不会被用到。
注意:返回值可以被忽略,但定义了有返回值的函数必须返回一个值,如果在函数定义底部没有返回任何值,将导致编译时错误。
多重返回值函数
你可以用元组(tuple)类型让多个值作为一个复合值从函数中返回。
下例中定义了一个名为 minMax(array:) 的函数,作用是在一个 Int 类型的数组中找出最小值与最大值。
func minMax(array: [Int]) -> (min: Int, max: Int) {
var currentMin = array[0]
var currentMax = array[0]
for value in array[1..<array.count] {
if value < currentMin {
currentMin = value
} else if value > currentMax {
currentMax = value
}
}
return (currentMin, currentMax)
}
minMax(array:) 函数返回一个包含两个 Int 值的元组,这些值被标记为 min 和 max ,以便查询函数的返回值时可以通过名字访问它们。
在 minMax(array:) 的函数体中,在开始的时候设置两个工作变量 currentMin 和 currentMax 的值为数组中的第一个数。然后函数会遍历数组中剩余的值并检查该值是否比 currentMin 和 currentMax 更小或更大。最后数组中的最小值与最大值作为一个包含两个 Int 值的元组返回。
因为元组的成员值已被命名,因此可以通过 . 语法来检索找到的最小值与最大值:
let bounds = minMax(array: [8, -6, 2, 109, 3, 71])
print("min is \(bounds.min) and max is \(bounds.max)")
需要注意的是,元组的成员不需要在元组从函数中返回时命名,因为它们的名字已经在函数返回类型中指定了。
可选元组返回类型
如果函数返回的元组类型有可能整个元组都“没有值”,你可以使用可选的 元组返回类型反映整个元组可以是 nil 的事实。你可以通过在元组类型的右括号后放置一个问号来定义一个可选元组,例如 (Int, Int)? 或 (String, Int, Bool)?
注意:可选元组类型如 (Int, Int)? 与元组包含可选类型如 (Int?, Int?) 是不同的。可选的元组类型,整个元组是可选的,而不只是元组中的每个元素值。
前面的 minMax(array:) 函数返回了一个包含两个 Int 值的元组。但是函数不会对传入的数组执行任何安全检查,如果 array 参数是一个空数组,如上定义的 minMax(array:) 在试图访问 array[0] 时会触发一个运行时错误。
为了安全地处理这个“空数组”问题,将 minMax(array:) 函数改写为使用可选元组返回类型,并且当数组为空时返回 nil:
func minMax(array: [Int]) -> (min: Int, max: Int)? {
if array.isEmpty { return nil }
var currentMin = array[0]
var currentMax = array[0]
for value in array[1..<array.count] {
if value < currentMin {
currentMin = value
} else if value > currentMax {
currentMax = value
}
}
return (currentMin, currentMax)
}
你可以使用可选绑定来检查 minMax(array:) 函数返回的是一个存在的元组值还是 nil:
if let bounds = minMax(array: [8, -6, 2, 109, 3, 71]) {
print("min is \(bounds.min) and max is \(bounds.max)")
}
隐式返回的函数
如果一个函数的整个函数体是一个单行表达式,这个函数可以隐式地返回这个表达式。举个例子,以下的函数有着同样的作用:
func greeting(for person: String) -> String {
"Hello, " + person + "!"
}
print(greeting(for: "Dave"))
func anotherGreeting(for person: String) -> String {
return "Hello, " + person + "!"
}
print(anotherGreeting(for: "Dave"))
greeting(for:) 函数的完整定义是打招呼内容的返回,这就意味着它能使用隐式返回这样更简短的形式。anothergreeting(for:) 函数返回同样的内容,却因为 return 关键字显得函数更长。任何一个可以被写成一行 return 语句的函数都可以忽略 return。
正如你将会在 简略的 Getter 声明 里看到的, 一个属性的 getter 也可以使用隐式返回的形式。
函数参数标签和参数名称
每个函数参数都有一个参数标签(argument label)以及一个参数名称(parameter name)。参数标签在调用函数的时候使用;调用的时候需要将函数的参数标签写在对应的参数前面。参数名称在函数的实现中使用。默认情况下,函数参数使用参数名称来作为它们的参数标签。
func someFunction(firstParameterName: Int, secondParameterName: Int) {
// 在函数体内,firstParameterName 和 secondParameterName 代表参数中的第一个和第二个参数值
}
someFunction(firstParameterName: 1, secondParameterName: 2)
所有的参数都必须有一个独一无二的名字。虽然多个参数拥有同样的参数标签是可能的,但是一个唯一的函数标签能够使你的代码更具可读性。
指定参数标签
你可以在参数名称前指定它的参数标签,中间以空格分隔:
func someFunction(argumentLabel parameterName: Int) {
// 在函数体内,parameterName 代表参数值
}
这个版本的 greet(person:) 函数,接收一个人的名字和他的家乡,并且返回一句问候:
func greet(person: String, from hometown: String) -> String {
return "Hello \(person)! Glad you could visit from \(hometown)."
}
print(greet(person: "Bill", from: "Cupertino"))
参数标签的使用能够让一个函数在调用时更有表达力,更类似自然语言,并且仍保持了函数内部的可读性以及清晰的意图。
忽略参数标签
如果你不希望为某个参数添加一个标签,可以使用一个下划线(_)来代替一个明确的参数标签。
func someFunction(_ firstParameterName: Int, secondParameterName: Int) {
// 在函数体内,firstParameterName 和 secondParameterName 代表参数中的第一个和第二个参数值
}
someFunction(1, secondParameterName: 2)
如果一个参数有一个标签,那么在调用的时候必须使用标签来标记这个参数。
默认参数值
你可以在函数体中通过给参数赋值来为任意一个参数定义默认值(Deafult Value)。当默认值被定义后,调用这个函数时可以忽略这个参数。
func someFunction(parameterWithoutDefault: Int, parameterWithDefault: Int = 12) {
// 如果你在调用时候不传第二个参数,parameterWithDefault 会值为 12 传入到函数体中。
}
someFunction(parameterWithoutDefault: 3, parameterWithDefault: 6) // parameterWithDefault = 6
someFunction(parameterWithoutDefault: 4) // parameterWithDefault = 12
将不带有默认值的参数放在函数参数列表的最前。一般来说,没有默认值的参数更加的重要,将不带默认值的参数放在最前保证在函数调用时,非默认参数的顺序是一致的,同时也使得相同的函数在不同情况下调用时显得更为清晰。
可变参数
一个可变参数(variadic parameter)可以接受零个或多个值。函数调用时,你可以用可变参数来指定函数参数可以被传入不确定数量的输入值。通过在变量类型名后面加入(…)的方式来定义可变参数。
可变参数的传入值在函数体中变为此类型的一个数组。例如,一个叫做 numbers 的 Double… 型可变参数,在函数体内可以当做一个叫 numbers 的 [Double] 型的数组常量。
下面的这个函数用来计算一组任意长度数字的 算术平均数(arithmetic mean):
func arithmeticMean(_ numbers: Double...) -> Double {
var total: Double = 0
for number in numbers {
total += number
}
return total / Double(numbers.count)
}
arithmeticMean(1, 2, 3, 4, 5)
arithmeticMean(3, 8.25, 18.75)
注意:一个函数最多只能拥有一个可变参数。
输出参数
函数参数默认是常量。试图在函数体中更改参数值将会导致编译错误。这意味着你不能错误地更改参数值。如果你想要一个函数可以修改参数的值,并且想要在这些修改 在函数调用结束后仍然存在,那么就应该把这个参数定义为输入输出参数(In-Out Parameters)。
定义一个输入输出参数时,在参数定义前加 inout 关键字。一个 输入输出参数有传入函数的值,这个值被函数修改,然后被传出函数,替换原来的值。想获取更多的关于输入输出参数的细节和相关的编译器优化,请查看 输入输出参数 一节。
你只能传递变量给输入输出参数。你不能传入常量或者字面量,因为这些量是不能被修改的。当传入的参数作为输入输出参数时,需要在参数名前加 & 符,表示这个值可以被函数修改。
注意:输入输出参数不能有默认值,而且可变参数不能用 inout 标记。
下例中,swapTwoInts(_:_:) 函数有两个分别叫做 a 和 b 的输入输出参数:
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
swapTwoInts(_:_:) 函数简单地交换 a 与 b 的值。该函数先将 a 的值存到一个临时常量 temporaryA 中,然后将 b 的值赋给 a,最后将 temporaryA 赋值给 b。
你可以用两个 Int 型的变量来调用 swapTwoInts(_:_:)。需要注意的是,someInt 和 anotherInt 在传入 swapTwoInts(_:_:) 函数前,都加了 & 的前缀:
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
从上面这个例子中,我们可以看到 someInt 和 anotherInt 的原始值在 swapTwoInts(:🙂 函数中被修改,尽管它们的定义在函数体外。
注意:输入输出参数和返回值是不一样的。上面的 swapTwoInts 函数并没有定义任何返回值,但仍然修改了 someInt 和 anotherInt 的值。输入输出参数是函数对函数体外产生影响的另一种方式。
函数类型
每个函数都有种特定的函数类型,函数的类型由函数的参数类型和返回类型组成。
例如:
func addTwoInts(_ a: Int, _ b: Int) -> Int {
return a + b
}
func multiplyTwoInts(_ a: Int, _ b: Int) -> Int {
return a * b
}
这个例子中定义了两个简单的数学函数:addTwoInts 和 multiplyTwoInts。这两个函数都接受两个 Int 值, 返回一个 Int 值。
这两个函数的类型是 (Int, Int) -> Int,可以解读为:
“这个函数类型有两个 Int 型的参数并返回一个 Int 型的值”。
下面是另一个例子,一个没有参数,也没有返回值的函数:
func printHelloWorld() {
print("hello, world")
}
这个函数的类型是:() -> Void,或者叫“没有参数,并返回 Void 类型的函数”。
使用函数类型
在 Swift 中,使用函数类型就像使用其他类型一样。例如,你可以定义一个类型为函数的常量或变量,并将适当的函数赋值给它:
var mathFunction: (Int, Int) -> Int = addTwoInts
这段代码可以被解读为:
”定义一个叫做 mathFunction 的变量,类型是‘一个有两个 Int 型的参数并返回一个 Int 型的值的函数’,并让这个新变量指向 addTwoInts 函数”。
addTwoInts 和 mathFunction 有同样的类型,所以这个赋值过程在 Swift 类型检查(type-check)中是允许的。
现在,你可以用 mathFunction 来调用被赋值的函数了:
print("Result: \(mathFunction(2, 3))")
有相同匹配类型的不同函数可以被赋值给同一个变量,就像非函数类型的变量一样:
mathFunction = multiplyTwoInts
print("Result: \(mathFunction(2, 3))")
就像其他类型一样,当赋值一个函数给常量或变量时,你可以让 Swift 来推断其函数类型:
let anotherMathFunction = addTwoInts
函数类型作为参数类型
你可以用 (Int, Int) -> Int 这样的函数类型作为另一个函数的参数类型。这样你可以将函数的一部分实现留给函数的调用者来提供。
下面是另一个例子,正如上面的函数一样,同样是输出某种数学运算结果:
func printMathResult(_ mathFunction: (Int, Int) -> Int, _ a: Int, _ b: Int) {
print("Result: \(mathFunction(a, b))")
}
这个例子定义了 printMathResult(_:_:_:) 函数,它有三个参数:第一个参数叫 mathFunction,类型是 (Int, Int) -> Int,你可以传入任何这种类型的函数;第二个和第三个参数叫 a 和 b,它们的类型都是 Int,这两个值作为已给出的函数的输入值。
当 printMathResult(_:_:_:) 被调用时,它被传入 addTwoInts 函数和整数 3 和 5。它用传入 3 和 5 调用 addTwoInts,并输出结果:8。
printMathResult(_:_:_:) 函数的作用就是输出另一个适当类型的数学函数的调用结果。它不关心传入函数是如何实现的,只关心传入的函数是不是一个正确的类型。这使得 printMathResult(_:_:_:) 能以一种类型安全(type-safe)的方式将一部分功能转给调用者实现。
函数类型作为返回值
你可以用函数类型作为另一个函数的返回类型。你需要做的是在返回箭头(->)后写一个完整的函数类型。
下面的这个例子中定义了两个简单函数,分别是 stepForward(_:) 和 stepBackward(_:)。stepForward(_:) 函数返回一个比输入值大 1 的值。stepBackward(_:) 函数返回一个比输入值小 1 的值。这两个函数的类型都是 (Int) -> Int:
func stepForward(_ input: Int) -> Int {
return input + 1
}
func stepBackward(_ input: Int) -> Int {
return input - 1
}
如下名为 chooseStepFunction(backward:) 的函数,它的返回类型是 (Int) -> Int 类型的函数。chooseStepFunction(backward:) 根据布尔值 backwards 来返回 stepForward(_:) 函数或 stepBackward(_:) 函数:
func chooseStepFunction(backward: Bool) -> (Int) -> Int {
return backward ? stepBackward : stepForward
}
你现在可以用 chooseStepFunction(backward:) 来获得两个函数其中的一个:
var currentValue = 3
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)
上面这个例子中计算出从 currentValue 逐渐接近到0是需要向正数走还是向负数走。currentValue 的初始值是 3,这意味着 currentValue > 0 为真(true),这将使得 chooseStepFunction(_:) 返回 stepBackward(_:) 函数。一个指向返回的函数的引用保存在了 moveNearerToZero 常量中。
现在,moveNearerToZero 指向了正确的函数,它可以被用来数到零:
print("Counting to zero:")
while currentValue != 0 {
print("\(currentValue)... ")
currentValue = moveNearerToZero(currentValue)
}
print("zero!")
嵌套函数
到目前为止本章中你所见到的所有函数都叫全局函数(global functions),它们定义在全局域中。你也可以把函数定义在别的函数体中,称作 嵌套函数(nested functions)。
默认情况下,嵌套函数是对外界不可见的,但是可以被它们的外围函数(enclosing function)调用。一个外围函数也可以返回它的某一个嵌套函数,使得这个函数可以在其他域中被使用。
你可以用返回嵌套函数的方式重写 chooseStepFunction(backward:) 函数:
func chooseStepFunction(backward: Bool) -> (Int) -> Int {
func stepForward(input: Int) -> Int { return input + 1 }
func stepBackward(input: Int) -> Int { return input - 1 }
return backward ? stepBackward : stepForward
}
var currentValue = -4
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)
// moveNearerToZero now refers to the nested stepForward() function
while currentValue != 0 {
print("\(currentValue)... ")
currentValue = moveNearerToZero(currentValue)
}
print("zero!")
// -4...
// -3...
// -2...
// -1...
// zero!
函数是一段完成特定任务的独立代码片段。你可以通过给函数命名来标识某个函数的功能,这个名字可以被用来在需要的时候“调用”这个函数来完成它的任务。
Swift 统一的函数语法非常的灵活,可以用来表示任何函数,包括从最简单的没有参数名字的 C 风格函数,到复杂的带局部和外部参数名的 Objective-C 风格函数。参数可以提供默认值,以简化函数调用。参数也可以既当做传入参数,也当做传出参数,也就是说,一旦函数执行结束,传入的参数值将被修改。
在 Swift 中,每个函数都有一个由函数的参数值类型和返回值类型组成的类型。你可以把函数类型当做任何其他普通变量类型一样处理,这样就可以更简单地把函数当做别的函数的参数,也可以从其他函数中返回函数。函数的定义可以写在其他函数定义中,这样可以在嵌套函数范围内实现功能封装。
当你定义一个函数时,你可以定义一个或多个有名字和类型的值,作为函数的输入,称为参数,也可以定义某种类型的值作为函数执行结束时的输出,称为返回类型。
每个函数有个函数名,用来描述函数执行的任务。要使用一个函数时,用函数名来“调用”这个函数,并传给它匹配的输入值(称作实参)。函数的实参必须与函数参数表里参数的顺序一致。
下面例子中的函数的名字是 greet(person:),之所以叫这个名字,是因为这个函数用一个人的名字当做输入,并返回向这个人问候的语句。为了完成这个任务,你需要定义一个输入参数——一个叫做 person 的 String 值,和一个包含给这个人问候语的 String 类型的返回值:
func greet(person: String) -> String {
let greeting = "Hello, " + person + "!"
return greeting
}
所有的这些信息汇总起来成为函数的定义,并以 func 作为前缀。指定函数返回类型时,用返回箭头 ->(一个连字符后跟一个右尖括号)后跟返回类型的名称的方式来表示。
该定义描述了函数的功能,它期望接收什么作为参数和执行结束时它返回的结果是什么类型。这样的定义使得函数可以在别的地方以一种清晰的方式被调用:
print(greet(person:"Anna"))
print(greet(person:"Brian"))
调用greet(person:)函数是,在圆括号内给它传一个String类型的实参,例如greet(person:"Anna")。正如上面所示,因为这个函数返回一个String类型的值,所以greet可以被包含在print(_:separator:terminator)的调用中,用来输出这个函数的返回值。
注意:print(_:separator:terminator:) 函数的第一个参数并没有设置一个标签,而其他的参数因为已经有了默认值,因此是可选的。关于这些函数语法上的变化详见下方关于 函数参数标签和参数名以及默认参数值。
在 greet(person:) 的函数体中,先定义了一个新的名为 greeting 的 String 常量,同时,把对 personName 的问候消息赋值给了 greeting 。然后用 return 关键字把这个问候返回出去。一旦 return greeting 被调用,该函数结束它的执行并返回 greeting 的当前值。
你可以用不同的输入值多次调用 greet(person:)。上面的例子展示的是用 “Anna” 和 “Brian” 调用的结果,该函数分别返回了不同的结果。
为了简化这个函数的定义,可以将问候消息的创建和返回写成一句:
func greetAgain(person: String) -> String {
return "Hello again, " + person + "!"
}
print(greetAgain(person: "Anna"))
函数参数与返回值在 Swift 中非常的灵活。你可以定义任何类型的函数,包括从只带一个未名参数的简单函数到复杂的带有表达性参数名和不同参数选项的复杂函数。
无参函数
函数可以没有参数。下面这个函数就是一个无参数函数,当被调用时,它返回固定的 String 消息:
func sayHelloWorld() -> String {
return "hello, world"
}
print(sayHelloWorld())
尽管这个函数没有参数,但是定义中在函数名后还是需要一对圆括号。当被调用时,也需要在函数名后写一对圆括号。
多参数函数
函数可以有多种输入参数,这些参数被包含在函数的括号之中,以逗号分隔。
下面这个函数用一个人名和是否已经打过招呼作为输入,并返回对这个人的适当问候语:
func greet(person: String, alreadyGreeted: Bool) -> String {
if alreadyGreeted {
return greetAgain(person: person)
} else {
return greet(person: person)
}
}
print(greet(person: "Tim", alreadyGreeted: true))
你可以通过在括号内使用逗号分隔来传递一个 String 参数值和一个标识为 alreadyGreeted 的 Bool 值,来调用 greet(person:alreadyGreeted:) 函数。注意这个函数和上面 greet(person:) 是不同的。虽然它们都有着同样的名字 greet,但是 greet(person:alreadyGreeted:) 函数需要两个参数,而 greet(person:) 只需要一个参数。
无返回值函数
函数可以没有返回值。下面是 greet(person:) 函数的另一个版本,这个函数直接打印一个 String 值,而不是返回它:
func greet(person: String) {
print("Hello, \(person)!")
}
greet(person: "Dave")
因为这个函数不需要返回值,所以这个函数的定义中没有返回箭头(->)和返回类型。
注意:严格地说,即使没有明确定义返回值,该 greet(Person:) 函数仍然返回一个值。没有明确定义返回类型的函数的返回一个 Void 类型特殊值,该值为一个空元组,写成 ()。
调用函数时,可以忽略该函数的返回值:
func printAndCount(string: String) -> Int {
print(string)
return string.count
}
func printWithoutCounting(string: String) {
let _ = printAndCount(string: string)
}
printAndCount(string: "hello, world")
printWithoutCounting(string: "hello, world")
第一个函数 printAndCount(string:),输出一个字符串并返回 Int 类型的字符数。第二个函数 printWithoutCounting(string:) 调用了第一个函数,但是忽略了它的返回值。当第二个函数被调用时,消息依然会由第一个函数输出,但是返回值不会被用到。
注意:返回值可以被忽略,但定义了有返回值的函数必须返回一个值,如果在函数定义底部没有返回任何值,将导致编译时错误。
多重返回值函数
你可以用元组(tuple)类型让多个值作为一个复合值从函数中返回。
下例中定义了一个名为 minMax(array:) 的函数,作用是在一个 Int 类型的数组中找出最小值与最大值。
func minMax(array: [Int]) -> (min: Int, max: Int) {
var currentMin = array[0]
var currentMax = array[0]
for value in array[1..<array.count] {
if value < currentMin {
currentMin = value
} else if value > currentMax {
currentMax = value
}
}
return (currentMin, currentMax)
}
minMax(array:) 函数返回一个包含两个 Int 值的元组,这些值被标记为 min 和 max ,以便查询函数的返回值时可以通过名字访问它们。
在 minMax(array:) 的函数体中,在开始的时候设置两个工作变量 currentMin 和 currentMax 的值为数组中的第一个数。然后函数会遍历数组中剩余的值并检查该值是否比 currentMin 和 currentMax 更小或更大。最后数组中的最小值与最大值作为一个包含两个 Int 值的元组返回。
因为元组的成员值已被命名,因此可以通过 . 语法来检索找到的最小值与最大值:
let bounds = minMax(array: [8, -6, 2, 109, 3, 71])
print("min is \(bounds.min) and max is \(bounds.max)")
需要注意的是,元组的成员不需要在元组从函数中返回时命名,因为它们的名字已经在函数返回类型中指定了。
可选元组返回类型
如果函数返回的元组类型有可能整个元组都“没有值”,你可以使用可选的 元组返回类型反映整个元组可以是 nil 的事实。你可以通过在元组类型的右括号后放置一个问号来定义一个可选元组,例如 (Int, Int)? 或 (String, Int, Bool)?
注意:可选元组类型如 (Int, Int)? 与元组包含可选类型如 (Int?, Int?) 是不同的。可选的元组类型,整个元组是可选的,而不只是元组中的每个元素值。
前面的 minMax(array:) 函数返回了一个包含两个 Int 值的元组。但是函数不会对传入的数组执行任何安全检查,如果 array 参数是一个空数组,如上定义的 minMax(array:) 在试图访问 array[0] 时会触发一个运行时错误。
为了安全地处理这个“空数组”问题,将 minMax(array:) 函数改写为使用可选元组返回类型,并且当数组为空时返回 nil:
func minMax(array: [Int]) -> (min: Int, max: Int)? {
if array.isEmpty { return nil }
var currentMin = array[0]
var currentMax = array[0]
for value in array[1..<array.count] {
if value < currentMin {
currentMin = value
} else if value > currentMax {
currentMax = value
}
}
return (currentMin, currentMax)
}
你可以使用可选绑定来检查 minMax(array:) 函数返回的是一个存在的元组值还是 nil:
if let bounds = minMax(array: [8, -6, 2, 109, 3, 71]) {
print("min is \(bounds.min) and max is \(bounds.max)")
}
隐式返回的函数
如果一个函数的整个函数体是一个单行表达式,这个函数可以隐式地返回这个表达式。举个例子,以下的函数有着同样的作用:
func greeting(for person: String) -> String {
"Hello, " + person + "!"
}
print(greeting(for: "Dave"))
func anotherGreeting(for person: String) -> String {
return "Hello, " + person + "!"
}
print(anotherGreeting(for: "Dave"))
greeting(for:) 函数的完整定义是打招呼内容的返回,这就意味着它能使用隐式返回这样更简短的形式。anothergreeting(for:) 函数返回同样的内容,却因为 return 关键字显得函数更长。任何一个可以被写成一行 return 语句的函数都可以忽略 return。
正如你将会在 简略的 Getter 声明 里看到的, 一个属性的 getter 也可以使用隐式返回的形式。
函数参数标签和参数名称
每个函数参数都有一个参数标签(argument label)以及一个参数名称(parameter name)。参数标签在调用函数的时候使用;调用的时候需要将函数的参数标签写在对应的参数前面。参数名称在函数的实现中使用。默认情况下,函数参数使用参数名称来作为它们的参数标签。
func someFunction(firstParameterName: Int, secondParameterName: Int) {
// 在函数体内,firstParameterName 和 secondParameterName 代表参数中的第一个和第二个参数值
}
someFunction(firstParameterName: 1, secondParameterName: 2)
所有的参数都必须有一个独一无二的名字。虽然多个参数拥有同样的参数标签是可能的,但是一个唯一的函数标签能够使你的代码更具可读性。
指定参数标签
你可以在参数名称前指定它的参数标签,中间以空格分隔:
func someFunction(argumentLabel parameterName: Int) {
// 在函数体内,parameterName 代表参数值
}
这个版本的 greet(person:) 函数,接收一个人的名字和他的家乡,并且返回一句问候:
func greet(person: String, from hometown: String) -> String {
return "Hello \(person)! Glad you could visit from \(hometown)."
}
print(greet(person: "Bill", from: "Cupertino"))
参数标签的使用能够让一个函数在调用时更有表达力,更类似自然语言,并且仍保持了函数内部的可读性以及清晰的意图。
忽略参数标签
如果你不希望为某个参数添加一个标签,可以使用一个下划线(_)来代替一个明确的参数标签。
func someFunction(_ firstParameterName: Int, secondParameterName: Int) {
// 在函数体内,firstParameterName 和 secondParameterName 代表参数中的第一个和第二个参数值
}
someFunction(1, secondParameterName: 2)
如果一个参数有一个标签,那么在调用的时候必须使用标签来标记这个参数。
默认参数值
你可以在函数体中通过给参数赋值来为任意一个参数定义默认值(Deafult Value)。当默认值被定义后,调用这个函数时可以忽略这个参数。
func someFunction(parameterWithoutDefault: Int, parameterWithDefault: Int = 12) {
// 如果你在调用时候不传第二个参数,parameterWithDefault 会值为 12 传入到函数体中。
}
someFunction(parameterWithoutDefault: 3, parameterWithDefault: 6) // parameterWithDefault = 6
someFunction(parameterWithoutDefault: 4) // parameterWithDefault = 12
将不带有默认值的参数放在函数参数列表的最前。一般来说,没有默认值的参数更加的重要,将不带默认值的参数放在最前保证在函数调用时,非默认参数的顺序是一致的,同时也使得相同的函数在不同情况下调用时显得更为清晰。
可变参数
一个可变参数(variadic parameter)可以接受零个或多个值。函数调用时,你可以用可变参数来指定函数参数可以被传入不确定数量的输入值。通过在变量类型名后面加入(…)的方式来定义可变参数。
可变参数的传入值在函数体中变为此类型的一个数组。例如,一个叫做 numbers 的 Double… 型可变参数,在函数体内可以当做一个叫 numbers 的 [Double] 型的数组常量。
下面的这个函数用来计算一组任意长度数字的 算术平均数(arithmetic mean):
func arithmeticMean(_ numbers: Double...) -> Double {
var total: Double = 0
for number in numbers {
total += number
}
return total / Double(numbers.count)
}
arithmeticMean(1, 2, 3, 4, 5)
arithmeticMean(3, 8.25, 18.75)
注意:一个函数最多只能拥有一个可变参数。
输出参数
函数参数默认是常量。试图在函数体中更改参数值将会导致编译错误。这意味着你不能错误地更改参数值。如果你想要一个函数可以修改参数的值,并且想要在这些修改 在函数调用结束后仍然存在,那么就应该把这个参数定义为输入输出参数(In-Out Parameters)。
定义一个输入输出参数时,在参数定义前加 inout 关键字。一个 输入输出参数有传入函数的值,这个值被函数修改,然后被传出函数,替换原来的值。想获取更多的关于输入输出参数的细节和相关的编译器优化,请查看 输入输出参数 一节。
你只能传递变量给输入输出参数。你不能传入常量或者字面量,因为这些量是不能被修改的。当传入的参数作为输入输出参数时,需要在参数名前加 & 符,表示这个值可以被函数修改。
注意:输入输出参数不能有默认值,而且可变参数不能用 inout 标记。
下例中,swapTwoInts(_:_:) 函数有两个分别叫做 a 和 b 的输入输出参数:
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
swapTwoInts(_:_:) 函数简单地交换 a 与 b 的值。该函数先将 a 的值存到一个临时常量 temporaryA 中,然后将 b 的值赋给 a,最后将 temporaryA 赋值给 b。
你可以用两个 Int 型的变量来调用 swapTwoInts(_:_:)。需要注意的是,someInt 和 anotherInt 在传入 swapTwoInts(_:_:) 函数前,都加了 & 的前缀:
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
从上面这个例子中,我们可以看到 someInt 和 anotherInt 的原始值在 swapTwoInts(:🙂 函数中被修改,尽管它们的定义在函数体外。
注意:输入输出参数和返回值是不一样的。上面的 swapTwoInts 函数并没有定义任何返回值,但仍然修改了 someInt 和 anotherInt 的值。输入输出参数是函数对函数体外产生影响的另一种方式。
函数类型
每个函数都有种特定的函数类型,函数的类型由函数的参数类型和返回类型组成。
例如:
func addTwoInts(_ a: Int, _ b: Int) -> Int {
return a + b
}
func multiplyTwoInts(_ a: Int, _ b: Int) -> Int {
return a * b
}
这个例子中定义了两个简单的数学函数:addTwoInts 和 multiplyTwoInts。这两个函数都接受两个 Int 值, 返回一个 Int 值。
这两个函数的类型是 (Int, Int) -> Int,可以解读为:
“这个函数类型有两个 Int 型的参数并返回一个 Int 型的值”。
下面是另一个例子,一个没有参数,也没有返回值的函数:
func printHelloWorld() {
print("hello, world")
}
这个函数的类型是:() -> Void,或者叫“没有参数,并返回 Void 类型的函数”。
使用函数类型
在 Swift 中,使用函数类型就像使用其他类型一样。例如,你可以定义一个类型为函数的常量或变量,并将适当的函数赋值给它:
var mathFunction: (Int, Int) -> Int = addTwoInts
这段代码可以被解读为:
”定义一个叫做 mathFunction 的变量,类型是‘一个有两个 Int 型的参数并返回一个 Int 型的值的函数’,并让这个新变量指向 addTwoInts 函数”。
addTwoInts 和 mathFunction 有同样的类型,所以这个赋值过程在 Swift 类型检查(type-check)中是允许的。
现在,你可以用 mathFunction 来调用被赋值的函数了:
print("Result: \(mathFunction(2, 3))")
有相同匹配类型的不同函数可以被赋值给同一个变量,就像非函数类型的变量一样:
mathFunction = multiplyTwoInts
print("Result: \(mathFunction(2, 3))")
就像其他类型一样,当赋值一个函数给常量或变量时,你可以让 Swift 来推断其函数类型:
let anotherMathFunction = addTwoInts
函数类型作为参数类型
你可以用 (Int, Int) -> Int 这样的函数类型作为另一个函数的参数类型。这样你可以将函数的一部分实现留给函数的调用者来提供。
下面是另一个例子,正如上面的函数一样,同样是输出某种数学运算结果:
func printMathResult(_ mathFunction: (Int, Int) -> Int, _ a: Int, _ b: Int) {
print("Result: \(mathFunction(a, b))")
}
这个例子定义了 printMathResult(_:_:_:) 函数,它有三个参数:第一个参数叫 mathFunction,类型是 (Int, Int) -> Int,你可以传入任何这种类型的函数;第二个和第三个参数叫 a 和 b,它们的类型都是 Int,这两个值作为已给出的函数的输入值。
当 printMathResult(_:_:_:) 被调用时,它被传入 addTwoInts 函数和整数 3 和 5。它用传入 3 和 5 调用 addTwoInts,并输出结果:8。
printMathResult(_:_:_:) 函数的作用就是输出另一个适当类型的数学函数的调用结果。它不关心传入函数是如何实现的,只关心传入的函数是不是一个正确的类型。这使得 printMathResult(_:_:_:) 能以一种类型安全(type-safe)的方式将一部分功能转给调用者实现。
函数类型作为返回值
你可以用函数类型作为另一个函数的返回类型。你需要做的是在返回箭头(->)后写一个完整的函数类型。
下面的这个例子中定义了两个简单函数,分别是 stepForward(_:) 和 stepBackward(_:)。stepForward(_:) 函数返回一个比输入值大 1 的值。stepBackward(_:) 函数返回一个比输入值小 1 的值。这两个函数的类型都是 (Int) -> Int:
func stepForward(_ input: Int) -> Int {
return input + 1
}
func stepBackward(_ input: Int) -> Int {
return input - 1
}
如下名为 chooseStepFunction(backward:) 的函数,它的返回类型是 (Int) -> Int 类型的函数。chooseStepFunction(backward:) 根据布尔值 backwards 来返回 stepForward(_:) 函数或 stepBackward(_:) 函数:
func chooseStepFunction(backward: Bool) -> (Int) -> Int {
return backward ? stepBackward : stepForward
}
你现在可以用 chooseStepFunction(backward:) 来获得两个函数其中的一个:
var currentValue = 3
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)
上面这个例子中计算出从 currentValue 逐渐接近到0是需要向正数走还是向负数走。currentValue 的初始值是 3,这意味着 currentValue > 0 为真(true),这将使得 chooseStepFunction(_:) 返回 stepBackward(_:) 函数。一个指向返回的函数的引用保存在了 moveNearerToZero 常量中。
现在,moveNearerToZero 指向了正确的函数,它可以被用来数到零:
print("Counting to zero:")
while currentValue != 0 {
print("\(currentValue)... ")
currentValue = moveNearerToZero(currentValue)
}
print("zero!")
嵌套函数
到目前为止本章中你所见到的所有函数都叫全局函数(global functions),它们定义在全局域中。你也可以把函数定义在别的函数体中,称作 嵌套函数(nested functions)。
默认情况下,嵌套函数是对外界不可见的,但是可以被它们的外围函数(enclosing function)调用。一个外围函数也可以返回它的某一个嵌套函数,使得这个函数可以在其他域中被使用。
你可以用返回嵌套函数的方式重写 chooseStepFunction(backward:) 函数:
func chooseStepFunction(backward: Bool) -> (Int) -> Int {
func stepForward(input: Int) -> Int { return input + 1 }
func stepBackward(input: Int) -> Int { return input - 1 }
return backward ? stepBackward : stepForward
}
var currentValue = -4
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)
// moveNearerToZero now refers to the nested stepForward() function
while currentValue != 0 {
print("\(currentValue)... ")
currentValue = moveNearerToZero(currentValue)
}
print("zero!")
// -4...
// -3...
// -2...
// -1...
// zero!
Swift 提供了多种流程控制结构,包括可以多次执行任务的 while 循环,基于特定条件选择执行不同代码分支的 if、guard 和 switch 语句,还有控制流程跳转到其他代码位置的 break 和 continue 语句。
Swift 还提供了 for-in 循环,用来更简单地遍历数组(Array),字典(Dictionary),区间(Range),字符串(String)和其他序列类型。
Swift 的 switch 语句比许多类 C 语言要更加强大。case 还可以匹配很多不同的模式,包括范围匹配,元组(tuple)和特定类型匹配。switch 语句的 case 中匹配的值可以声明为临时常量或变量,在 case 作用域内使用,也可以配合 where 来描述更复杂的匹配条件。
你可以使用for-in循环来遍历一个集合中的所有元素,例如数组中的元素,、范围内的数字或者字符串中的字符。
用for-in遍历数组中所有元素:
let Names = ["Anna","Alex","Brian","Jack"]
for name in Names {
print ("Hello, /(name)!")
}
你也可以通过遍历一个字典来访问它的键值对。遍历字典时,字典的每项元素会以 (key, value) 元组的形式返回,你可以在 for-in 循环中使用显式的常量名称来解读 (key, value) 元组。下面的例子中,字典的键声明会为 AnimalName 常量,字典的值会声明为 LegCount 常量:
let NumberOfLegs = ["Spider":8,"Ant":6,"Cat":4]
for (AnimalName,LegCount) in NumberOfLegs{
print("\(AnimalName)s have \(LegCount) legs.")
}
字典的内容理论上是无序的,遍历元素时的顺序是无法确定的。将元素插入字典的顺序并不会决定它们被遍历的顺序。关于数组和字典的细节,参见 集合类型。
for-in 循环还可以使用数字范围。下面的例子用来输出乘法表的一部分内容:
for index in 1...5 {
print("\(index) times 5 is \(index * 5)")
}
// 1 times 5 is 5
// 2 times 5 is 10
// 3 times 5 is 15
// 4 times 5 is 20
// 5 times 5 is 25
例子中用来进行遍历的元素是使用闭区间操作符(…)表示的从 1 到 5 的数字区间。index 被赋值为闭区间中的第一个数字(1),然后循环中的语句被执行一次。在本例中,这个循环只包含一个语句,用来输出当前 index 值所对应的乘 5 乘法表的结果。该语句执行后,index 的值被更新为闭区间中的第二个数字(2),之后 print(_:separator:terminator:) 函数会再执行一次。整个过程会进行到闭区间结尾为止。
上面的例子中,index 是一个每次循环遍历开始时被自动赋值的常量。这种情况下,index 在使用前不需要声明,只需要将它包含在循环的声明中,就可以对其进行隐式声明,而无需使用 let 关键字声明。
如果你不需要区间序列内每一项的值,你可以使用下划线(_)替代变量名来忽略这个值:
let base = 3
let power = 10
var answer = 1
for _ in i...power{
answer*=base;
}
print ("/(base) to the power of \(power) is \(answer)")
这个例子计算 base 这个数的 power 次幂(本例中,是 3 的 10 次幂),从 1(3 的 0 次幂)开始做 3 的乘法, 进行 10 次,使用 1 到 10 的闭区间循环。这个计算并不需要知道每一次循环中计数器具体的值,只需要执行了正确的循环次数即可。下划线符号 _ (替代循环中的变量)能够忽略当前值,并且不提供循环遍历时对值的访问。
在某些情况下,你可能不想使用包括两个端点的闭区间。想象一下,你在一个手表上绘制分钟的刻度线。总共 60 个刻度,从 0 分开始。使用半开区间运算符(..<)来表示一个左闭右开的区间。
let minutes = 60
for tickMark in 0..<minutes {
// 每一分钟都渲染一个刻度线(60次)
}
一些用户可能在其 UI 中可能需要较少的刻度。他们可以每 5 分钟作为一个刻度。使用 stride(from:to:by:) 函数跳过不需要的标记。
let minuteInterval = 5
for tickMark in stride(from: 0, to: minutes, by: minuteInterval) {
// 每5分钟渲染一个刻度线(0, 5, 10, 15 ... 45, 50, 55)
}
可以在闭区间使用 stride(from:through:by:) 起到同样作用:
let hours = 12
let hourInterval = 3
for tickMark in stride(from: 3, through: hours, by: hourInterval) {
// 每3小时渲染一个刻度线(3, 6, 9, 12)
}
while 循环会一直运行一段语句直到条件变成 false。这类循环适合使用在第一次迭代前,迭代次数未知的情况下。Swift 提供两种 while 循环形式:
while 循环,每次在循环开始时计算条件是否符合;
repeat-while 循环,每次在循环结束时计算条件是否符合。
while
while 循环从计算一个条件开始。如果条件为 true,会重复运行一段语句,直到条件变为 false。
下面是 while 循环的一般格式:
while condition {
statements
}
下面的例子来玩一个叫做蛇和梯子(也叫做滑道和梯子)的小游戏:

游戏的规则如下:
let finalSquare = 25
var board = [Int](repeating: 0, count: finalSquare + 1)
一些方格被设置成特定的值来表示有蛇或者梯子。梯子底部的方格是一个正值,使你可以向上移动,蛇头处的方格是一个负值,会让你向下移动:
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
3 号方格是梯子的底部,会让你向上移动到 11 号方格,我们使用 board[03] 等于 +08(来表示 11 和 3 之间的差值)。为了对齐语句,这里使用了一元正运算符(+i)和一元负运算符(-i),并且小于 10 的数字都使用 0 补齐(这些语法的技巧不是必要的,只是为了让代码看起来更加整洁)。
玩家由左下角空白处编号为 0 的方格开始游戏。玩家第一次掷骰子后才会进入游戏盘面:
var square = 0
var diceRoll = 0
while square < finalSquare {
// 掷骰子
diceRoll += 1
if diceRoll == 7 { diceRoll = 1 }
// 根据点数移动
square += diceRoll
if square < board.count {
// 如果玩家还在棋盘上,顺着梯子爬上去或者顺着蛇滑下去
square += board[square]
}
}
print("Game over!")
本例中使用了最简单的方法来模拟掷骰子。diceRoll 的值并不是一个随机数,而是以 0 为初始值,之后每一次 while 循环,diceRoll 的值增加 1 ,然后检测是否超出了最大值。当 diceRoll 的值等于 7 时,就超过了骰子的最大值,会被重置为 1。所以 diceRoll 的取值顺序会一直是 1,2,3,4,5,6,1,2 等。
掷完骰子后,玩家向前移动 diceRoll 个方格,如果玩家移动超过了第 25 个方格,这个时候游戏将会结束,为了应对这种情况,代码会首先判断 square 的值是否小于 board 的 count 属性,只有小于才会在 board[square] 上增加 square,来向前或向后移动(遇到了梯子或者蛇)。
注意:如果没有这个检测(square < board.count),board[square] 可能会越界访问 board 数组,导致运行时错误。
当本轮 while 循环运行完毕,会再检测循环条件是否需要再运行一次循环。如果玩家移动到或者超过第 25 个方格,循环条件结果为 false,此时游戏结束。
while 循环比较适合本例中的这种情况,因为在 while 循环开始时,我们并不知道游戏要跑多久,只有在达成指定条件时循环才会结束。
Repeat-While
while 循环的另外一种形式是 repeat-while,它和 while 的区别是在判断循环条件之前,先执行一次循环的代码块。然后重复循环直到条件为 false。
注意:Swift 语言的 repeat-while 循环和其他语言中的 do-while 循环是类似的。
下面是repeat-while循环的一般格式:
repeat {
statements
} while condition
还是蛇和梯子的游戏,使用 repeat-while 循环来替代 while 循环。finalSquare、board、square 和 diceRoll 的值初始化同 while 循环时一样:
let finalSquare = 25
var board = [Int](repeating: 0, count: finalSquare + 1)
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
var square = 0
var diceRoll = 0
repeat-while 的循环版本,循环中第一步就需要去检测是否在梯子或者蛇的方块上。没有梯子会让玩家直接上到第 25 个方格,所以玩家不会通过梯子直接赢得游戏。这样在循环开始时先检测是否踩在梯子或者蛇上是安全的。
游戏开始时,玩家在第 0 个方格上,board[0] 一直等于 0, 不会有什么影响:
repeat {
// 顺着梯子爬上去或者顺着蛇滑下去
square += board[square]
// 掷骰子
diceRoll += 1
if diceRoll == 7 { diceRoll = 1 }
// 根据点数移动
square += diceRoll
} while square < finalSquare
print("Game over!")
检测完玩家是否踩在梯子或者蛇上之后,开始掷骰子,然后玩家向前移动 diceRoll 个方格,本轮循环结束。
循环条件(while square < finalSquare)和 while 方式相同,但是只会在循环结束后进行计算。在这个游戏中,repeat-while 表现得比 while 循环更好。repeat-while 方式会在条件判断 square 没有超出后直接运行 square += board[square] ,这种方式可以比起前面 while 循环的版本,可以省去数组越界的检查。
根据特定的条件执行特定的代码通常是十分有用的。当错误发生时,你可能想运行额外的代码;或者,当值太大或太小时,向用户显示一条消息。要实现这些功能,你就需要使用条件语句。
Swift 提供两种类型的条件语句:if 语句和 switch 语句。通常,当条件较为简单且可能的情况很少时,使用 if 语句。而 switch 语句更适用于条件较复杂、有更多排列组合的时候。并且 switch 在需要用到模式匹配(pattern-matching)的情况下会更有用。
IF
if 语句最简单的形式就是只包含一个条件,只有该条件为 true 时,才执行相关代码:
var temperatureInFahrenheit = 30
if temperatureInFahrenheit <= 32 {
print("It's very cold. Consider wearing a scarf.")
}
//It's very cold. Consider wearing a scarf.
上面的例子会判断温度是否小于等于 32 华氏度(水的冰点)。如果是,则打印一条消息;否则,不打印任何消息,继续执行 if 块后面的代码。
当然,if 语句允许二选一执行,叫做 else 从句。也就是当条件为 false 时,执行 else 语句:
temperatureInFahrenheit = 40
if temperatureInFahrenheit <= 32 {
print("It's very cold. Consider wearing a scarf.")
} else {
print("It's not that cold. Wear a t-shirt.")
}
// It's not that cold. Wear a t-shirt.
显然,这两条分支中总有一条会被执行。由于温度已升至 40 华氏度,不算太冷,没必要再围围巾。因此,else 分支就被触发了。
你可以把多个 if 语句链接在一起,来实现更多分支:
temperatureInFahrenheit = 90
if temperatureInFahrenheit <= 32 {
print("It's very cold. Consider wearing a scarf.")
} else if temperatureInFahrenheit >= 86 {
print("It's really warm. Don't forget to wear sunscreen.")
} else {
print("It's not that cold. Wear a t-shirt.")
}
// 输出“It's really warm. Don't forget to wear sunscreen.”
在上面的例子中,额外的 if 语句用于判断是不是特别热。而最后的 else 语句被保留了下来,用于打印既不冷也不热时的消息。
实际上,当不需要完整判断情况的时候,最后的 else 语句是可选的:
temperatureInFahrenheit = 72
if temperatureInFahrenheit <= 32 {
print("It's very cold. Consider wearing a scarf.")
} else if temperatureInFahrenheit >= 86 {
print("It's really warm. Don't forget to wear sunscreen.")
}
在这个例子中,由于既不冷也不热,所以不会触发 if 或 else if 分支,也就不会打印任何消息。
Switch
switch 语句会尝试把某个值与若干个模式(pattern)进行匹配。根据第一个匹配成功的模式,switch 语句会执行对应的代码。当有可能的情况较多时,通常用 switch 语句替换 if 语句。
switch 语句最简单的形式就是把某个值与一个或若干个相同类型的值作比较:
switch some value to consider {
case value 1:
respond to value 1
case value 2,
value 3:
respond to value 2 or 3
default:
otherwise, do something else
}
switch 语句由多个 case 构成,每个由 case 关键字开始。为了匹配某些更特定的值,Swift 提供了几种方法来进行更复杂的模式匹配,这些模式将在本节的稍后部分提到。
与 if 语句类似,每一个 case 都是代码执行的一条分支。switch 语句会决定哪一条分支应该被执行,这个流程被称作根据给定的值切换(switching)。
switch 语句必须是完备的。这就是说,每一个可能的值都必须至少有一个 case 分支与之对应。在某些不可能涵盖所有值的情况下,你可以使用默认(default)分支来涵盖其它所有没有对应的值,这个默认分支必须在 switch 语句的最后面。
下面的例子使用 switch 语句来匹配一个名为 someCharacter 的小写字符:
let someCharacter: Character = "z"
switch someCharacter {
case "a":
print("The first letter of the alphabet")
case "z":
print("The last letter of the alphabet")
default:
print("Some other character")
}
// 输出“The last letter of the alphabet”
在这个例子中,第一个 case 分支用于匹配第一个英文字母 a,第二个 case 分支用于匹配最后一个字母 z。因为 switch 语句必须有一个 case 分支用于覆盖所有可能的字符,而不仅仅是所有的英文字母,所以 switch 语句使用 default 分支来匹配除了 a 和 z 外的所有值,这个分支保证了 swith 语句的完备性。
不存在隐式贯穿
与 C 和 Objective-C 中的 switch 语句不同,在 Swift 中,当匹配的 case 分支中的代码执行完毕后,程序会终止 switch 语句,而不会继续执行下一个 case 分支。这也就是说,不需要在 case 分支中显式地使用 break 语句。这使得 switch 语句更安全、更易用,也避免了漏写 break 语句导致多个语言被执行的错误。
注意:虽然在 Swift 中 break 不是必须的,但你依然可以在 case 分支中的代码执行完毕前使用 break 跳出。
每一个 case 分支都必须包含至少一条语句。像下面这样书写代码是无效的,因为第一个 case 分支是空的:
let anotherCharacter: Character = "a"
switch anotherCharacter {
case "a": // 无效,这个分支下面没有语句
case "A":
print("The letter A")
default:
print("Not the letter A")
}
// 这段代码会报编译错误
不像 C 语言里的 switch 语句,在 Swift 中,switch 语句不会一起匹配 “a” 和 “A”。相反的,上面的代码会引起编译期错误:case “a”: 不包含任何可执行语句——这就避免了意外地从一个 case 分支贯穿到另外一个,使得代码更安全、也更直观。
为了让单个 case 同时匹配 a 和 A,可以将这个两个值组合成一个复合匹配,并且用逗号分开:
let anotherCharacter: Character = "a"
switch anotherCharacter {
case "a", "A":
print("The letter A")
default:
print("Not the letter A")
}
// 输出“The letter A”
为了可读性,符合匹配可以写成多行形式.
注意:如果想要显式贯穿 case 分支,请使用 fallthrough 语句
区间匹配
case 分支的模式也可以是一个值的区间。下面的例子展示了如何使用区间匹配来输出任意数字对应的自然语言格式:
let approximateCount = 62
let countedThings = "moons orbiting Saturn"
let naturalCount: String
switch approximateCount {
case 0:
naturalCount = "no"
case 1..<5:
naturalCount = "a few"
case 5..<12:
naturalCount = "several"
case 12..<100:
naturalCount = "dozens of"
case 100..<1000:
naturalCount = "hundreds of"
default:
naturalCount = "many"
}
print("There are \(naturalCount) \(countedThings).")
// 输出“There are dozens of moons orbiting Saturn.”
在上例中,approximateCount 在一个 switch 声明中被评估。每一个 case 都与之进行比较。因为 approximateCount 落在了 12 到 100 的区间,所以 naturalCount 等于 “dozens of” 值,并且此后的执行跳出了 switch 语句。
元组
我们可以使用元组在同一个 switch 语句中测试多个值。元组中的元素可以是值,也可以是区间。另外,使用下划线(_)来匹配所有可能的值。
下面的例子展示了如何使用一个 (Int, Int) 类型的元组来分类下图中的点 (x, y):
let somePoint = (1, 1)
switch somePoint {
case (0, 0):
print("\(somePoint) is at the origin")
case (_, 0):
print("\(somePoint) is on the x-axis")
case (0, _):
print("\(somePoint) is on the y-axis")
case (-2...2, -2...2):
print("\(somePoint) is inside the box")
default:
print("\(somePoint) is outside of the box")
}
// 输出“(1, 1) is inside the box”
在上面的例子中,switch 语句会判断某个点是否是原点 (0, 0),是否在红色的 x 轴上,是否在橘黄色的 y 轴上,是否在一个以原点为中心的4×4的蓝色矩形里,或者在这个矩形外面。
不像 C 语言,Swift 允许多个 case 匹配同一个值。实际上,在这个例子中,点 (0, 0)可以匹配所有四个 case。但是,如果存在多个匹配,那么只会执行第一个被匹配到的 case 分支。考虑点 (0, 0)会首先匹配 case (0, 0),因此剩下的能够匹配的分支都会被忽视掉。
值绑定(Value Bindings)
case 分支允许将匹配的值声明为临时常量或变量,并且在 case 分支体内使用 —— 这种行为被称为值绑定(value binding),因为匹配的值在 case 分支体内,与临时的常量或变量绑定。
下面的例子将下图中的点 (x, y),使用 (Int, Int) 类型的元组表示,然后分类表示:
let anotherPoint = (2, 0)
switch anotherPoint {
case (let x, 0):
print("on the x-axis with an x value of \(x)")
case (0, let y):
print("on the y-axis with a y value of \(y)")
case let (x, y):
print("somewhere else at (\(x), \(y))")
}
// 输出“on the x-axis with an x value of 2”
在上面的例子中,switch 语句会判断某个点是否在红色的 x 轴上,是否在橘黄色的 y 轴上,或者不在坐标轴上。
这三个 case 都声明了常量 x 和 y 的占位符,用于临时获取元组 anotherPoint 的一个或两个值。第一个 case ——case (let x, 0) 将匹配一个纵坐标为 0 的点,并把这个点的横坐标赋给临时的常量 x。类似的,第二个 case ——case (0, let y) 将匹配一个横坐标为 0 的点,并把这个点的纵坐标赋给临时的常量 y。
一旦声明了这些临时的常量,它们就可以在其对应的 case 分支里使用。在这个例子中,它们用于打印给定点的类型。
请注意,这个 switch 语句不包含默认分支。这是因为最后一个 case ——case let(x, y) 声明了一个可以匹配余下所有值的元组。这使得 switch 语句已经完备了,因此不需要再书写默认分支。
Where
case 分支的模式可以使用 where 语句来判断额外的条件。
下面的例子把下图中的点 (x, y)进行了分类:
let yetAnotherPoint = (1, -1)
switch yetAnotherPoint {
case let (x, y) where x == y:
print("(\(x), \(y)) is on the line x == y")
case let (x, y) where x == -y:
print("(\(x), \(y)) is on the line x == -y")
case let (x, y):
print("(\(x), \(y)) is just some arbitrary point")
}
// 输出“(1, -1) is on the line x == -y”
在上面的例子中,switch 语句会判断某个点是否在绿色的对角线 x == y 上,是否在紫色的对角线 x == -y 上,或者不在对角线上。
这三个 case 都声明了常量 x 和 y 的占位符,用于临时获取元组 yetAnotherPoint 的两个值。这两个常量被用作 where 语句的一部分,从而创建一个动态的过滤器(filter)。当且仅当 where 语句的条件为 true 时,匹配到的 case 分支才会被执行。
就像是值绑定中的例子,由于最后一个 case 分支匹配了余下所有可能的值,switch 语句就已经完备了,因此不需要再书写默认分支。
复合型 Cases
当多个条件可以使用同一种方法来处理时,可以将这几种可能放在同一个 case 后面,并且用逗号隔开。当 case 后面的任意一种模式匹配的时候,这条分支就会被匹配。并且,如果匹配列表过长,还可以分行书写:
let someCharacter: Character = "e"
switch someCharacter {
case "a", "e", "i", "o", "u":
print("\(someCharacter) is a vowel")
case "b", "c", "d", "f", "g", "h", "j", "k", "l", "m",
"n", "p", "q", "r", "s", "t", "v", "w", "x", "y", "z":
print("\(someCharacter) is a consonant")
default:
print("\(someCharacter) is not a vowel or a consonant")
}
// 输出“e is a vowel”
这个 switch 语句中的第一个 case,匹配了英语中的五个小写元音字母。相似的,第二个 case 匹配了英语中所有的小写辅音字母。最终,default 分支匹配了其它所有字符。
复合匹配同样可以包含值绑定。复合匹配里所有的匹配模式,都必须包含相同的值绑定。并且每一个绑定都必须获取到相同类型的值。这保证了,无论复合匹配中的哪个模式发生了匹配,分支体内的代码,都能获取到绑定的值,并且绑定的值都有一样的类型。
let stillAnotherPoint = (9, 0)
switch stillAnotherPoint {
case (let distance, 0), (0, let distance):
print("On an axis, \(distance) from the origin")
default:
print("Not on an axis")
}
// 输出“On an axis, 9 from the origin”
上面的 case 有两个模式:(let distance, 0) 匹配了在 x 轴上的值,(0, let distance) 匹配了在 y 轴上的值。两个模式都绑定了 distance,并且 distance 在两种模式下,都是整型——这意味着分支体内的代码,只要 case 匹配,都可以获取到 distance 值。
控制转移语句改变你代码的执行顺序,通过它可以实现代码的跳转。Swift 有五种控制转移语句:
我们将会在下面讨论 continue、break 和 fallthrough 语句。return 语句将会在 函数 章节讨论,throw 语句会在 错误抛出 章节讨论。
Continue
continue 语句告诉一个循环体立刻停止本次循环,重新开始下次循环。就好像在说“本次循环我已经执行完了”,但是并不会离开整个循环体。
下面的例子把一个小写字符串中的元音字母和空格字符移除,生成了一个含义模糊的短句:
let puzzleInput = "great minds think alike"
var puzzleOutput = ""
for character in puzzleInput {
switch character {
case "a", "e", "i", "o", "u", " ":
continue
default:
puzzleOutput.append(character)
}
}
print(puzzleOutput)
// 输出“grtmndsthnklk”
在上面的代码中,只要匹配到元音字母或者空格字符,就调用 continue 语句,使本次循环结束,重新开始下次循环。这种行为使 switch 匹配到元音字母和空格字符时不做处理,而不是让每一个匹配到的字符都被打印。
Break
break 语句会立刻结束整个控制流的执行。break 可以在 switch 或循环语句中使用,用来提前结束 switch 或循环语句。
当在一个循环体中使用 break 时,会立刻中断该循环体的执行,然后跳转到表示循环体结束的大括号(})后的第一行代码。不会再有本次循环的代码被执行,也不会再有下次的循环产生。
当在一个 switch 代码块中使用 break 时,会立即中断该 switch 代码块的执行,并且跳转到表示 switch 代码块结束的大括号(})后的第一行代码。
这种特性可以被用来匹配或者忽略一个或多个分支。因为 Swift 的 switch 需要包含所有的分支而且不允许有为空的分支,有时为了使你的意图更明显,需要特意匹配或者忽略某个分支。那么当你想忽略某个分支时,可以在该分支内写上 break 语句。当那个分支被匹配到时,分支内的 break 语句立即结束 switch 代码块。
注意:当一个 switch 分支仅仅包含注释时,会被报编译时错误。注释不是代码语句而且也不能让 switch 分支达到被忽略的效果。你应该使用 break 来忽略某个分支。
下面的例子通过 switch 来判断一个 Character 值是否代表下面四种语言之一。为了简洁,多个值被包含在了同一个分支情况中。
let numberSymbol: Character = "三" // 简体中文里的数字 3
var possibleIntegerValue: Int?
switch numberSymbol {
case "1", "١", "一", "๑":
possibleIntegerValue = 1
case "2", "٢", "二", "๒":
possibleIntegerValue = 2
case "3", "٣", "三", "๓":
possibleIntegerValue = 3
case "4", "٤", "四", "๔":
possibleIntegerValue = 4
default:
break
}
if let integerValue = possibleIntegerValue {
print("The integer value of \(numberSymbol) is \(integerValue).")
} else {
print("An integer value could not be found for \(numberSymbol).")
}
// 输出“The integer value of 三 is 3.”
这个例子检查 numberSymbol 是否是拉丁,阿拉伯,中文或者泰语中的 1 到 4 之一。如果被匹配到,该 switch 分支语句给 Int? 类型变量 possibleIntegerValue 设置一个整数值。
当 switch 代码块执行完后,接下来的代码通过使用可选绑定来判断 possibleIntegerValue 是否曾经被设置过值。因为是可选类型的缘故,possibleIntegerValue 有一个隐式的初始值 nil,所以仅仅当 possibleIntegerValue 曾被 switch 代码块的前四个分支中的某个设置过一个值时,可选的绑定才会被判定为成功。
在上面的例子中,想要把 Character 所有的的可能性都枚举出来是不现实的,所以使用 default 分支来包含所有上面没有匹配到字符的情况。由于这个 default 分支不需要执行任何动作,所以它只写了一条 break 语句。一旦落入到 default 分支中后,break 语句就完成了该分支的所有代码操作,代码继续向下,开始执行 if let 语句。
注意:如果上述的 break 语句没有使用 gameLoop 标签,那么它将会中断 switch 语句而不是 while 循环。使用 gameLoop 标签清晰的表明了 break 想要中断的是哪个代码块。
同时请注意,当调用 continue gameLoop 去跳转到下一次循环迭代时,这里使用 gameLoop 标签并不是严格必须的。因为在这个游戏中,只有一个循环体,所以 continue 语句会影响到哪个循环体是没有歧义的。然而,continue 语句使用 gameLoop 标签也是没有危害的。这样做符合标签的使用规则,同时参照旁边的 break gameLoop,能够使游戏的逻辑更加清晰和易于理解。
提前退出
像 if 语句一样,guard 的执行取决于一个表达式的布尔值。我们可以使用 guard 语句来要求条件必须为真时,以执行 guard 语句后的代码。不同于 if 语句,一个 guard 语句总是有一个 else 从句,如果条件不为真则执行 else 从句中的代码。
func greet(person: [String: String]) {
guard let name = person["name"] else {
return
}
print("Hello \(name)!")
guard let location = person["location"] else {
print("I hope the weather is nice near you.")
return
}
print("I hope the weather is nice in \(location).")
}
greet(person: ["name": "John"])
// 输出“Hello John!”
// 输出“I hope the weather is nice near you.”
greet(person: ["name": "Jane", "location": "Cupertino"])
// 输出“Hello Jane!”
// 输出“I hope the weather is nice in Cupertino.”
如果 guard 语句的条件被满足,则继续执行 guard 语句大括号后的代码。将变量或者常量的可选绑定作为 guard 语句的条件,都可以保护 guard 语句后面的代码。
如果条件不被满足,在 else 分支上的代码就会被执行。这个分支必须转移控制以退出 guard 语句出现的代码段。它可以用控制转移语句如 return、break、continue 或者 throw 做这件事,或者调用一个不返回的方法或函数,例如 fatalError()。
相比于可以实现同样功能的 if 语句,按需使用 guard 语句会提升我们代码的可读性。它可以使你的代码连贯的被执行而不需要将它包在 else 块中,它可以使你在紧邻条件判断的地方,处理违规的情况。
Swift 内置支持检查 API 可用性,这可以确保我们不会在当前部署机器上,不小心地使用了不可用的 API。
编译器使用 SDK 中的可用信息来验证我们的代码中使用的所有 API 在项目指定的部署目标上是否可用。如果我们尝试使用一个不可用的 API,Swift 会在编译时报错。
我们在 if 或 guard 语句中使用 可用性条件(availability condition)去有条件的执行一段代码,来在运行时判断调用的 API 是否可用。编译器使用从可用性条件语句中获取的信息去验证,在这个代码块中调用的 API 是否可用。
if #available(iOS 10, macOS 10.12, *) {
// 在 iOS 使用 iOS 10 的 API, 在 macOS 使用 macOS 10.12 的 API
} else {
// 使用先前版本的 iOS 和 macOS 的 API
}
以上可用性条件指定,if 语句的代码块仅仅在 iOS 10 或 macOS 10.12 及更高版本才运行。最后一个参数,*,是必须的,用于指定在所有其它平台中,如果版本号高于你的设备指定的最低版本,if 语句的代码块将会运行。
在它一般的形式中,可用性条件使用了一个平台名字和版本的列表。平台名字可以是 iOS,macOS,watchOS 和 tvOS——请访问 声明属性 来获取完整列表。除了指定像 iOS 8 或 macOS 10.10 的大版本号,也可以指定像 iOS 11.2.6 以及 macOS 10.13.3 的小版本号。
if #available(平台名称 版本号, ..., *) {
APIs 可用,语句将执行
} else {
APIs 不可用,语句将不执行
}
Swift 提供了多种流程控制结构,包括可以多次执行任务的 while 循环,基于特定条件选择执行不同代码分支的 if、guard 和 switch 语句,还有控制流程跳转到其他代码位置的 break 和 continue 语句。
Swift 还提供了 for-in 循环,用来更简单地遍历数组(Array),字典(Dictionary),区间(Range),字符串(String)和其他序列类型。
Swift 的 switch 语句比许多类 C 语言要更加强大。case 还可以匹配很多不同的模式,包括范围匹配,元组(tuple)和特定类型匹配。switch 语句的 case 中匹配的值可以声明为临时常量或变量,在 case 作用域内使用,也可以配合 where 来描述更复杂的匹配条件。
你可以使用for-in循环来遍历一个集合中的所有元素,例如数组中的元素,、范围内的数字或者字符串中的字符。
用for-in遍历数组中所有元素:
let Names = ["Anna","Alex","Brian","Jack"]
for name in Names {
print ("Hello, /(name)!")
}
你也可以通过遍历一个字典来访问它的键值对。遍历字典时,字典的每项元素会以 (key, value) 元组的形式返回,你可以在 for-in 循环中使用显式的常量名称来解读 (key, value) 元组。下面的例子中,字典的键声明会为 AnimalName 常量,字典的值会声明为 LegCount 常量:
let NumberOfLegs = ["Spider":8,"Ant":6,"Cat":4]
for (AnimalName,LegCount) in NumberOfLegs{
print("\(AnimalName)s have \(LegCount) legs.")
}
字典的内容理论上是无序的,遍历元素时的顺序是无法确定的。将元素插入字典的顺序并不会决定它们被遍历的顺序。关于数组和字典的细节,参见 集合类型。
for-in 循环还可以使用数字范围。下面的例子用来输出乘法表的一部分内容:
for index in 1...5 {
print("\(index) times 5 is \(index * 5)")
}
// 1 times 5 is 5
// 2 times 5 is 10
// 3 times 5 is 15
// 4 times 5 is 20
// 5 times 5 is 25
例子中用来进行遍历的元素是使用闭区间操作符(…)表示的从 1 到 5 的数字区间。index 被赋值为闭区间中的第一个数字(1),然后循环中的语句被执行一次。在本例中,这个循环只包含一个语句,用来输出当前 index 值所对应的乘 5 乘法表的结果。该语句执行后,index 的值被更新为闭区间中的第二个数字(2),之后 print(_:separator:terminator:) 函数会再执行一次。整个过程会进行到闭区间结尾为止。
上面的例子中,index 是一个每次循环遍历开始时被自动赋值的常量。这种情况下,index 在使用前不需要声明,只需要将它包含在循环的声明中,就可以对其进行隐式声明,而无需使用 let 关键字声明。
如果你不需要区间序列内每一项的值,你可以使用下划线(_)替代变量名来忽略这个值:
let base = 3
let power = 10
var answer = 1
for _ in i...power{
answer*=base;
}
print ("/(base) to the power of \(power) is \(answer)")
这个例子计算 base 这个数的 power 次幂(本例中,是 3 的 10 次幂),从 1(3 的 0 次幂)开始做 3 的乘法, 进行 10 次,使用 1 到 10 的闭区间循环。这个计算并不需要知道每一次循环中计数器具体的值,只需要执行了正确的循环次数即可。下划线符号 _ (替代循环中的变量)能够忽略当前值,并且不提供循环遍历时对值的访问。
在某些情况下,你可能不想使用包括两个端点的闭区间。想象一下,你在一个手表上绘制分钟的刻度线。总共 60 个刻度,从 0 分开始。使用半开区间运算符(..<)来表示一个左闭右开的区间。
let minutes = 60
for tickMark in 0..<minutes {
// 每一分钟都渲染一个刻度线(60次)
}
一些用户可能在其 UI 中可能需要较少的刻度。他们可以每 5 分钟作为一个刻度。使用 stride(from:to:by:) 函数跳过不需要的标记。
let minuteInterval = 5
for tickMark in stride(from: 0, to: minutes, by: minuteInterval) {
// 每5分钟渲染一个刻度线(0, 5, 10, 15 ... 45, 50, 55)
}
可以在闭区间使用 stride(from:through:by:) 起到同样作用:
let hours = 12
let hourInterval = 3
for tickMark in stride(from: 3, through: hours, by: hourInterval) {
// 每3小时渲染一个刻度线(3, 6, 9, 12)
}
while 循环会一直运行一段语句直到条件变成 false。这类循环适合使用在第一次迭代前,迭代次数未知的情况下。Swift 提供两种 while 循环形式:
while 循环,每次在循环开始时计算条件是否符合;
repeat-while 循环,每次在循环结束时计算条件是否符合。
while
while 循环从计算一个条件开始。如果条件为 true,会重复运行一段语句,直到条件变为 false。
下面是 while 循环的一般格式:
while condition {
statements
}
下面的例子来玩一个叫做蛇和梯子(也叫做滑道和梯子)的小游戏:

游戏的规则如下:
let finalSquare = 25
var board = [Int](repeating: 0, count: finalSquare + 1)
一些方格被设置成特定的值来表示有蛇或者梯子。梯子底部的方格是一个正值,使你可以向上移动,蛇头处的方格是一个负值,会让你向下移动:
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
3 号方格是梯子的底部,会让你向上移动到 11 号方格,我们使用 board[03] 等于 +08(来表示 11 和 3 之间的差值)。为了对齐语句,这里使用了一元正运算符(+i)和一元负运算符(-i),并且小于 10 的数字都使用 0 补齐(这些语法的技巧不是必要的,只是为了让代码看起来更加整洁)。
玩家由左下角空白处编号为 0 的方格开始游戏。玩家第一次掷骰子后才会进入游戏盘面:
var square = 0
var diceRoll = 0
while square < finalSquare {
// 掷骰子
diceRoll += 1
if diceRoll == 7 { diceRoll = 1 }
// 根据点数移动
square += diceRoll
if square < board.count {
// 如果玩家还在棋盘上,顺着梯子爬上去或者顺着蛇滑下去
square += board[square]
}
}
print("Game over!")
本例中使用了最简单的方法来模拟掷骰子。diceRoll 的值并不是一个随机数,而是以 0 为初始值,之后每一次 while 循环,diceRoll 的值增加 1 ,然后检测是否超出了最大值。当 diceRoll 的值等于 7 时,就超过了骰子的最大值,会被重置为 1。所以 diceRoll 的取值顺序会一直是 1,2,3,4,5,6,1,2 等。
掷完骰子后,玩家向前移动 diceRoll 个方格,如果玩家移动超过了第 25 个方格,这个时候游戏将会结束,为了应对这种情况,代码会首先判断 square 的值是否小于 board 的 count 属性,只有小于才会在 board[square] 上增加 square,来向前或向后移动(遇到了梯子或者蛇)。
注意:如果没有这个检测(square < board.count),board[square] 可能会越界访问 board 数组,导致运行时错误。
当本轮 while 循环运行完毕,会再检测循环条件是否需要再运行一次循环。如果玩家移动到或者超过第 25 个方格,循环条件结果为 false,此时游戏结束。
while 循环比较适合本例中的这种情况,因为在 while 循环开始时,我们并不知道游戏要跑多久,只有在达成指定条件时循环才会结束。
Repeat-While
while 循环的另外一种形式是 repeat-while,它和 while 的区别是在判断循环条件之前,先执行一次循环的代码块。然后重复循环直到条件为 false。
注意:Swift 语言的 repeat-while 循环和其他语言中的 do-while 循环是类似的。
下面是repeat-while循环的一般格式:
repeat {
statements
} while condition
还是蛇和梯子的游戏,使用 repeat-while 循环来替代 while 循环。finalSquare、board、square 和 diceRoll 的值初始化同 while 循环时一样:
let finalSquare = 25
var board = [Int](repeating: 0, count: finalSquare + 1)
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
var square = 0
var diceRoll = 0
repeat-while 的循环版本,循环中第一步就需要去检测是否在梯子或者蛇的方块上。没有梯子会让玩家直接上到第 25 个方格,所以玩家不会通过梯子直接赢得游戏。这样在循环开始时先检测是否踩在梯子或者蛇上是安全的。
游戏开始时,玩家在第 0 个方格上,board[0] 一直等于 0, 不会有什么影响:
repeat {
// 顺着梯子爬上去或者顺着蛇滑下去
square += board[square]
// 掷骰子
diceRoll += 1
if diceRoll == 7 { diceRoll = 1 }
// 根据点数移动
square += diceRoll
} while square < finalSquare
print("Game over!")
检测完玩家是否踩在梯子或者蛇上之后,开始掷骰子,然后玩家向前移动 diceRoll 个方格,本轮循环结束。
循环条件(while square < finalSquare)和 while 方式相同,但是只会在循环结束后进行计算。在这个游戏中,repeat-while 表现得比 while 循环更好。repeat-while 方式会在条件判断 square 没有超出后直接运行 square += board[square] ,这种方式可以比起前面 while 循环的版本,可以省去数组越界的检查。
根据特定的条件执行特定的代码通常是十分有用的。当错误发生时,你可能想运行额外的代码;或者,当值太大或太小时,向用户显示一条消息。要实现这些功能,你就需要使用条件语句。
Swift 提供两种类型的条件语句:if 语句和 switch 语句。通常,当条件较为简单且可能的情况很少时,使用 if 语句。而 switch 语句更适用于条件较复杂、有更多排列组合的时候。并且 switch 在需要用到模式匹配(pattern-matching)的情况下会更有用。
IF
if 语句最简单的形式就是只包含一个条件,只有该条件为 true 时,才执行相关代码:
var temperatureInFahrenheit = 30
if temperatureInFahrenheit <= 32 {
print("It's very cold. Consider wearing a scarf.")
}
//It's very cold. Consider wearing a scarf.
上面的例子会判断温度是否小于等于 32 华氏度(水的冰点)。如果是,则打印一条消息;否则,不打印任何消息,继续执行 if 块后面的代码。
当然,if 语句允许二选一执行,叫做 else 从句。也就是当条件为 false 时,执行 else 语句:
temperatureInFahrenheit = 40
if temperatureInFahrenheit <= 32 {
print("It's very cold. Consider wearing a scarf.")
} else {
print("It's not that cold. Wear a t-shirt.")
}
// It's not that cold. Wear a t-shirt.
显然,这两条分支中总有一条会被执行。由于温度已升至 40 华氏度,不算太冷,没必要再围围巾。因此,else 分支就被触发了。
你可以把多个 if 语句链接在一起,来实现更多分支:
temperatureInFahrenheit = 90
if temperatureInFahrenheit <= 32 {
print("It's very cold. Consider wearing a scarf.")
} else if temperatureInFahrenheit >= 86 {
print("It's really warm. Don't forget to wear sunscreen.")
} else {
print("It's not that cold. Wear a t-shirt.")
}
// 输出“It's really warm. Don't forget to wear sunscreen.”
在上面的例子中,额外的 if 语句用于判断是不是特别热。而最后的 else 语句被保留了下来,用于打印既不冷也不热时的消息。
实际上,当不需要完整判断情况的时候,最后的 else 语句是可选的:
temperatureInFahrenheit = 72
if temperatureInFahrenheit <= 32 {
print("It's very cold. Consider wearing a scarf.")
} else if temperatureInFahrenheit >= 86 {
print("It's really warm. Don't forget to wear sunscreen.")
}
在这个例子中,由于既不冷也不热,所以不会触发 if 或 else if 分支,也就不会打印任何消息。
Switch
switch 语句会尝试把某个值与若干个模式(pattern)进行匹配。根据第一个匹配成功的模式,switch 语句会执行对应的代码。当有可能的情况较多时,通常用 switch 语句替换 if 语句。
switch 语句最简单的形式就是把某个值与一个或若干个相同类型的值作比较:
switch some value to consider {
case value 1:
respond to value 1
case value 2,
value 3:
respond to value 2 or 3
default:
otherwise, do something else
}
switch 语句由多个 case 构成,每个由 case 关键字开始。为了匹配某些更特定的值,Swift 提供了几种方法来进行更复杂的模式匹配,这些模式将在本节的稍后部分提到。
与 if 语句类似,每一个 case 都是代码执行的一条分支。switch 语句会决定哪一条分支应该被执行,这个流程被称作根据给定的值切换(switching)。
switch 语句必须是完备的。这就是说,每一个可能的值都必须至少有一个 case 分支与之对应。在某些不可能涵盖所有值的情况下,你可以使用默认(default)分支来涵盖其它所有没有对应的值,这个默认分支必须在 switch 语句的最后面。
下面的例子使用 switch 语句来匹配一个名为 someCharacter 的小写字符:
let someCharacter: Character = "z"
switch someCharacter {
case "a":
print("The first letter of the alphabet")
case "z":
print("The last letter of the alphabet")
default:
print("Some other character")
}
// 输出“The last letter of the alphabet”
在这个例子中,第一个 case 分支用于匹配第一个英文字母 a,第二个 case 分支用于匹配最后一个字母 z。因为 switch 语句必须有一个 case 分支用于覆盖所有可能的字符,而不仅仅是所有的英文字母,所以 switch 语句使用 default 分支来匹配除了 a 和 z 外的所有值,这个分支保证了 swith 语句的完备性。
不存在隐式贯穿
与 C 和 Objective-C 中的 switch 语句不同,在 Swift 中,当匹配的 case 分支中的代码执行完毕后,程序会终止 switch 语句,而不会继续执行下一个 case 分支。这也就是说,不需要在 case 分支中显式地使用 break 语句。这使得 switch 语句更安全、更易用,也避免了漏写 break 语句导致多个语言被执行的错误。
注意:虽然在 Swift 中 break 不是必须的,但你依然可以在 case 分支中的代码执行完毕前使用 break 跳出。
每一个 case 分支都必须包含至少一条语句。像下面这样书写代码是无效的,因为第一个 case 分支是空的:
let anotherCharacter: Character = "a"
switch anotherCharacter {
case "a": // 无效,这个分支下面没有语句
case "A":
print("The letter A")
default:
print("Not the letter A")
}
// 这段代码会报编译错误
不像 C 语言里的 switch 语句,在 Swift 中,switch 语句不会一起匹配 “a” 和 “A”。相反的,上面的代码会引起编译期错误:case “a”: 不包含任何可执行语句——这就避免了意外地从一个 case 分支贯穿到另外一个,使得代码更安全、也更直观。
为了让单个 case 同时匹配 a 和 A,可以将这个两个值组合成一个复合匹配,并且用逗号分开:
let anotherCharacter: Character = "a"
switch anotherCharacter {
case "a", "A":
print("The letter A")
default:
print("Not the letter A")
}
// 输出“The letter A”
为了可读性,符合匹配可以写成多行形式.
注意:如果想要显式贯穿 case 分支,请使用 fallthrough 语句
区间匹配
case 分支的模式也可以是一个值的区间。下面的例子展示了如何使用区间匹配来输出任意数字对应的自然语言格式:
let approximateCount = 62
let countedThings = "moons orbiting Saturn"
let naturalCount: String
switch approximateCount {
case 0:
naturalCount = "no"
case 1..<5:
naturalCount = "a few"
case 5..<12:
naturalCount = "several"
case 12..<100:
naturalCount = "dozens of"
case 100..<1000:
naturalCount = "hundreds of"
default:
naturalCount = "many"
}
print("There are \(naturalCount) \(countedThings).")
// 输出“There are dozens of moons orbiting Saturn.”
在上例中,approximateCount 在一个 switch 声明中被评估。每一个 case 都与之进行比较。因为 approximateCount 落在了 12 到 100 的区间,所以 naturalCount 等于 “dozens of” 值,并且此后的执行跳出了 switch 语句。
元组
我们可以使用元组在同一个 switch 语句中测试多个值。元组中的元素可以是值,也可以是区间。另外,使用下划线(_)来匹配所有可能的值。
下面的例子展示了如何使用一个 (Int, Int) 类型的元组来分类下图中的点 (x, y):
let somePoint = (1, 1)
switch somePoint {
case (0, 0):
print("\(somePoint) is at the origin")
case (_, 0):
print("\(somePoint) is on the x-axis")
case (0, _):
print("\(somePoint) is on the y-axis")
case (-2...2, -2...2):
print("\(somePoint) is inside the box")
default:
print("\(somePoint) is outside of the box")
}
// 输出“(1, 1) is inside the box”
在上面的例子中,switch 语句会判断某个点是否是原点 (0, 0),是否在红色的 x 轴上,是否在橘黄色的 y 轴上,是否在一个以原点为中心的4×4的蓝色矩形里,或者在这个矩形外面。
不像 C 语言,Swift 允许多个 case 匹配同一个值。实际上,在这个例子中,点 (0, 0)可以匹配所有四个 case。但是,如果存在多个匹配,那么只会执行第一个被匹配到的 case 分支。考虑点 (0, 0)会首先匹配 case (0, 0),因此剩下的能够匹配的分支都会被忽视掉。
值绑定(Value Bindings)
case 分支允许将匹配的值声明为临时常量或变量,并且在 case 分支体内使用 —— 这种行为被称为值绑定(value binding),因为匹配的值在 case 分支体内,与临时的常量或变量绑定。
下面的例子将下图中的点 (x, y),使用 (Int, Int) 类型的元组表示,然后分类表示:
let anotherPoint = (2, 0)
switch anotherPoint {
case (let x, 0):
print("on the x-axis with an x value of \(x)")
case (0, let y):
print("on the y-axis with a y value of \(y)")
case let (x, y):
print("somewhere else at (\(x), \(y))")
}
// 输出“on the x-axis with an x value of 2”
在上面的例子中,switch 语句会判断某个点是否在红色的 x 轴上,是否在橘黄色的 y 轴上,或者不在坐标轴上。
这三个 case 都声明了常量 x 和 y 的占位符,用于临时获取元组 anotherPoint 的一个或两个值。第一个 case ——case (let x, 0) 将匹配一个纵坐标为 0 的点,并把这个点的横坐标赋给临时的常量 x。类似的,第二个 case ——case (0, let y) 将匹配一个横坐标为 0 的点,并把这个点的纵坐标赋给临时的常量 y。
一旦声明了这些临时的常量,它们就可以在其对应的 case 分支里使用。在这个例子中,它们用于打印给定点的类型。
请注意,这个 switch 语句不包含默认分支。这是因为最后一个 case ——case let(x, y) 声明了一个可以匹配余下所有值的元组。这使得 switch 语句已经完备了,因此不需要再书写默认分支。
Where
case 分支的模式可以使用 where 语句来判断额外的条件。
下面的例子把下图中的点 (x, y)进行了分类:
let yetAnotherPoint = (1, -1)
switch yetAnotherPoint {
case let (x, y) where x == y:
print("(\(x), \(y)) is on the line x == y")
case let (x, y) where x == -y:
print("(\(x), \(y)) is on the line x == -y")
case let (x, y):
print("(\(x), \(y)) is just some arbitrary point")
}
// 输出“(1, -1) is on the line x == -y”
在上面的例子中,switch 语句会判断某个点是否在绿色的对角线 x == y 上,是否在紫色的对角线 x == -y 上,或者不在对角线上。
这三个 case 都声明了常量 x 和 y 的占位符,用于临时获取元组 yetAnotherPoint 的两个值。这两个常量被用作 where 语句的一部分,从而创建一个动态的过滤器(filter)。当且仅当 where 语句的条件为 true 时,匹配到的 case 分支才会被执行。
就像是值绑定中的例子,由于最后一个 case 分支匹配了余下所有可能的值,switch 语句就已经完备了,因此不需要再书写默认分支。
复合型 Cases
当多个条件可以使用同一种方法来处理时,可以将这几种可能放在同一个 case 后面,并且用逗号隔开。当 case 后面的任意一种模式匹配的时候,这条分支就会被匹配。并且,如果匹配列表过长,还可以分行书写:
let someCharacter: Character = "e"
switch someCharacter {
case "a", "e", "i", "o", "u":
print("\(someCharacter) is a vowel")
case "b", "c", "d", "f", "g", "h", "j", "k", "l", "m",
"n", "p", "q", "r", "s", "t", "v", "w", "x", "y", "z":
print("\(someCharacter) is a consonant")
default:
print("\(someCharacter) is not a vowel or a consonant")
}
// 输出“e is a vowel”
这个 switch 语句中的第一个 case,匹配了英语中的五个小写元音字母。相似的,第二个 case 匹配了英语中所有的小写辅音字母。最终,default 分支匹配了其它所有字符。
复合匹配同样可以包含值绑定。复合匹配里所有的匹配模式,都必须包含相同的值绑定。并且每一个绑定都必须获取到相同类型的值。这保证了,无论复合匹配中的哪个模式发生了匹配,分支体内的代码,都能获取到绑定的值,并且绑定的值都有一样的类型。
let stillAnotherPoint = (9, 0)
switch stillAnotherPoint {
case (let distance, 0), (0, let distance):
print("On an axis, \(distance) from the origin")
default:
print("Not on an axis")
}
// 输出“On an axis, 9 from the origin”
上面的 case 有两个模式:(let distance, 0) 匹配了在 x 轴上的值,(0, let distance) 匹配了在 y 轴上的值。两个模式都绑定了 distance,并且 distance 在两种模式下,都是整型——这意味着分支体内的代码,只要 case 匹配,都可以获取到 distance 值。
控制转移语句改变你代码的执行顺序,通过它可以实现代码的跳转。Swift 有五种控制转移语句:
我们将会在下面讨论 continue、break 和 fallthrough 语句。return 语句将会在 函数 章节讨论,throw 语句会在 错误抛出 章节讨论。
Continue
continue 语句告诉一个循环体立刻停止本次循环,重新开始下次循环。就好像在说“本次循环我已经执行完了”,但是并不会离开整个循环体。
下面的例子把一个小写字符串中的元音字母和空格字符移除,生成了一个含义模糊的短句:
let puzzleInput = "great minds think alike"
var puzzleOutput = ""
for character in puzzleInput {
switch character {
case "a", "e", "i", "o", "u", " ":
continue
default:
puzzleOutput.append(character)
}
}
print(puzzleOutput)
// 输出“grtmndsthnklk”
在上面的代码中,只要匹配到元音字母或者空格字符,就调用 continue 语句,使本次循环结束,重新开始下次循环。这种行为使 switch 匹配到元音字母和空格字符时不做处理,而不是让每一个匹配到的字符都被打印。
Break
break 语句会立刻结束整个控制流的执行。break 可以在 switch 或循环语句中使用,用来提前结束 switch 或循环语句。
当在一个循环体中使用 break 时,会立刻中断该循环体的执行,然后跳转到表示循环体结束的大括号(})后的第一行代码。不会再有本次循环的代码被执行,也不会再有下次的循环产生。
当在一个 switch 代码块中使用 break 时,会立即中断该 switch 代码块的执行,并且跳转到表示 switch 代码块结束的大括号(})后的第一行代码。
这种特性可以被用来匹配或者忽略一个或多个分支。因为 Swift 的 switch 需要包含所有的分支而且不允许有为空的分支,有时为了使你的意图更明显,需要特意匹配或者忽略某个分支。那么当你想忽略某个分支时,可以在该分支内写上 break 语句。当那个分支被匹配到时,分支内的 break 语句立即结束 switch 代码块。
注意:当一个 switch 分支仅仅包含注释时,会被报编译时错误。注释不是代码语句而且也不能让 switch 分支达到被忽略的效果。你应该使用 break 来忽略某个分支。
下面的例子通过 switch 来判断一个 Character 值是否代表下面四种语言之一。为了简洁,多个值被包含在了同一个分支情况中。
let numberSymbol: Character = "三" // 简体中文里的数字 3
var possibleIntegerValue: Int?
switch numberSymbol {
case "1", "١", "一", "๑":
possibleIntegerValue = 1
case "2", "٢", "二", "๒":
possibleIntegerValue = 2
case "3", "٣", "三", "๓":
possibleIntegerValue = 3
case "4", "٤", "四", "๔":
possibleIntegerValue = 4
default:
break
}
if let integerValue = possibleIntegerValue {
print("The integer value of \(numberSymbol) is \(integerValue).")
} else {
print("An integer value could not be found for \(numberSymbol).")
}
// 输出“The integer value of 三 is 3.”
这个例子检查 numberSymbol 是否是拉丁,阿拉伯,中文或者泰语中的 1 到 4 之一。如果被匹配到,该 switch 分支语句给 Int? 类型变量 possibleIntegerValue 设置一个整数值。
当 switch 代码块执行完后,接下来的代码通过使用可选绑定来判断 possibleIntegerValue 是否曾经被设置过值。因为是可选类型的缘故,possibleIntegerValue 有一个隐式的初始值 nil,所以仅仅当 possibleIntegerValue 曾被 switch 代码块的前四个分支中的某个设置过一个值时,可选的绑定才会被判定为成功。
在上面的例子中,想要把 Character 所有的的可能性都枚举出来是不现实的,所以使用 default 分支来包含所有上面没有匹配到字符的情况。由于这个 default 分支不需要执行任何动作,所以它只写了一条 break 语句。一旦落入到 default 分支中后,break 语句就完成了该分支的所有代码操作,代码继续向下,开始执行 if let 语句。
注意:如果上述的 break 语句没有使用 gameLoop 标签,那么它将会中断 switch 语句而不是 while 循环。使用 gameLoop 标签清晰的表明了 break 想要中断的是哪个代码块。
同时请注意,当调用 continue gameLoop 去跳转到下一次循环迭代时,这里使用 gameLoop 标签并不是严格必须的。因为在这个游戏中,只有一个循环体,所以 continue 语句会影响到哪个循环体是没有歧义的。然而,continue 语句使用 gameLoop 标签也是没有危害的。这样做符合标签的使用规则,同时参照旁边的 break gameLoop,能够使游戏的逻辑更加清晰和易于理解。
提前退出
像 if 语句一样,guard 的执行取决于一个表达式的布尔值。我们可以使用 guard 语句来要求条件必须为真时,以执行 guard 语句后的代码。不同于 if 语句,一个 guard 语句总是有一个 else 从句,如果条件不为真则执行 else 从句中的代码。
func greet(person: [String: String]) {
guard let name = person["name"] else {
return
}
print("Hello \(name)!")
guard let location = person["location"] else {
print("I hope the weather is nice near you.")
return
}
print("I hope the weather is nice in \(location).")
}
greet(person: ["name": "John"])
// 输出“Hello John!”
// 输出“I hope the weather is nice near you.”
greet(person: ["name": "Jane", "location": "Cupertino"])
// 输出“Hello Jane!”
// 输出“I hope the weather is nice in Cupertino.”
如果 guard 语句的条件被满足,则继续执行 guard 语句大括号后的代码。将变量或者常量的可选绑定作为 guard 语句的条件,都可以保护 guard 语句后面的代码。
如果条件不被满足,在 else 分支上的代码就会被执行。这个分支必须转移控制以退出 guard 语句出现的代码段。它可以用控制转移语句如 return、break、continue 或者 throw 做这件事,或者调用一个不返回的方法或函数,例如 fatalError()。
相比于可以实现同样功能的 if 语句,按需使用 guard 语句会提升我们代码的可读性。它可以使你的代码连贯的被执行而不需要将它包在 else 块中,它可以使你在紧邻条件判断的地方,处理违规的情况。
Swift 内置支持检查 API 可用性,这可以确保我们不会在当前部署机器上,不小心地使用了不可用的 API。
编译器使用 SDK 中的可用信息来验证我们的代码中使用的所有 API 在项目指定的部署目标上是否可用。如果我们尝试使用一个不可用的 API,Swift 会在编译时报错。
我们在 if 或 guard 语句中使用 可用性条件(availability condition)去有条件的执行一段代码,来在运行时判断调用的 API 是否可用。编译器使用从可用性条件语句中获取的信息去验证,在这个代码块中调用的 API 是否可用。
if #available(iOS 10, macOS 10.12, *) {
// 在 iOS 使用 iOS 10 的 API, 在 macOS 使用 macOS 10.12 的 API
} else {
// 使用先前版本的 iOS 和 macOS 的 API
}
以上可用性条件指定,if 语句的代码块仅仅在 iOS 10 或 macOS 10.12 及更高版本才运行。最后一个参数,*,是必须的,用于指定在所有其它平台中,如果版本号高于你的设备指定的最低版本,if 语句的代码块将会运行。
在它一般的形式中,可用性条件使用了一个平台名字和版本的列表。平台名字可以是 iOS,macOS,watchOS 和 tvOS——请访问 声明属性 来获取完整列表。除了指定像 iOS 8 或 macOS 10.10 的大版本号,也可以指定像 iOS 11.2.6 以及 macOS 10.13.3 的小版本号。
if #available(平台名称 版本号, ..., *) {
APIs 可用,语句将执行
} else {
APIs 不可用,语句将不执行
}
声明:
这里使用的Swift版本为 Swift 5.1 运行环境为Mac OS Catanina 10.15 developerbeta上运行Xcode 11.0版本
本文部分代码来自 gitbook
通常来说,学习一门编程语言第一件事就是学习输出“Hello world!”。在Swift上,可以用一行代码实现:
print(“Hello world!”)
如果你学习过python,你会很熟悉这种形式;但如果之前只学过C或者Java的,对这种形式可能不太熟悉—-在Swift中,这一行代码就是一个完整的程序。你不需要为了输入输出或者字符串处理导入一个单独的库。全局作用域的代码会被自动当作成程序的入口,所以你也不需要main()函数。你同样不需要在每个语句结尾写山分号。
我们先来简单接触一下Swift语言,知识点会在接下来的文章逐个讲解。
简单值
使用let来申明常量,var来申明变量。
一个常量的值,在编译时,并不需要有明确的值(不一定是一个值,可以是文件等其他东西),但是你只能为它赋一次值。这说明你可以用一个常量来命名一个值,一次赋值就可在多处使用。
var Variable=1231
Variable=2000
let Constant=20001231
常量和变量的类型必须和你赋给它的值一致。然而你不用明确地声明它的类型。当你通过一个值来声明变量和常量时,编译器会自动推断其类型。如:在上面的程序中 Variable会被自动推断为整型,因为它的初始值是整型。
如果初始值没有提供足够的信息(或者是没有初始值),那你需要在变量后面声明类型,用冒号分隔(注意格式,和其他的语言不太一样)。
let IsInteger = 70
let IsDouble = 70.0
let ISDouble: Double = 70
练习
创建一个常量,显式调用制定类型 Float ,并初始化为10
值永远不会隐式转换为其他类型。如果你需要吧其他值转为其他类型,必须显示转换。
let Str = "The width is "
let Width = 22
let StrForWidth = Str + string(Width)
还有一种更简单的办法:将值写入括号中,并在括号之前写一个反斜杠/。
let Width = 22
let Str = "The width is /(Width)."
使用一对三个单引号(“””)来包含多行内容,字符串中的内容(包括引号、空格和换行符等)都会保留下来。
let ApplesNumber = 3
let OrangesNumber = 5
let Quotation = """
I said "I have \(Apples) apples."
I said "I have \(Apples + Oranges) pieces of fruit."
"""
使用方括号[]来创建数组和字典,并使用下标和键(key)来访问元素。
最后一个元素后面允许有逗号。
var ShoppingList = ["catfish", "water", "tulips", "blue paint"]
ShoppingList[1] = "bottle of water"
var Occupations = [
"Malcolm": "Captain",
"Kaylee": "Mechanic",
]
Occupations["Jayne"] = "Public Relations"
使用初始化语言来创建一个空数组或者空字典。
var EmptyArray = [String]()
var EmptyDictionary = [String: String]()
如果类型信息可以被推断出来,你可以使用[]和[:]来创建空数组和空字典(就像你声明变量或者给函数传参的时候一样,编译器会自动推断类型)
var ShoppingList = []
var Occupations = [:]
控制流
使用 if 和 switch 来进行条件操作,使用 for-in、while 和 repeat-while 来进行循环。包裹条件和循环变量的括号可以省略,但是语句体的大括号是必须的。
let IndividualScores = [75, 43, 103, 87, 12]
var TeamScore = 0
for Score in IndividualScores {
if Score > 50 {
TeamScore += 3
} else {
TeamScore += 1
}
}
print(TeamScore)
在 if 语句中,条件必须是一个布尔表达式——这意味着像 if score { ... }这样的代码将报错,而不会隐形地与 0 做对比。
你可以一起使用 if 和 let 一起来处理值缺失的情况。这些值可由可选值来代表。一个可选的值是一个具体的值或者是 nil 以表示值缺失。在类型后面加一个问号(?)来标记这个变量的值是可选的。
var OptionalString: String? = "Hello"
print(OptionalString == nil)
var OptionalName: String? = "John Appleseed"
var Greeting = "Hello!"
if let name = optionalName {
Greeting = "Hello, \(name)"
}
如果变量的可选值是 nil,条件会判断为 false,大括号中的代码会被跳过。如果不是 nil,会将值解包并赋给 let 后面的常量,这样代码块中就可以使用这个值了。 另一种处理可选值的方法是通过使用 ?? 操作符来提供一个默认值。如果可选值缺失的话,可以使用默认值来代替。
var NickName: String?
var FullName: String = "John Appleseed"
let InformalGreeting = "Hi \(NickName ?? FullName)"
switch 支持任意类型的数据以及各种比较操作——不仅仅是整数以及测试相等。
let Vegetable = "red pepper"
switch Vegetable {
case "celery":
print("Add some raisins and make ants on a log.")
case "cucumber", "watercress":
print("That would make a good tea sandwich.")
case let x where x.hasSuffix("pepper"):
print("Is it a spicy \(x)?")
default:
print("Everything tastes good in soup.")
}
注意 let 在上述例子的等式中是如何使用的,它将匹配等式的值赋给常量 x。
运行 switch 中匹配到的 case 语句之后,程序会退出 switch 语句,并不会继续向下运行,所以不需要在每个子句结尾写 break。
你可以使用 for-in 来遍历字典,需要一对儿变量来表示每个键值对。字典是一个无序的集合,所以他们的键和值以任意顺序迭代结束。
let InterestingNumbers = [
"Prime": [2, 3, 5, 7, 11, 13],
"Fibonacci": [1, 1, 2, 3, 5, 8],
"Square": [1, 4, 9, 16, 25],
]
var Largest = 0
for (Kind, Numbers) in InterestingNumbers {
for Number in Numbers {
if Number > Largest {
Largest = Number
}
}
}
print(Largest)
使用 while 来重复运行一段代码直到条件改变。循环条件也可以在结尾,保证能至少循环一次。
var n = 2
while n < 100 {
n *= 2
}
print(n)
var m = 2
repeat {
m *= 2
} while m < 100
print(m)
你可以在循环中使用 ..< 来表示下标范围。
var Total = 0
for i in 0..<4 {
Total += i
}
print(Total)
使用 ..< 创建的范围不包含上界,如果想包含的话需要使用 ...。
函数和闭包
使用func来声明一个函数,使用名字和参数来调用函数。
使用->来指定函数返回类型。
func Greeting (Person :String ,Day: String) -> String{
return "Hello \(Person), today is \(Day)."
}
print(Greeting(Person:"Bob", Day: "Tuesday”))
默认情况下,函数使用它们的参数名称作为参数标签,在参数名称前可自定义参数标签,或使用_表示不使用参数标签。
func Greeting (_ Person :String ,Tag Day: String) -> String{
return "Hello \(Person), today is \(Day)."
}
print(Greeting("Bob", Tag: "Tuesday”))
使用元组来生成复合值,比如让一个函数来返回多个值。改元组的元素可以用名称和数字来获取。
func CalculateStatics(Sorces: [Int])->(Min:Int,Max:Int,Sum:Int){
var Min = Sorces[0]
var Max = Sorces[0]
var Sum = 0
for sorce in Sorces{
if sorce > Max{
Max = sorce
}
if sorce < Min{
Min = sorce
}
sum += sorce
}
return (Min,Max,Sum)
}
let Ans = CalculatStatics(Sorces:[1,2,3,4,5,6,7,8,9,0])
print(Ans.Sum)
print(Ans.2)
函数可以嵌套。
被嵌套的函数可以访问外侧函数的变量,你可以使用嵌套函数来重构一个太长或者太复杂的函数。
func Fifteen()->Int{
var Number = 10
func AddFive(){
y+=5
}
add()
return y
}
print(Fifteen())
函数是第一等类型,这意味着函数可以作为另一个函数的返回值。
func MakeIncrementer() -> ((Int)->Int){
func AddOne(Number: Int)->Int{
return Number + 1
}
return AddOne
}
var Incrementer=MakeIncrementer()
print(Incrementer(4))
函数也可以当作参数传入另一个函数
func HasAnyMatches(List: [Int],Condition: (Int) ->Bool) -> Bool{
for Itme in List {
if Condition(Itme){
return true
}
}
return false
}
func IsLessTen(Number: Int)->bool{
return Number < 10
}
var Numbers = [1,2,3,4,5,6,7,8,9,10]
print(HasAnyMatches(List: Numbers,Condition: IsLessTen))
函数实际上是一种特殊的闭包:它是一段之后能被调用的代码。闭包中的代码能访问闭包作用域中的变量和函数,即使闭包是在一个不同的作用域执行的 — 这一部分已经在嵌套函数中有了。你可以使用{}来创建一个匿名闭包。使用in将参数和返回值类型的声明与闭包函数体进行分离。
Numbers.map({
(Number: Int)-> Int in
let Result = 3*Number
return Result
})
有很多种创建更简洁的闭包方法。
如果一个闭包的类型已知,比如作为一个代理的回调,你可以忽略参数,返回值,甚至两个都可以忽略。单个语句闭包会把它语句的值当作返回值。
let MappedNumbers = Number.map({Number in 3 * Number})
print(MappedNumbers)
你可以通过参数位置和参数名来引用参数—-这个方法在非常短的闭包中非常有用。当一个闭包作为最后一个参数传给一个函数的时候,它可以直接跟在括号后面。当一个闭包是传给其函数的唯一参数,你可以完全忽略括号。
let SortedNumbers = Number.sorted{$0 > $1}
print (SortedNumbers)
对象和类
使用 class 和类名来创建一个类。类中属性的声明和常量、变量声明一样,唯一的区别就是它们的上下文是类。同样,方法和函数声明也一样。
class Shape {
var numberOfSides = 0
func simpleDescription() -> String {
return "A shape with \(numberOfSides) sides."
}
}
要创建一个类的实例,在类名后面加上括号。使用点语法来访问实例的属性和方法。
var shape = Shape()
shape.numberOfSides = 7
var shapeDescription = shape.simpleDescription()
这个版本的 Shape 类缺少了一些重要的东西:一个构造函数来初始化类实例。使用 init 来创建一个构造器。
class NamedShape {
var numberOfSides: Int = 0
var name: String
init(name: String) {
self.name = name
}
func simpleDescription() -> String {
return "A shape with \(numberOfSides) sides."
}
}
注意 self 被用来区别实例变量 name 和构造器的参数 name。当你创建实例的时候,像传入函数参数一样给类传入构造器的参数。每个属性都需要赋值——无论是通过声明(就像 numberOfSides)还是通过构造器(就像 name)。
如果你需要在对象释放之前进行一些清理工作,使用 deinit创建一个析构函数。
子类的定义方法是在它们的类名后面加上父类的名字,用冒号分割。创建类的时候并不需要一个标准的根类,所以你可以根据需要添加或者忽略父类。
子类如果要重写父类的方法的话,需要用 override 标记——如果没有添加 override 就重写父类方法的话编译器会报错。编译器同样会检测 override 标记的方法是否确实在父类中。
class Square: NamedShape {
var sideLength: Double
init(sideLength: Double, name: String) {
self.sideLength = sideLength
super.init(name: name)
numberOfSides = 4
}
func area() -> Double {
return sideLength * sideLength
}
override func simpleDescription() -> String {
return "A square with sides of length \(sideLength)."
}
}
let test = Square(sideLength: 5.2, name: "my test square")
test.area()
test.simpleDescription()
除了储存简单的属性之外,属性可以有 getter 和 setter 。
class EquilateralTriangle: NamedShape {
var sideLength: Double = 0.0
init(sideLength: Double, name: String) {
self.sideLength = sideLength
super.init(name: name)
numberOfSides = 3
}
var perimeter: Double {
get {
return 3.0 * sideLength
}
set {
sideLength = newValue / 3.0
}
}
override func simpleDescription() -> String {
return "An equilateral triangle with sides of length \(sideLength)."
}
}
var triangle = EquilateralTriangle(sideLength: 3.1, name: "a triangle")
print(triangle.perimeter)
triangle.perimeter = 9.9
print(triangle.sideLength)
在 perimeter 的 setter 中,新值的名字是 newValue。你可以在 set 之后显式的设置一个名字。
注意 EquilateralTriangle 类的构造器执行了三步:
1.设置子类声明的属性值
2.调用父类的构造器
3.改变父类定义的属性值。其他的工作比如调用方法、getters 和 setters 也可以在这个阶段完成。
如果你不需要计算属性,但是仍然需要在设置一个新值之前或者之后运行代码,使用 willSet 和 didSet。写入的代码会在属性值发生改变时调用,但不包含构造器中发生值改变的情况。比如,下面的类确保三角形的边长总是和正方形的边长相同。
class TriangleAndSquare {
var triangle: EquilateralTriangle {
willSet {
square.sideLength = newValue.sideLength
}
}
var square: Square {
willSet {
triangle.sideLength = newValue.sideLength
}
}
init(size: Double, name: String) {
square = Square(sideLength: size, name: name)
triangle = EquilateralTriangle(sideLength: size, name: name)
}
}
var triangleAndSquare = TriangleAndSquare(size: 10, name: "another test shape")
print(triangleAndSquare.square.sideLength)
print(triangleAndSquare.triangle.sideLength)
triangleAndSquare.square = Square(sideLength: 50, name: "larger square")
print(triangleAndSquare.triangle.sideLength)
处理变量的可选值时,你可以在操作(比如方法、属性和子脚本)之前加 ?。如果 ? 之前的值是 nil,? 后面的东西都会被忽略,并且整个表达式返回 nil。否则,? 之后的东西都会被运行。在这两种情况下,整个表达式的值也是一个可选值。
let optionalSquare: Square? = Square(sideLength: 2.5, name: "optional square")
let sideLength = optionalSquare?.sideLength
枚举和结构体
使用 enum 来创建一个枚举。就像类和其他所有命名类型一样,枚举可以包含方法。
enum Rank: Int {
case ace = 1
case two, three, four, five, six, seven, eight, nine, ten
case jack, queen, king
func simpleDescription() -> String {
switch self {
case .ace:
return "ace"
case .jack:
return "jack"
case .queen:
return "queen"
case .king:
return "king"
default:
return String(self.rawValue)
}
}
}
let ace = Rank.ace
let aceRawValue = ace.rawValue
默认情况下,Swift 按照从 0 开始每次加 1 的方式为原始值进行赋值,不过你可以通过显式赋值进行改变。在上面的例子中,Ace 被显式赋值为 1,并且剩下的原始值会按照顺序赋值。你也可以使用字符串或者浮点数作为枚举的原始值。使用 rawValue 属性来访问一个枚举成员的原始值。
使用 init?(rawValue:) 初始化构造器来创建一个带有原始值的枚举成员。如果存在与原始值相应的枚举成员就返回该枚举成员,否则就返回 nil。
if let convertedRank = Rank(rawValue: 3) {
let threeDescription = convertedRank.simpleDescription()
}
枚举的关联值是实际值,并不是原始值的另一种表达方法。实际上,如果没有比较有意义的原始值,你就不需要提供原始值。
enum Suit {
case spades, hearts, diamonds, clubs
func simpleDescription() -> String {
switch self {
case .spades:
return "spades"
case .hearts:
return "hearts"
case .diamonds:
return "diamonds"
case .clubs:
return "clubs"
}
}
}
let hearts = Suit.hearts
let heartsDescription = hearts.simpleDescription()
注意在上面的例子中用了两种方式引用 hearts 枚举成员:给 hearts 常量赋值时,枚举成员 Suit.hearts 需要用全名来引用,因为常量没有显式指定类型。在 switch 里,枚举成员使用缩写 .hearts 来引用,因为 self 的值已经是一个 suit 类型,在已知变量类型的情况下可以使用缩写。
如果枚举成员的实例有原始值,那么这些值是在声明的时候就已经决定了,这意味着不同枚举实例的枚举成员总会有一个相同的原始值。当然我们也可以为枚举成员设定关联值,关联值是在创建实例时决定的。这意味着不同的枚举成员的关联值都可以不同。你可以把关联值想象成枚举成员的寄存属性。例如,考虑从服务器获取日出和日落的时间的情况。服务器会返回正常结果或者错误信息。
enum ServerResponse {
case result(String, String)
case failure(String)
}
let success = ServerResponse.result("6:00 am", "8:09 pm")
let failure = ServerResponse.failure("Out of cheese.")
switch success {
case let .result(sunrise, sunset):
print("Sunrise is at \(sunrise) and sunset is at \(sunset)")
case let .failure(message):
print("Failure... \(message)")
}
注意日升和日落时间是如何从 ServerResponse 中提取到并与 switch 的 case 相匹配的。
使用 struct 来创建一个结构体。结构体和类有很多相同的地方,包括方法和构造器。它们之间最大的一个区别就是结构体是传值,类是传引用。
struct Card {
var rank: Rank
var suit: Suit
func simpleDescription() -> String {
return "The \(rank.simpleDescription()) of \(suit.simpleDescription())"
}
}
let threeOfSpades = Card(rank: .three, suit: .spades)
let threeOfSpadesDescription = threeOfSpades.simpleDescription()
协议和拓展
使用protocol来声明一个协议
protocol ExamplePotocol{
var SimpleDescription: String {get}
mutating func adjust()
}
类、枚举和结构体都可以遵循协议。
class SimpleClass:ExamplePotocol{
var SimpleDiscription: String = "A very simple class."
var AnotherProperty: Int = 69105
func adjust(){
SimpleDiscription += " Now 100% adjusted."
}
}
var a = SimpleClass()
a.adjust()
let ADescription = a.SimpleDiscreption
struct SimpleStructure:ExamplePotocol{
var SimpleDiscreption: String = "A very simple class."
mutating func adjust(){
SimpleDiscription += " Now 100% adjusted."
}
}
var b = SimpleStructure()
b.adjust()
let BDiscreption = b.SimpleDiscreption
注意:在声明SimpleSturcture时,mutating关键字用来标记一个会修改结构体的方法。而在声明SimpleClass时不需要标记任何办法,因为类中的方法通常可以修改类属性(类的性质)。
使用extension来为现有的类添加功能,比如新的方法和计算属性。
你可以使用拓展让某个在别处声明的类来遵守某个协议,这同样适用于从外部库或者框架引入的类型。
extension Int: ExampleProtocol {
var SimpleDescription: String{
return "The number \(self)"
}
mutating func adjust(){
self += 42
}
}
print (7.SimpleDiscreption)
你可以像使用其他命名类型一样使用协议名—例如,创建一个有不同类型但是都实现一个协议的对象集合。当你处理类型是协议的值时,协议外定义的方法不可以用。
let ProtocolValue: ExampleProtocol = a
print(PotocolValue.SimpleDescreption)
即使ProtocolValue变量运行时的类型是SimpleClass,编译器还是会把它的类型当作ExampleProtocol。这表示你不能调用在协议之外的方法或属性。
错误处理
使用采用Error协议的类型来表示错误。
enum PtinterError: Error{
case OutOfPaper
case NoToner
case InFire
}
使用throw来抛出一个错误,使用throws表示一个可以抛出错误的函数。如果在函数中抛出一个错误,这个函数会立刻返回并调用该函数的代码会进行错误处理。
func send(Job: Int , ToPrinter PrinterName: String)throws -> String{
if PrinterName == "Never Has Toner" {
throw PinterError.NoToner
}
return "Job sent"
}
有很多方式可以来进行错误处理。一种方式是使用do-catch。在do代码块中,使用try来标记可以抛出错误的代码。在catch代码块中,除非你另外命名,否则错误会自动命名为error。
do{
let PinterResponse = try send(job: 404,ToPrtinter:"Never Has Toner")
print(PinterResponse)
}catch{
print(error)
}
可以使用多个catch块来处理特定的错误。参照switch中的case风格来写catch。
do {
let PrinterResponse = try send(Job: 1440, ToPrinter: "Gutenberg")
print(PrinterResponse)
} catch PrinterError.OnFire {
print("I'll just put this over here, with the rest of the fire.")
} catch let printerError as PrinterError {
print("Printer error: \(printerError).")
} catch {
print(error)
}
另一种处理错误的方式是使用try?将结果转化为可选的。如果函数抛出错误,该错误会被抛弃并且结果nil。否则,结果会是一个包含函数返回值的可选值。
let PrinterSuccess = try? send(Job: 1884, ToPrinter: "Mergenthaler")
let PrinterFailure = try? send(Job: 1885, ToPrinter: "Never Has Toner")
使用defer代码来表示在函数返回值前,函数中最后执行的代码。无论函数是否会抛出错误,这段代码都将执行。使用defer,可以把函数调用之初就要执行的代码和函数调用结束时的扫尾代码写在一起,虽然这两者执行时期截然不同。
var FridgeIsOpen = false
let FridgeContent = ["milk", "eggs", "leftovers"]
func FridgeContains(_ Food: String) -> Bool {
FridgeIsOpen = true
defer {
FridgeIsOpen = false
}
let Result = FridgeContent.contains(Food)
return result
}
FridgeContains("banana")
print(FridgeIsOpen)
范型
在尖括号<>里写一个名字来创建一个范型函数或者类型。
func MakeArray <Item> (Repeating AddItem: Item, NUmberOfTimes: Int) -> [Item]{
var result = [Item]()
for _ in 0..<NumberOfTime{
result.append(AddItem)
}
return result
}
你也可以创建泛型函数、方法、类、枚举和结构体。
enum OptionalValue<Wrapped> {
case none
case some(Wrapped)
}
var p=PossibleInteger: OptionalValue<Int> = .none
PossibleInteger = .some(100)
在类型名后面使用 where 来指定对类型的一系列需求,比如,限定类型实现某一个协议,限定两个类型是相同的,或者限定某个类必须有一个特定的父类。
func AnyCommonElements<T: Sequence, U: Sequence>(_ LHS: T, _ RHS: U) -> Bool
where T.Iterator.Element: Equatable, T.Iterator.Element == U.Iterator.Element {
for LHSItem in LHS {
for RHSItem in RHS {
if LHSItem == RHSItem {
return true
}
}
}
return false
}
AnyCommonElements([1, 2, 3], [3])
<T: Equatable> 和 <T> ... where T: Equatable> 的写法是等价的。
Swift 语言提供 Arrays、Sets 和 Dictionaries 三种基本的集合类型用来存储集合数据。数组(Arrays)是有序数据的集。集合(Sets)是无序无重复数据的集。字典(Dictionaries)是无序的键值对的集。

Swift 语言中的 Arrays、Sets 和 Dictionaries 中存储的数据值类型必须明确。这意味着我们不能把错误的数据类型插入其中。同时这也说明你完全可以对取回值的类型非常放心。
注意:Swift 的 Arrays、Sets 和 Dictionaries 类型被实现为泛型集合。更多关于泛型类型和集合,之后会在 泛型 一节中详细讲解。
集合的可变性
如果创建一个 Arrays、Sets 或 Dictionaries 并且把它分配成一个变量,这个集合将会是可变的。这意味着你可以在创建之后添加更多或移除已存在的数据项,或者改变集合中的数据项。如果我们把 Arrays、Sets 或 Dictionaries 分配成常量,那么它就是不可变的,它的大小和内容都不能被改变。
注意:在我们不需要改变集合的时候创建不可变集合是很好的实践。如此 Swift 编译器可以优化我们创建的集合。
数组是使用有序列表存储同一类型的多个值。相同的值可以出现多次在一个数组的多个位置中。
注意:Swift的Array类型被桥连接到Foundation中的NSArray类。
数组的简单语法
写 Swift 数组应该遵循像 Array<Element> 这样的形式,其中 Element 是这个数组中唯一允许存在的数据类型。我们也可以使用像 [Element] 这样的简单语法。尽管两种形式在功能上是一样的,但是推荐较短的那种,而且在本文中都会使用这种形式来使用数组。
创建一个空数组
我们可以使用构造语法来创建一个由特定数据类型构成的空数组:
var IntArray = [Int]()
print ("IntArray is of type [Int] with \(IntArray.count) items.")
注意,通过构造函数的类型,someInts 的值类型被推断为 [Int]。
或者,如果代码上下文中已经提供了类型信息,例如一个函数参数或者一个已经定义好类型的常量或者变量,我们可以使用空数组语句创建一个空数组,它的写法很简单:[](一对空方括号):
IntArray.append(3)
// IntArray 现在包含一个 Int 值
IntArray = []
// IntArray 现在是空数组,但是仍然是 [Int] 类型的。
创建一个带有默认值的数组
Swift中的Array类型还提供一个可以创建特定大小并且所有数据都被默认的构造方法。我们可以把准备加入新数组的数据项数量(count)和适当类型的初始值(repeating)传入数组的构造函数。
var ThreeDouble = Array(repeating: 0.0, count: 3)
通过两个数组相加创建一个数组
我们可以使用加法操作符(+)来组合两种已存在的相同类型数组。新数组的数据类型会被从两个数组的数据类型中推断出来:
var AnotherThreeDoubles = Array(repeating: 2.5, count: 3)
// anotherThreeDoubles 被推断为 [Double],等价于 [2.5, 2.5, 2.5]
var SixDoubles = ThreeDoubles + AnotherThreeDoubles
// sixDoubles 被推断为 [Double],等价于 [0.0, 0.0, 0.0, 2.5, 2.5, 2.5]
用数组字面量构造数组
我们可以使用数组字面量来进行数组构造,这是一种用一个或者多个数值构造数组的简单方法。数组字面量是一系列由逗号分割并由方括号包含的数值:
[value 1, value 2, value 3]
下面这个例子创建了一个叫做 ShoppingList 并且存储 String 的数组:
var ShoppingList: [String] = ["Egg","Milk"]
ShoppingList 变量被声明为“字符串类型的数组”,记做[String]。因为这个数组被规定只有String一种数据类型,所以只有String类型可以再其中被存储。在这里,ShoppingList数组有两个String值构造并且由数组字面量定义。
注意:ShoppingList 数组被声明为变量(var 关键字创建)而不是常量(let 创建)是因为以后可能会有更多的数据项被插入其中。
在这个例子中,字面量仅仅包含两个 String 值。匹配了该数组的变量声明(只能包含 String 的数组),所以这个字面量的分配过程可以作为用两个初始项来构造 ShoppingList 的一种方式。
由于 Swift 的类型推断机制,当我们用字面量构造只拥有相同类型值数组的时候,我们不必把数组的类型定义清楚。ShoppingList 的构造也可以这样写:
var ShoppingList = ["Egg","Milk"]
因为所有数组字面量中的值都是相同的类型,Swift 可以推断出 [String] 是 shoppingList 中变量的正确类型。
访问和修改数组
我们可以通过数组的方法和属性来访问和修改数组,或者使用下标语法。
可以使用数组的只读属性 count 来获取数组中的数据项数量:
print("The shopping list contains \(ShoppingList.count) items.")
使用Bool属性isEmpty作为缩写形式去检查count属性是否为0
if ShoppingList.isEmpty {
print("The shopping list is empty.")
} else {
print("The shopping list is not empty.")
}
也可以使用 append(_:) 方法在数组后面添加新的数据类型
ShoppingList.append("Flour")
除此之外还可以使用加法赋值运算(+=)也可以直接在数组后面添加一个或者多个拥有相同类型的数据项。
ShoppingList += ["Baking Powder"]
ShoppingList += ["Chocolate Spread" , "Cheese" , "Butter"]
可以使用下标语法来获取数组中的数据项,把我们需要的数据项的索引值直接放在数组名称的方括号中:
var FirstItem = ShoppingList[0]
注意:第一项在数组中的索引是0而不是1.
我们也可以通过下标来改变某个已有索引值对应的数据值:
ShoppingList[0] = "Six Eggs"
还可以利用下标来一次改变一系列数据值,即使新数据和原有数据的数量是不一样的。下面的例子把 “Chocolate Spread”、”Cheese” 和 “Butter” 替换为 “Bananas” 和 “Apples”:
ShoppingList[4...6] = ["Bananas", "Apples"]
注意:不可以用下标访问的形式去在数组尾部添加新的值。
调用数组的 insert(_:at:) 方法来在某个具体索引值之前添加数据项:
ShoppingList.insert("Maple Syrup", at: 0)
这次 insert(_:at:) 方法调用把值为 “Maple Syrup” 的新数据项插入列表的最开始位置,并且使用 0 作为索引值。
类似的我们可以使用 remove(at:) 方法来移除数组中的某一项。这个方法把数组在特定索引值中存储的数据项移除并且返回这个被移除的数据项(我们不需要的时候就可以无视它):
let MapleSyrup = ShoppingList.remove(at: 0)
MapleSyrup.remove(at:MapleSyrup.count - 1)
注意:如果我们试着对索引越界的数据进行检索或者设置新值的操作,会引发一个运行期错误。我们可以使用索引值和数组的 count 属性进行比较来在使用某个索引之前先检验是否有效。除了当 count 等于 0 时(说明这是个空数组),最大索引值一直是 count – 1,因为数组都是零起索引。
数据项被移除后数组中的空出项会被自动填补,所以现在索引值为 0 的数据项的值再次等于 “Six eggs”:
FirstItem = shoppingList[0]
// FirstItem 现在等于“Six eggs”
如果我们只想把数组中的最后一项移除,可以使用 removeLast() 方法而不是 remove(at:) 方法来避免我们需要获取数组的 count 属性。就像后者一样,前者也会返回被移除的数据项:
let Apples = shoppingList.removeLast()
// 数组的最后一项被移除了
// ShoppingList 现在只有5项,不包括 Apples
// Apples 常量的值现在等于“Apples”字符串
数组的遍历
我们可以使用 for-in 循环来遍历所有数组中的数据项:
for item in ShoppingList {
print(item)
}
如果我们同时需要每个数据项的值和索引值,可以使用 enumerated() 方法来进行数组遍历。enumerated() 返回一个由每一个数据项索引值和数据值组成的元组。我们可以把这个元组分解成临时常量或者变量来进行遍历:
for (index , value) in ShoppingList.enumerated() {
print("Item \(String(Index + 1): \(value)")
}
集合(Set)用来存储相同类型并且没有确定顺序的值。当集合元素顺序不重要时或者希望确保每个元素只出现一次时可以使用集合而不是数组。
注意: Swift 的 Set 类型被桥接到 Foundation 中的 NSSet 类。
集合类型的哈希值
一个类型为了存储在集合中,该类型必须是可哈希化的——也就是说,该类型必须提供一个方法来计算它的哈希值。一个哈希值是 Int 类型的,相等的对象哈希值必须相同,比如 a==b,因此必须 a.hashValue == b.hashValue。
Swift 的所有基本类型(比如 String、Int、Double 和 Bool)默认都是可哈希化的,可以作为集合的值的类型或者字典的键的类型。没有关联值的枚举成员值默认也是可哈希化的。
注意:你可以使用你自定义的类型作为集合的值的类型或者是字典的键的类型,但你需要使你的自定义类型遵循 Swift 标准库中的 Hashable 协议。遵循 Hashable 协议的类型需要提供一个类型为 Int 的可读属性 hashValue。由类型的 hashValue 属性返回的值不需要在同一程序的不同执行周期或者不同程序之间保持相同。因为 Hashable 协议遵循 Equatable 协议,所以遵循该协议的类型也必须提供一个“是否相等”运算符(==)的实现。这个 Equatable 协议要求任何遵循 == 实现的实例间都是一种相等的关系。也就是说,对于 a,b,c 三个值来说,== 的实现必须满足下面三种情况:
集合类型语法
Swift 中的 Set 类型被写为 Set<Element>,这里的 Element 表示 Set 中允许存储的类型,和数组不同的是,集合没有等价的简化形式。
创建和构造一个空的集合
你可以通过构造器语法创建一个特定类型的空集合:
var Letters = Set<Character>()
print("letters is of type Set<Character> with \(Letters.count) items.")
注意:通过构造器,这里的 letters 变量的类型被推断为 Set<Character>。
此外,如果上下文提供了类型信息,比如作为函数的参数或者已知类型的变量或常量,我们可以通过一个空的数组字面量创建一个空的 Set:
Letters.insert("a")
// Letters 现在含有1个 Character 类型的值
Letters = []
// Letters 现在是一个空的 Set,但是它依然是 Set<Character> 类型
用数组字面量创建集合
你可以使用数组字面量来构造集合,并且可以使用简化形式写一个或者多个值作为集合元素。
下面的例子创建一个称之为 FavoriteGenres 的集合来存储 String 类型的值:
var FavoriteGenres: Set<String> = ["Rock", "Classical", "Hip hop"]
这个 FavoriteGenres 变量被声明为“一个 String 值的集合”,写为 Set<String>。由于这个特定的集合含有指定 String 类型的值,所以它只允许存储 String 类型值。这里的 FavoriteGenres 变量有三个 String 类型的初始值(”Rock”,”Classical” 和 “Hip hop”),并以数组字面量的方式出现。
注意:FavoriteGenres 被声明为一个变量(拥有 var 标示符)而不是一个常量(拥有 let 标示符),因为它里面的元素将会在下面的例子中被增加或者移除。
一个 Set 类型不能从数组字面量中被单独推断出来,因此 Set 类型必须显式声明。然而,由于 Swift 的类型推断功能,如果你想使用一个数组字面量构造一个 Set 并且该数组字面量中的所有元素类型相同,那么你无须写出 Set 的具体类型。FavoriteGenres 的构造形式可以采用简化的方式代替:
var FavoriteGenres: Set = ["Rock", "Classical", "Hip hop"]
由于数组字面量中的所有元素类型相同,Swift 可以推断出 Set<String> 作为 FavoriteGenres 变量的正确类型。
访问和修改一个集合
你可以通过 Set 的属性和方法来访问和修改一个 Set。
为了找出一个 Set 中元素的数量,可以使用其只读属性 count:
print("I have \(FavoriteGenres.count) favorite music genres.")
使用布尔属性 isEmpty 作为一个缩写形式去检查 count 属性是否为 0:
if FavoriteGenres.isEmpty {
print("As far as music goes, I'm not picky.")
} else {
print("I have particular music preferences.")
}
你可以通过调用 Set 的 insert(_:) 方法来添加一个新元素:
FavoriteGenres.insert("Jazz")
你可以通过调用 Set 的 remove(_:) 方法去删除一个元素,如果该值是该 Set 的一个元素则删除该元素并且返回被删除的元素值,否则如果该 Set 不包含该值,则返回 nil。另外,Set 中的所有元素可以通过它的 removeAll() 方法删除。
if let RemovedGenre = FavoriteGenres.remove("Rock") {
print("\(RemovedGenre)? I'm over it.")
} else {
print("I never much cared for that.")
}
使用 contains(_:) 方法去检查 Set 中是否包含一个特定的值:
if FavoriteGenres.contains("Funk") {
print("I get up on the good foot.")
} else {
print("It's too funky in here.")
}
遍历一个集合
你可以在一个 for-in 循环中遍历一个 Set 中的所有值。
for Genre in FavoriteGenres {
print("\(Genre)")
}
// Classical
// Jazz
// Hip hop
Swift 的 Set 类型没有确定的顺序,为了按照特定顺序来遍历一个 Set 中的值可以使用 sorted() 方法,它将返回一个有序数组,这个数组的元素排列顺序由操作符'<‘对元素进行比较的结果来确定。
for genre in favoriteGenres.sorted() {
print("\(genre)")
}
// Classical
// Hip hop
// Jazz
集合操作
你可以高效地完成 Set 的一些基本操作,比如把两个集合组合到一起,判断两个集合共有元素,或者判断两个集合是否全包含,部分包含或者不相交。
基本集合操作
下面的插图描述了两个集合 a 和 b,以及通过阴影部分的区域显示集合各种操作的结果。

intersection(_:) 方法根据两个集合中都包含的值创建的一个新的集合。symmetricDifference(_:) 方法根据在一个集合中但不在两个集合中的值创建一个新的集合。union(_:) 方法根据两个集合的值创建一个新的集合。subtracting(_:) 方法根据不在该集合中的值创建一个新的集合。let OddDigits: Set = [1, 3, 5, 7, 9]
let EvenDigits: Set = [0, 2, 4, 6, 8]
let SingleDigitPrimeNumbers: Set = [2, 3, 5, 7]
OddDigits.union(EvenDigits).sorted()
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
OddDigits.intersection(EvenDigits).sorted()
// []
OddDigits.subtracting(SingleDigitPrimeNumbers).sorted()
// [1, 9]
OddDigits.symmetricDifference(SingleDigitPrimeNumbers).sorted()
// [1, 2, 9]
集合成员关系和相等
下面的插图描述了三个集合 a、b 和 c,以及通过重叠区域表述集合间共享的元素。集合 a 是集合 b 的父集合,因为 a 包含了 b 中所有的元素,相反的,集合 b 是集合 a 的子集合,因为属于 b 的元素也被 a 包含。集合 b 和集合 c 彼此不关联,因为它们之间没有共同的元素。

let HouseAnimals: Set = ["🐶", "🐱"]
let FarmAnimals: Set = ["🐮", "🐔", "🐑", "🐶", "🐱"]
let CityAnimals: Set = ["🐦", "🐭"]
HouseAnimals.isSubset(of: FarmAnimals)
// true
FarmAnimals.isSuperset(of: HouseAnimals)
// true
FarmAnimals.isDisjoint(with: CityAnimals)
// true
字典是一种存储多个相同类型的值的容器。每个值(value)都关联唯一的键(key),键作为字典中的这个值数据的标识符。和数组中的数据项不同,字典中的数据项并没有具体顺序。我们在需要通过标识符(键)访问数据的时候使用字典,这种方法很大程度上和我们在现实世界中使用字典查字义的方法一样。
注意:Swift 的 Dictionary 类型被桥接到 Foundation 的 NSDictionary 类。
字典类型简化语法
Swift 的字典使用 Dictionary<Key, Value> 定义,其中 Key 是字典中键的数据类型,Value 是字典中对应于这些键所存储值的数据类型。
注意: 一个字典的 Key 类型必须遵循 Hashable 协议,就像 Set 的值类型。
我们也可以用 [Key: Value] 这样简化的形式去创建一个字典类型。虽然这两种形式功能上相同,但是后者是首选,并且这本指导书涉及到字典类型时通篇采用后者。
创建一个空字典
我们可以像数组一样使用构造语法创建一个拥有确定类型的空字典:
var NamesOfIntegers = [Int: String]()
这个例子创建了一个 [Int: String] 类型的空字典来储存整数的英语命名。它的键是 Int 型,值是 String 型。
如果上下文已经提供了类型信息,我们可以使用空字典字面量来创建一个空字典,记作 [:](中括号中放一个冒号):
NamesOfIntegers[16] = "sixteen"
// NamesOfIntegers 现在包含一个键值对
NamesOfIntegers = [:]
// NamesOfIntegers 又成为了一个 [Int: String] 类型的空字典
用字典字面量创建字典
我们可以使用字典字面量来构造字典,这和我们刚才介绍过的数组字面量拥有相似语法。字典字面量是一种将一个或多个键值对写作 Dictionary 集合的快捷途径。
一个键值对是一个 key 和一个 value 的结合体。在字典字面量中,每一个键值对的键和值都由冒号分割。这些键值对构成一个列表,其中这些键值对由方括号包含、由逗号分割:
[key 1: value 1, key 2: value 2, key 3: value 3]
下面的例子创建了一个存储国际机场名称的字典。在这个字典中键是三个字母的国际航空运输相关代码,值是机场名称:
var Airports: [String: String] = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]
Airports 字典被声明为一种 [String: String] 类型,这意味着这个字典的键和值都是 String 类型。
注意:Airports 字典被声明为变量(用 var 关键字)而不是常量(let 关键字)因为后来更多的机场信息会被添加到这个示例字典中。
Airports 字典使用字典字面量初始化,包含两个键值对。第一对的键是 YYZ,值是 Toronto Pearson。第二对的键是 DUB,值是 Dublin。
这个字典语句包含了两个 String: String 类型的键值对。它们对应 Airports 变量声明的类型(一个只有 String 键和 String 值的字典)所以这个字典字面量的任务是构造拥有两个初始数据项的 airport 字典。
和数组一样,我们在用字典字面量构造字典时,如果它的键和值都有各自一致的类型,那么就不必写出字典的类型。 Airports 字典也可以用这种简短方式定义:
var Airports = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]
因为这个语句中所有的键和值都各自拥有相同的数据类型,Swift 可以推断出 Dictionary<String, String> 是 airports 字典的正确类型。
访问和修改字典
我们可以通过字典的方法和属性来访问和修改字典,或者通过使用下标语法。
和数组一样,我们可以通过字典的只读属性 count 来获取某个字典的数据项数量:
print("The dictionary of airports contains \(Airports.count) items.")
使用布尔属性 isEmpty 作为一个缩写形式去检查 count 属性是否为 0:
if Airports.isEmpty {
print("The airports dictionary is empty.")
} else {
print("The airports dictionary is not empty.")
}
我们也可以在字典中使用下标语法来添加新的数据项。可以使用一个恰当类型的键作为下标索引,并且分配恰当类型的新值:
airports["LHR"] = "London"
我们也可以使用下标语法来改变特定键对应的值:
airports["LHR"] = "London Heathrow"
作为另一种下标方法,字典的 updateValue(_:forKey:) 方法可以设置或者更新特定键对应的值。就像上面所示的下标示例,updateValue(_:forKey:) 方法在这个键不存在对应值的时候会设置新值或者在存在时更新已存在的值。和上面的下标方法不同的,updateValue(_:forKey:) 这个方法返回更新值之前的原值。这样使得我们可以检查更新是否成功。
updateValue(_:forKey:) 方法会返回对应值的类型的可选值。举例来说:对于存储 String 值的字典,这个函数会返回一个 String? 或者“可选 String”类型的值。
如果有值存在于更新前,则这个可选值包含了旧值,否则它将会是 nil。
if let OldValue = Airports.updateValue("Dublin Airport", forKey: "DUB") {
print("The old value for DUB was \(oldValue).")
}
我们也可以使用下标语法来在字典中检索特定键对应的值。因为有可能请求的键没有对应的值存在,字典的下标访问会返回对应值的类型的可选值。如果这个字典包含请求键所对应的值,下标会返回一个包含这个存在值的可选值,否则将返回 nil:
if let AirportName = Airports["DUB"] {
print("The name of the airport is \(AirportName).")
} else {
print("That airport is not in the airports dictionary.")
}
我们还可以使用下标语法来通过给某个键的对应值赋值为 nil 来从字典里移除一个键值对:
airports["APL"] = "Apple Internation"
// “Apple Internation”不是真的 APL 机场,删除它
airports["APL"] = nil
// APL 现在被移除了
此外,removeValue(forKey:) 方法也可以用来在字典中移除键值对。这个方法在键值对存在的情况下会移除该键值对并且返回被移除的值或者在没有值的情况下返回 nil:
if let RemovedValue = Airports.removeValue(forKey: "DUB") {
print("The removed airport's name is \(RemovedValue).")
} else {
print("The airports dictionary does not contain a value for DUB.")
}
字典遍历
我们可以使用 for-in 循环来遍历某个字典中的键值对。每一个字典中的数据项都以 (key, value) 元组形式返回,并且我们可以使用临时常量或者变量来分解这些元组:
for (AirportCode, AirportName) in Airports {
print("\(AirportCode): \(AirportName)")
}
通过访问 keys 或者 values 属性,我们也可以遍历字典的键或者值:
for AirportCode in Airports.keys {
print("Airport code: \(AirportCode)")
}
for AirportName in Airports.values {
print("Airport name: \(AirportName)")
}
如果我们只是需要使用某个字典的键集合或者值集合来作为某个接受 Array 实例的 API 的参数,可以直接使用 keys 或者 values 属性构造一个新数组:
let AirportCodes = [String](Airports.keys)
// airportCodes 是 ["YYZ", "LHR"]
let AirportNames = [String](Airports.values)
// airportNames 是 ["Toronto Pearson", "London Heathrow"]
Swift 的字典类型是无序集合类型。为了以特定的顺序遍历字典的键或值,可以对字典的 keys 或 values 属性使用 sorted() 方法。
字符串是是一系列字符的集合,例如 ”hello, world","albatross"。Swift 的字符串通过 String 类型来表示。而 String 内容的访问方式有多种,例如以 Character 值的集合。
Swift 的 String 和 Character 类型提供了一种快速且兼容 Unicode 的方式来处理代码中的文本内容。创建和操作字符串的语法与 C 语言中字符串操作相似,轻量并且易读。通过 + 符号就可以非常简单的实现两个字符串的拼接操作。与 Swift 中其他值一样,能否更改字符串的值,取决于其被定义为常量还是变量。你可以在已有字符串中插入常量、变量、字面量和表达式从而形成更长的字符串,这一过程也被成为字符串插值。尤其是在为显示、存储和打印创建自定义字符串值时,字符串插值操作尤其有用。
尽管语法简易,但 Swift 中的 String 类型的实现却很快速和现代化。每一个字符串都是由编码无关的 Unicode 字符组成,并支持访问字符的多种 Unicode 表示形式。
注意:Swift 的 String 类型与 Foundation NSString 类进行了无缝桥接。Foundation 还对 String 进行扩展使其可以访问 NSString 类型中定义的方法。这意味着调用那些 NSString 的方法,你无需进行任何类型转换。
字符串字面量
你可以在代码里使用一段预定义的字符串值作为字符串字面量。字符串字面量是由一对双引号包裹着的具有固定顺序的字符集。
字符串字面量可以用于为常量和变量提供初始值:
let someString = "Some string literal value"
注意,Swift 之所以推断 someString 常量为字符串类型,是因为它使用了字面量方式进行初始化。
多行字符串字面量
如果你需要一个字符串是跨越多行的,那就使用多行字符串字面量 — 由一对三个双引号包裹着的具有固定顺序的文本字符集:
let quotation = """
The White Rabbit put on his spectacles. "Where shall I begin,
please your Majesty?" he asked.
"Begin at the beginning," the King said gravely, "and go on
till you come to the end; then stop."
"""
一个多行字符串字面量包含了所有的在开启和关闭引号(”””)中的行。这个字符从开启引号(”””)之后的第一行开始,到关闭引号(”””)之前为止。这就意味着字符串开启引号之后(”””)或者结束引号(”””)之前都没有换行符号。(译者:下面两个字符串其实是一样的,虽然第二个使用了多行字符串的形式)
let singleLineString = "These are the same."
let multilineString = """
These are the same.
"""
如果你的代码中,多行字符串字面量包含换行符的话,则多行字符串字面量中也会包含换行符。如果你想换行,以便加强代码的可读性,但是你又不想在你的多行字符串字面量中出现换行符的话,你可以用在行尾写一个反斜杠(\)作为续行符。
let softWrappedQuotation = """
The White Rabbit put on his spectacles. "Where shall I begin, \
please your Majesty?" he asked.
"Begin at the beginning," the King said gravely, "and go on \
till you come to the end; then stop."
"""
了让一个多行字符串字面量开始和结束于换行符,请将换行写在第一行和最后一行,例如:
let lineBreaks = """
This string starts with a line break.
It also ends with a line break.
"""
一个多行字符串字面量能够缩进来匹配周围的代码。关闭引号(”””)之前的空白字符串告诉 Swift 编译器其他各行多少空白字符串需要忽略。然而,如果你在某行的前面写的空白字符串超出了关闭引号(”””)之前的空白字符串,则超出部分将被包含在多行字符串字面量中。
例如
let softWrappedQuotation = """
The White Rabbit put on his spectacles. "Where shall I begin, \
please your Majesty?" he asked.
"Begin at the beginning," the King said gravely, "and go on \
till you come to the end; then stop."
"""
上面和下面是一样的
if true{
let softWrappedQuotation = """
The White Rabbit put on his spectacles. "Where shall I begin, \
please your Majesty?" he asked.
"Begin at the beginning," the King said gravely, "and go on \
till you come to the end; then stop."
"""
}
在上面的例子中,尽管整个多行字符串字面量都是缩进的(源代码缩进),第一行和最后一行没有以空白字符串开始(实际的变量值)。中间一行的缩进用空白字符串(源代码缩进)比关闭引号(”””)之前的空白字符串多,所以,它的行首将有4个空格。
字符串字面量的特殊字符
字符串字面量可以包含以下特殊字符:
转义字符 \0(空字符)、\\(反斜线)、\t(水平制表符)、\n(换行符)、\r(回车符)、\”(双引号)、\’(单引号)。
Unicode 标量,写成 \u{n}(u 为小写),其中 n 为任意一到八位十六进制数且可用的 Unicode 位码。
下面的代码为各种特殊字符的使用示例。 wiseWords 常量包含了两个双引号。 dollarSign、blackHeart 和 sparklingHeart 常量演示了三种不同格式的 Unicode 标量:
let wiseWords = "\"Imagination is more important than knowledge\" - Einstein"
// "Imageination is more important than knowledge" - Enistein
let dollarSign = "\u{24}" // $,Unicode 标量 U+0024
let blackHeart = "\u{2665}" // ♥,Unicode 标量 U+2665
let sparklingHeart = "\u{1F496}" // 💖,Unicode 标量 U+1F496
由于多行字符串字面量使用了三个双引号,而不是一个,所以你可以在多行字符串字面量里直接使用双引号(”)而不必加上转义符(\)。要在多行字符串字面量中使用 “”” 的话,就需要使用至少一个转义符(\):
let threeDoubleQuotes = """
Escaping the first quote \"""
Escaping all three quotes \"\"\"
"""
拓展字符串分隔符
您可以将字符串文字放在扩展分隔符中,这样字符串中的特殊字符将会被直接包含而非转义后的效果。将字符串放在引号(”)中并用数字符号(#)括起来。例如,打印字符串文字 #"Line 1 \ nLine 2"# 打印换行符转义序列(\n)而不是进行换行打印。
如果需要字符串文字中字符的特殊效果,请匹配转义字符(\)后面添加与起始位置个数相匹配的 # 符。 例如,如果您的字符串是 #"Line 1 \ nLine 2"# 并且您想要换行,则可以使用 #“Line 1 \ #nLine 2”# 来代替。 同样,###”Line1 \ ### nLine2”### 也可以实现换行效果。
扩展分隔符创建的字符串文字也可以是多行字符串文字。 您可以使用扩展分隔符在多行字符串中包含文本 “””,覆盖原有的结束文字的默认行为。例如:
let threeMoreDoubleQuotationMarks = #"""
Here are three more double quotes: """
"""#
初始化空字符串
要创建一个空字符串作为初始值,可以将空的字符串字面量赋值给变量,也可以初始化一个新的 String 实例:
var emptyString = ""
var anotherEmptyString = String()
上面两个值等价
你可以通过检查 Bool 类型的 isEmpty 属性来判断该字符串是否为空:
if emptyString.isEmpty {
print("Nothing to see here")
}
字符串的可变性
你可以通过将一个特定字符串分配给一个变量来对其进行修改,或者分配给一个常量来保证其不会被修改:
var variableString = "Horse"
variableString += " and carriage"
let constantString = "Highlander"
constantString += " and another Highlander”//错误 let 常量不可修改
注意:在 Objective-C 和 Cocoa 中,需要通过选择两个不同的类(NSString 和 NSMutableString)来指定字符串是否可以被修改。
字符串是值类型
在 Swift 中 String 类型是值类型。如果你创建了一个新的字符串,那么当其进行常量、变量赋值操作,或在函数/方法中传递时,会进行值拷贝。在前述任一情况下,都会对已有字符串值创建新副本,并对该新副本而非原始字符串进行传递或赋值操作。值类型在 结构体和枚举是值类型 中进行了详细描述。
Swift 默认拷贝字符串的行为保证了在函数/方法向你传递的字符串所属权属于你,无论该值来自于哪里。你可以确信传递的字符串不会被修改,除非你自己去修改它。
在实际编译时,Swift 编译器会优化字符串的使用,使实际的复制只发生在绝对必要的情况下,这意味着你将字符串作为值类型的同时可以获得极高的性能。
使用字符
你可通过 for-in 循环来遍历字符串,获取字符串中每一个字符的值:
for character in "Dog!🐶" {
print(character)
}
另外,通过标明一个 Character 类型并用字符字面量进行赋值,可以建立一个独立的字符常量或变量:
let exclamationMark: Character = "!"
字符串可以通过传递一个值类型为 Character 的数组作为自变量来初始化:
let catCharacters: [Character] = ["C", "a", "t", "!", "🐱"]
let catString = String(catCharacters)
print(catString)
连接字符串和字符
字符串可以通过加法运算符(+)相加在一起(或称“连接”)创建一个新的字符串:
let string1 = "hello"
let string2 = " there"
var welcome = string1 + string
你也可以通过加法赋值运算符(+=)将一个字符串添加到一个已经存在字符串变量
var instruction = "look over"
instruction += string2
你可以用 append() 方法将一个字符附加到一个字符串变量的尾部:
let exclamationMark: Character = "!"
welcome.append(exclamationMark)
注意:你不能将一个字符串或者字符添加到一个已经存在的字符变量上,因为字符变量只能包含一个字符。
如果你需要使用多行字符串字面量来拼接字符串,并且你需要字符串每一行都以换行符结尾,包括最后一行:
let badStart = """
one
two
"""
let end = """
three
"""
print(badStart + end)
// 打印两行:
// one
// twothree
let goodStart = """
one
two
"""
print(goodStart + end)
// 打印三行:
// one
// two
// three
上面的代码,把 badStart 和 end 拼接起来的字符串非我们想要的结果。因为 badStart 最后一行没有换行符,它与 end 的第一行结合到了一起。相反的,goodStart 的每一行都以换行符结尾,所以它与 end 拼接的字符串总共有三行,正如我们期望的那样。
字符串插值
字符串插值是一种构建新字符串的方式,可以在其中包含常量、变量、字面量和表达式。字符串字面量和多行字符串字面量都可以使用字符串插值。你插入的字符串字面量的每一项都在以反斜线为前缀的圆括号中:
let multiplier = 3
let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)"
在上面的例子中,multiplier 作为 \(multiplier) 被插入到一个字符串常量量中。当创建字符串执行插值计算时此占位符会被替换为 multiplier 实际的值。
multiplier 的值也作为字符串中后面表达式的一部分。该表达式计算 Double(multiplier) * 2.5 的值并将结果(7.5)插入到字符串中。在这个例子中,表达式写为 \(Double(multiplier) * 2.5) 并包含在字符串字面量中。
注意:插值字符串中写在括号中的表达式不能包含非转义反斜杠(\),并且不能包含回车或换行符。不过,插值字符串可以包含其他字面量。
Unicode
Unicode是一个用于在不同书写系统中对文本进行编码、表示和处理的国际标准。它使你可以用标准格式表示来自任意语言几乎所有的字符,并能够对文本文件或网页这样的外部资源中的字符进行读写操作。Swift 的 String 和 Character 类型是完全兼容 Unicode 标准的。
Unicode标量
Swift 的 String 类型是基于 Unicode 标量 建立的。Unicode 标量是对应字符或者修饰符的唯一的 21 位数字,例如 U+0061 表示小写的拉丁字母(LATIN SMALL LETTER A)(”a”),U+1F425 表示小鸡表情(FRONT-FACING BABY CHICK)(”🐥”)。
请注意,并非所有 21 位 Unicode 标量值都分配给字符,某些标量被保留用于将来分配或用于 UTF-16 编码。已分配的标量值通常也有一个名称,例如上面示例中的 LATIN SMALL LETTER A 和 FRONT-FACING BABY CHICK。
可拓展的字形群集
每一个 Swift 的 Character 类型代表一个可扩展的字形群。而一个可扩展的字形群构成了人类可读的单个字符,它由一个或多个(当组合时) Unicode 标量的序列组成。
举个例子,字母 é 可以用单一的 Unicode 标量 é(LATIN SMALL LETTER E WITH ACUTE, 或者 U+00E9)来表示。然而一个标准的字母 e(LATIN SMALL LETTER E 或者 U+0065) 加上一个急促重音(COMBINING ACTUE ACCENT)的标量(U+0301),这样一对标量就表示了同样的字母 é。 这个急促重音的标量形象的将 e 转换成了 é。
在这两种情况中,字母 é 代表了一个单一的 Swift 的 Character 值,同时代表了一个可扩展的字形群。在第一种情况,这个字形群包含一个单一标量;而在第二种情况,它是包含两个标量的字形群:
let eAcute: Character = "\u{E9}" // é
let combinedEAcute: Character = "\u{65}\u{301}" // e 后面加上 ́
可扩展的字形集是一个将许多复杂的脚本字符表示为单个字符值的灵活方式。例如,来自朝鲜语字母表的韩语音节能表示为组合或分解的有序排列。在 Swift 都会表示为同一个单一的 Character 值:
let precomposed: Character = "\u{D55C}" // 한
let decomposed: Character = "\u{1112}\u{1161}\u{11AB}" // ᄒ, ᅡ, ᆫ
可拓展的字符群集可以使包围记号(例如 COMBINING ENCLOSING CIRCLE 或者 U+20DD)的标量包围其他 Unicode 标量,作为一个单一的 Character 值:
let enclosedEAcute: Character = "\u{E9}\u{20DD}" // é⃝
地域性指示符号的 Unicode 标量可以组合成一个单一的 Character 值,例如 REGIONAL INDICATOR SYMBOL LETTER U(U+1F1FA)和 REGIONAL INDICATOR SYMBOL LETTER S(U+1F1F8):
let regionalIndicatorForUS: Character = "\u{1F1FA}\u{1F1F8}" // 🇺🇸
计算字符数量
如果想要获得一个字符串中 Character 值的数量,可以使用 count 属性:
let unusualMenagerie = "Koala 🐨, Snail 🐌, Penguin 🐧, Dromedary 🐪"
print("unusualMenagerie has \(unusualMenagerie.count) characters")
注意在 Swift 中,使用可拓展的字符群集作为 Character 值来连接或改变字符串时,并不一定会更改字符串的字符数量。
例如,如果你用四个字符的单词 cafe 初始化一个新的字符串,然后添加一个 COMBINING ACTUE ACCENT(U+0301)作为字符串的结尾。最终这个字符串的字符数量仍然是 4,因为第四个字符是 é,而不是 e:
var word = "cafe"
print("the number of characters in \(word) is \(word.count)") //4
word += "\u{301}" // 拼接一个重音,U+0301
print("the number of characters in \(word) is \(word.count)”) //4
注意:可扩展的字形群可以由多个 Unicode 标量组成。这意味着不同的字符以及相同字符的不同表示方式可能需要不同数量的内存空间来存储。所以 Swift 中的字符在一个字符串中并不一定占用相同的内存空间数量。因此在没有获取字符串的可扩展的字符群的范围时候,就不能计算出字符串的字符数量。如果你正在处理一个长字符串,需要注意 count 属性必须遍历全部的 Unicode 标量,来确定字符串的字符数量。另外需要注意的是通过 count 属性返回的字符数量并不总是与包含相同字符的 NSString 的 length 属性相同。NSString 的 length 属性是利用 UTF-16 表示的十六位代码单元数字,而不是 Unicode 可扩展的字符群集。
访问和修改字符串
你可以通过字符串的属性和方法来访问和修改它,当然也可以用下标语法完成。
每一个 String 值都有一个关联的索引(index)类型,String.Index,它对应着字符串中的每一个 Character 的位置。
前面提到,不同的字符可能会占用不同数量的内存空间,所以要知道 Character 的确定位置,就必须从 String 开头遍历每一个 Unicode标量直到结尾。因此,Swift 的字符串不能用整数(integer)做索引。
使用 startIndex 属性可以获取一个 String 的第一个 Character 的索引。使用 endIndex 属性可以获取最后一个 Character 的后一个位置的索引。因此,endIndex 属性不能作为一个字符串的有效下标。如果 String 是空串,startIndex 和 endIndex 是相等的。
通过调用 String 的 index(before:) 或 index(after:) 方法,可以立即得到前面或后面的一个索引。你还可以通过调用 index(_:offsetBy:) 方法来获取对应偏移量的索引,这种方式可以避免多次调用 index(before:) 或 index(after:) 方法。
你可以使用下标语法来访问 String 特定索引的 Character。
let greeting = "Guten Tag!"
greeting[greeting.startIndex]
// G
greeting[greeting.index(before: greeting.endIndex)]
// !
greeting[greeting.index(after: greeting.startIndex)]
// u
let index = greeting.index(greeting.startIndex, offsetBy: 7)
greeting[index]
// a
试图获取越界索引对应的 Character,将引发一个运行时错误。
greeting[greeting.endIndex] // error
greeting.index(after: endIndex) // error
使用 indices 属性会创建一个包含全部索引的范围(Range),用来在一个字符串中访问单个字符。
for index in greeting.indices {
print("\(greeting[index]) ", terminator: "")
}
//“G u t e n T a g ! ”
注意:你可以使用 startIndex 和 endIndex 属性或者 index(before:) 、index(after:) 和 index(_:offsetBy:) 方法在任意一个确认的并遵循 Collection 协议的类型里面,如上文所示是使用在 String 中,你也可以使用在 Array、Dictionary 和 Set 中。
调用 insert(_:at:) 方法可以在一个字符串的指定索引插入一个字符,调用 insert(contentsOf:at:) 方法可以在一个字符串的指定索引插入一个段字符串。
var welcome = "hello"
welcome.insert("!", at: welcome.endIndex)
//"hello!"
welcome.insert(contentsOf:" there", at: welcome.index(before: welcome.endIndex))
//"hello there!"
调用 remove(at:) 方法可以在一个字符串的指定索引删除一个字符,调用 removeSubrange(_:) 方法可以在一个字符串的指定索引删除一个子字符串。
welcome.remove(at: welcome.index(before: welcome.endIndex))
//"hello there"
let range = welcome.index(welcome.endIndex, offsetBy: -6)..<welcome.endIndex
welcome.removeSubrange(range)
//“hello"
注意:你可以使用 insert(_:at:)、insert(contentsOf:at:)、remove(at:) 和 removeSubrange(_:) 方法在任意一个确认的并遵循 RangeReplaceableCollection 协议的类型里面,如上文所示是使用在 String 中,你也可以使用在 Array、Dictionary 和 Set 中。
字串
当你从字符串中获取一个子字符串 —— 例如,使用下标或者 prefix(_:) 之类的方法 —— 就可以得到一个 SubString 的实例,而非另外一个 String。Swif 里的 SubString 绝大部分函数都跟 String 一样,意味着你可以使用同样的方式去操作 SubString 和 String。然而,跟 String 不同的是,你只有在短时间内需要操作字符串时,才会使用 SubString。当你需要长时间保存结果时,就把 SubString 转化为 String 的实例:
let greeting = "Hello, world!"
let index = greeting.firstIndex(of: ",") ?? greeting.endIndex
let beginning = greeting[..<index]
// beginning 的值为 "Hello"
// 把结果转化为 String 以便长期存储。
let newString = String(beginning)
就像 String,每一个 SubString 都会在内存里保存字符集。而 String 和 SubString 的区别在于性能优化上,SubString 可以重用原 String 的内存空间,或者另一个 SubString 的内存空间(String 也有同样的优化,但如果两个 String 共享内存的话,它们就会相等)。这一优化意味着你在修改 String 和 SubString 之前都不需要消耗性能去复制内存。就像前面说的那样,SubString 不适合长期存储 —— 因为它重用了原 String 的内存空间,原 String 的内存空间必须保留直到它的 SubString 不再被使用为止。
上面的例子,greeting 是一个 String,意味着它在内存里有一片空间保存字符集。而由于 beginning 是 greeting 的 SubString,它重用了 greeting 的内存空间。相反,newString 是一个 String —— 它是使用 SubString 创建的,拥有一片自己的内存空间。下面的图展示了他们之间的关系:

注意:String 和 SubString 都遵循 StringProtocol<//apple_ref/swift/intf/s:s14StringProtocolP> 协议,这意味着操作字符串的函数使用 StringProtocol 会更加方便。你可以传入 String 或 SubString 去调用函数。
比较字符串
Swift 提供了三种方式来比较文本值:字符串字符相等、前缀相等和后缀相等。
字符串/字符可以用等于操作符(==)和不等于操作符(!=),详细描述在 比较运算符:
let quotation = "We're a lot alike, you and I."
let sameQuotation = "We're a lot alike, you and I."
if quotation == sameQuotation {
print("These two strings are considered equal")
}
如果两个字符串(或者两个字符)的可扩展的字形群集是标准相等,那就认为它们是相等的。只要可扩展的字形群集有同样的语言意义和外观则认为它们标准相等,即使它们是由不同的 Unicode 标量构成。
例如,LATIN SMALL LETTER E WITH ACUTE(U+00E9)就是标准相等于 LATIN SMALL LETTER E(U+0065)后面加上 COMBINING ACUTE ACCENT(U+0301)。这两个字符群集都是表示字符 é 的有效方式,所以它们被认为是标准相等的:
// "Voulez-vous un café?"
let eAcuteQuestion = "Voulez-vous un caf\u{E9}?"
// "Voulez-vous un café?"
let combinedEAcuteQuestion = "Voulez-vous un caf\u{65}\u{301}?"
if eAcuteQuestion == combinedEAcuteQuestion {
print("These two strings are considered equal")
}
// These two strings are considered equal
相反,英语中的 LATIN CAPITAL LETTER A(U+0041,或者 A)不等于俄语中的 CYRILLIC CAPITAL LETTER A(U+0410,或者 A)。两个字符看着是一样的,但却有不同的语言意义:
let latinCapitalLetterA: Character = "\u{41}"
let cyrillicCapitalLetterA: Character = "\u{0410}"
if latinCapitalLetterA != cyrillicCapitalLetterA {
print("These two characters are not equivalent")
}
// These two characters are not equivalent
注意:在 Swift 中,字符串和字符并不区分地域(not locale-sensitive)。
通过调用字符串的 hasPrefix(_:)/hasSuffix(_:) 方法来检查字符串是否拥有特定前缀/后缀,两个方法均接收一个 String 类型的参数,并返回一个布尔值。
下面的例子以一个字符串数组表示莎士比亚话剧《罗密欧与朱丽叶》中前两场的场景位置:
let romeoAndJuliet = [
"Act 1 Scene 1: Verona, A public place",
"Act 1 Scene 2: Capulet's mansion",
"Act 1 Scene 3: A room in Capulet's mansion",
"Act 1 Scene 4: A street outside Capulet's mansion",
"Act 1 Scene 5: The Great Hall in Capulet's mansion",
"Act 2 Scene 1: Outside Capulet's mansion",
"Act 2 Scene 2: Capulet's orchard",
"Act 2 Scene 3: Outside Friar Lawrence's cell",
"Act 2 Scene 4: A street in Verona",
"Act 2 Scene 5: Capulet's mansion",
"Act 2 Scene 6: Friar Lawrence's cell"
]
你可以调用 hasPrefix(_:) 方法来计算话剧中第一幕的场景数:
var act1SceneCount = 0
for scene in romeoAndJuliet {
if scene.hasPrefix("Act 1 ") {
act1SceneCount += 1
}
}
print("There are \(act1SceneCount) scenes in Act 1")
// There are 5 scenes in Act 1
相似地,你可以用 hasSuffix(_:) 方法来计算发生在不同地方的场景数:
var mansionCount = 0
var cellCount = 0
for scene in romeoAndJuliet {
if scene.hasSuffix("Capulet's mansion") {
mansionCount += 1
} else if scene.hasSuffix("Friar Lawrence's cell") {
cellCount += 1
}
}
print("\(mansionCount) mansion scenes; \(cellCount) cell scenes")
// 6 mansion scenes; 2 cell scenes
注意:hasPrefix(_:) 和 hasSuffix(_:) 方法都是在每个字符串中逐字符比较其可扩展的字符群集是否标准相等,详细描述在 字符串/字符相等。
字符串的 Unicode 表示形式
当一个 Unicode 字符串被写进文本文件或者其他储存时,字符串中的 Unicode 标量会用 Unicode 定义的几种 编码格式(encoding forms)编码。每一个字符串中的小块编码都被称 代码单元(code units)。这些包括 UTF-8 编码格式(编码字符串为 8 位的代码单元), UTF-16 编码格式(编码字符串位 16 位的代码单元),以及 UTF-32 编码格式(编码字符串32位的代码单元)。
Swift 提供了几种不同的方式来访问字符串的 Unicode 表示形式。你可以利用 for-in 来对字符串进行遍历,从而以 Unicode 可扩展的字符群集的方式访问每一个 Character 值。该过程在 使用字符 中进行了描述。
另外,能够以其他三种 Unicode 兼容的方式访问字符串的值:
下面由 D,o,g,‼(DOUBLE EXCLAMATION MARK, Unicode 标量 U+203C)和 🐶(DOG FACE,Unicode 标量为 U+1F436)组成的字符串中的每一个字符代表着一种不同的表示:
let dogString = "Dog‼🐶"
UTF-8 表示
你可以通过遍历 String 的 utf8 属性来访问它的 UTF-8 表示。其为 String.UTF8View 类型的属性,UTF8View 是无符号 8 位(UInt8)值的集合,每一个 UInt8 值都是一个字符的 UTF-8 表示:

for codeUnit in dogString.utf8 {
print("\(codeUnit) ", terminator: "")
}
print("")
// 68 111 103 226 128 188 240 159 144 182
上面的例子中,前三个 10 进制 codeUnit 值(68、111、103)代表了字符 D、o 和 g,它们的 UTF-8 表示与 ASCII 表示相同。接下来的三个 10 进制 codeUnit 值(226、128、188)是 DOUBLE EXCLAMATION MARK 的3字节 UTF-8 表示。最后的四个 codeUnit 值(240、159、144、182)是 DOG FACE 的4字节 UTF-8 表示。
UTF-16 表示
你可以通过遍历 String 的 utf16 属性来访问它的 UTF-16 表示。其为 String.UTF16View 类型的属性,UTF16View 是无符号16位(UInt16)值的集合,每一个 UInt16 都是一个字符的 UTF-16 表示:

for codeUnit in dogString.utf16 {
print("\(codeUnit) ", terminator: "")
}
print("")
// 68 111 103 8252 55357 56374
同样,前三个 codeUnit 值(68、111、103)代表了字符 D、o 和 g,它们的 UTF-16 代码单元和 UTF-8 完全相同(因为这些 Unicode 标量表示 ASCII 字符)。
第四个 codeUnit 值(8252)是一个等于十六进制 203C 的的十进制值。这个代表了 DOUBLE EXCLAMATION MARK 字符的 Unicode 标量值 U+203C。这个字符在 UTF-16 中可以用一个代码单元表示。
第五和第六个 codeUnit 值(55357 和 56374)是 DOG FACE 字符的 UTF-16 表示。第一个值为 U+D83D(十进制值为 55357),第二个值为 U+DC36(十进制值为 56374)。
Unicode 标量表示
你可以通过遍历 String 值的 unicodeScalars 属性来访问它的 Unicode 标量表示。其为 UnicodeScalarView 类型的属性,UnicodeScalarView 是 UnicodeScalar 类型的值的集合。
每一个 UnicodeScalar 拥有一个 value 属性,可以返回对应的 21 位数值,用 UInt32 来表示:

for scalar in dogString.unicodeScalars {
print("\(scalar.value) ", terminator: "")
}
print("")
// 68 111 103 8252 128054
前三个 UnicodeScalar 值(68、111、103)的 value 属性仍然代表字符 D、o 和 g。
第四个 codeUnit 值(8252)仍然是一个等于十六进制 203C 的十进制值。这个代表了 DOUBLE EXCLAMATION MARK 字符的 Unicode 标量 U+203C。
第五个 UnicodeScalar 值的 value 属性,128054,是一个十六进制 1F436 的十进制表示。其等同于 DOG FACE 的 Unicode 标量 U+1F436。
作为查询它们的 value 属性的一种替代方法,每个 UnicodeScalar 值也可以用来构建一个新的 String 值,比如在字符串插值中使用:
for scalar in dogString.unicodeScalars {
print("\(scalar) ")
}
// D
// o
// g
// ‼
// 🐶
您必须登录才能发表评论。