概述
上一篇文章《iOS开发系列--Swift语言》中对Swift的语法特点以及它和C、ObjC等其他语言的用法区别进行了介绍。当然,这只是Swift的入门基础,但是仅仅了解这些对于使用Swift进行iOS开发还是不够的。在这篇文章中将继续介绍一些Swift开发中一些不常关注但是又必备的知识点,以便对Swift有进一步的了解。
访问控制
和其他高级语言一样Swift中也增加了访问控制,在Swift中提供了private、internal、public三种访问级别,但是不同的是Swift中的访问级别是基于模块(module,或者target)和源文件(.swift文件)的,而不是基于类型、命名空间声明。
- private:只能访问当前源文件中的实体(注意Swift中的private和其他语言不太一样,它是基于源文件的,作用范围是整个源文件,如果一个源文件中有两个类,那么一个类可以访问另外一个类的私有成员)。
- internal:可以访问当前模块中的其他任何实体,但是在模块外无法访问,这是所有实体的默认访问级别(通常在一个单目标Application中不需要自行设置访问级别)。
- public:可以访问当前模块及其他模块中的任何实体(通常用于Framework)。
下面是关于Swift关于不同成员访问级别的约定规则:
- 如果一个类的访问级别是private那么该类的所有成员都是private(此时成员无法修改访问级别),如果一个类的访问级别是internal或者public那么它的所有成员都是internal(如果类的访问级别是public,成员默认internal,此时可以单独修改成员的访问级别),类成员的访问级别不能高于类的访问级别(注意:嵌套类型的访问级别也符合此条规则);
- 常量、变量、属性、下标脚本访问级别低于其所声明的类型级别,并且如果不是默认访问级别(internal)要明确声明访问级别(例如一个常量是一个private类型的类类型,那么此常量必须声明为private);
- 在不违反1、2两条规则的情况下,setter的访问级别可以低于getter的访问级别(例如一个属性访问级别是internal,那么可以添加private(set)修饰将setter权限设置为private,在当前模块中只有此源文件可以访问,对外部是只读的);
- 必要构造方法(required修饰)的访问级别必须和类访问级别相同,结构体的默认逐一构造函数的访问级别不高于其成员的访问级别(例如一个成员是private那么这个构造函数就是private,但是可以通过自定义来声明一个public的构造函数),其他方法(包括其他构造方法和普通方法)的访问级别遵循规则1;
- 子类的访问级别不高于父类的访问级别,但是在遵循三种访问级别作用范围的前提下子类可以将父类低访问级别的成员重写成更高的访问级别(例如父类A和子类B在同一个源文件,A的访问级别是public,B的访问级别是internal,其中A有一个private方法,那么A可以覆盖其private方法并重写为internal);
- 协议中所有必须实现的成员的访问级别和协议本身的访问级别相同,其子协议的访问级别不高于父协议;
- 如果一个类继承于另一个类的同时实现了某个协议那么这个类的访问级别为父类和协议的最低访问级别,并且此类中方法访问级别和所实现的协议中的方法相同;
- 扩展的成员访问级别遵循规则1,但是对于类、结构体、枚举的扩展可以明确声明访问级别并且可以更低(例如对于internal的类,你可以声明一个private的扩展),而协议的访问级别不可以明确声明;
- 元组的访问级别是元组中各个元素的最低访问级别,注意:元组的访问级别是自动推导的,无法直接使用以上三个关键字修饰其访问级别;
- 函数的访问级是函数的参数、返回值的最低级别,并且如果其访问级别和默认访问级别(internal)不符需要明确声明;
- 枚举成员的访问级别等同于枚举的访问级别(无法单独设置),同时枚举的原始值、关联值的访问级别不能低于枚举的访问级别;
- 泛型类型或泛型函数的访问级别是泛型类型、泛型函数、泛型类型参数三者中最低的一个;
- 类型别名的访问级别不能高于原类型的访问级别;
上面这些规则看上去比较繁琐,但其实很多内容理解起来也是顺理成章的(如果你是一个语言设计者相信大部分规则也会这么设计),下面通过一个例子对于规则3做一解释,这一点和其他语言有所不同但是却更加实用。在使用ObjC开发时大家通常会有这样的经验:在一个类中希望某个属性对外界是只读的,但是自己又需要在类中对属性进行写操作,此时只能直接访问属性对应的成员变量,而不能直接访问属性进行设置。但是Swift为了让语法尽可能精简,并没有成员变量的概念,此时就可以通过访问控制来实现。
Person.swift
import Foundation public class Person { //设置setter私有,但是getter为public public private(set) var name:String public init(name:String){ self.name = name } public func showMessage(){ println("name=\(name)") } }
main.swift
import Foundation var p = Person(name:"Kenshin") //此时不能设置name属性,但是可读 //p.name = "Kaoru" println("name=\(p.name)") p.showMessage()
Xcode中的每个构建目标(Target)可以当做是一个模块(Module),这个构建目标可以是一个Application,也可以是一个通用的Framework(更多的时候是一个Application)。
Swift命名空间
熟悉ObjC的朋友都知道ObjC没有命名空间,为了避免类名重复苹果官方推荐使用类名前缀,这种做法从一定程度上避免了大部分问题,但是当你在项目中引入一个第三方库而这个第三方库引用了一个和你当前项目中用到的同一个库时就会出现问题。因为静态库最终会编译到同一个域,最终导致编译出错。当然作为一个现代化语言Swift一定会解决这个问题,可是如果查看Swift的官方文档,里面关于Swift的命名空间并没有太多详细的说明。但是Swift的作者Chris Lattner在Twitter中回答了这个问题:
Namespacing is implicit in swift, all classes (etc) are implicitly scoped by the module (Xcode target) they are in. no class prefixes needed
Swift中是实现了命名空间功能的,只是这个命名空间不像C#的namespace或者Java中的package那样需要显式在文件中指定,而是采用模块(Module)的概念:在同一个模块中所有的Swift类处于同一个命名空间,它们之间不需要导入就可以相互访问。很明显Swift的这种做法是为了最大限度的简化Swift编程。其实一个module就可以看成是一个project中的一个target,在创建项目的时候默认就会创建一个target,这个target的默认模块名称就是这个项目的名称(可以在target的Build Settings—Product Module Name配置)。
下面不妨看一个命名空间的例子,创建一个Single View Application应用“NameSpaceDemo”。默认情况下模块名称为“NameSpaceDemo”,这里修改为“Network”,并且添加”HttpRequest.swift"。然后添加一个Cocoa Touch Framework类型的target并命名为“IO”,添加“File.swift”。然后在ViewController.swift中调用HttpRequest发送请求,将请求结果利用File类来保存起来。
File.swift
import Foundation public class File { public var path:String! public init(path:String) { self.path = path } public func write(content:String){ var error:NSError? content.writeToFile(path, atomically: true, encoding:NSUTF8StringEncoding, error: &error) if error != nil { println("write failure...") } } public func read() ->String?{ var error:NSError? var content = String(contentsOfFile: path, encoding: NSUTF8StringEncoding, error: &error) if error != nil { println("write failure...") } return content } }
HttpRequest.swift
import Foundation class HttpRequest { class func request(urlStr:String,complete:(responseText:String?)->()){ var url = NSURL(string: urlStr) let task = NSURLSession.sharedSession().dataTaskWithURL(url!) { (data, response, error) -> Void in var str:String? if error == nil { str = NSString(data: data, encoding: NSUTF8StringEncoding) as? String } complete(responseText: str) } task.resume() } }
ViewController.swift
import UIKit //导入模块 import IO class ViewController: UIViewController { let url = "http://www.cnblogs.com/kenshincui" let filePath = "/Users/KenshinCui/Desktop/file.txt" override func viewDidLoad() { super.viewDidLoad() //加上命名空间Network调用,注意这里命名空间可以省略 Network.HttpRequest.request(url, complete: { (responseText) -> () in if let txt = responseText { //调用模块中的类和方法 var file = File(path: self.filePath) file.write(txt) // println(file.read()!) }else{ println("error...") } }) } }
可以看到首先同一个Module中的HttpRequest类可以加上命名空间调用(当然这里可以省略),另外对于不同Modle下的File类通过导入IO模块可以直接使用File类,但是这里需要注意访问控制,可以看到File类及其成员均声明为了public访问级别。 用模块进行命名空间划分的方式好处就是可以不用显式指定命名空间,然而这种方式无法在同一个模块中再进行划分,不过这个问题可以使用Swift中的嵌套类型来解决。在下面的例子中仍然使用前面的两个类HttpRequest和File类来演示,不同的是两个类分别嵌套在两个结构体Network和IO之中。
Network.HttpRequest.swift
import Foundation struct Network { class HttpRequest { class func request(urlStr:String,complete:(responseText:String?)->()){ var url = NSURL(string: urlStr) let task = NSURLSession.sharedSession().dataTaskWithURL(url!) { (data, response, error) -> Void in var str:String? if error == nil { str = NSString(data: data, encoding: NSUTF8StringEncoding) as? String