iOS开发系列--Swift语言
概述
Swift是苹果2014年推出的全新的编程语言,它继承了C语言、ObjC的特性,且克服了C语言的兼容性问题。Swift发展过程中不仅保留了ObjC很多语法特性,它也借鉴了多种现代化语言的特点,在其中你可以看到C#、Java、Javascript、Python等多种语言的影子。同时在2015年的WWDC上苹果还宣布Swift的新版本Swift2.0,并宣布稍后Swift即将开源,除了支持iOS、OS X之外还将支持linux。
本文将继续iOS开发系列教程,假设读者已经有了其他语言基础(强烈建议初学者从本系列第一章开始阅读,如果您希望从Swift学起,那么推荐你首先阅读苹果官方电子书《the swift programming language》),不会从零基础一点点剖析这门语言的语法,旨在帮助大家快速从ObjC快速过度到Swift开发中。即便如此,要尽可能全面的介绍Swift的语法特点也不是一件容易的事情,因此本文将采用较长的篇幅进行介绍。
基础部分
第一个Swift程序
创建一个命令行程序如下:
import Foundation /** * Swift没有main函数,默认从top level code的上方开始自上而下执行(因此不能有多个top level代码) */ println("Hello, World!")
从上面的代码可以看出:
- Swift没有main函数,从top level code的上方开始往下执行(就是第一个非声明语句开始执行[表达式或者控制结构,类、结构体、枚举和方法等属于声明语句]),不能存在多个top level code文件(否则编译器无法确定执行入口,事实上swift隐含一个main函数,这个main函数会设置并调用全局 “C_ARGC C_ARGV”并调用由top level code构成的top_level_code()函数);
- Swift通过import引入其他类库(和Java比较像);
- Swift语句不需要双引号结尾(尽管加上也不报错),除非一行包含多条语句(和Python有点类似);
数据类型
Swift包含了C和ObjC语言中的所有基础类型,Int整形,Float和Double浮点型,Bool布尔型,Character字符型,String字符串类型;当然还包括enum枚举、struct结构体构造类型;Array数组、Set集合、Dictionary字典集合类型;不仅如此还增加了高阶数据类型元组(Tuple),可选类型(Optinal)。
基础类型
Xcode 从6.0开始加入了Playground代码测试,可以实时查看代码执行结果,下面使用Playground简单演示一下Swift的基础内容,对Swift有个简单的认识:
import Foundation var a:Int=1 //通过var定义一个变量 //下面变量b虽然没有声明类型,但是会自动进行类型推断,这里b推断为Int类型 var b=2 var c:UInt=3 let d=a+b //通过let定义一个变量 //下面通过"\()"实现了字符串和变量相加(字符串插值),等价于println("d="+String(d)) println("d=\(d)") //结果:d=3 //注意由于Swift是强类型语言,a是Int类型而c是UInt类型,二者不能运算,下面的语句报错;但是注意如果是类似于:let a=1+2.0是不会报错的,因为两个都是字面量,Swift会首先计算出结果再推断a的类型 //let e=a+c //Int.max是Int类型的最大值,类似还有Int.min、Int32.max、Int32.min等 let e=Int.max //结果:9223372036854775807 var f:Float=1.0 var g=2.0 //浮点型自动推断为Double类型 var h:String="hello " //emoj表情也可以作为变量或者常量,事实上所有Unicode字符都是可以的 var 💖🍎="love and apple" //两个字符串相加,但是注意不同类型不能相加 var i=h+💖🍎 //结果:hello love and apple //布尔类型只有两个值true、false,类似于if语句中的条件只能是布尔类型不能像ObjC一样非0即真 var j:Bool=true //字符类型,同样使用双引号,但是只能是一个字符,如果不指定类型则"c"默认会推断为字符串(var k:Character="c"是字符类型,但是var k="c"是字符串类型) var k:Character="c" var l=00100 //等于100,可以在前面添加额外的0 var m=10_000_000 //等于10000000,可以使用增加额外的下划线方便阅读而不改变值的大小
- Swift通过var进行变量定义,通过let进行常量定义(这和其他高级语言比较类似,例如F#);
- Swift添加了类型推断,对于赋值的常量或者变量会自动推断其具体类型;
- Swift是强类型语言(应该说它比C#、Java等强类型语言控制还要严格),不同的数据类型之间不能隐式转化,如果需要转化只能强制转化;
- 在Swift中类型转换直接通过其类型构造函数即可,降低了API的学习成本;
集合类型
Swift提供了三种集合类型:数组Array、集合Set、字典Dictionary。和ObjC不同的是,由于Swift的强类型,集合中的元素必须是同一类型,而不能像ObjC一样可以存储任何对象类型,并且注意Swift中的集合是值类型而非引用类型(事实上包括String、结构体struct、枚举enum都是值类型)。
首先看一下Swift中的数组:
//声明数组的时候必须确定其类型,下面使用[String]声明一个字符串数组([String]是Array<String>简单表达形式) //var a:Array<String>=["hello","world"] var a:[String]=["hello","world"] a[0] //访问数组元素 //下面创建一个Double类型的数组,这里没有使用字面量,当前是一个空数组,当然也可以写成var b:[Double]=[] var b=[Double]() for i in a{ println("i=\(i)") } //添加元素,Swift中可变类型不再由单独的一个类型来表示,统统使用Array,如果想声明为不可变数组只要使用let定义即可 a.append("!") a+=["I" ,"am" ,"Kenshin"] //追加元素 println("a.count=\(a.count)") //结果:a.count=6 a[3...5]=["I","Love","Swift"] //修改元素,但是注意无法用这种方式添加元素 //a[6]=["."]//这种方式是错误的 a.insert("New", atIndex: 5) //插入元素:hello world! I Love New Swift a.removeAtIndex(5) //删除指定元素 //使用全局enumerate函数遍历数据索引和元素 for (index,element) in enumerate(a){ println("index=\(index),element=\(element)") } //使用构造函数限制数组元素个数并且指定默认值,等价于var c=Array(count: 3, repeatedValue: 1),自动推断类型 var c=[Int](count: 3, repeatedValue: 1)
Set表示没有顺序的集合:
//注意集合没有类似于数组的简化形式,例如不能写成var a:[String]=["hello","world"] var a:Set<String>=["hello","world"] var b:Set=[1,2] //类型推断:Set<Int> a.insert("!") //注意这个插入不保证顺序 if !a.isEmpty { //判断是否为空 a.remove("!") } if !a.contains("!"){ a.insert("!") }
Dictionary字典同样是没有顺序的,并且在Swift中字典同样要在使用时明确具体的类型。和ObjC中一样,字典必须保证key是唯一的,而这一点就要求在Swift中key必须是可哈希的,不过幸运的是Swift中的基本类型(如Int、Float、Double、Bool、String)都是可哈希的,都可以作为key。
//通过字面量进行字典初始化,注意等价于var a:Dictionary<Int,String>=[200:"success",404:"not found"] var a:[Int:String]=[200:"success",404:"not found"] var b=[200:"success",404:"not found"] //不声明类型,根据值自动推断类型 a[200] //读取字典 a[404]="can not found" //修改 a[500]="internal server error" //添加 //a=[:] //设置为空字典,等价于:a=[Int:String]() for code in a.keys{ println("code=\(code)") } for description in a.values{ println("description=\(description)") } for (code,description) in a{ println("code=\(code),description=\(description)") }
注意:在Swift中集合的可变性不是像ObjC一样由单独的数据类型来控制的,而是通过变量和常量来控制,这一点和其他高级语言比较类似。
元组(Tuple)
在开发过程中有时候会希望临时组织一个数据类型,此时可以使用一个结构体或者类,但是由于这个类型并没有那么复杂,如果定义起来又比较麻烦,此时可以考虑使用元组。
/** * 元组的基本用法 */ var point=(x:50,y:100) //自动推断其类型:(Int,Int) point.x //可以用类似于结构体的方式直接访问元素,结果:50 point.y //结果:100 point.0 //也可以采用类似数组的方式使用下标访问,结果:50 point.1 //结果:100 //元组也可以不指定元素名称,访问的时候只能使用下标 let frame:(Int,Int,Int,Float)=(0,0,100,100.0) println(frame) //结果:(0, 0, 100, 100.0) //注意下面的语句是错误的,如果指定了元组的类型则无法指定元素名称 //let frame:(Int,Int,Int,Int)=(x:0,y:0,width:100,height:100) var size=(width:100,25) //仅仅给其中一个元素命名 size.width //结果:100 size.1 //结果:25 var httpStatus:(Int,String)=(200,"success") //元组的元素类型并不一定相同 var (status,description)=httpStatus //一次性赋值给多个变量,此时status=200,description="success" //接收元组的其中一个值忽略另一个值使用"_"(注意在Swift中很多情况下使用_忽略某个值或变量) var (sta,_)=httpStatus println("sta=\(sta)") //结果:sta=200 /** * 元组作为函数的参数或返回值,借助元组实现了函数的多个返回值 */ func request()->(code:Int,description:String){ return (404,"not found") } var result=request() result.0 //结果:404 result.1 //结果:not found result.code //结果:404 result.description //结果:not found
可选类型
所谓可选类型就是一个变量或常量可能有值也可能没有值则设置为可选类型。在ObjC中如果一个对象类型没有赋值,则默认为nil,同时nil类型也只能作为对象类型的默认值,对于类似于Int等基本类型则对应0这样的默认值。由于Swift是强类型语言,如果在声明变量或常量时没有进行赋值,Swift并不会默认设置初值(这一点和其他高级语言不太一样,例如C#虽然也有可选类型,但是要求并没有那么严格)。
/** * 可选类型基础 */ var x:Float? //使用?声明成一个可选类型,如果不赋值默认为nil x=172.0 var y:Float=60.0 //var z=x+y //注意此句报错,因为Int和Int?根本就是两种不同的类型,在Swift中两种不同的类型不能运算(因为不会自动进行类型转化) var z=x!+y //使用!进行强制解包 var age="29" var ageInt=age.toInt() //注意ageInt是Int可选类型而不是Int类型(因为String的toInt()方法并不能保证其一定能转化为Int类型)
- Swift中类似于Int和Int?并不是同一种类型,不能进行相关运算,如果要运算只能解包;
- 可选类型其本质就是此类型内部存储分为“Some”和“None”两个部分,如果有值则存储到“Some”中,没有值则为“None”(早期Playground中可以看到两个部分,如今已经取消显示Some等描述了),使用感叹号强制解包的过程就是取出“Some”部分;
既然可选类型有可能有值,也可能没有值那么往往有时候就需要判断。可以使用if直接判断一个可选类型是否为nil,这样一来就可以根据情况进行强制解包(从Some部分取出值的过程);另一个选择就是在判断的同时如果有值则将值赋值给一个临时变量或常量,否则不进入此条件语句,这个过程称之为“可选绑定”。
/** * 可选类型判断 */ var age="29" var ageInt=age.toInt() //注意ageInt是Int可选类型而不是Int类型(因为String的toInt()方法并不能保证其一定能转化为Int类型) if ageInt==nil { println("ageInt=nil") }else{ println("ageInt=\(ageInt!)") //注意这里使用感叹号!强制解析 } /** * 可选类型绑定 * 如果可选类型有值则将值赋值给一个临时变量或者常量(此时此变量或者常量接受的值已经不是可选类型),如果没有值则不执行此条件 */ if let newAge=ageInt{ //此时newAge可以定义成常量也可以定义成变量 println("newAge=\(newAge)") //注意这里并不需要对newAge强制解包 }else{ println("ageInt=nil") }
通过前面的演示可以看出Swift中的可选绑定如果实际计算不得不进行强制解包,如果一个可选类型从第一次赋值之后就能保证有值那么使用时就不必进行强制解包了,这种情况下可以使用隐式可选解析类型(通过感叹号声明而不是问号)
/** * 隐式解析可选类型 */ var age:Int!=0 //通过感叹号声明隐式解析可选类型,此后使用时虽然是可选类型但是不用强制解包 age=29 var newAge:Int=age //不用强制解包直接赋值给Int类型(程序会自动解包) if var tempAge=age { println("tempAge=\(tempAge)") }else{ println("age=nil") }
运算符
Swift中支持绝大多数C语言的运算符并改进以减少不必要的错误(例如等号赋值后不返回值),算术运算会检查溢出情况,必要时还能使用新增的溢出运算符。另外Swift中还可以对浮点数使用取余运算符,新增了区间运算符。对于基本的运算符这里不再一一介绍,简单看一下Swift中的区间运算符和溢出运算符。
/** * 区间运算符,通常用于整形或者字符范围(例如"a"..."z") */ for i in 1...5 { //闭区间运算符...(从1到5,包含5) println("i=\(i)") } for i in 1..<5{ //半开区间运算符..<(从1到4) println("i=\(i)") } var str = "hello world." var range="a"..."z" for t in str { if range.contains(String(t)) { print(t) //结果:helloworld } } /** * 溢出运算符 */ var a=UInt8.max //a=255 //var b:UInt8=a+1 //注意b会出现溢出,此句报错 //下面使用溢出运算符,结果为:0,类似的还有&-、&*、&/ //使用溢出运算符可以在最大值和最小值之前循环而不会报错 var b:UInt8=a &+ 1
溢出运算符的原理其实很简单,例如对于UInt8,如果8位均为1则十进制表示是255,但是当加1之后则变成了9位“100000000”,出现了溢出但是UInt8本身值只能存储8位,所以取后面8位就变成了“00000000”,十进制表示就是0。
控制流
Swift中的多数控制流和其他语言差别并不大,例如for、while、do while、if、switch等,而且有些前面已经使用过(例如for in循环),这里将着重介绍一些不同点。
var a=["a","b","c","d","e","f","g"] let b=a[1] /** * switch支持一个case多个模式匹配,同时case后不用写break也会在匹配到种情况后自动跳出匹配,不存在隐式贯穿,如果想要贯穿在case之后添加"fallthrough"关键字 */ switch b{ case "a","b": println("b=a or b=b") case "c","d","e","f": println("b in (c,d,e,f)") default: println("b=g") } /** * 匹配区间,同时注意switch必须匹配所有情况,否则必须加上default */ let c:Int=88 switch c{ case 1...60: println("1-60") case 61...90: println("61-90") case 91...100: println("91-100") default: println("1>c>100") } /** * 元组匹配、值绑定、where条件匹配 * 注意下面的匹配没有default,因为它包含了所有情况 */ var d=(x:900,y:0) switch d{ case (0,0): println("d in (0,0)") case (_,0): //忽略x值匹配 println("d in y") case (0,let y)://值绑定 println("d in x,y=\(y)") case (-100...100,-100...100): //注意这里有可能和第一、二、三个条件重合,但是Swift允许多个case匹配同一个条件,但是只会执行第一个匹配 println("x in(0-100),y in (0-100)") case let (x,y) where x==y: //where条件匹配,注意这里的写法等同于:(let x,let y) where x==y println("x=y=\(x)") case let (x, y): println("x=\(x),y=\(y)") }
在其他语言中通常可以使用break、continue、return(Swift中添加了fallthrough)等来终止或者跳出某个执行语句,但是对于其行为往往是具有固定性的,例如break只能终止其所在的内层循环,而return只能跳出它所在的函数。在Swift中这种控制转移功能得到了加强,那就是使用标签。利用标签你可以随意指定转移的位置,例如下面的代码演示了如何直接通过标签跳出最外层循环:
var a=5 whileLoop: while --a>0 { for var i=0;i<a;++i{ println("a=\(a),i=\(i)") break whileLoop //如果此处直接使用break将跳出for循环,而由于这里使用标签直接跳出了while,结果只会打印一次,其结果为:a=4,i=0 } }
函数和闭包
函数
函数是一个完成独立任务的代码块,Swift中的函数不仅可以像C语言中的函数一样作为函数的参数和返回值,而且还支持嵌套,并且有C#一样的函数参数默认值、可变参数等。
//定义一个函数,注意参数和返回值,如果没有返回值可以不写返回值或者写成Void、空元组()(注意Void的本质就是空元组) func sum(num1:Int,num2:Int)->Int{ return num1 + num2 } sum(1, 2)
可以看到Swift中的函数仅仅表达形式有所区别(定义形式类似于Javascript,但是js不用书写返回值),但是本质并没有太大的区别。不过Swift中对函数参数强调两个概念就是局部参数名(又叫“形式参数”)和外部参数名,这极大的照顾到了ObjC开发者的开发体验。在上面的例子中调用sum函数并没有传递任何参数名,因为num1、num2仅仅作为局部参数名在函数内部使用,但是如果给函数指定一个外部参数名在调用时就必须指定参数名。另外前面也提到关于Swift中的默认参数、可变长度的参数,包括一些高级语言中的输入输出参数,通过下面的例子大家会有一个全面的了解。
/** /** * 函数参数名分为局部参数名和外部参数名 */ func split(string a:String,seperator b:Character)->[String]{ return split(a, maxSplit: Int.max, allowEmptySlices: false, isSeparator: {$0==b}) } //由于给split函数设置了外部参数名string和seperator,所以执行的时候必须带上外部参数名,此处可以看到一个有意义的外部参数名大大节省开发者使用成本 split(string: "hello,world,!", seperator: ",") //结果:["hello", "world", "!"] //下面通过在局部参数名前加上#来简写外部参数名(此时局部参数名和外部参数名相同) func split2(#string:String,#seperator:Character)->[String]{ return split(string, maxSplit: Int.max, allowEmptySlices: false, isSeparator: {$0==seperator}) } split2(string: "hello,world,!", seperator: ",") //上面的split函数的最后一个参数默认设置为",",注意如果使用默认参数那么此参数名将默认作为外部参数名(此时局部参数名和外部参数名相同) func split3(#string:String,seperator:Character=",")->[String]{ return split(string, maxSplit: Int.max, allowEmptySlices: false, isSeparator: {$0==seperator}) } split3(string: "hello,world,!", seperator: ",") //结果:["hello", "world", "!"] split3(string: "hello world !", seperator: " ") //结果:["hello", "world", "!"] //但是如果有默认值,又不想指定局部参数名可以使用“_”取消外部参数名 func split4(string:String,_ seperator:Character=",")->[String]{ return split(string, maxSplit: Int.max, allowEmptySlices: false, isSeparator: {$0==seperator}) } split4("hello,world,!", ",") //结果:["hello", "world", "!"] /** * 可变参数,一个函数最多有一个可变参数并且作为最后一个参数 * 下面strings参数在内部是一个[String],对于外部是不定个数的String参数 */ func joinStr(seperator:Character=",",strings:String...)->String{ var result:String="" for var i=0;i<strings.count;++i{ if i != 0{ result.append(seperator) } result+=strings[i] } return result } joinStr(seperator:" ", "hello","world","!") //结果:"hello world !" /** * 函数参数默认是常量,不能直接修改,通过声明var可以将其转化为变量(但是注意C语言参数默认是变量) * 但是注意这个变量对于外部是无效的,函数执行完就消失了 */ func sum2(var num1:Int,num2:Int)->Int{ num1 = num1 + num2 return num1 } sum2(1, 2) //结果:3 /** * 输入输出参数 * 通过输入输出参数可以在函数内部修改函数外部的变量(注意调用时不能是常量或字面量) * 注意:下面的swap仅仅为了演示,实际使用时请用Swift的全局函数swap */ func swap(inout a:Int ,inout b:Int){ a=a+b b=a-b a=a-b } var a=1,b=2 swap(&a, &b) //调用时参数加上“&”符号 println("a=\(a),b=\(b)") //结果:"a=2,b=1"
和很多语言一样,Swift中的函数本身也可以看做一种类型,既可以作为参数又可以作为返回值。
/** * 函数类型 */ var sum3=sum //自动推断sum3的类型:(Int,Int)->Int,注意不同的函数类型之间不能直接赋值 sum3(1,2) //结果:3 //函数作为返回值 func fn()->(Int,Int)->Int{ //下面的函数是一个嵌套函数,作用于是在fn函数内部 func minus(a:Int,b:Int)->Int{ return a-b } return minus; } var minus=fn() //函数作为参数 func caculate(num1:Int,num2:Int,fn:(Int,Int)->Int)->Int{ return fn(num1,num2) } caculate(1, 2, sum) //结果:3 caculate(1,2, minus) //结果:-1
闭包
Swift中的闭包其实就是一个函数代码块,它和ObjC中的Block及C#、Java中的lambda是类似的。闭包的特点就是可以捕获和存储上下文中的常量或者变量的引用,即使这些常量或者变量在原作用域已经被销毁了在代码块中仍然可以使用。事实上前面的全局函数和嵌套函数也是一种闭包,对于全局函数它不会捕获任何常量或者变量,而对于嵌套函数则可以捕获其所在函数的常量或者变量。通常我们说的闭包更多的指的是闭包表达式,也就是没有函数名称的代码块,因此也称为匿名闭包。
在Swift中闭包表达式的定义形式如下:
{ ( parameters ) -> returnType in
statements
}
下面通过一个例子看一下如何通过闭包表达式来简化一个函数类型的参数,在下面的例子中闭包的形式也是一而再再而三的被简化,充分说明了Swift语法的简洁性:
func sum(num1:Int,num2:Int)->Int{ return num1 + num2 } func minus(num1:Int,num2:Int)->Int{ return num1 - num2 } func caculate(num1:Int,num2:Int,fn:(Int,Int)->Int)->Int{ return fn(num1,num2) } var (a,b)=(1,2) caculate(a, b, sum) //结果:3 caculate(a, b, minus) //结果:-1 //利用闭包表达式简化闭包函数 caculate(a, b, {(num1:Int,num2:Int)->Int in return num1 - num2 }) //结果:-1 //简化形式,根据上下文推断类型并且对于单表达式闭包(只有一个语句)可以隐藏return关键字 caculate(a, b, { num1,num2 in num1 - num2 }) //结果:-1 //再次简化,使用参数名缩写,使用$0...$n代表第n个参数,并且此in关键字也省略了 caculate(a, b, { $0 - $1 }) //结果:-1
考虑到闭包表达式的可读取性,Swift中如果一个函数的最后一个参数是一个函数类型的参数(或者说是闭包表达式),则可以将此参数写在函数括号之后,这种闭包称之为“尾随闭包”。
func sum(num1:Int,num2:Int)->Int{ return num1 + num2 } func minus(num1:Int,num2:Int)->Int{ return num1-num2 } func caculate(num1:Int,num2:Int,fn:(Int,Int)->Int)->Int{ return fn(num1,num2) } var (a,b)=(1,2) //尾随闭包,最后一个参数是闭包表达式时可以卸载括号之后,同时注意如果这个函数只有一个闭包表达式参数时可以连通括号一块省略 //请注意和函数定义进行区分 caculate(a, b){ $0 - $1 } //结果:-1
前面说过闭包之所以称之为“闭包”就是因为其可以捕获一定作用域内的常量或者变量进而闭合并包裹着。
func add()->()->Int{ var total=0 var step=1 func fn()->Int{ total+=step return total } return fn } //fn捕获了total和step,尽管下面的add()执行完后total和step被释放,但是由于fn捕获了二者的副本,所以fn会随着两个变量的副本一起被存储 var a=add() a() //结果:1 a() //结果:2,说明a中保存了total的副本(否则结果会是1) var b=add() b() //结果:1 ,说明a和b单独保存了total的副本(否则结果会是3) var c=b c() //结果:2,说明闭包是引用类型,换句话说函数是引用类型(否则结果会是1)
Swift会自动决定捕获变量或者常量副本的拷贝类型(值拷贝或者引用拷贝)而不需要开发者关心,另外被捕获的变量或者常量的内存管理同样是由Swift来管理,例如当上面的函数a不再使用了那么fn捕获的两个变量也就释放了。
类
作为一门面向对象语言,类当然是Swift中的一等类型。首先通过下面的例子让大家对Swift的class有一个简单的印象,在下面的例子中可以看到Swift中的属性、方法(包括构造方法和析构方法):
//Swift中一个类可以不继承于任何其他基类,那么此类本身就是一个基类 class Person { //定义属性 var name:String var height=0.0 //构造器方法,注意如果不编写构造方法默认会自动创建一个无参构造方法 init(name:String){ self.name=name } //定义方法 func showMessage(){ println("name=\(name),height=\(height)") } //析构方法,在对象被释放时调用,类似于ObjC的dealloc,注意此函数没有括号,没有参数,无法直接调用 deinit{ println("deinit...") } } var p=Person(name: "Kenhin") p.height=172.0 p.showMessage() //结果:name=Kenhin,height=172.0 //类是引用类型 var p2 = p p2.name = "Kaoru" println(p.name) //结果:Kaoru if p === p2 { //“===”表示等价于,这里不能使用等于“==”(等于用于比较值相等,p和p2是不同的值,只是指向的对象相同) println("p===p2") //p等价于p2,二者指向同一个对象 }
从上面的例子不难看出:
- Swift中的类不必须继承一个基类(但是ObjC通常必须继承于NSObject),如果一个类没有继承于任何其他类则这个类也称为“基类”;
- Swift中的属性定义形式类似于其他语句中的成员变量(或称之为“实例变量”),尽管它有着成员变量没有的特性;
- Swift中如果开发者没有自己编写构造方法那么默认会提供一个无参数构造方法(否则不会自动生成构造方法);
- Swift中的析构方法没有括号和参数,并且不支持自行调用;
属性
Swift中的属性分为两种:存储属性(用于类、结构体)和计算属性(用于类、结构体、枚举),并且在Swift中并不强调成员变量的概念。 无论从概念上还是定义方式上来看存储属性更像其他语言中的成员变量,但是不同的是可以控制读写操作、通过属性监视器来属性的变化以及快速实现懒加载功能。
class Account { var balance:Double=0.0 } class Person { //firstName、lastName、age是存储属性 var firstName:String var lastName:String let age:Int //fullName是一个计算属性,并且由于只定义了get方法,所以是一个只读属性 var fullName:String{ get{ return firstName + "." + lastName } set{ let array=split(newValue, maxSplit: Int.max, allowEmptySlices: false, isSeparator: {$0=="."}) if array.count == 2 { firstName=array[0] lastName=array[1] } } //set方法中的newValue表示即将赋的新值,可以自己设置set中的newValue变量,如下: // set(myValue){ // } } //如果fullName只有get则是一个只读属性,只读属性可以简写如下: // var fullName:String{ // return firstName + "." + lastName // } //属性的懒加载,第一次访问才会计算初始值,在Swift中懒加载的属性不一定就是对象类型,也可以是基本类型 lazy var account = Account() //构造器方法,注意如果不编写构造方法默认会自动创建一个无参构造方法 init(firstName:String,lastName:String,age:Int){ self.firstName=firstName self.lastName=lastName self.age=age } //定义方法 func showMessage(){ println("name=\(self.fullName)") } } var p=Person(firstName: "Kenshin", lastName: "Cui",age:29) p.fullName="Kaoru.Sun" p.account.balance=10 p.showMessage()
需要提醒大家的是:
- 计算属性并不直接存储一个值,而是提供getter来获取一个值,或者利用setter来间接设置其他属性;
- lazy属性必须有初始值,必须是变量不能是常量(因为常量在构造完成之前就已经确定了值);
- 在构造方法之前存储属性必须有值,无论是变量属性(var修饰)还是常量属性(let修饰)这个值既可以在属性创建时指定也可以在构造方法内指定;
从上面的例子中不难区分存储属性和计算属性,计算属性通常会有一个setter、getter方法,如果要监视一个计算属性的变化在setter方法中即可办到(因为在setter方法中可以newValue或者自定义参数名),但是如果是存储属性就无法通过监视属性的变化过程了,因为在存储属性中是无法定义setter方法的。不过Swift为我们提供了另外两个方法来监视属性的变化那就是willSet和didSet,通常称之为“属性监视器”或“属性观察器”。
class Account { //注意设置默认值0.0时监视器不会被调用 var balance:Double=0.0{ willSet{ self.balance=2.0 //注意newValue可以使用自定义值,并且在属性监视器内部调用属性不会引起监视器循环调用,注意此时修改balance的值没有用 println("Account.balance willSet,newValue=\(newValue),value=\(self.balance)") } didSet{ self.balance=3.0 //注意oldValue可以使用自定义值,并且在属性监视器内部调用属性不会引起监视器循环调用,注意此时修改balance的值将作为最终结果 println("Account.balance didSet,oldValue=\(oldValue),value=\(self.balance)") } } } class Person { var firstName:String var lastName:String let age:Int var fullName:String{ get{ return firstName + "." + lastName } set{ //对于计算属性可以直接在set方法中进行属性监视 let array=split(newValue, maxSplit: Int.max, allowEmptySlices: false, isSeparator: { $0 == "." }) if array.count == 2 { firstName=array[0] lastName=array[1] } } } lazy var account = Account() init(firstName:String,lastName:String,age:Int){ self.firstName=firstName self.lastName=lastName self.age=age } //类型属性 static var skin:Array<String>{ return ["yellow","white","black"]; } } var p=Person(firstName: "Kenshin", lastName: "Cui",age:29) p.account.balance=1.0 println("p.account.balance=\(p.account.balance)") //结果:p.account.balance=3.0 for color in Person.skin { println(color) }
- 和setter方法中的newValue一样,默认情况下载willSet和didSet中会有一个newValue和oldValue参数表示要设置的新值和已经被修改过的旧值(当然参数名同样可以自定义);
- 存储属性的默认值设置不会引起属性监视器的调用(另外在构造方法中赋值也不会引起属性监视器调用),只有在外部设置存储属性才会引起属性监视器调用;
- 存储属性的属性监视器willSet、didSet内可以直接访问属性,但是在计算属性的get、set方法中不能直接访问计算属性,否则会引起循环调用;
- 在didSet中可以修改属性的值,这个值将作为最终值(在willSet中无法修改);
方法
方法就是与某个特定类关联的函数,其用法和前面介绍的函数并无二致,但是和ObjC相比,ObjC中的函数必须是C语言,而方法则必须是ObjC。此外其他语言中方法通常存在于类中,但是Swift中的方法除了在类中使用还可以在结构体、枚举中使用。关于普通的方法这里不做过多赘述,用法和前面的函数区别也不大,这里主要看一下构造方法。
class Person { //定义属性 var name:String var height:Double var age=0 //指定构造器方法,注意如果不编写构造方法默认会自动创建一个无参构造方法 init(name:String,height:Double,age:Int){ self.name=name self.height=height self.age=age } //便利构造方法,通过调用指定构造方法、提供默认值来简化构造方法实现 convenience init(name:String){ self.init(name:name,height:0.0,age:0) } //实例方法 func modifyInfoWithAge(age:Int,height:Double){ self.age=age self.height=height } //类型方法 class func showClassName(){ println("Class name is \"Person\"") } //析构方法,在对象被释放时调用,类似于ObjC的dealloc,注意此函数没有括号,没有参数,无法直接调用 deinit{ println("deinit...") } } //通过便利构造方法创建对象 var p=Person(name: "kenshin")
- 除构造方法、析构方法外的其他方法的参数默认除了第一个参数是局部参数,从第二个参数开始既是局部参数又是外部参数(这种方式和ObjC的调用方式很类似,当然,可以使用“#”将第一个参数同时声明为外部参数名,也可以使用“_”将其他参数设置为非外部参数名)。但是,对于函数,默认情况下只有默认参数既是局部参数又是外部参数,其他参数都是局部参数。
- 构造方法的所有参数默认情况下既是外部参数又是局部参数;
- Swift中的构造方法分为“指定构造方法”和“便利构造方法(convenience)”,指定构造方法是主要的构造方法,负责初始化所有存储属性,而便利构造方法是辅助构造方法,它通过调用指定构造方法并指定默认值的方式来简化多个构造方法的定义,但是在一个类中至少有一个指定构造方法。
下标脚本
下标脚本是一种访问集合的快捷方式,例如:var a:[string],我们经常使用a[0]、a[1]这种方式访问a中的元素,0和1在这里就是一个索引,通过这种方式访问或者设置集合中的元素在Swift中称之为“下标脚本”(类似于C#中的索引器)。从定义形式上通过“subscript”关键字来定义一个下标脚本,很像方法的定义,但是在实现上通过getter、setter实现读写又类似于属性。假设用Record表示一条记录,其中有多列,下面示例中演示了如何使用下标脚本访问并设置某一列的值。
class Record { //定义属性,假设store是Record内部的存储结构 var store:[String:String] init(data:[String:String]){ self.store=data } //下标脚本(注意也可以实现只有getter的只读下标脚本) subscript(index:Int)->String{ get{ var key=sorted(Array(self.store.keys))[index] return self.store[key]! } set{ var key=sorted(Array(self.store.keys))[index] self.store[key]=newValue //newValue参数名可以像属性一样重新自定义 } } //下标脚本(重载) subscript(key:String)->String{ get{ return store[key]! } set{ store[key]=newValue } } } var r=Record(data:["name":"kenshin","sex":"male"]) println("r[0]=\(r[0])") //结果:r[0]=kenshin r["sex"]="female" println(r[1]) //结果:female
继承
和ObjC一样,Swift也是单继承的(可以实现多个协议,此时协议放在后面),子类可以调用父类的属性、方法,重写父类的方法,添加属性监视器,甚至可以将只读属性重写成读写属性。
class Person { var firstName:String,lastName:String var age:Int=0 var fullName:String{ get{ return firstName+" "+lastName } } init(firstName:String,lastName:String){ self.firstName=firstName self.lastName=lastName } func showMessage(){ println("name=\(fullName),age=\(age)") } //通过final声明,子类无法重写 final func sayHello(){ println("hello world.") } } class Student: Person { //重写属性监视器 override var firstName:String{ willSet{ println("oldValue=\(firstName)") } didSet{ println("newValue=\(firstName)") } } var score:Double //子类指定构造方法一定要调用父类构造方法 //并且必须在子类存储属性初始化之后调用父类构造方法 init(firstName:String,lastName:String, score:Double){ self.score=score super.init(firstName: firstName, lastName: lastName) } convenience init(){ self.init(firstName:"",lastName:"",score:0) } //将只读属性重写成了可写属性 override var fullName:String{ get{ return super.fullName; } set{ let array=split(newValue, maxSplit: Int.max, allowEmptySlices: false, isSeparator: { $0 == "." }) if array.count == 2 { firstName=array[0] lastName=array[1] } } } //重写方法 override func showMessage() { println("name=\(fullName),age=\(age),score=\(score)") } } var p=Student() p.firstName="kenshin"
在使用ObjC开发时init构造方法并不安全,首先无法保证init方法只调用一次,其次在init中不能访问属性。但是这些完全依靠文档约定,编译时并不能发现问题,出错检测是被动的。在Swift中构造方法(init)有了更为严格的规定:构造方法执行完之前必须保证所有存储属性都有值。这一点不仅在当前类中必须遵循,在整个继承关系中也必须保证,因此就有了如下的规定:
- 子类的指定构造方法必须调用父类构造方法,并确保调用发生在子类存储属性初始化之后。而且指定构造方法不能调用同一个类中的其他指定构造方法;
- 便利构造方法必须调用同一个类中的其他指定构造方法(可以是指定构造方法或者便利构造方法),不能直接调用父类构造方法(用以保证最终以指定构造方法结束);
- 如果父类仅有一个无参构造方法(不管是否包含便利构造方法),子类的构造方法默认就会自动调用父类的无参构造方法(这种情况下可以不用手动调用);
- 常量属性必须默认指定初始值或者在当前类的构造方法中初始化,不能在子类构造方法中初始化;
协议
协议是对实例行为的一种约束,和ObjC类似,在Swift中可以定义属性和方法(ObjC中之所以能定义属性是因为@property的本质就是setter、getter方法)。和其他语言不同的是Swift中的协议不仅限于类的实现,它同样可以应用于枚举、结构体(如果只想将一个协议应用于类,可以在定义协议时在后面添加class关键字来限制其应用范围)。
protocol Named{ //定义一个实例属性 var name:String { get set } //定义一个类型属性 static var className:String { get } //定义构造方法 init(name:String) //定义一个实例方法 func showName() //定义一个类型方法 static func showClassName() } protocol Scored{ var score:Double { get set } } //Person遵循了Named协议 class Person:Named { //注意从Named协议中并不知道name是存储属性还是计算属性,这里将其作为存储属性实现 var name:String var age:Int = 0 static var className:String{ return "Person" } //协议中规定的构造方法,必须使用required关键字声明,除非类使用final修饰 required init(name:String){ self.name=name } //遵循showName方法 func showName() { println("name=\(name)") } //遵循showClassName方法 static func showClassName() { println("Class name is \"Person\"") } } //Student继承于Person并且实现了Scored协议 class Student: Person,Scored { var score:Double=0.0 init(name:String, score:Double){ self.score = score super.init(name: name) } //由于上面自定义了构造方法则必须实现协议中规定的构造方法 required init(name: String) { super.init(name: name) } func test(){ println("\(self.name) is testing.") } } var p=Person(name: "Kenshin Cui") p.showName() //结果:name=Kenshin Cui println("className=\(Person.className)") //结果:className=Person Person.showClassName() //结果:Class name is "Person" p.age=28 var s:Named=Student(name: "Kaoru",score:100.0) //尽管这里将s声明为Named类型,但是运行时仍然可以正确的解析(多态),但是注意此时编译器并不知道s有test方法,所以此时调用test()会报错 s.showName() //在下面的函数中要求参数stu必须实现两个协议 func showMessage(stu:protocol<Named,Scored>){ println("name=\(stu.name),score=\(stu.score)") } var s2=Student(name: "Tom",score:99.0) showMessage(s2) //结果:name=Tom,age=99.0 //检测协议 let b1 = s is Scored //判断p是否遵循了Scored协议 if b1 { println("s has score property.") } //类型转化 if let s3 = s as? Scored { //如果s转化成了Scored类型则返回实例,否则为nil println("s3' score is \(s3.score)") //结果:s3' score is 100.0 } let s4 = s as! Scored //强制转换,如果转化失败则报错 println("s4' score is \(s4.score)") //结果:s4' score is 100.0
- 协议中虽然可以指定属性的读写,但即使协议中规定属性是只读的但在使用时也可以将其实现成可读写的;
- Swift的协议中可以约定属性是实例属性还是类型属性、是读写属性还是只读属性,但是不能约束其是存储属性还是计算属性;
- 协议中的类型属性和类型方法使用static修饰而不是class(尽管对于类的实现中类型属性、类型方法使用class修饰);
- 协议中约定的方法支持可变参数,但是不支持默认参数;
- 协议中约定的构造方法,在实现时如果不是final类则必须使用require修饰(以保证子类如果需要自定义构造方法则必须覆盖父类实现的协议构造方法,如果子类不需要自定义构造方法则不必);
- 一个协议可以继承于另外一个或多个协议,一个类只能继承于一个类但可以实现多个协议;
- 协议本身就是一种类型,这也体现除了面向对象的多态特征,可以使用多个协议的合成来约束一个实例参数必须实现某几个协议;
扩展
Swift中的扩展就类似于ObjC中的分类(事实上在其他高级语言中更多的称之为扩展而非分类),但是它要比分类强大的多,它不仅可以扩展类还可以扩展协议、枚举、结构体,另外扩展也不局限于扩展方法(实例方法或者类型方法),还可以扩展便利构造方法、计算属性、下标脚本、
class Person { var firstName:String,lastName:String var age:Int=0 var fullName:String{ get{ return firstName+" "+lastName } } init(firstName:String,lastName:String){ self.firstName=firstName self.lastName=lastName } func showMessage(){ println("name=\(fullName),age=\(age)") } } extension Person{ //只能扩展便利构造方法,不能扩展指定构造方法 convenience init(){ self.init(firstName:"",lastName:"") } //只能扩展计算属性,无法扩展存储属性 var personInfo:String{ return "firstName=\(firstName),lastName=\(lastName),age=\(age)"; } //扩展实例方法 func sayHello(){ println("hello world.") } //嵌套类型 enum SkinColor{ case Yellow,White,Black } //扩展类型方法 static func skin()->[SkinColor]{ return [.Yellow,.White,.Black] } } var p=Person() p.firstName="Kenshin" p.lastName="Cui" p.age=28 println(p.personInfo) //结果:firstName=Kenshin,lastName=Cui,age=28 p.sayHello() //结果:hello world. Person.skin()
枚举和结构体
结构体
结构体和类是构造复杂数据类型时常用的构造体,在其他高级语言中结构体相比于类要简单的多(在结构体内部仅仅能定义一些简单成员),但是在Swift中结构体和类的关系要紧密的多,这也是为什么将结构体放到后面来说的原因。Swift中的结构体可以定义属性、方法、下标脚本、构造方法,支持扩展,可以实现协议等等,很多类可以实现的功能结构体都能实现,但是结构体和类有着本质区别:类是引用类型,结构体是值类型。
struct Person { var firstName:String var lastName:String var fullName:String{ return firstName + " " + lastName } var age:Int=0 //构造函数,如果定义了构造方法则不会再自动生成默认构造函数 // init(firstName:String,lastName:String){ // self.firstName=firstName // self.lastName=lastName // } func showMessage(){ println("firstName=\(firstName),lastName=\(lastName),age=\(age)") } //注意对于类中声明类型方法使用关键字class修饰,但结构体里使用static修饰 static func showStructName(){ println("Struct name is \"Person\"") } } //注意所有结构体默认生成一个全员逐一构造函数,一旦自定义构造方法,这个默认构造方法将不会自动生成 var p=Person(firstName: "Kenshin", lastName: "Cui", age: 28) println(p.fullName) //结果:Kenshin Cui p.showMessage() //结果:firstName "Kenshin", lastName "Cui", age 28 Person.showStructName() //结果:Struct name is "Person" //由于结构体(包括枚举)是值类型所以赋值、参数传递时值会被拷贝(所以下面的实例中p2修改后p并未修改,但是如果是类则情况不同) var p2 = p p2.firstName = "Tom" println(p2.fullName) //结果:Tom Cui println(p.fullName) //结果:Kenshin Cui
- 默认情况下如果不自定义构造函数那么将自动生成一个无参构造函数和一个全员的逐一构造函数;
- 由于结构体是值类型,所以它虽然有构造函数但是没有析构函数,内存释放系统自动管理不需要开发人员过多关注;
- 类的类型方法使用class修饰(以便子类可以重写),而结构体、枚举的类型方法使用static修饰(补充:类方法也可以使用static修饰,但是不是类型方法而是静态方法;另外类的存储属性如果是类型属性使用static修饰,而类中的计算属性如果是类型属性使用class修饰以便可以被子类重写;换句话说class作为“类型范围作用域”来理解时只有在类中定义类型方法或者类型计算属性时使用,其他情况使用static修饰[包括结构体、枚举、协议和类型存储属性]);
类的实例通常称之为“对象”,而在Swift中结构体也可以有实例,因此对于很多二者都可以实现的功能,在文中称之为实例而没有使用对象的概念。
枚举
在其他语言中枚举本质就是一个整形,只是将这组相关的值组织起来并指定一个有意义的名称。但是在Swift中枚举不强调一个枚举成员必须对应一个整形值(当然如果有必要仍然可以指定),并且枚举类型的可以是整形、浮点型、字符、字符串。首先看一下枚举的基本使用:
//注意Swift中的枚举默认并没有对应的整形值,case用来定义一行新的成员,也可以将多个值定义到同一行使用逗号分隔,例如:case Spring,Summer,Autumn,Winter enum Season{ case Spring case Summer case Autumn case Winter } var s=Season.Spring //一旦确定了枚举类型,赋值时可以去掉类型实现简写 s = .Summer switch s { case .Spring: //由于Swift的自动推断,这里仍然可以不指明类型 println("spring") case .Summer: println("summer") case .Autumn: println("autumn") default: println("winter") }
事实上Swift中也可以指定一个值和枚举成员对应,就像其他语言一样(通常其他语言的枚举默认就是整形),但是Swift又不局限于整形,它可以是整形、浮点型、字符串、字符,但是原始值必须是一种固定类型而不能存储多个不同的类型,同时如果原始值为整形则会像其他语言一样默认会自动递增。
//指定原始值(这里定义成了整形) enum Season:Int{ case Spring=10 //其他值会默认递增,例如Summer默认为11,如果此处也不指定值会从0开始依次递增 case Summer case Autumn case Winter } var summer=Season.Summer //使用rawValue访问原始值 println("summer=\(summer),rawValue=\(summer.rawValue)") //通过原始值创建枚举类型,但是注意它是一个可选类型 var autumn=Season(rawValue: 12) //可选类型绑定 if let newAutumn=autumn{ println("summer=\(newAutumn),rawValue=\(newAutumn.rawValue)") }
如果一个枚举类型能够和一些其他类型的数据一起存储起来往往会很有用,因为这可以让你存储枚举类型之外的信息(类似于其他语言中对象的tag属性,但是又多了灵活性),这在其他语言几乎是不可能实现的,但是在Swift中却可以做到,这在Swift中称为枚举类型相关值。要注意的是相关值并不是原始值,原始值需要事先存储并且只能是同一种类型,但是相关值只有创建一个基于枚举的变量或者常量时才会指定,并且类型可以不同(原始值更像其他语言的枚举类型)。
//相关值 enum Color{ case RGB(String) //注意为了方便演示这里没有定义成三个Int类型(例如: RGB(Int,Int,Int))而使用16进制字符串形式 case CMYK(Float,Float,Float,Float) case HSB(Int,Int,Int) } var red=Color.RGB("#FF0000") var green=Color.CMYK(0.61, 0.0, 1.0, 0.0) var blue=Color.HSB(240, 100, 100) switch red { case .RGB(let colorStr): println("colorStr=\(colorStr)") case let .CMYK(c,m,y,k): println("c=\(c),m=\(m),y=\(y),k=\(k)") case let .HSB(h,s,b): println("h=\(h),s=\(s),b=\(b)") }
上面提到其实枚举也有一些类型和结构体的特性,例如计算属性(包括类型属性,枚举只能定义计算属性不能定义存储属性,存储属性只能应用于类和结构体)、构造方法(其实上面使用原始值创建枚举的例子就是一个构造方法)、方法(实例方法、类型方法)、下标脚本 。
enum Season:Int{ case Spring=0 ,Summer,Autumn,Winter //定义计算属性 var tag:Int{ return self.rawValue } //类型属性 static var enumName:String{ return "Season" } // //定义构造方法,注意在枚举的构造函数中则必须保证self有值(正如类的构造方法必须保证其存储属性有值一样) // init(prefix:String){ // switch prefix.lowercaseString { // case "sp": // self = .Spring // case "su": // self = .Summer // case "au": // self = .Autumn // default: // self = .Winter // } // } //其实上面的构造器有些不合理,那就是default就是Winter,事实上这类构造器可能传任何参数,此时可以使用可失败构造函数来解决 //可失败构造函数返回nil(尽管Swift中构造函数是不返回值的,但是此时约定返回nil代表构造失败) init?(prefix:String){ switch prefix.lowercaseString { case "sp": self = .Spring case "su": self = .Summer case "au": self = .Autumn case "wi": self = .Winter default: return nil } } //定义实例方法 func showMessage(){ println("rowValue=\(self.rawValue)") } //定义类型方法 static func showEnumName(){ println("Enum name is \"Season\"") } } var summer=Season.Summer println(summer.tag) //结果:1 println(Season.enumName) //结果:Season Season.showEnumName() //结果:Enum name is "Season" summer.showMessage() //结果:rowValue=1 if let spring = Season(prefix: "au") { //可选绑定,构造函数返回值可能为nil println(spring.tag) //结果:2 }
之所以没有将枚举、结构体放到上面的数据类型部分介绍一方面Swift中的枚举、结构体和其他语言中有较大差别,另一方面是因为这个部分的介绍要用到前面的知识。
泛型
泛型可以让你根据需求使用一种抽象类型来完成代码定义,在使用时才真正知道其具体类型。这样一来就好像在定义时使用一个占位符做一个模板,实际调用时再进行模板套用,所以在C++中也称为“模板”。泛型在Swift中被广泛应用,上面介绍的Array<>、Dictionary<>事实上都是泛型的应用。通过下面的例子简单看一下泛型参数和泛型类型的使用。
/*泛型参数*/ //添加了约束条件的泛型(此时T必须实现Equatable协议) func isEqual<T:Equatable>(a:T,b:T)->Bool{ return a == b } var a:Int=1,b:Int=2 println(isEqual(a,b)) //结果:false var c:String="abc",d:String="abc" println(isEqual(c,d)) //结果:true /*泛型类型*/ struct Stack<T> { var store:[T]=[] //在结构体、枚举中修改其变量需要使用mutating修饰(注意类不需要) mutating func push(item:T){ store.append(item) } mutating func pop()->T{ return store.removeLast() } } var s = Stack<Int>() s.push(1) let t = s.pop() println("t=\(t)") //结果:t=1 //扩展泛型类型 extension Stack{ var top:T?{ return store.last } } s.push(2) println(s.top!) //结果:2
上面演示了泛型结构体用法,其实类同样是类似的,这里就不在赘述了,但是如果遇到泛型协议怎么办呢?假设Stack必须遵循一个Stackable协议,此时就必须在协议中引入一个关联类型来解决。
protocol Stackable{ //声明一个关联类型 typealias ItemType mutating func push(item:ItemType) mutating func pop()->ItemType; } struct Stack:Stackable{ var store:[T]=[] mutating func push(item:T){ store.append(item) } mutating func pop()->T{ return store.removeLast() } } var s = Stack() s.push("hello") s.push("world") let t = s.pop() println("t=\(t)") //结果:t=world
本作品采用知识共享署名 2.5 中国大陆许可协议进行许可,欢迎转载,演绎或用于商业目的。但转载请注明来自崔江涛(KenshinCui),并包含相关链接。 |