2014年的苹果全球开发者大会(WWDC),当Craig Federighi向全世界宣布“We have new programming language”(我们有了新的编程语言)的时候,全场响起了最热烈和持久的掌声,伴随着掌声到来的语言叫Swift。接下来Craig Federighi更是毫不掩饰的告诉大家,Swift将成为主宰iOS和Mac开发的新语言,甚至是整个软件行业中最举足轻重的语言。
Swift正如它的名字那样迅速、敏捷,但这并不是它的全部。Swift是一个博采众长的语言,Chris Lattner博士在设计Swift的时候汲取了Objective-C、Rust、Haskell、Ruby、C#和JavaScript等众多优秀编程语言的优点,并且将面向对象编程和函数式编程的优势完美的融合在一起,最终形成了Swift的核心语法。正因如此,每个程序员都能从Swift中找到自己熟悉的编程语言的影子。在WWDC大会后不到一个月的时间里,Swift已经在TIOBE的编程语言排行榜上进入前20名,虽然不知道是不是后无来者,但这已经是前无古人之举了(当年的Java也没有火爆到这个程度)。
语言基础
我们先花一点时间来看看Swift的基本语法和核心语言元素,这些是探索这门新语言所必须的。
Hello, Swift
我们还是从最入门级的程序输出“Hello, Swift”开始来认识这门语言。
print("Hello, Swift!")
我们可以使用Xcode创建一个playground来执行上面的代码。
效果如下图所示:
说明:Swift 1.x中的println函数在Swift 2.x中更名为print。
变量、常量和字符串
对程序设计略有了解的人都知道,程序中需要存储数据并对数据进行各种运算(操作),因此我们的程序中会有许多的变量和常量,变量相当于是内存中的一块存储空间,可以存储一个值而且这个值可以被改变;常量也是内存中的一块存储空间,但是其中的值在第一次赋值以后不能够被改变。
var greeting:String = "Hello, world!"
print(greeting)
在上面的代码中greeting是一个变量,它的类型是String(字符串),既然是变量,也就意味着它的值是可以修改的,如下所示:
var greeting:String = "Hello, world!"
print(greeting)
greeting = "Goodbye world!"
print(greeting)
当然,在声明变量时也可以像下面这样写:
var greeting = "Hello, world!"
如果你这样做了,编译器会根据你赋给变量的值进行类型推断(type inference)并确定变量的类型,上面的代码中变量greeting的类型被推断为字符串,相信这个不难理解。当然,由于类型推断的存在,Swift是静态类型的语言,也就是说编译器在编译过程中会检查出类型错误,并提示开发者尽早修正程序中存在的问题。
var greeting = "Hello, world!"
greeting = 123
print(greeting)
对于上面的代码,编译器会在第2行报出“Cannot assign value of type ‘Int’ to type ‘String””(不能将Int类型的值赋给String类型)的错误。
建议:可以省略类型标记尽可能使用类型推断来让你的代码更简短并获得更好的可读性。
let num = 123
num = 321
&esmp; 在上面的代码中,我们将声明变量的关键字var换成了let,这样num不再是变量而是常量,也就是说它的值不能在首次赋值后被再次修改,因此上面的代码会在第2行报出“Cannot assign to value: ‘num’ is a let constant”(不能给用let声明的常量num赋值)的错误。
建议:只要可以应当声明常量而不是变量,这样做不仅会让你的代码更健壮,而且编译器也可以对代码进行进一步的优化获得更好的性能。
Swift允许在一行代码中声明多个变量时将类型标记放在最后面,代码如下所示:
var x, y, z: Double;
还有一点需要说明,对于常量和变量的命名,Swift几乎可以使用任意的Unicode字符,除了空格、数学符号、箭头、特殊用途或无效的Unicode字符,如果命名中有数字,数字可以放在首字母之外的任何地方。请看下面的例子:
let π = 3.1416
let 你好 = "你好, 世界"
let �� = "pig"
说明:通过上面的例子不难发现,中文、希腊字母甚至Emoji字符都可以做变量名,在Xcode中可以通过Edit菜单中的“Emoji & Symbols”菜单项来选择输入Emoji字符,也可以使用快捷键Control+⌘+Space来开启Emoji字符输入。
分号
很多语言都是以分号作为语句的分隔符,还有一部分语言要求一行只能书写一条语句(相当于用换行符作为语句的分隔符)。在这一点上,Swift做了一个折衷,如果一行只书写一条语句则可以不使用分号来表示语句的结束;如果一行有多于一条语句,则语句间需要用分号隔开,代码如下所示:
var x = 10; print(x)
建议:在Swfit编程中尽量做到一行一条语句而不去使用分号,分号会占用一个字符的空间但对你的程序没有任何的帮助。
数值类型和类型转换
var radius = 4
let PI = 3.1416
上面的代码创建了一个数值型的变量和一个数值型的常量,它们的类型分别是Int和Double。Swift中表示整数的类型很多,例如:Int8(8位有符号整数)、UInt16(16位无符号整数),此外表示小数类型的还有Float。需要指出的是,除非你的程序有特殊的需求,Int和Double应当是整数类型和小数类型的首选,绝大多数应用程序都可以使用Int和Double来搞定所有的数值,而且编译会根据你的系统是32位还是64位的来决定Int类型的大小(占用多少字节的内存)。
在Swift中,可以在书写数值型的字面量时使用下划线来增强数值的可读性,如下所示:
var million = 1_000_000
当然,在给变量赋值时允许使用运算符计算出数值,但是需要参与运算的各个部分类型保持一致,如下所示:
var radius = 4
let PI = 3.1416
var area = radius * radius * PI
上面的代码会报出编译错误“Binary Operator ‘*’ cannot be applied to operands of type ‘Int’ and ‘Double’”。在Swift中,所有的运算符本质都是函数,上面的错误是告诉我们乘法函数无法接受一个Int型作为左值又接受一个Double型作为其右值,正确的代码如下所示:
var area = Double(radius) * Double(radius) * PI
你也许会认为上面的操作就是所谓的类型转换,其实不然,代码Double(radius)创建了新的Double型的对象然后用radius的值进行了初始化操作。如果不清楚什么意思,没关系,接着往后看吧。
提示:可以按住键盘上的Command键再点击Swift中的类型来查看头文件中对类型、函数、协议等的定义。
当变量的值超出了变量的表示范围时,Swift会报出编译错误来阻止这样的操作,这是Swift在编程安全方面做的处理。
布尔型
Swift中有布尔类型,布尔类型只有两种取值true或者false。下面的代码演示了如果创建布尔类型的常量:
let alwaysTrue = true
布尔类型经常被用于循环和分支结构,在Swift中,循环和分支结构中的条件只能使用布尔类型的值,这一点不同于C、C++和Objective-C。
元组
元组是由多个值组成的单一类型,与类和结构体不同,你不需要定义元组类型就能使用它。
var address = (610000, "Chengdu Sichuan")
print(address.0)
print(address.1)
address.0 = 618000
address.1 = "Deyang Sichuan"
let (zipcode, city) = address
print(zipcode)
print(city)
上面的代码使用了类型推断来得到元组的数据类型,当然你也可以书写如下的代码来指定元组中的数据对应的数据类型:
var address: (Int, String) = (610000, "Chengdu Sichuan")
此外,还可以为元组中的数据命名,如下所示:
var address = (zipcode: 610000, city: "ChengduSichuan")
print(address.zipcode)
print(address.city)
提示:元组只适用于快速的构造简单的复合数据类型,更多的时候我们需要的还是类和结构体。
字符串插值
如果你用过其他语言拼接字符串的操作,你才能体会到Swift中的字符串插值有多么的方便,在Objective-C拼接字符串需要这么做。
NSInteger a = 123;
NSInteger b = 321;
NSLog(@"%ld + %ld = %ld", a, b, a + b);
但是,在Swift中你可以用字符串插值来优雅的完成上面的操作,代码如下所示:
var a = 123
var b = 321
print("/(a) + /(b) = /(a+b)")
流程控制
如果你有其他任何编程语言的经验,对于流程控制的概念一定不会陌生。不管你使用什么样的语言,不管你的程序是简单还是复杂,程序中的代码只有三种结构:顺序结构、分支结构和循环结构。顺序结构相信不难理解,就是按照步骤一步一步的执行,从第一步到最后一步;分支结构是程序中需要做决策的地方,决策的结果不同,程序就会沿着不同的分支执行,例如写程序控制机器人从流水线上将生产的球取出来,如果是红色就放到A框中,如果是蓝色就放到B框中,判断球的颜色就是做决策的地方,而将球放入A框或B框就是两个不同的分支;循环结构是程序中的某些步骤需要重复的执行,例如流水线上会源源不断的将球送到机器人的手上,那么机器人就要重复刚才的动作,根据球的颜色不断的将球放入A框或B框中。
下面用一个最简单的1-100求和的程序来演示Swift中的循环。
- for循环
var sum1: Int = 0
for var i = 1; i <= 100; i++ {
sum1 += i
}
print(sum1)
可以在for循环中使用范围运算符,代码如下所示:
var sum2: Int = 0
for i in 1...100 {
sum2 += i
}
print(sum2)
var sum2: Int = 0
for i in 1..<101 {
sum2 += i
}
print(sum2)
- while循环
var sum3: Int = 0
var i = 1
while i <= 100 {
sum3 += i++
}
print(sum3)
- repeat-while循环
var sum4: Int = 0
var j = 1
repeat {
sum4 += j++
} while j <= 100
print(sum4)
提示:repeat-while循环是Swift 2.x中引入的,替代了do-while循环,因为在Swift 2.x中do关键字有了新的意义和用法。
再看看Swift中的分支结构吧。
-
if-else
下面的代码实现了分段函数求值。
var x = 1.5
var y: Double
if x < -1 {
y = 3 * x + 5
}
else if x <= 1 {
y = x
}
else {
y = 5 * x - 3
}
print(y)
- switch-case-default
var dir = "up"
switch dir {
case "down":
print("Going down!")
case "up":
print("Going up!")
default:
print("Going nowhere!")
}
对于有其他语言编程经验的程序员来说上面的代码没有什么难度,事实上,不管以前做什么开发的程序员总能从Swift身上找到自己熟悉的语言的影子,因此对于非零基础的初学者来说,Swift上手还是比较容易的。但是Swift中的循环和分支结构还是有自己的特点,首先省略了对设置条件的圆括号的使用(这一点和C、C++、C#、Java、Objective-C都是不一样的),另外条件只能是布尔类型的值(这一点和C、C++、Object-ive-C是不同的,与Java和C#一致),此外,switch-case-default每个case后面不需要写break,只有一个case或default会执行,default是必须要写的;如果真的希望执行完一个case后继续执行后续的case,可以在case之后写上fallthrough;还有每个case可以写多个常量或范围与switch中的表达式进行等值匹配,而且switch接受的表达式还可以是Swift中的元组类型。
let salary = 18000
switch salary {
case 0:
print("无业游民")
case 1...3000:
print("码畜")
case 3001...6500:
print("码农")
case 6501...10000:
print("IT民工")
case 10001...20000:
print("IT工程师")
case 20001...50000:
print("IT人才")
default:
print("IT精英")
}
可空类型
使用过C或C++的程序员一定被空指针问题困扰过,我们可以在程序中声明一个指针并将其默认值设置为空NULL,然而在后续的程序中我们可能忘记了初始化就去使用该指针,这样的程序在运行时(注意是运行时)会发生异常状况。在Swift 1.x中没有提供运行时异常机制,而是选择让问题在编译的时候暴露出来(这其实是一个很好的决定,因为在编译时发现的问题通常比在运行时发现的问题更容易修正,而且修改代码所付出的代价也更小),为了避免空指针引发的运行时异常,Swift中引入了可空类型(optional)的概念,我们通过下面的代码加以说明。
var str = "Hello, world"
print(str)
上面的代码会输出Hello, world,这一点大家都不会陌生,但是如果我们尝试将第一行代码的赋值语句却掉,看看编译器会说什么。
var str
print(str)
编译器会报告“Type annotation missing in pattern”(缺失类型标记)的错误。在没有赋值语句的情况下,我们需要为变量指定类型,因为无法根据赋给变量的值来推断出变量的类型。我们试着为str变量指定它的类型,再看看编译器又说了什么。
var str: String
print(str)
这一次,编译器将错误报告在打印的地方,说“Variable ‘str’ used before being initialized”(变量str在使用前没有被初始化)。显然str没有初始化,编译器是不会让这样的代码运行的。但是我们可能需要一个变量暂时是一个空值,但是当我们尝试把代码变成下面的样子时,编译器又有话说了。
var str: String = nil
print(str)
这一次,编译器说“nil cannot initialize specified type ‘String’”(nil值不能初始化String类型),即字符串不能被赋值为nil。如果真的希望这么做,可以使用可空类型,代码如下所示:
var str: String?
print(str)
我们在类型标记的后面加一个问号,就可以将变量声明为可空类型,而且你不用为它赋值为nil,它的值已经是nil,这样我们可以在需要的时候再为变量赋值,完整的代码如下所示:
var str: String?
print(str)
str = "Hello, world"
print(str?.uppercaseString)
print(str!.uppercaseString)
在playground中执行的情况如下图所示:
可以通过!强制将可空类型还原成变量的原始类型,当然如果变量的值是nil,则会产生运行时错误,如果变量值不是nil则可以被顺利的还原。还可以通过下面的方式来获取可空变量的值。
var str: String? = "Hello, world"
if let newstr = str {
print(newstr.uppercaseString)
}
提示:不知道出于什么样的考虑,Swift 2.x中又引入异常机制,同时还增加和修改了一系列的关键字,最莫名其妙的就是将do关键字改变了意义,异常机制的语法类似于Java和C#中的语法,本文暂且不做介绍。
容器
很多语言都提供了容器类型的数据结构,容器顾名思义就是承载其他对象的对象,也就是说是一个对象的持有者。最常见的容器是数组、字典和集合(跟我们的数学书上提到的集合一样,不允许有重复元素),当然,有的地方也把容器叫做集合框架(这个名字容易跟数学上的集合混淆,因此这里我们把对象的持有者都称为容器)。在Objective-C的Foundation框架中,最常用的容器有NSArray和NSDictionary,Swift中也有这两种容器。
数组
跟其他很多语言一样,Swift中的数组也是一系列元素的有序集合,注意这里说的有序并不是指从小到大或者从大到小的顺序,而是指元素一个挨着一个,它们有自己的序号(索引)。下面的代码演示了如何创建和使用数组。
var array = [12, 7, 38, 65]
print(array[2]) // 38
array.append(93)
print(array) // [12, 7, 38, 65, 93]
array.appendContentsOf(100...103)
print(array) // [12, 7, 38, 65, 93, 100, 101, 102, 103]
array.removeAtIndex(0)
print(array) // [7, 38, 65 93, 100, 101, 102, 103]
上面使用了字面量语法创建了数组对象,如果愿意,你也可以使用下面的方式来创建数组对象并指定数组元素的类型。
var myArray = Array<Int>(count: 5, repeatedValue: 0)
myArray[4] = 1000
print(myArray) // [0, 0, 0, 0, 1000]
var thyArray = [Int](count: 5, repeatedValue: 100)
thyArray[2] = 55
print(thyArray) // [100, 100, 55, 100, 100]
字典
顾名思义,被称为字典的容器中存储的元素不是单个的值,而是键和值的组合,通常称之为键值对映射,就像《新华字典》一样,上面每个可以查的字就是一个键,对这个字的解释就是值。
var myDict = [1: "Dog", 2: "Cat"]
print(myDict[2]) // Optional("Cat")
var yourDict: [Int: String] = [1: "飞机", 2: "坦克"]
print(yourDict[3]) // nil
上面的代码创建了两个字典,它们的键都是Int类型,值都是String类型。需要注意的是,当我们想从字典容器中取出数据时,需要通过下标运算用键取到键对应的值,但是这个值可能是不存在的,就像你自己臆造的字在字典中是查不到的,所以如果有和键对应的值,则取出的值是可空类型,如果没有和键对应的值则得到nil。
可以定义常量容器,这样的容器只能在定义的时候完成初始化操作,之后不能再添加元素、删除元素或修改元素的值。
可以通过循环对容器中的元素进行遍历,请看下面的代码。
var myArray = [12, 7, 38, 65]
for x in myArray {
print(x)
}
var myDict = [1: "Dog", 2: "Cat", 31: "飞机", 32: "坦克"]
for x in myDict.keys {
print("/(x) ---> /(myDict[x])")
}
函数和闭包
函数是现代编程语言中最重要的构建块,它允许你将执行特定任务的业务逻辑封装在一个独立的可重用的工作单元中。对于调用者来说,函数就像是一个独立的黑盒子,调用者在使用函数提供的功能时并不需要了解它的内部实现。Swift支持全局函数和方法,方法是跟类或者某种类型的对象相关联的函数。Swift中也提供了对闭包、匿名函数等的支持,这些在Swift中都是被作为一等公民来对待的。
先看一个例子吧。可以创建一个Playground项目,然后键入下面的代码。
import Foundation
let a = 3.0, b = 4.0
let c = sqrt(a * a + b * b)
print(c)
大家可能注意到了,Swift中并没有计算平方的内置函数,不过我们可以自己写一个。可以如下改写上面的代码。
import Foundation
func square(x:Double) -> Double {
return x * x
}
let a = 3.0, b = 4.0
let c = sqrt(square(a) + square(b))
print(c)
上面的square函数接受一个Double类型的参数并返回一个Double类型的值。定义函数的关键字是func,后面是函数的名字,函数的命名也遵循标识符命名的规则,同时也要做到见名知意,这些应该是不用解释的常识。函数拥有零个或多个参数和一个返回值或者没有返回值。Swift中被称为函数的东西都是全局性的,如果在类和接口中定义函数,这种函数跟某种类型或者某种类型的对象绑定在一起,我们通常称之为方法。
在Swift中的函数是一等公民,Swift中函数的用法和JavaScript非常类似,你可以将函数赋值给一个变量或常量,当然你也可以将一个函数作为另外一个函数的参数。代码如下所示:
import Foundation
func square(x:Double) -> Double {
return x * x
}
let f = square // 使用类型推断获得变量f的类型
let a = 3.0, b = 4.0
let c = sqrt(f(a) + f(b))
print(c)
你可能觉得上面的代码其实也挺眼熟的,不错,因为这种写法就相当于C语言中的函数指针。还有一点需要说明的是,上面的代码中常量f的类型是通过类型推断得到的,如果想要完整的给出常量f的类型,代码可以如下书写。
var f:(Double) -> Double = square
当然,也可以像指向函数的指针那样,先声明一个新的变量类型,再将一个函数赋值给这种新类型的变量。Swift中声明类型别名的关键字是typealias,代码如下所示:
typealias FType = (Double) -> Double
var f:FType = square
提示:在Xcode中想查看一个函数的定义,可以按住Command键点击函数,就可以让代码跳转到函数定义之处。
Swift中支持函数重载,所谓的重载是同名的函数拥有不同的参数列表,那么它们就可以和平共处,而且重载也是实现编译时多态性的重要手段,且看下面的代码。
func assertEquals(value: Int, expected: Int, message: String) {
if expected != value {
print(message)
}
}
func assertEquals(value: String, expected: String, message: String) {
if expected != value {
print(message)
}
}
assertEquals(100, expected: 1000, message: "Two integers are not equal!")
assertEquals("Hello", expected: "Good", message: "Two strings are not equal!")
当然,如果重载的函数都向上面一样只是不同的参数却执行了相同的代码,那么这些代码将来的维护将是一场恶梦。编程大师Martin Fowler在Refactoring Improving the Design of Existing Code(《重构:改善既有代码的设计》)一书中指出:“代码有很多种坏味道,重复是最坏的一种”。要消除重复代码,可以使用泛型来改写上面的函数,如下所示。
func assertEquals<T: Equatable>(value: T, expected: T, message: String) {
if expected != value {
print(message)
}
}
assertEquals(100, expected: 1000, message: "Two integers are not equal!")
assertEquals("Hello", expected: "Good", message: "Two strings are not equal!")
上面的代码中T是泛型参数,相当于定义了一种虚拟的类型,冒号后面的Equatable是对泛型的限定(如果不理解可以先不管他,后面会讲到这个知识),这种类型在编译时会根据传入该函数的实际参数的类型来决定,就如上面的代码中,第一次调用assertEquals函数时,T被替换成Int类型;而第二次调用时,T被替换成String类型。
提示:在Swift 2.x中,函数调用传参时从第二个参数开始必须要给出外部参数名,这一点从某种程度上来讲照顾了Objective-C程序员的代码书写习惯。
Swift中,函数的参数可以是inout参数,所谓inout参数是指可以在函数中被修改并影响到调用者的参数(这一点应该是借鉴了C#的语法,C#中方法的参数可以设置为ref参数或out参数)。先看看下面的代码。
func swap(inout x: Int, inout y: Int) {
let temp: Int = x;
x = y;
y = temp;
}
func bubbleSort(inout x: [Int]) {
var swapped:Bool = true
for var i = 1; swapped && i <= x.count - 1; i++ {
swapped = false
for var j = 0; j < x.count - i; j++ {
if x[j] > x[j + 1] {
swap(&x[j], &x[j + 1])
swapped = true
}
}
}
}
var x = [34, 12, 85, 7, 96, 63, 40]
bubbleSort(&x)
print(x)
上面的代码中有两个函数,swap函数实现交换两个参数的值,下面的bubbleSort函数实现冒泡排序,如果没有inout参数,你只能修改函数参数在函数内部的拷贝而不会影响到调用者,但是有了inout参数一切就不同了,调用者在传入参数的时候用在参数前加上&,有C或者C++开发经验的都知道,这种语法叫做传指针(引用)而不是简单的传值。当然,对于inout参数的使用还是应当小心,因为不是所有的时候我们都希望通过函数调用来修改传入的参数并影响调用者,一定要警惕这种形式的参数所带来的副作用。
Swift1.x中,如果要为函数的参数指定内部参数名和外部参数名,可以按照下面的方式来做。
// greeting是第一个参数的外部参数名
// g是第一个参数的内部参数名
func say(greeting g: String, name n: String, counter c: Int) {
for var i = 0; i < c; ++i {
print("/(g), /(n)")
}
}
say(greeting: "Hello", name: "Jack", counter: 3)
提示:Swift 1.x中可以通过在内部参数名前加#让内部参数和外部参数同名,Swift 2.x默认内部参数名就是外部参数名。如果不愿意在调用函数时书写外部参数名,可以在函数的参数列表的内部参数名前添加一个下划线,第一个参数除外。
如果需要使用外部参数名,比较好的做法是让参数的外部名字和内部名字保持一致,在Swift 2.x中可以使用下面的写法。
func say(greeting greeting: String, name: String, counter: Int) {
for var i = 0; i < counter; ++i {
print("/(greeting), /(name)")
}
}
say(greeting: "Hello", name: "Jack", counter: 3)
说明:这种语法对于使用Objective-C的开发人员来说一点都不陌生,但是对于使用Java或C#这样编程语言的程序员来说可能会觉得比较别扭。
在类或结构中定义的函数我们通常称之为方法,这一点稍后予以讲解。
Swift既然支持函数式编程,那么不可避免的要支持闭包(closure)。关于闭包一词,你可能会听到各种各样的解释,下面给出其中的一种:“闭包是可以包含自由(未绑定到特定对象)变量的代码块;这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。闭包一词来源于以下两者的结合:要执行的代码块(由于自由变量被包含在代码块中,这些自由变量以及它们引用的对象没有被释放)和为自由变量提供绑定的计算环境(作用域)”。闭包最常见的一种用法就是用闭包表达式实现匿名函数的功能,我们用下面的代码来加以说明。
var animals = ["fish", "cat", "panda", "dog"]
func compare(one: String, two: String) -> Bool {
return one < two;
}
animals.sortInPlace(compare)
print(animals)
上面的代码实现了对字符串数组的排序,我们将一个包含排序规则的函数作为数组sort方法的参数传入,sort方法就能够完成对数组的排序。
事实上,完全不需要单独定义这样的一个函数,因为这个函数想表达的就是一个临时的排序规则,我们可以用下面的方式将代码修改得更加简短。
var animals = ["fish", "cat", "panda", "dog"]
animals.sortInPlace({ (one: String, two: String) -> Bool in return one < two })
print(animals)
当然,这样看起来仍然不够简单明了。如果你还记得Swift有强大的类型推断能力,那么上面的代码就可以做出如下的改进。
var animals = ["fish", "cat", "panda", "dog"]
animals.sortInPlace({ (one, two) -> Bool in return one < two })
print(animals)
你甚至还可以省略掉那个return,代码仍然可以很好的工作。还有什么可以省略吗?答案是肯定的,既然有类型推断,这次我们连返回类型也一并省去。
var animals = ["fish", "cat", "panda", "dog"]
animals.sortInPlace({ (one, two) in one < two })
print(animals)
写到这我们已经觉得做到极致了,似乎已经没有什么办法可以让代码写得更简单了。不,Swift还能做到更加的简单明了。
var animals = ["fish", "cat", "panda", "dog"]
animals.sortInPlace({ $0 < $1 })
print(animals)
OMG!参数名都可以省略掉,
0很明显表示匿名函数的第一个参数,而
var animals = ["fish", "cat", "panda", "dog"]
animals.sortInPlace(<)
print(animals)
最后还有一个很特殊的东西叫尾随闭包(tailing closure)需要提一下,请看下面的代码。
var animals = ["fish", "cat", "panda", "dog"]
animals.sortInPlace() { $0 < $1 }
print(animals)
闭包另一个重要的用途是将其所在的代码块中的常量或变量的生命周期延长,在离开代码块以后,我们还能够访问到这些常量或变量的值。我们可以在一个函数中返回一个闭包表达式,这相当于返回了一个匿名函数,在这个匿名函数中我们还能使用刚才返回这个闭包的函数(有的地方称之为封闭函数)中的局部变量或常量,专业的说法称之为“捕获值”(capturing value)。
typealias StateMachineType = () -> Int
func makeStateMachine(maxState: Int) -> StateMachineType {
var currentState: Int = 0
return {
currentState++;
if currentState > maxState {
currentState = 1
}
return currentState
}
}
let myStateMachine = makeStateMachine(3)
for i in 1...10 {
print(myStateMachine())
}
上面的代码中,makeStateMachine函数返回了一个匿名函数(闭包表达式),由于这个闭包的存在,本来是函数局部变量的currentState和参数maxState可以在函数的生命周期结束以后仍然被继续使用。闭包让你免除了对全局变量的使用(因为全局变量总是让你的代码变得糟糕,因为你不知道这个变量什么时候会被哪段代码意外的修改),但是它通过延长局部变量生命周期的方式让你以可控制的方式使用这些值。
最后,我们对闭包做一个小小的总结:
- 全局函数都是闭包,有名字但是不能捕获任何值。
- 嵌套函数都是闭包,有名字也能捕获封闭函数内的值。
-
闭包表达式都是匿名函数,可以根据上下文环境捕获值。
Swift中的闭包较之其他语言更简单更优秀,主要表现在:
-
可以根据上下文推断参数和返回值的类型。
- 对于单行闭包表达式,可以省略return,隐式返回。
- 可以省略掉参数名而使用
1, … 替换之。
0,
- 提供了尾随闭包的语法,让代码更自然。
类和结构体
前面我们曾经提到过Swift支持面向对象的编程范式。所谓面向对象的编程理念,就是用对象来组织数据以及操作数据的方法,这也就意味着数据和操作数据的方法在逻辑上是一个整体,从而保证了操作的有效性和数据的完整性。面向对象编程和函数编程是目前最流行的两种编程范式,Swift对两种编程范式都提供了很好的支持。我们先看看面向对象编程。
定义和使用类
在Swift中,除了可以使用Swift已有的数据类型还可以自定义数据类型,类和结构体都是实现自定义数据类型的手段。例如,Swift没有一种类型来描述现实世界中的人,但是我们可以通过类或结构体来定义一种新的类型叫人,而且在人这种新类型中可以包含拥有的属性和方法。我们先看看如何定义和使用类。
类是对象的蓝图和模板。当我们把世界上一类对象共同的特征抽取出来的时候,我们就可以定义一个类。例如,我们把人的共同特征抽象出来的时候,我们就可以定义人类。创建类的过程就是一个抽象的过程,我们需要做两种抽象:数据抽象和行为抽象。所谓数据抽象就是找到对象的静态属性;所谓行为抽象就是找到对象动态属性,也就是对象能够执行的动作。例如:人的静态属性有名字和年龄等,人的动态属性有吃饭、睡觉、行走等。这些静态属性会成为类中的字段,而动态属性会成为类中的方法。
下面的代码定义了人类。
/**人类*/
class Person {
var name: String // 姓名
var age: Int // 年龄
/**初始化方法*/
init(name: String, age: Int) {
self.name = name;
self.age = age;
}
/**吃饭*/
func eat() {
print("/(name)正在吃饭")
}
/**看片*/
func watchJapaneseAV() {
if age < 18 {
print("/(name)不能观看岛国爱情动做片")
}
else {
print("/(name)正在观看岛国爱情动作片")
}
}
}
上面的代码用class关键字定义了名为Person的类,其中有name和age两个属性,类型是字符串和整型,分别代表人的姓名和年龄;接下来的代码中用func关键字定义了两个方法,分别代表了人吃饭和看片的行为。除此之外,在Person类中还有一个名为init的代码块,它是类的初始化方法(在C++、Java和C#中通常将其称为构造器或构造方法),承担了创建并初始化对象的职责,当程序中需要创建Person类的对象时,就可以调用该初始化器,初始化器有两个参数,前者是姓名,后者是年龄,在初始化器中我们把这两个参数分别赋值给了类的name和age字段。由于字段名和初始化器的参数名完全一致,为了加以区分,我们使用self关键表示类的字段,而没有self.前缀的则是参数。接下来可以创建人类的对象,也就是一个具体的人,然后让他执行吃饭和行走的行为,代码如下所示:
var p = Person(name: "王大锤", age: 20)
p.eat()
p.watchJapaneseAV()
有C++或Java编程经验的程序员都知道,类中的构造器可以重载,这样的话可以根据需要选择某个构造器来创建对象。Swift中使用初始化器来创建并初始化对象,而且提供了一种叫做便利初始化器(convenience initializer)的语法,便利初始化器会调用初始化器(通常称之为非便利初始化器或指派初始化器[designated initializer])器来创建对象,代码如下所示。
/**人类*/
class Person {
var name: String // 姓名
var age: Int // 年龄
/**便利初始化器*/
convenience init(name: String) {
self.init(name: name, age: 20)
}
/**初始化器*/
init(name: String, age: Int) {
self.name = name
self.age = age
}
/**吃饭*/
func eat() {
print("/(name)正在吃饭")
}
/**看片*/
func watchJapaneseAV() {
if age < 18 {
print("/(name)不能观看岛国爱情动做片")
}
else {
print("/(name)正在观看岛国爱情动作片")
}
}
}
var p1 = Person(name: "骆昊", age: 35)
print("姓名: /(p1.name)/n年龄: /(p1.age)")
p1.eat()
var p2 = Person(name: "王大锤")
print("姓名: /(p2.name)/n年龄: /(p2.age)")
p2.watchJapaneseAV()
继承
继承是面向对象编程中一个重要的概念,它是从已有的类创建新类的过程。提供继承信息的类被称为父类,而得到继承信息的类被称为子类。下面我们试着用继承从刚才的人类(Person)派生出老师(Teacher)类和学生类(Student)。
import Foundation
/**老师*/
class Teacher: Person {
var title: String // 职称
/**初始化器*/
init(name: String, age:Int, title: String) {
self.title = title
super.init(name: name, age: age) // 调用父类初始化器
}
/**教学*/
func teach(courseName: String) {
print("/(name)/(title)正在教/(courseName)")
}
}
/**学生*/
class Student: Person {
var grade: String // 年级
/**初始化器*/
init(name: String, age:Int, grade: String) {
self.grade = grade
super.init(name: name, age: age)// 调用父类初始化器
}
/**学习*/
func study(courseName: String) -> Int {
print("/(name)选修了/(courseName)")
return Int(arc4random() % 101)
}
}
var t = Teacher(name: "骆昊", age: 35, title: "砖家")
var s = Student(name: "王大锤", age: 20, grade: "大二")
t.eat()
t.teach("iOS开发")
s.watchJapaneseAV()
var score:Int = s.study("Swift")
print("/(s.name)得到了/(score)分")
在上面的例子中,Teacher类和Student类从Person类得到了继承信息,因此它们是子类,Person类是父类。子类通常也称为派生类,而父类通常称为超类或基类。子类和父类之间的关系是“IS-A”关系,我们可以说老师是人,学生是人。通过继承Teacher类和Student类复用了Person类中的代码,即学生和老师作为人的那些属性和行为,因此继承是复用代码的手段之一。在Swift中,继承的语法是定义子类时在子类后面跟一个冒号再跟父类的名字。在上面的例子中,Teacher类和Student类继承了Person的name和age静态属性,还继承到了eat和watchJapaneseAV方法,因此子类不用再重新定义这些属性和方法。当然,子类在继承父类的过程中还可以定义自己特有的属性和方法,比如Teacher中的title属性和Student中的grade属性,此外Teacher类中还有teach方法,学生类中有study方法这些都是子类对父类的扩展。显然,子类拥有比父类更多的能力,也就是说,继承一定是让子类扩展父类的能力而绝不会缩小父类的能力。就好比让猫继承狗之后,猫没有狗看门的行为,子类就要去掉父类的这项功能,这种继承明显是错误的;同理,如果让狗继承猫,狗没有猫爬树的行为,这种继承显然也是错误的。还有一点,子类的初始化器中必须调用父类的初始化器,而且必须调用父类的非便利初始化器。
在Swift中,还可以通过协议(protocol)来扩展类的能力,所谓协议就是定义了遵循该协议的类要遵守的规范。我们可以为刚才的程序添加下面的代码来看看如何使用协议来扩展类。
// 协议
protocol MoreInfo {
var info: String { get }
}
// 类扩展
extension Person: MoreInfo {
var info: String {
get { return String("/(name): /(age)") }
}
}
var t = Teacher(name: "骆昊", age: 35, title: "砖家")
var s = Student(name: "王大锤", age: 20, grade: "大二")
print(t.info) // 显示老师信息
print(s.info) // 显示学生信息
当我们对Person类做出扩展以后,它的子类Student和Teacher也继承到了它扩展的内容。
多态
多态简单的说是同样的同样的方法执行了不同的行为,就好比猫和狗都有发出叫声的行为,但是发出的声音是完全不一样的。多态有两种实现方式:方法的重载(overload)和方法的重写(override)。方法的重载是在一个类中同名的方法有不同的参数列表,而方法重写是在继承过程,子类对父类已有的方法重新做出实现,不同的子类给出不同的实现版本。
- 函数的重载
func add(a: Int, b: Int) -> Int {
return a + b
}
func add(a: Int, b: Int, c: Int) -> Int {
return a + b + c
}
print(add(1, b: 2)) // 3
print(add(1, b: 2, c: 3)) // 6
/**猫*/
class Cat {
func makeSound() { print("喵...") }
}
/**狗*/
class Dog {
func makeSound() { print("汪...") }
}
func foo(animal: Cat) { animal.makeSound() }
func foo(animal: Dog) { animal.makeSound() }
foo(Cat()) // 喵...
foo(Dog()) // 汪...
- 方法的重写
/**图形*/
class Shape {
/**计算面积*/
func area() -> Double { return 0 }
/**计算周长*/
func perimeter() -> Double { return 0 }
}
/**圆形*/
class Circle : Shape {
var radius: Double // 半径
init(radius: Double) {
self.radius = radius
}
// 方法重写
override func area() -> Double {
return M_PI * radius * radius
}
// 方法重写
override func perimeter() -> Double {
return 2 * M_PI * radius;
}
}
/**矩形*/
class Rectangle: Shape {
var width, height: Double // 宽、高
init(width: Double, height: Double) {
self.width = width
self.height = height
}
// 方法重写
override func area() -> Double {
return width * height
}
// 方法重写
override func perimeter() -> Double {
return (width + height) * 2
}
}
var shapeArray:[Shape] = [Circle(radius: 3), Rectangle(width: 5, height: 6)]
for currentShape in shapeArray {
print("面积: /(currentShape.area())")
print("周长: /(currentShape.perimeter())")
}
在上面的例子中,父类Shape定义了两个方法一个计算图形周长(perimeter方法)一个计算图形面积(area方法),但是Shape类中并不能计算出图形的周长和面积,因为尚不知道是哪种类型的图形,因此这两个方法需要在子类中进行重写。子类在重写父类方法时需要用override关键字进行说明,而且重写的方法需要与父类被重写的方法拥有相同的方法签名(方法名和参数列表)和相同的返回类型。
重载实现的多态性是编译期的多态性,编译器会根据函数(或方法)的参数来决定调用哪个函数(或方法);重写实现的多态性是运行期的多态性,编译器并不知道应该调用哪个子类重写过的方法,只有在运行时当确定了对象的类型后,不同的子类对象会调用自己重写过的方法。这样,虽然都是同种类型的引用,调用的也是相同的方法,但是会做不同的事情(圆周长和面积的计算方法完全不同于矩形周长和面积的计算方法)。
如果不希望一个类被继承,可以在声明类的class关键字前加上final。
上面的程序更好的做法不是使用类的继承而是协议,代码如下所示。
import Foundation
protocol Shape {
var area: Double { get }
var perimeter: Double { get }
}
/**圆*/
class Circle : Shape {
var radius: Double
init(radius: Double) {
self.radius = radius
}
var area: Double {
return M_PI * radius * radius
}
var perimeter: Double {
return 2 * M_PI * radius
}
}
/**矩形*/
class Rectangle: Shape {
var width, height: Double
init(width: Double, height: Double) {
self.width = width
self.height = height
}
var area: Double {
return width * height
}
var perimeter: Double {
return (width + height) * 2
}
}
var shapeArray:[Shape] = [
Circle(radius: 3), Rectangle(width: 5, height: 6)
]
for currentShape in shapeArray {
print("面积: /(currentShape.area)")
print("周长: /(currentShape.perimeter)")
}
运算符重载
在上面的例子中,我们将各种图形装在一个Shape类型的数组中,我们可以对这个数组做一次排序,然后再将排序后的结果遍历一次。但是如何对象图形对象进行排序呢?我们可以规定面积较小的图形排在前面,面积较大的图形排在后面,于是我们可以通过运算符重载的方式定义两个图形比较大小的规则,代码如下所示。
import Foundation
protocol Shape {
var area: Double { get }
var perimeter: Double { get }
}
// 为遵循Shape协议的类型重载<运算符
func <(a: Shape, b: Shape) -> Bool {
return a.area < b.area
}
/**圆*/
class Circle : Shape {
var radius: Double
init(radius: Double) {
self.radius = radius
}
var area: Double {
return M_PI * radius * radius
}
var perimeter: Double {
return 2 * M_PI * radius
}
}
/**矩形*/
class Rectangle: Shape {
var width, height: Double
init(width: Double, height: Double) {
self.width = width
self.height = height
}
var area: Double {
return width * height
}
var perimeter: Double {
return (width + height) * 2
}
}
var shapeArray:[Shape] = [Circle(radius: 3), Rectangle(width: 5, height: 6), Circle(radius: 2), Rectangle(width: 2, height: 8)]
shapeArray.sortInPlace(<);
// 按面积从小到大输出图形的面积和周长
for currentShape in shapeArray {
print("面积: /(currentShape.area)")
print("周长: /(currentShape.perimeter)")
}
提示:Swift 2.x中,如果要在原数组上进行排序需要调用sortInPlace方法,Swift 1.x中做这件事情是调用sort方法。
访问控制
迄今为止,我们在类中声明的变量和方法只要在同一个源文件中,它们可以在任何地方被访问。当然,如果你不希望这样,也可以使用访问修饰符来控制变量和方法的访问权限。在Swift中有三个访问修饰符:
- private:私有,只有当前源文件中能够访问。
- internal:默认的访问修饰符,除了当前源文件中的代码,同一个应用程序或者同一个库文件中的其他代码也能访问。
- public:可以在任何地方被访问。
建议:在一个源文件中只写一个类,对于类中不允许外界访问的变量和方法都应该加上private修饰符,如果要自定义库文件或自己写框架让别人调用,那么应该将类中需要暴露的编程接口都加上public修饰符。
结构体
在Swift中,还可以使用结构体来实现自定义数据类型,结构体和类非常相似,我们可以像上面的例子中定义类那样定义结构体,下面的代码定义了一个结构体来描述复数,复数由实部(real)和虚部(image)构成,还有一个toString方法将复数转换成字符串,此外可以重载了+运算符实现复数的加法运算。
/**复数*/
struct Complex {
var real: Double // 实部
var img: Double // 虚部
var description: String {
return "/(real) + /(img)i"
}
}
// 重载复数类型的加法运算符
func +(a: Complex, b: Complex) -> Complex {
return Complex(real: (a.real + b.real), img: (a.img + b.img))
}
var c1 = Complex(real: 3, img: 5)
var c2 = Complex(real: 2, img: -3)
print((c1 + c2).description) // 5.0 + 2.0i
结构体和类最大的不同在于结构体是值类型(value type)而类是引用类型(reference type),我们可以通过下面的例子来了解这二者到底有什么区别。
class Cat {
var name = "Kitty"
}
struct Dog {
var name = "Wangcai"
}
var cat1 = Cat()
var dog1 = Dog()
var cat2 = cat1
var dog2 = dog1
cat2.name = "Mimi"
dog2.name = "Xiaoqiang"
print(cat1.name) // Mimi
print(dog1.name) // Wangcai
在上面的代码中,var cat2 = cat1定义了一个引用类型的变量cat2,它和cat1引用内存中同一个Cat对象,因为Cat是一个类,所以cat1是引用类型。当我们用cat2.name修改了Cat对象的name属性后再打印cat1.name,打印出来的应该是修改过后的结果。此外,我们用var dog2 = dog1时,由于Dog是结构体,Dog的实例是一个值类型的对象,执行该语句会在内存中复制出另一个Dog实例,dog2和dog1代表的是不同的对象,当我们用dog2.name修改了Dog对象的name属性后再打印dog1.name,仍然是原来的值。
我们再看一个例子。
var a1: Int = 5, a2: Int = 10
func swap(var x: Int, var y: Int) {
let temp = x;
x = y;
y = temp;
}
swap(a1, y: a2)
print("a1 = /(a1), a2 = /(a2)")
在这里例子中,我们试图用通过swap函数来交换两个参数的值,但是程序的结果仍然是“a1 = 5, a2 = 10”。注意,swap函数的参数前要加上var关键字,否则在默认的情况下,函数的参数是常量,其值是无法修改的(这个设计是非常好的,因为可以防止意外的修改了参数的值,其他很多语言在默认情况下函数参数都是变量类型,如果需要处理成常量要自己加修饰符,比如Java中可以用final来修饰函数的参数使其变成一个常量)。几乎所有的语言函数的参数传递都是值传递,Swift也不例外。在上面的代码中,a1将它的值传给了x,a2将它的值传给了y,在swap函数中,不管对x和y做什么操作,都不会影响到a1和a2。再看个例子吧。
class Cat {
var name: String
init(name: String) { self.name = name }
}
var c = Cat(name: "Kitty")
func change(cat: Cat, name: String) {
cat.name = "Mimi"
}
print(c.name) // Kitty
change(c, name: "Mimi")
print(c.name) // Mimi
函数change的第一个参数是引用类型,当我们将Cat的引用c传入change函数并在函数中修改所引用对象的name属性时,由于c和函数中的cat引用了内存中的同一个对象,因此调用函数后再次打印c.name时,结果是“Mimi”。不要着急,还有一个例子。
class Cat {
var name: String
init(name: String) { self.name = name }
}
var c = Cat(name: "Kitty")
func change(var cat: Cat, name: String) {
cat = Cat(name: name)
}
print(c.name) // Kitty
change(c, name: "Mimi")
print(c.name) // Kitty
注意这个例子和上一个例子的区别,然后想一想为什么调用了change函数后打印c.name看到的仍然是Kitty。
结构体和类最重要的区别我们已经说过了,但是它们的区别还不止于此。一个类可以被其他类继承,但是结构体是不能够被继承的;一个类通常需要自己写初始化器,但是结构体会有隐式的初始化器,隐式的初始化器的参数会根据结构体中的属性自动生成;结构体中的属性也是值类型的,在对一个结构体赋值时,它的属性也会完成值的拷贝。
枚举
枚举是定义符号常量的手段,它把一堆相似的值组织在一起。例如你在指定文字对齐方式的时候通常有三种可选的值:左对齐、右对齐和居中对齐;你在处理游戏中的方法时可能的取值有东、西、南、北。Swift中的枚举比你了解的其他语言的枚举更加强大,它的行为类似于类和结构体,它甚至可以有自己的方法,包括构造器。
创建枚举
我们先通过一个简单的例子来认识一下枚举。玩过扑克牌的都知道,扑克有四种花色,分别是黑桃(spade)、红心(heart)、草花(club)和方块(diamond),如果我们要做一个扑克游戏,那么就可以用枚举来描述这四种花色,代码如下所示:
enum Suite {
case Spade
case Heart
case Club
case Diamond
}
又比如我们要做一个绘图软件,该软件支持绘制矩形、圆形和三角形,那么可以用下面的枚举来表示不同类型的图形。
enum ShapeType {
case Rectangle
case Triangle
case Circle
}
在switch多分支结构中使用枚举
我们可以使用switch多分支结构来处理枚举类型的值,如下所示:
var shapeType:ShapeType = ShapeType.Circle
switch(shapeType) {
case .Rectangle:
print("You choose a rectangle")
case .Triangle:
print("You choose a triangle")
case .Circle:
print("You choose a circle")
}
Swift中的switch语句要求必须覆盖所有可能的情况,如果你将上面的代码修改成下面的样子,就会产生一个编译错误:“switch must be exhaustive, consider adding a default clause.”(switch必须是穷尽的,考虑添加一个default语句)。
enum ShapeType {
case Rectangle
case Triangle
case Circle
}
var shapeType:ShapeType = ShapeType.Circle
switch(shapeType) {
case .Triangle:
print("You choose a triangle")
case .Circle:
print("You choose a circle")
}
按照错误提示,我们可以添加一个default分支来消除此编译错误,当所有的case都没有匹配成功时,就会执行default分支。
enum ShapeType {
case Rectangle
case Triangle
case Circle
}
var shapeType:ShapeType = ShapeType.Circle
switch(shapeType) {
case .Triangle:
print("You choose a triangle")
case .Circle:
print("You choose a circle")
default:
print("You choose a rectangle")
}
当然也可以在一个case中书写多种可能性,这一点也是Swift对switch结构的改进,不同于Java、C#等语言,代码如下所示:
enum ShapeType {
case Rectangle
case Square
case Diamond
case Triangle
case Circle
case Oval
}
var shapeType:ShapeType = ShapeType.Circle
switch(shapeType) {
case .Triangle:
print("You choose a triangle")
case .Circle, .Oval:
print("You choose a circle or an oval")
case .Rectangle, .Square, .Diamond:
print("You choose a quadrilateral")
}
枚举类型
在Swift中,枚举在某些程度上跟类和结构体非常类似,它和结构体有相同的值语义,因此也是基于栈分配存储空间。如果需要,枚举甚至可以有成员方法,如下所示:
import Foundation
enum Shape {
case Rectangle(width: Double, height: Double)
case Square(side: Double)
case Triangle(base: Double, height: Double)
case Circle(radius: Double)
func area() -> Double {
var result:Double = 0;
switch(self) {
case .Rectangle(let width, let height):
result = width * height
case .Square(let side):
result = side * side
case .Triangle(let base, let height):
result = base * height
case .Circle(let radius):
result = M_PI * radius * radius
}
return result
}
}
var circle = Shape.Circle(radius: 5)
circle.area()
我们还可以在枚举中添加一个工厂方法来创建枚举类型的变量,如下所示:
import Foundation
enum Shape {
case Rectangle(width: Double, height: Double)
case Square(side: Double)
case Triangle(base: Double, height: Double)
case Circle(radius: Double)
func area() -> Double {
var result:Double = 0;
switch(self) {
case .Rectangle(let width, let height):
result = width * height
case .Square(let side):
result = side * side
case .Triangle(let base, let height):
result = base * height
case .Circle(let radius):
result = M_PI * radius * radius
}
return result
}
static func factory(shapeType: String) -> Shape? {
var shape:Shape?
switch(shapeType) {
case "rectangle":
shape = Shape.Rectangle(width: 5, height: 10)
case "square":
shape = Shape.Square(side: 5)
case "triangle":
shape = Shape.Triangle(base: 5, height: 10)
case "circle":
shape = Shape.Circle(radius: 5)
default:
shape = nil
}
return shape
}
}
var rect = Shape.factory("rectangle")
rect!.area()
函数式编程
面向对象编程和函数式编程是目前最主流的两种编程范式,而关于这两种范式孰优孰劣的讨论一直都没有停止过。事实上,真正理解两种编程范式的程序员不会武断的说这二者孰优孰劣,因为任何编程语言都没有什么灵丹妙药让其使用者成为优秀的程序员。其实,像Java这样很经典的面向对象的编程语言,也能够看到函数式编程的影子,如果你使用过访问者模式、命令模式,如果你使用过接口回调,你实际上已经使用了函数式编程的理念,而且在新版本的Java中,已经开始支持Lambda表达式和函数式接口,这些都是Java为了支持函数式编程所作出的改进。同样,我们也可以用C语言写出面向对象风格的代码,如果你对Objective-C的运行时有所了解的话,你就知道面向对象的本质是什么了。其实,只要在适当的地方使用适当的编程范式就能够写出优质的代码,我们不应该让自己的程序囿于某一种编程范式,就如同一个优秀的程序员绝不会声称自己效忠于某种语言。
函数式编程有如下一些特性:
- 高阶函数:函数可以作为函数的参数传给函数。
- 一等公民:可以将函数视为变量来使用。
- 闭包:可以使用匿名函数。
函数式编程的例子
我们写一段将数组中的偶数求和的代码,传统的做法是这样的:
var array = [11, 23, 10, 8, 6, 5, 97, 12]
var evenSum = 0
for num in array {
if num % 2 == 0 {
evenSum += num
}
}
print(evenSum)
用函数式编程的方式,代码如下所示:
var array = [11, 23, 10, 8, 6, 5, 97, 12]
var sum = array.filter{ $0 % 2 == 0 }.reduce(0){ $0 + $1 }
print(sum)
上面的代码用到了我们之前提到过的尾随闭包,将两个匿名函数分别作为过滤(filter)和归约(reduce)方法的参数。
柯里化(currying)
柯里化就是把接受多个参数的方法变成接受第一个参数的方法,然后返回接受余下的参数并返回结果的新方法的过程。柯里化是一种产生模板方法的手段,要理解这一点,我们先看看下面的例子。
import Foundation
let data = "5,7;3,4;55,6"
// ["5,7", "3,4", "55,6"]
print(data.componentsSeparatedByString(";"))
// ["5", "7;3", "4;55", "6"]
print(data.componentsSeparatedByString(","))
在上面的例子中,我们使用字符串的componentsSeparatedByString()方法根据指定的字符(串)将字符串拆分成字符串的数组。有些时候,我们可能需要用指定的字符(串)反复的对出现的字符串进行拆分,于是我们可以如下修改我们的代码。
import Foundation
let data = "5,7;3,4;55,6"
func createSplitter(separator: String) -> (String -> [String]) {
func split(source: String) -> [String] {
return source.componentsSeparatedByString(separator)
}
return split // 将函数作为函数的返回值
}
let commaSplitter = createSplitter(",")
// ["5", "7;3", "4;55", "6"]
print(commaSplitter(data))
let semiColonSplitter = createSplitter(";")
// ["5,7", "3,4", "55,6"]
print(semiColonSplitter(data))
明显,按照上面的做法,我们可以重复的使用两种拆分器commaSplitter和semiColonSplitter对字符串进行拆分,而不用每次调用字符串的拆分函数并指定拆分字符(串)。这种编程理念通常称之为“partial application”,其原理是将函数中的一个或多个参数先固定下来,创建出一个新的函数,再调用这个新的函数传入后续参数。
让我们再来看一个例子吧。
func add(one: Int, two: Int, three: Int) -> Int {
return one + two + three
}
let sum = add(1, 2, 3)
print(sum)
我们也可以这样来写改写add()函数。
func add(one: Int)(two: Int)(three: Int) -> Int {
return one + two + three
}
let step1 = add(1)
let step2 = step1(two: 2)
let step3 = step2(three: 3)
print(step3)
上面的例子纯粹为了告诉大家什么是柯里化,当然它并有什么意义和价值。下面换一个例子,希望他能够帮助你理解柯里化的用法和意义。
import Foundation
// 柯里化的字符串填充函数
func stringPadding(startIndex: Int, paddingString: String)
(source: String, length: Int) -> String {
return source.stringByPaddingToLength(length, withString: paddingString, startingAtIndex: startIndex)
}
let text = "Swift"
let dottedPadding = stringPadding(0, paddingString: ".")
let paddingText = dottedPadding(source: text, length: 10)
print(paddingText) // Swift.....
个人观点:函数编程可以将程序员从冗长的代码中解放出来。虽然面向对象是很好的编程理念,但是面向对象在处理某些问题时可能需要写很长的代码,要给某个或某些对象发出一系列的消息才能完成某项操作,而使用函数式编程很可能一行代码就能把这些事情做完。很多Objective-C的程序员钟爱ReactiveCocoa不也是因为这样的原因吗!
到这里,我们对Swift的语言做了一个走马观花的讲解,希望这些内容能够帮助你抓住这门语言最基本也是最重要的东西。
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/5315.html