自 Swift 开源以来,Swift 的开发与演变已经完全由社区和核心团队所共同主导,它们在 Swift 3 当中为我们带来了巨大的变化和改进,而这正是我们所需要详细了解的。这些变化的原因与 Swift 邮件列表当中那些冗长、深入的讨论密切相关,都是经过充分地论证才执行这些变化的。然而,我们中的大部分人其实并没有很多时间去尽可能地跟上这些变化,不过在本次 App Builders CH 的讲演中,Daniel Steinberg 会带您快速了解那些「关于 Swift 3 您不得不知道的那些事情」。所涉及的范围包括了新的关键词、移除 C 风格的 for-loop 循环从而遵循新的 API 设计指南,此外还有其他常见的争议话题,Daniel 将会快速带您了解过去一年来,Swift 都发生了哪些改变。
编者按:要查看 Swift Evolution 远程仓库中官方核心团队是如何对 Swift 提案进行考虑的,您可以点击每个章节开头所对应的提案编号。
Swift 3 有哪些新特性? (0:00)
Swift 现已开源。
很多其他社区的人都会问这样一个问题:『这是否真的意味着 Swift 开源了?』
是的,没错,Swift 现已开源。
我们当中有很多人已经很熟悉开源语言了,比如说开源的 Java、开源的其他语言等等,这些人对贡献开源语言已经无比熟悉。这是非常惊人的一点。Swift 的邮件列表是 非常 开放的。关于这门语言的讨论是公开进行的。核心团队会谈论他们的想法,以及为什么他们要这么做。他们还会从社区中采纳合理的建议,并且允许所有人都参与到这个过程当中来。
这样一来,我们就已经能够知道 Swift 3 将会出现哪些新特性了。
0025 – 限定作用域的访问级别 (1:19)
所以,如果您想问 Swift 的未来会是怎样的话,那么我想说在即将到来的 Swift 3 当中,有一个很重要的东西将会出现,那就是不同的访问级别 (access level)。所以在这一点上,我们有 3 种访问级别:
- 公开(public)
- 内部(internal)
- 私有(private)
默认的访问级别是 internal
,这意味着此成员只能在模组 (module) 内可见。如果要让其能够被模组外的成员访问到的话,那么就要将其设置为 public
。此外就是 private
了,在 Swift 中这和其他语言的「私有」访问级别是有所不同的,这意味着私有成员能够在文件内可见。
在 Swift 3 当中,我们将迎来另一种访问级别,private
将会被重命名为 fileprivate
,这就让这个访问级别变得十分清晰:文件私有成员只能够在文件内可见;我们将得到的第四种访问级别就是 private
,这也就是说即使是在同一个文件当中,私有成员也只能够在对应的作用域当中访问。
举个例子,在这个文件当中我创建了一个类。在这个类当中我添加了一个 private
的属性和一个 private
的方法。这两个成员只能够在这个类当中可见,就算位于相同文件内,其他类也是无法访问这两个成员的。因此,也就是说,在过去,这两个成员在整个文件当中都可以访问得到;而到了现在,private
方法只能够在相同类当中可见了。也就是说,昔日的 private
变成了今日的 fileprivate
,而新的访问级别让我们有了更私有的访问权限。
有很多人很希望能够看到诸如 protected
之类的东西,这样子类就可以访问到父类的属性。我们并不打算引入这个东西,至少对于 Swift 3 来说。因此,Swift 3 中最终的访问级别就是:
- 公开(public)
- 内部(internal)
- 文件外私有(fileprivate)
- 私有(private)
0004 – 移除 ++ 和 -- (2:49)
另一个变化就是 ++
和 —
运算符被移除了,这可能会让很多人非常伤心。当然,您也能够例举出很多您为什么需要它们的理由。
不过在绝大多数情况下您是不会使用这两个运算符的,不过一旦您需要让某个量自增的时候您往往就会想使用它们了,不过我们不再能够使用 count++
了。
while count < upperLimit {
print(myArray[count++])
}
现在,我们如果要让某个量自增的话,我们就必须要使用明确的 count += 1
了。
while count < upperLimit {
print(myArray[count])
count += 1
}
通常情况下,如果您需要让某个量自增,并以此来获取返回值的话,那么接下来您还必须将增加的量减回去,因此有些时候代码写起来就不如从前一样好看了。
0007 – 移除带有条件和自增的 for-loops C 风格循环 (3:25)
for (int i = 0; i < array.count; i++)
由于 ++
被移除了,那么 C 风格的 for-loops 循环被移除只是时间问题了。因此,C 风格的 for-loops 循环将不能在 Swift 3 当中可用;事实上,如果您正在使用 Xcode 7.3 ,那么您会注意到很多警告,提示一旦 Swift 3 发布这些特性都将失效。
0053 – 从函数参数中移除 let
的显式使用 (3:47)
func double(~~let~~ input: Int) -> Int {
// ...
}
这是一个很有意思的特性。我不知道各位是否有深入思考过这种特性的意义,不过当您在 Swift 当中调用一个方法的时候,方法将会将您所传递的参数拷贝出一个不可修改的副本。这里真正所暗示的是 let
这个单词,虽然没有人会主动写下这个这个词,因为 let
是默认的行为。这个 let
已经不复存在了。
这是 Swift 当中最近的一个变化。之所以会推出这个变化是因为下一个提案的出现:
0003 – 从函数参数中移除 var
(4:14)
这个提案将 var
从函数参数当中移除掉了,他们说:一旦我们移除了 var
,那么我们是不是也要把 let
移除掉?我们中的很多人都一脸懵逼,什么?之前还可以在这里写 let
?
func double(~~var~~ input: Int) -> Int {
input = input * 2
return input
}
举个例子,在这个方法中我获取了 input 这个参数,然后我想要让其翻倍然后作为返回值返回,要记住我是对一个不可修改的副本进行操作的,因此我是没办法修改 input 的值的。因此,如果我们不想再声明一个新的变量的话,我们之前会选择再声明中使用 var
,以让其变成一个可修改的副本。
但是这仍然是一个副本,不过它现在可以修改了,因此我就可以修改它的值。这段代码目前是没法用了。var
已经被移除掉了,我们必须要在这里显式声明一个新的变量。
func double(input: Int) -> Int {
var localInput = input
localInput = localInput * 2
return localInput
}
在这里,我创建了一个名为 localInput
的可修改副本。我使用 input
对其进行赋值,这样我就可以对可修改副本进行操作了。不过绝大多数人可能会选择这样子做:
func double(input: Int) -> Int {
var input = input
input = input * 2
return input
}
他们使用相同的名称来创建变量,这就是一种被称为__命名遮罩 (name shadowing)__ 的特性,而不是使用其他的名称来为局部变量命名。这会让一些人感到困惑;当然也有许多人喜欢这样做。之所以会迷惑人的原因在于:两个输入当中的值截然不同。右边的那个 input 值是参数值,而左边的 input 则是能够被改变的值。
在这个例子当中,可能并不太让人困惑。我觉得为了让语法更为清晰,我们应该使用 var input
的方式,这样就不必将参数值赋值回 input 当中了,这样我们就可以以之前使用可变参数的方式来使用这个变量了。
0031 – 将 inout
声明调整为类型修饰 (5:39)
虽然我说过参数是一种不可修改的拷贝,因此如果您想要获取一份可修改的拷贝的话,您需要在下面的代码中使用 var
来单独创建一个局部变量。不过如果您切实想要修改您传入的参数值,并禁止拷贝的发生,那么至今为止只能够使用 inout
的方式。
func double(input: inout Int) {
input = input * 2
}
inout
参数现在不出现在参数名的前面了,不过它也没有跑得太远。现在它成为了类型的一个修饰符,而不是变量名称的一个部分。因此,只需要将其向右边调整一点点就可以了。
之所以做这样的决定,是因为实际上 inout
确实只是一个额外的描述符而已,它并不是参数名的一部分,因此需要把它移到正确的地方。
0035 – inout
限制为只能捕获 @noescape
上下文 (6:14)
关于 inout
发生的另一个改变,在于 inout
的捕获机制目前受到了限制。
func escape(f: () -> ()) {}
func example(x: inout Int) {
escape { _ = x }
}
假设我有一个名为 escape()
的函数,它接受一个简单的方法作为其参数。在 example()
方法当中,我引入了 inout
类型的 x
参数,我会在函数当中使用这个参数,也就是将其传递到 escape()
当中。
因此,escape()
现在就会对 inout x
开始进行操作,而这个时候会造成一个问题,由于 inout
的开销非常大。inout
换句话说会对我传入的变量进行修改,二这个时候我并不知道 example()
是否能够继续执行,调用我在 example()
本身作用域范围之外的函数。
为了解决这个问题,我们必须要说明『嗯,我只在这里面使用它;不要担心编译器会出错!』,因此我们可以在这里使用 @noescape
进行标记:
// safe because @noescape => closure
// can't be called after function returns
func noEscape(@noescape f: () -> ()) {}
func example(x: inout Int) {
noEscape { _ = x }
}
如果这个方法参数被标记为 @noescape
,那么它就可以正常使用了。编译器知道我传递的这个函数不会使用任何作用域范围之外的东西,因此程序就能够正常执行。
我们还有第二种方法来处理。
// [x] is a capture list
// a constant is initialized to have the value of x
func escape(f: () -> ()) {}
func example(x: inout Int) {
escape { [x] in _ = x }
}
如果 example()
在 escape()
当中使用了这个 inout x
,这个时候它不是一个数组,虽然它看起来像。现在这玩意儿叫__捕获列表 (capture list)__。当您需要在捕获列表当中将某个量标记为 weak
或者 unowned
的时候,就需要使用这种形式。这里我们只是明确的说:我想要对 x
进行捕获,默认情况下的捕获级别是 strong
类型的。这同样也表达了『没错,我是在使用这个 inout x
,但是我准备在这里捕获它的现有值。』这样做就会对传递给 inout
的变量创建一份拷贝,这样就不用担心会发生问题了。
因此,如果要处理闭包当中的 inout
值的话,我们可以采用这两种方法进行。
0049 – 将 @noescape 和 @autoclosure 转变为类型特性 (8:11)
好的,之前关于 @noescape
的内容我发现我写错了,@noescape
的位置发生了改变,它现在不能放在那里了。
func noEscape(f: @noescape () -> ()) {}
func noEscape(f: @autoclosure () -> ()) {}
Swift 3 即将发生的变化之一,就是将这些参数用以描述被传递的实际函数,而不是放在外面,这对 @autoclosure
也是同样的道理。如果您发现您的代码在迁移之前完全不能运行了,这是怎样的一种体验?
0002 – 移除柯里化函数声明语法 (8:35)
「移除柯里化 (curried) 函数声明语法」可能会让很多人感到焦虑,其实完全不必,因为他们误解了这句话的意思,并不是说 Swift 移除了柯里化特性。他们并没有移除柯里化。他们只是将柯里化函数的一种写法移除掉了。
func curried(x: Int)(y: Int) -> Int {
return {(y: Int) -> Int in
return x * y
}
}
举个例子,在这个柯里化函数当中,注意到它接受 X 和 Y,然后返回 Int
。如果看得仔细一点,您会发现它其实是先接受一个参数,然后再接受另一个参数,因此需要这样子调用:curried(7)(8)
。这很容易让人误解。不是说调用的部分,而是说定义的部分很容易让人误解。这样定义的方式将被移除,因为这里实际发生的只是对 7 进行柯里化而已。
func curried(x: Int) -> (y: Int) -> Int {
return {(y: Int) -> Int in
return x * y
}
}
我向 X 传递 7 这个值,然后我得到一个返回的函数值,这个函数就是将您传递进来的值乘以 y。随后当我传递 8,得到的结果就是 7 x 8.
因此我实际上是将它分成了两部分。我使用了一个柯里化函数来捕获 X。这实际上就是闭包;一旦捕获成功,我再将 Y 传递进去,然后就执行后续的操作。
Swift 核心团队是这么说的:「看吧,这种做法很容易让人迷惑,因为这里出现了一堆堆的括号,让我们更明确一点,要说明您正在做的是什么。比如说您传递了 X 之后,它会返回一个函数,然后再讲这个函数应用到下一个元素当中」。因此,柯里化仍然存在,这是语法发生了变化。
0022 – 将 Objective-C 的 selector 变为方法的引用 (10:43)
我想要谈及一些发生在 Objective-C 上的变化。其中之一就是我们需要改变使用 Objective-C selector 的方式,它现在变成了方法的引用。随着 Swift 3 的退出,我们需要使用 #selector
来实现这个功能。对于那些从 Objective-C 转过来的开发者来说,这一点看起来非常熟悉。这个用法就是和 @selector
类似,和诸位此前所做的是很类似的。
#selector(callbackMethod)
#selector(MyClass.callbackMethod)
#selector(MyClass.callbackMethod(with:))
您会注意到在 @selector
当中,您需要在调用这个 callback 方法的时候加上冒号,因为这个方法需要接受参数。在这个例子当中,由于这里只有一个 callback 方法,因此我们就可以在这里使用这样的形式。如果我们需要调用其他类的方法,我们可以在 selector 当中使用点语法调用该类的方法。不过如果我们又添加了一个同名但是参数不同的方法的时候,就会发生错误。
我们必须要指明我们需要调用的是哪个方法,要做的就是要明确指明需要调用的是哪个类,并且指明要接受的参数标签是什么。不过如果您不需要指定类或者参数的时候,您可以将它们忽略掉。
0033 – 将 Objective-C 常量变为 Swift 类型 (12:21)
我很喜欢另一个与 Objective-C 相关的 Swift 的特性。开发团队这段时间以来都在尽量让大家习惯这样的用法,大家可以好好回想一下。比如说那些 TableCellRowAnimations、所有的 UIButton 类型、所有的 UIButton
状态等等。如果您之前使用了这种类型的常量的话,那么开发团队会尽量让您切换为使用相同前缀的常量用法:
HK_EXTERN NSString * const HKQuantityTypeIdentifierBodyMassIndex;
HK_EXTERN NSString * const HKQuantityTypeIdentifierBodyFatPercentage;
HK_EXTERN NSString * const HKQuantityTypeIdentifierHeight;
HK_EXTERN NSString * const HKQuantityTypeIdentifierBodyMass;
HK_EXTERN NSString * const HKQuantityTypeIdentifierLeanBodyMass;
现在他们会说「如果您听从了我们的建议,并且按照我们所说的做了,那么我们就可以将其翻译到 Swift 当中来,这些东西会变成枚举」:
enum HKQuantityTypeIdentifier : String {
case BodyMassIndex
case BodyFatPercentage
case Height
case BodyMass
case LeanBodyMass
}
这样您的 API 就能很好地融入到 Swift 当中,并且这种用法也和官方 API 类似,而这正是未来您所能看到的变化之一。
那么我们是怎么知道它是字符串的呢?您只需要看一下您引入的这些常量就会发现,它们全都是字符串。因此我们就知道这些枚举的元数据必须是字符串。因此这就是我们最终所能得到的枚举。真的非常好用。
0005 – 更好地将 Objective-C API 迁移到 Swift (13:23)
通常情况下,我们在使用 Objective-C API 的时候,它们已经能很好地翻译为 Swift 了。有一个例子是将变量方法明精简,并去除掉某些重复的内容。
// Swift 2.1
let color = NSColor.blueColor()
// Swift 3.0 - prune redundant type names
let color = NSColor.blue()
如果有这样一个蓝色的 NSColor,由于 NSColor
和 blueShadow
有重读的地方,因此这里就需要被隐藏掉,这样它就会简单地蒙蔽住了。
下面的例子展示了一些更有用、更自然的例子:
// Swift 2.1
rootViewController.presentViewController(alert,
animated: true,
completion: nil)
// Swift 3.0
// Nullable trailing closures default = nil
// Animated default = true
// Prune redundant type names
rootViewController.present(alert)
您会发现 Swift 的词语更加精简了,并且我们还并没有遗漏任何信息。它不再啰嗦,这样您就不必一遍又一遍地重复相同的词语了。
接下来,如果我有一个布尔属性的话,接下来在 Swift 3 当中发生的改变就是会在这些属性前面预加上 “is”,因此所有我们从 Objective-C 获取的类似 getter=isSomething
之类的属性的时候,转换成 Swift 就会变成 isEmpty
之类的样式。
// Swift 2.1
addLineToPoint(myPoint) // Calling
func addLineToPoint(_: CGPoint) // Definition
// Swift 3.0 - prune redundant type names
addLine(to: myPoint) // Calling
func addLine(to point: CGPoint) // Definition
再次强调一遍,减少冗余;这里我调用了一个将 line 添加到 point 上的方法,然后我将 point 传递进去,所以这个方法写出来是这个样子的。那么如果我们必须要传递 point 的时候就必须要声明 “我要将 point 传递到 point “当中呢?因此,在 Swift 3 中您将会看到大量的类似例子。这一类的示例将到处都是。在 Swift 3 中, 外部标签 变成了 to, 内部标签 变成了 point,这样当我们在调用的时候,就可以很简单的说:将线添加「到」「某个点」上了。
我没有必要告诉其他人这是一个 point。只要你知编译器知,那么久万事大吉。我发现现在的这种使用方式越来越像是在和编译器对话了。
func addLine(to point: CGPoint) // Definition
注意到在最后面这个例子当中,我们添加了首参数标签。我们在 Swift 当中将会大量使用它,因此我们需要讨论得更深入一点。通常情况下,外部标签是显示给调用者看的,我们并不会使用它,而内部标签则是给我们自己看的,因此需要使用。
另一个让有些人很沮丧的是:如果 URLHandler 是作为属性进行声明的,那么它的前面将会被改写成小写样式。
var URLHandler
// turns into
var urlHandler
另一个让我很不开心的就是,这种规则同样也出现在枚举当中了。
enum Currency {
case Dollars
case Euros
case Pounds
case Yen
}
我们不能够再写出这样的枚举了,我们必须要向下面这样写枚举,这看起来怪怪的。
enum Currency {
case dollars
case euros
case pounds
case yen
}
但是习惯就好,四个月后我们就会觉得这才是枚举应有的样子。
0036 – 在实现枚举实例成员的时候,需要加上左前缀点 (17:12)
enum Currency {
case dollars
case euros
case pounds
case yen
var symbol: String {
switch self {
case .dollars:
return "$"
default:
return "I don't know"
}
}
}
在枚举当中,我们需要使用左前缀点 (Leading Dot prefixes),这意味着:现在这是位于枚举内部,对 self 进行 switch,以便找到 dollars 这个 case。目前这里有两种写法,自 Swift 3 开始,左前缀点就必需加上,即使是在枚举内部进行使用。也就是说,左前缀点在枚举外部是始终都需要的,而自 Swift 3 开始,每个 case 前面都要加上左前缀点。我很喜欢这一类的变���,因为它能确保代码的一致性。这意味着我们能够明确知道每个词的用途,也能够知道将会发生是呢嘛操作。这意味着不管什么样的代码出现,我们都能意识到这是一个枚举。这难道不是一件很赞的事情么?
0043 – 可以在 case
标签后声明多个关联值变量 (17:53)
enum MyEnum {
case case1(Int, Float)
case case2(Float, Int)
}
switch value {
case let .case(x, 2), let .case2(2, x):
print(x)
case .case1, .case2:
break
}
在我的这个枚举当中,我写了两个 case,注意到 case1 中 Int 是放在前面的,而 case2 中 Int 是放在后面的。因此,当我需要对这个值进行 switch 的时候,我需要分别指明 case1 的第二个关联值是 Float 类型,case2 的第一个关联值是 Float 类型,并且剩余的部分我将值赋为 2.
自 Swift 3 开始,我可以将这些操作放到一步当中来。我们不再使用分离的 case 语句来编写,也就是说我们不必多次撰写相同的 case 声明语句,我可以在一行当中将这两个 Float 值直接关联到,我们使用 x 来表示这个 Float 值。这样我们就可以直接使用 x 来进行相关的操作了。这样做的好处是,如果枚举 case 不满足其中的某一个,那么它就会选用另一个进行匹配,直到两者都不满足匹配为止。
最后面那个写法是有点愚蠢,不过我还是写出来,这样可以让大家更明显地看到它们的区别。
0001 – 允许(绝大多数)关键词作为参数标签 (18:53)
// Swift 3 calling with argument label:
calculateRevenue(for sales: numberOfCopies,
in .dollars)
// Swift 3 declaring with argument label:
calculateRevenue(for sales: Int,
in currency: Currency)
允许绝大多数作为参数标签存在了。比如说 var
即将成为参数标签。但是有一点很奇怪:当我们迁移到 Swift 3 的时候,您会发现介词将会被大量使用,比如说 calculateRevenue 这个方法当中的 for
和 in
。现在这些关键词可以用在参数标签上面了。
它们实际上可以让您的代码更具备可读性,从上面的代码中就可以看出来了。我们现在可以在语言级别使用绝大多数关键词作为参数标签了。
0009 – 访问实例成员需要 self (19:32)
在 Objective-C 当中,我们到处都在使用 self。如果您之前是写 Java、C# 的话,那么您很可能只会在必需的时候才使用 self。在 Swift 之后的版本中,我们只需要在必要的时候才需使用 self。
struct Friend {
let name: String
let location: String
func nameBadge() {
print("I'm", self.name, "from", self.location)
// REJECTED: self. not required
}
}
在这个例子当中,我们有两个属性:name 和 location,此外还添加了一个 nameBadge 方法,这个方法中使用了这两个属性。对大家来说,这里的示例是非常清楚的,我们很清楚它们引用的是属性而不是其他东西。这可能会让有些人感到失望。他们总喜欢到处使用 self,就像我们此前在 Objective-C 当中所做的那样,现在我很高兴地告诉大家这种做法将会被拒绝。
0011 – 将用于关联类型声明的 typealias
关键字替换为 associatedtype
(20:09)
类型别名 (Type alias) 是一个挺有意思的玩意儿。在 Swift 中类型别名有两种不同的用途:
protocol Prot {
associatedtype Container : SequenceType
}
extension Prot {
typealias Element = Container.Generator.Element
}
它的作用是:「这是一个占位符。您需要明确告知 Container 随后会关联哪种类型,另一种的用法和 #define
很类似,将 Element 作为 Container.Generator.Element 的替代。」Swift 目前这样设计的目的在于要将这两种用途给分离开来,在第一种情况下,它只是一个占位符。随后您需要明确告知它所代表的类型。我们将它从 typealias
更改为 associatedtype
。
再次声明一点,这个设计是非常好的,因为之前我们必须要使用相同的关键词来完成两种极为不同的用法,而现在两种用法都有各自的关键词归属了。
0046 – 将所有参数标签进行一致化,包括首标签 (20:50)
我们将为所有参数标签进行一致化操作。不知道诸位还记得在 Swift 1 当中,您需要写出函数中的所有参数标签,其中也包括首标签。这个时候您可能会选择将函数放到某个类或者某个方法当中,以确保不写那个讨厌的首标签。在 Swift 2 当中,开发团队将首标签给抛弃掉了,以便统一两者的行为,但是在构造器中仍是不一样的。自 Swift 3 开始,我们将必须写出所有的参数标签。
// Swift 3.0
func increase(ourNumber: Int, delta: Int) -> Int {
}
increase(ourNumber: 6, delta: 3)
比如说这个例子当中,当我们调用这个方法的时候,delta
在 Swift 2 当中是必须要写出来的,但是 ourNumber
是不会显现的。自 Swift 3 开始,outNumber
作为首标签就必须要写出来了。所有的参数标签都必须这么做。
「但是 Daniel」,您可能会问了,「有些时候我不想让这个首标签显示出来。」好吧,您仍然可以使用下划线来将其隐藏掉。
// Swift 3.0
func increase(_ ourNumber: Int, delta: Int) -> Int {
}
increase(6, delta: 3)
0023 – Swift API 设计指南 (21:41)
或许,Swift 演变清单中最热门的讨论就是关于 Swift 未来的 API 设计指南了。如果您还没有看过这个清单的话,那么我建议您之后好好地阅读一下上面的这些提案,这样您就能够对未来有所预见。这上面的内容非常繁杂;您只需要挑选感兴趣的那部分查看就行了。
我们会在这里讨论关于 Swift API 指南的相关内容,以及我们该如何命名相关的元素。您会发现这个话题非常的火爆,如果您不想查看这些讨论的话,您还可以去 Swift.org 上去观看正式的 Swift API 指南。
// Name functions and methods according to their side-effects
// Array:
sort()
sorted()
在 Swift 3 中,我们将转回到 Swift 1 的命名约定来,因为它已经足够优秀了。我们要根据函数和方法的侵染效应 (side effect) 来决定它们的命名方式,关于这方面有许多的例子。
第一个例子是:如果方法没有任何的侵染效应(也就是不会对变量本身做出修改),那么它应该以名词的方式进行命名。例如:distance()
或者 successor()
是没有任何侵染效应的。
x.distance(to: y)
i.successor()
如果存在侵染效应的话,那么就应该以祈使短语的方式进行命名。如果我需要排序的话,我们应该要说:某某,对自己排序;或者 X 添加 Y 元素。
x.sort()
x.append(y)
现在,将这两个规则结合起来,我们有:如果我有一个可修改版本,它对应了一个不可修改的版本,那么它应该使用 “-ed” 或者 “-ing” 后缀,因为要 完全明确其涵义 。不加后缀的版本我们第一反映会认为其是『原生的』方法。每次看到这种类型,我都必须要看一下哪个方法满足我的需求。
sort()
// vs.
sorted()/sorting()
当然,您也可以总是按住 Option (⌥) 键然后点击这个方法,然后查看它有没有返回值,以便确定它是否是可修改的类型。当然,关于命名这一点还有很多建议。这些建议可能会发生变化,不过这是目前 Swift 3 所采取的方式。
在 Swift 2 当中,官方鼓励我们尽可能使用方法而不是使用全局函数。在 Swift 1 当中,您或许还记得那些诸如 map
、filter
、reduce
之类的全局函数。这些都属于全局函数。您必须要将数组传递进去,然后标明您如何对其进行变化。之后我们有了协议扩展,这些函数就变成了 SequenceType
中的方法,这样 map
、filter
、reduce
就定义在了 SequenceType
协议当中了。
通常而言,您都应当尽量使用方法,而不是使用全局函数,这正是我们所推荐的。不过仍然有一些例外,例子如下:
min(x, y, z)
如果没有明显的对象能够单独调用这个 min
方法的话,那么使用全局函数仍然是可以的。和 array.filter
有所不同,您所调用的数组是 filter
的主体对象,而如果要取三个数字的最小值的话,就没有任何主体了。
print(x)
sin(x)
如果函数是不受约束的泛型函数的话,那么将其作为全局函数也是可行的,比如 print
之类。我们可以使用 Math.sin()
,但是我们通常情况下都会使用 sin()
。我们为什么要改变它呢?因此,除非它本身就推荐使用域符号,否则的话我们就按照习惯来就可以了。
方法可以共享一个基础名称。要注意到我们在 Swift 中可以使用重载 (overload) 机制,而这正是 Objective-C 当中所没有的。因此,当某个方法有三种不同版本的时候,使用这个机制可以完成一些类似的任务。共享基础名称是一个非常好用的功能。
然而,如果您看一下 UITableView
的 API,您会发现几乎每个方法都被命名为 tableView!
,然后在内部再执行不同的操作。我希望您在执行相似任务的时候,将这类方法都以一个基础名称进行命名。
// Choose good parameter names
func move(from startingIndex: Int, to endingIndex: Int)
这一点似乎我们没必要和大家说明,但是我们还是想提及这一点,因此如果您有一个 move()
函数的话,那么添加起始索引以及结束索引是一个非常好的选择,因为这样就能很清晰地看到所引用的类型。我觉得这个特性是专门为那些从 Java 迁移过来的人所准备的……
// Take advantage of default values
func hello(name: String = "World")
// Prefer to locate parameters with defaults at the end
init(name: String, hometown: String? = nil)
另一个很方便的东西就是要利用好默认值,这样就不必写一堆 hello()
的不同版本。
默认值所带来的好处是您可以在构造器当中摆脱大量无用值的编写。如果您有一大堆东西需要构造,如果其中包含有可空值,并且可以被设置为空的话,那么它们的默认值就应当被设置为空。这样一来,您不仅可以设置有 hometown 数值的变量,还可以通过少写一个参数来快速设置一个 hometown 默认为空的变量。
我发现这极大地精简了代码。这样就可以避免了大量重复的构造器参数声明。
通常情况下,虽然这不是一个必须的要求,不过如果您使用了默认参数,那么最好将它们置于函数的末尾。因为它们不必成为命名参数 (named parameters)。因此如果您在其他语言当中拥有多个默认值,一旦您指明了某个默认值参数,那么您就必须要指明其之后的所有默认值参数。我们没必要在 Swift 当中这么做。这并不是一个必须的要求,但是这会让代码变得更为干净整洁。
外部参数标签 (27:10)
// Definition
func move(from startingIndex: Int, to endingIndex: Int)
// Calling
move(from: here to: there)
我们已经讨论了内部标签,但是您也应当让外部参数标签干净整洁,从而让使用该方法的人们能够在调用的时候就能够轻易理解这个方法,而不是让它要到函数定义当中去研究如何使用。
什么是一个好的 API 呢?对我来说就是「从」这里「移动」「到」那里。即使第一个内部名称是「起始索引」,第二个内部名称是「结尾索引」。在方法的内部我会使用这两个名称。
考虑到您的 API 总会发布给别人去使用,我发现在 Swift 3 中似乎介词更受欢迎一些。这是所有人都热切期盼的。
对于参数标签来说,还是存有一些例外的。例如,让我们回到 min
那里去:
// Omit labels when arguments can't be distinguished
min(number1, number2)
min 当中的变量位置对结果没有任何影响。我并不关心第一个数字是哪个,第二个数字是哪个。因此,我不必添加任何的参数标签来区分它们。如果参数之间并没有任何的区别的话,为什么要给它们添加标签呢?这完全没问题。
// Omit labels in full width inits
Double(someInt)
Double(_ anInt: someInt)
如果您想要创建一个从 Int 转换到 Double 的方法,我有一个建议,那就是没必要声明参数标签。事实上,如果您看一下标准库当中是如何定义的,您会发现他们将第一个参数标为了下划线。如果您执行这些构造器的话,您完全没必要声明外部参数。因此您可以看到,我们可以直接简单的调用 init,而不是使用 Objective-C 风格的 initWithSomethings
。
有些时候您会看到调用的 init 构造器的第一个标签是 frame,但是如果参数是数值类型,并且作用是将其转换为另一个数值类型,那么我们就不必指明外部参数。
// When the preposition applies to the each parameter
func move(from startingIndex: Int, to endingIndex: Int)
// When the preposition applies to the whole
func moveTo(x: Int, y: Int)
x.removeBoxes(having Length: 12)
介词:在这种情况下,我们往往使用 from…to 介词来表明动作从某个地方执行到另一个地方,但是有些时候动作的范围可能同时适用于这两个地方,这个时候我们就倾向于将介词移动到外面来。通过将介词移到外面来,然后将那两个标签保持在内部,在这个例子当中,由于里面的参数只有一个,因此 boxes 在外面还是在里面都是没有任何影响的。
好的,我的演讲到此结束!这个讲演只是一个关于 Swift 3 演变和改进的快速概览。谢谢大家!
问答时间到! (29:33)
问:有时候我觉得省略 self
会让其他开发者阅读我代码的时候觉得难以理解。您对明确声明 self.property
以及直接声明 property
怎么看呢?
Daniel:我的答案和 Apple 的稍有不同。Apple 的答案是,你想使用 self 就用,想不用就不用,选择权在你手上。因此,您的团队可以为了阅读性显式声明 self
,而其他团队则选择省略。
就我个人而言,我认为当方法中有一个局部变量的名称和属性的名称相同的时候,这种省略的做法就会变得非常混乱。在这种情况下,我会使用 self.property
来明确两者之间的区别。不然的话我就会简单地使用 property
,因为它是相对明确的,就是直接来自实例变量的,对不对?一部分是处于简洁起见,另一部分则是个人偏好了。
问:您对隐式解包可空值有什么看法呢?何时该使用,而何时不该使用?
Daniel:UIKit 在某些时候需要使用隐式解包,例如 IBOutlet 之类的,因为实例化这些输出口是没有价值的;它们会在随后故事板加载的时候进行实例化,因此它们在您代码当中基本等同于永恒存在。这个时候就是使用隐式解包的绝佳良机,因为这就非常必要。我很喜欢看到这种使用输出口的延迟加载方式,因为它们会在视图加载的过程中才开始加载,并且如果在开发的时候没有做好正确的连接的时候它也会发生崩溃。我很希望能够在 Swift 演变列表中看到您提出这个问题。
问:我很喜欢 Objective-C 运行时以及其所提供的反射功能。这些功能在 Swift 当中变得非常局限;您对此有没有什么看法?
Daniel:这是一个很庞大的话题,因为大家都希望拥有可选的方法和可选的协议,就像我们在 Objective-C 当中拥有的那样。这些功能目前在 Swift 当中仍为实现。Objective-C 的一个很赞的特性就是动态性了,它允许我们检查某个对象是否能够对某个选择器 (selector) 作出回应,并且根本不必知晓它的相关信息。然而,Swift 的类型安全从一开始就减轻了这个需要,它希望您在编译的时候就确定哪些可用而哪些不可用,而不是在运行时进行检查。
因此,我并不希望在 Swift 通过检查一致性来使用可选的方法和协议,因为这个想法和类型安全格格不入。Apple 的反应很强硬:您应当知道某个对象应该能够做什么事——这是您应当为此负责的事情。相反在 Objective-C 中,我可以在运行时对任何我想要处理的对象进行交换操作。因此,我认为直接将 Objective-C 翻译为 Objective-C 会导致我们遇上许多问题。因此我们不能直接对 Objective-C 进行翻译,我们应当重新思考一下我们的架构,然后适应 Swift 类型安全的架构。
我知道 Cocoa Touch API 通常会依赖于这个动态特性,因为它是使用 Objective-C 编写的,因此我希望以后我们能够拥有更 Swift 友好的 API 版本,我们就不必将 Objective-C 完全地翻译为 Swift 了。
不过我们现在可以想想,Objective-C 比以前要少了很多动态性,这几年来,Apple 一直在把我们往 Swift 的路上逼,比如说 id
指针现在变成了 UIButton
之类的事务,泛型的出现允许我们能够更加精确地指明类型。
问:在 Cocoa API 当中,有没有一种说法:要自动将古老的委托模式转变为闭包?
Daniel:Apple 这四年来一直在推动这个方向的进展。三年前的 WWDC 上,Apple 的 Modern Objective-C 的讲演鼓励开发者传递闭包(Objective-C 当中的代码块),而不是使用委托。他们希望我们自行提供对象的行为,反对使用依赖状态回调。
问:Swift 3 上的 KVO 目前现状如何?
Daniel:和 Swift 2 基本上是一样的,没有任何变化。如果您想使用键值观察功能的话,您需要继承 NSObject
。这并不是一种语言特性,而是一个由 NSObject
提供功能的库,因此这使得 KVO只能对一个类使用,而不是使用一个结构体或者枚举。也就是说,您可以写一大堆恶心的 didSet
和 willSet
,这样也能够得到和 KVO 相同的体验。
问:有没有一种好的方法来跟上 Swift 演变的邮件列表,只要不是一行一行地读就行?
Daniel:Erica Sadun 对您有很大帮助。她经常对这个列表进行总结:『这里有新的东西了。这里有一个正在活跃讨论的话题』。我想说的是她现在正在为社区提供一个极为重要的服务。她的博客在这里,如果您在她的网站订阅了她的周刊的话,那么她的博客将是一个极好的消息来源。
编者按:另一个很好的消息来源是 Jesse Squires 的 Swift Weekly Brief,他总结了 Swift repo 的最近进展、能够进行贡献的初创项目、提案进程的最新进展、以及邮件列表的热点内容。