Swift入门指南(iOS9 Programming Fundamentals With swift)
Swift语法中最具特色也最重要的就是声明和调用函数的方式;
所有代码都位于函数中,而动作则是由函数触发的;
print(sum(x: 4, 5))
func sum(x:Int , _ y:Int) -> Int {
let z = x + y
return z
}
基于这个函数,我们分块介绍:
1.声明从关键字func开始,后跟函数的名字;
调用函数时必须使用函数名,所谓的调用就是运行函数所包含的代码;
2.每个参数都有严格的格式:参数名,一个冒号,然后是参数类型;
3.y之前的下划线和空格,对应的是隐藏对外参数名(之后介绍);
4.箭头运算符->,后跟函数所返回的值类型;
5.使用关键字return,后跟返回值;
关键字return实际上做了两件事:它返回后面的值,同时又终止了函数的执行;
1.无返回类型参数:
有三种声明方式:可以返回Void,可以返回(),还可以完全省略箭头运算符与返回类型;
如果函数没有返回值,那么函数体就无须包含return语句;如果包含了return语句,那么目的就纯粹是在该处终止函数的执行;
Void(无返回值的函数的返回类型)
()是一个字面值(后面会介绍字面值()的含义)
函数声明return()是合法的;无论是否声明,()就是函数所返回的,写成return()或return;(后面加一个分号)有助于消除歧义,否则Swift可能会认为函数会返回下一行的内容;
可以将函数返回值捕获并赋给Void类型的变量;
2.无参数的函数:
函数声明中的参数列表可以完全为空;
省略参数列表圆括号本身是不可以的,圆括号需要出现在函数声明中,位于函数名之后;
就像不能省略函数声明中的圆括号(参数列表)一样,你也不能省略函数调用中的圆括号;
testfunc()
func testfunc() -> () {
return()
}
可以通过输入输出的类型来描述一个函数,如 (Int,Int)-> Int ,这是一个合法的表达式,他就是函数签名;
该签名描述了接收这些类型,具有这些数量的参数,并返回该类型结果的函数;
函数的签名是其类型——函数的类型;
函数的签名必须包含参数列表与返回类型,两者为空时,可有四种等价方式,包括Void -> Void 与 () -> ();
函数可以外化其参数名;
外部名称要作为实参的标签出现在对函数的调用中;
原因如下:
1.阐明了每个实参的作用:每个实参名都能表示出自己对函数动作的作用;
2.将函数区分开来:两个名字可能有相同的名字和标签,但却拥有不同的外化参数名;
3.有助于Swift与OC和Cocoa通信,后者的方法参数几乎总是有外化名字的;
外化参数名:将外部名字放在内部参数名之前,中间用逗号隔开;
标准规则:默认下,除第一个参数外,所有参数名都会自动外化(实际上是所有的参数均自动外化);
原因有二:
1.第一个参数通常不需要外部名,因为函数名通常能够清晰的表明第一个参数的含义;
2.这个约定使得Swift函数能够与OC方法交互,而后者采取的就是这种方式;
Swift的String和OC的NSString能够自动桥接彼此;
如果函数是自己定义的(即是你声明的),并且OC永远不会调用它,这样就没必要遵循OC的要求了;
外化参数名,将外部名放到内部名之前即可,可相同;
若不想对参数进行外化,可在内部名之前加一个下划线和一个空格;
sum(x: 3, 4)
sum(3, y: 4)
sum(xx: 3, yy: 4)
func sum(_ x:Int , y:Int) -> Int {
let z = x + y
return z
}
func sum(xx x:Int , yy y:Int) -> Int {
let z = x + y
return z
}
什么是函数的名字:
从技术上来说,一个Swift函数的名字是由圆括号之前的名字加上参数的外部名共同构成的;如果阻止了参数的外部名,那么可以使用一个下划线来表示其外部名;
在Swift中,函数重载是合法的:具有相同函数名(包括外部参数名)的两个函数可以共存,只要签名不同即可;
重载也适用于返回类型:具有相同名字与相同参数类型的两个函数可以有不同的返回类型;不过调用上下文一定不能有歧义;也就是说,一定要清楚调用者需要什么样的返回类型;
OC是不允许重载的:如果OC中声明了相同的方法的两个重载版本,那么编译器就会报“Duplicate declaration”错误;
实际上,如果在Swift中声明了两个重载方法,但是OC却能看到它们,那么就会遇到一个Swift编译错误,因为这种重载与OC是不兼容的;
具有相同标签和不同外部参数名的两个函数并不构成重载;由于函数有着不同的外部参数名,因此他们是名字不同的两个不同函数;
参数可以有一个默认值:调用者完全可以省略参数,不为其提供实参,对应参数的值就是默认值;
要提供默认值,在声明中的参数类型后追加一个=号和默认值;
请保证调用时实参的顺序与声明时形参的顺序一致;
saySome("woof")
saySome("woof", times: 2)
func saySome(_ word:String , times:Int = 3) -> Void {
print("say \(word) \(times)times")
}
参数可以是可变参数:调用者可以根据需要提供多个参数类型的值,中间用逗号隔开;函数体会将这些值当做数组;
sayStrings(strings: "a","b","c","d", times: 3)
func sayStrings(strings:String... , times:Int = 1) -> Void {
for _ in 1...times {
for str in strings {
print(str + " print", separator: ",", terminator: ",")
}
print("\n")
}
}
一个函数最多只能声明一个可变参数(否则就无法确定值列表结束的位置)
全局print函数的第一个参数就是一个可变参数;
遗憾的是,Swift语言有一个陷阱:没法将数组转换为逗号分隔的参数列表(相比于Ruby中的splat)。如果一开始就有一个某种类型的数组,那么你不能在需要该类型可变参数的地方使用它;
局部名为下划线的参数会被忽略,由于没有内部名,也无法在函数体中被引用,仅作为对自己的提示“我知道这里有个参数,只不过故意不用”
如果希望函数能够修改传递给他的实参的初始值,需要作出三种改变:
1.要修改的参数必须声明为inout;
2.在调用时,持有待修改值的变量必须要声明为var,而不是let;
3.相比于将变量作为实参进行传递,我们传递的是地址,这是通过在名字前面加上&符号做到的;
var s = "hello"
let result = removeFromString(s: &s, charater: Character("l"))
print(result,s)
func removeFromString( s:inout String , charater c:Character) -> Int {
var howMany = 0
while let ix = s.characters.index(of: c) {
s.removeSubrange(ix...ix)
howMany += 1
}
return howMany;
}
当调用具有inout参数的函数时,地址作为实参传递给参数的变量总是会被设定;
在使用Cocoa时,会遇到该模式的变种:
Cocoa的API是使用C与OC编写的,因此看不到Swift术语inout;你可能会看到一些奇怪的类型,如UnsafeMutablePointer;不过从调用者的视角来看,它们是一回事;依然是准备var变量并传递其地址;
这种可以应用在多返回值的情况,函数本身不返回值,而是将对应实参的地址传过来,通过地址修改它们,这样他们就是操作的结果了;
有时当参数是某个类的实例时,函数需要修改这个没有声明为inout的参数;
这是类的一个特殊特性,其他两种对象类型(枚举和结构体)风格不同;String不是类,是结构体;
对于类的实例:
let d = Dog()
d.name = "Fido"
changeNameOfDog(d: d, to: "Rover")
class Dog {
var name = ""
}
func changeNameOfDog(d:Dog , to toString:String){
d.name = toString
}
并没有将Dog实例d最为inout参数传递;d一开始使用let而非var进行声明;但我们依然可以修改它的属性
这是因为:
类实例的一个特性,即实例本身是可变的;
从技术上说,类是引用类型,而其他对象类型风格则是值类型;
在将结构体的实例作为参数传递给函数时,实际上使用的是该结构体实例的一个独立的副本(所以要用inout修饰,改为传地址);
不过,在将类实例作为参数传递给函数时,传递的则是实例本身;
可以在任何地方声明函数,包括在函数体中声明;
声明在函数体中的函数(也叫局部函数)可以被相同的作用域的后续代码调用,不过在作用域之外则完全不可见;
对于那些旨在辅助其他函数的函数,这是个又优雅的架构;如果只有函数A需要调用函数B,那么函数B就可以放在函数A中;
有时,即便函数只会在一个地方调用,使用局部函数也是值得的;结构清晰;
局部函数说明做什么,外部作用域去做;
局部函数就是带有函数值的局部变量;因此,局部函数不能与相同作用域中的局部变量同名,相同作用域中的两个局部函数也不能同名;
函数调用自身
countDownFrom(ix: 5)
func countDownFrom(ix:Int){
print(ix)
if ix > 0 {
countDownFrom(ix: ix - 1)
}
}
在Swift中,函数时一等公民,这意味着函数可以用在任何可以使用值的地方;
函数可以赋给变量;函数可以作为函数调用的参数;函数可以作为函数的结果返回;
函数是有类型的,函数的签名就是其类型;
将函数调用封装到函数中(作为参数传递给其他函数,然后调用),这样做是很有价值的,因为其他函数可以以特殊的方式来调用参数函数;比如完成或之后调用;
举例:Cocoa中的绘图
let size = CGSize.init(width: 45, height: 20)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
let p = UIBezierPath.init(roundedRect: CGRect.init(x: 0, y: 0, width: 45, height: 20), cornerRadius: 8)
p.stroke()
_ = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
这样做十分丑陋;
所有的代码的唯一目的就是获取result,即图像;
每次绘制时唯一不同的就是步骤2,只需编写一个副主函数表示出样板化过程就能解决问题;
func drawing(){
let p = UIBezierPath.init(roundedRect: CGRect.init(x: 0, y: 0, width: 45, height: 20), cornerRadius: 8)
p.stroke()
}
_ = imageOfSize(size: CGSize.init(width: 45, height: 20), whatToDraw: drawing)
func imageOfSize(size:CGSize , whatToDraw:()->()) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(size, false, 0)
whatToDraw()
let result = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return result
}
这是将绘制指令转换为图像的一种漂亮的方式
OC中的block块语法,在Swift中就是个函数,将其当做函数并传递就可以了;
为了让函数类型说明符更加清晰,请通过Swift的typealias特性创建一个类型别名,为函数类型赋予一个名字;这个名字可以是描述性的,请不要与箭头运算符符号搞混;
如果定义typealias VoidVoidFunction = ()-> (),那就可以通过改签名指定函数类型是使用VoidVoidFunction;
匿名函数:不需要函数名,传递时只传递函数的函数体即可;
构造匿名函数,你需要完成两件事:
1.创建函数体本身,包括外面的花括号,但不需要函数声明;
2.如果必要,将函数的参数列表和返回值类型作为花括号的第一行,后跟关键字in;
Swift提供了一些便捷写法:
1.省略返回类型:
如果编译器知道匿名函数的返回类型,那么你就可以省略箭头运算符及返回类型说明;
2.如果没有参数,那么可以省略in这一行:
如果匿名函数不接收参数,并且返回类型可以省略,那么in这一行就可以被完全省略;
3.省略参数类型:
如果匿名函数接受参数,并且编译器知道其类型,那么类型就可以省略;
4.省略圆括号:
如果省略参数类型,那么包围参数列表的圆括号也可以省略;
5.有参数时也可以省略in这一行:
如果返回类型可以省略,并且编译器知道参数类型,那就可以省略in这一行,直接在匿名函数体中引用参数,方式是使用魔法名$0、$1等,并且要按顺序引用;
6.省略参数名:
如果匿名函数体不需要引用某个参数,那就可以在in这一行通过下划线来代替参数列表中该参数的名字;事实上,如果匿名函数体不需要引用任何参数,那就可以通过一个下划线来代替整个参数列表;
不过注意:如果匿名函数接收参数,那就必须要以某种方式承认他们的存在,可以省略in这一行,然后通过魔法名$0等来使用参数,或是保留in这一行,然后通过下划线省略参数,但不能在省略in这一行的同时又不通过魔法名来使用参数,否则,代码编译不过;
7.省略函数实参标签:
如果匿名函数是函数调用的最后一个参数,那么你可以在最后一个参数前通过右圆括号关闭函数调用,然后放置匿名函数体且不带任何标签(这叫尾函数)
8.省略调用函数圆括号:
如果使用尾函数语法,并且调用的函数只接收传递它的参数,那就可以在调用中省略空的圆括号;这是唯一一个可以从函数调用中省略圆括号的情形;
9.省略return关键字:
如果匿名函数体中只包含一条语句,并且该语句使用关键字return返回一个值,那么关键字return就可以省略;
示例:
数组map方法接收一个函数,该函数接收一个参数,并且返回一个与数组元素相同类型的值;
let arr1 = [2,4,6,8]
let arr2 = arr1.map{$0*2}
print(arr2)
示例中,返回类型已知,参数类型已知,只有一个参数,省略in一行,使用魔法名$0引用参数,只有一条语句,作为return语句,map不接收其他参数,因此省略圆括号,在名字后直接跟着尾函数即可;
Swift中非常常见的一种模式模式就是定义一个匿名函数然后调用它
let funcPara = {
(a:Int , b:Int) in
return a + b
}(3,4)
print(funcPara)
注意花括号后面的圆括号;花括号定义一个匿名函数体,圆括号则调用了这个匿名函数
动作在必要的时候发生(函数在必要的时候调用),而不必依赖于一系列准备步骤(一系列配置),把这些步骤放到匿名函数体中,然后调用返回实例,作为动作的参数即可;
self.view.addSubview({
(rect:CGRect) -> UIView in
let view = UIView.init(frame: rect)
view.backgroundColor = UIColor.red
return view
}(CGRect.init(x: 0, y: 0, width: 20, height: 30))
)
Swift函数就是闭包:这意味着他们可以在函数体作用域中捕获对外部变量的引用;
函数在传递时会持有其所在的环境,甚至在传递到另一个全新环境中再调用时也如此;
对于“捕获”,是当函数作为参数传递时,他会持有对外部变量的内部引用,这使得函数成为一个闭包;
被捕获的引用由函数维护,当函数被调用时,可以使用该引用指向的对象;
self.view.addSubview({
let imgView = UIImageView.init(frame: CGRect.init(x: 100, y: 100, width: 50, height: 60))
imgView.image = makeRoundedRectangle(sz: CGSize.init(width: imgView.frame.size.width, height: imgView.frame.size.height))
return imgView
}())
func makeRoundedRectangle(sz:CGSize) -> UIImage? {
let image = imageOfSize(size: sz){
let p = UIBezierPath.init(roundedRect: CGRect.init(origin: CGPoint.zero, size: sz), cornerRadius: 8)
p.stroke()
}
return image
}
func imageOfSize(size:CGSize , whatToDraw:()->()) -> UIImage? ;函数调用时,尾函数语法传递的是一个匿名闭包,该闭包捕获了外部函数 func makeRoundedRectangle(sz:CGSize) -> UIImage? ;的sz参数的引用,这样做代码更加通用,也变得更紧凑了;
依照前面的例子;
相对于返回一张图片,函数可以返回一个函数,这个函数可以创建指定大小的圆角矩形
let maker = makeRoundedRectangleMaker(sz: CGSize.init(width: 100, height: 200))
self.view.addSubview({
let imgView = UIImageView.init(frame: CGRect.init(x: 100, y: 100, width: 100, height: 100))
imgView.image = maker()
return imgView
}())
func makeRoundedRectangleMaker(sz:CGSize) -> () -> UIImage? {
return {
return imageOfSize(size: sz){
let p = UIBezierPath.init(roundedRect: CGRect.init(origin: CGPoint.zero, size: sz), cornerRadius: 8)
p.stroke()
}
}
}
注意函数makeRoundedRectangleMaker的声明:请记住每个箭头运算符后面的内容就是返回值的类型;
makeRoundedRectangleMaker类似于一个工厂,用于创建类似于maker的一系列函数,其中每个函数都会生成特定尺寸的一张图片,这是对闭包功能的最好说明:捕获了特定尺寸,调用即可生成图片;
闭包可以捕获其环境的能力甚至要超过之前所介绍的,如果闭包捕获了对外部变量的引用,并且该变量的值是可以修改的,那么闭包就可以设置该变量;
当闭包捕获其环境后,即便什么都不做,他也可以保存该环境;
举一个比较经典的例子:一个可以修改函数的函数;
let countedGreet = countAdder {
print("hello dm")
}
countedGreet()
countedGreet()
countedGreet()
func countAdder(f:@escaping () -> ()) -> () -> () {
var ct = 0
return {
ct = ct + 1
print("count is \(ct)")
f()
}
}
关于@escape标注的使用:一个被保存在某个地方等待稍后(比如函数返回以后)再调用的闭包就叫做逃逸闭包(传递给map的闭包会在map中同步执行,是非逃逸的)
countAdder向传递给它的函数增加了调用次数的功能;
ct故意声明在了所返回的匿名参数外,这样做,ct只会被初始化为0一次,然后会由匿名函数所捕获,这样,该变量就会被保存为counterGreet环境的一部分;
同时这个示例(可以保存环境状态)还有助于说明函数是引用类型;
赋值或是参数传递就会生成对相同函数的新引用;
在之前的示例中,生成图片的cornerRadius是硬编码,我们希望能够制定圆角半径,可以使用下面的方式:
现在makeRoundedRectangleMaker所返回的函数不接受参数,我们可以让它接收一个参数;
self.view.addSubview({
let imgView = UIImageView.init(frame: CGRect.init(x: 200, y: 100, width: 80, height: 80))
imgView.image = makeRoundedRectangleMakerKLH(sz: CGSize.init(width: 80, height: 80))(10)
return imgView
}())
func makeRoundedRectangleMakerKLH(sz:CGSize) -> (CGFloat) -> UIImage? {
return {
cornerRadius in
return imageOfSize(size: sz){
let p = UIBezierPath.init(roundedRect: CGRect.init(origin: CGPoint.zero, size: sz), cornerRadius: cornerRadius)
p.stroke()
}
}
}
函数返回的函数接收一个参数就叫做柯里化函数;