Swift 基础小结二
-
不同于 Objective-C 中 nil 表示的是一个指向地址为 0 的指针,且只能用于实例对象,在 Swift 中,其是一个具体的值,表示值缺失,可用于保存任意类型的可选值。当然,其也只能赋值给可选类型。
-
针对可选类型,Swift 中提供了一个运算符
??
,提供可选值为空时的默认值。let str: String? = nil let temp: String = str ?? "str" ==> let temp: String = str != nil ? str! : "str"
-
在 Swift 中的字符串
String
作为字符Character
的集合,使用String.index
类型表示每一个字符的位置,而不是使用整数类型作为索引。其中startIndex
和endIndex
两个属性分别表示字符串中的第一个字符位置和最后一个字符后的位置。通过下面的方法获取索引后,再使用下标的方式获取字符串中具体的字符值。public func index(after i: Substring.Index) -> Substring.Index public func index(before i: Substring.Index) -> Substring.Index public func index(_ i: Substring.Index, offsetBy n: Int) -> Substring.Index
-
在 Swift 中声明集合变量时,需要指定其存储的具体类型。
var set1: Set<Character> = [] var set2 = Set<Character>()
不同于数组变量的声明是其不可以使用
[]
来指定类型,因为这和数组的声明冲突。var array1: Array<Character> = [] var array2 = Array<Character>() var array3 = [Character]() var array4: [Character] = []
但是,集合同样可以根据上下文推断出其存储的数据类型。
var set3: Set = ["a","b","c"] set3 = []
-
Swift 中的
switch
功能更加强大,不在局限于判断数值。并且,case
的匹配选项可以是范围、元组,甚至可以将待匹配的值绑定到一个临时的常量或变量中,以供在case
分支体内使用。let positions = [(1,0),(0,2),(2,3),(3,4),(4,5)] for point in positions { switch point { case (let x, 0): print("the point is in x-axis, the value of x is \(x)") case (0, let y): print("the point is in y-axis, the value of y is \(y)") case (2, _): print("the value of x is 2") fallthrough case (0...3, 4): print("the value if x is between 0 and 3, and y is 4") case (let x, let y) where x < y: print("the point is above the diagonal line") case (let x, let y): print("the point is (\(x),\(y))") } }
最后,打印结果为:
the point is in x-axis, the value of x is 1 the point is in y-axis, the value of y is 2 the value of x is 2 the value if x is between 0 and 3, and y is 4 the value if x is between 0 and 3, and y is 4 the point is above the diagonal line the point is (4,5)
可知,使用
_
可以匹配任意值,使用值绑定,绑定的值也不参与匹配,还可以使用where
来添加额外的条件。另外,这里不在默认贯穿case
分支,所以不需要使用break
来使分支结束,但是如果需要,可以使用fallthought
显示的进行贯穿,但是注意贯穿到下一个分支时,不会检查该分支的条件是否满足,这就意味着如果要贯穿到的下一个分支中存在绑定值时,那么这个绑定值必须在当前分支是存在的,否则会编译错误。此外,还可以将多个case
分支条件写在一个case
后面,使用,
分隔即可。 -
除了使用
fallthought
来贯穿case
分支外,还可以使用continue
或break
跳过不需要执行的代码。对于多层分支,可以使用标签进行标记,使代码更清晰。var i = 20,j = 20,k = 20 first: while i > 0 { i -= 4 second: while j > 0 { if i > j { continue first } j -= 2 third: while k > 0 { if j > k { break third } k -= 1 } } } print(i,j,k)
这里使得三个变量最终都减为 0 。
-
在 Swift 中,引入了一个
guard
条件判断,不同于if else
,该判断没有条件为真时的执行体,只有为假时的执行体,并且执行完毕后必须结束当前函数的执行。并且,判断条件中的临时变量不可以在执行体中使用,但可以在执行体外之后的代码中使用。func test(_ str: String?) -> String { guard let s = str else { return "str" } return s } let s = test4(Optional("name")) print(s)
-
递归枚举,是指其枚举成员使用了该枚举类型的实例作为其关联值。使用
indirect
声明该枚举是可递归的,那么自然可以使用递归函数处理枚举值中存储的信息。class Test: NSObject { static func test() { let value1 = RecursionEnum.value("a") let value2 = RecursionEnum.value("b") let value3 = RecursionEnum.add(value1, value2) let value4 = value1.add(value2) let value5 = value1.add(value3) let array = [value1,value2,value3,value4,value5] for value in array { print(value,"->",RecursionEnum.getValue(value)) } } } enum RecursionEnum { case value(String) indirect case add(RecursionEnum,RecursionEnum) func add(_ value: RecursionEnum) -> RecursionEnum { switch self { case .value(let str1): switch value { case .value(let str2): let result = str1 + str2 return RecursionEnum.value(result) case .add(let rec1,let rec2): let result = rec1.add(rec2) return self.add(result) } case .add(let rec1,let rec2): switch value { case .value: let result = rec1.add(rec2) return result.add(value) case .add(let recV1,let recV2): let result = rec1.add(rec2) let resultV = recV1.add(recV2) return result.add(resultV) } } } static func getValue(_ value: RecursionEnum) -> String { switch value { case .value(let str): return str case .add(let value1, let value2): return RecursionEnum.getValue(value1) + RecursionEnum.getValue(value2) } } }
最终打印结果如下:
value("a") -> a value("b") -> b add(Test_mac.RecursionEnum.value("a"), Test_mac.RecursionEnum.value("b")) -> ab value("ab") -> ab value("aab") -> aab
在测试代码中,为递归枚举定义了一个实例方法和一个静态递归方法,可见,如果使用穷举的方式处理递归类型的数据是十分繁琐的。
-
结构体和类有很多相似之处,但是结构体不能像类继承其他类一样继承其他结构体。结构体作为值类型,其在代码中进行传递时会对整个结构体中的所有值类型属性进行复制,而其所包含的引用类型属性,传递的仍然是一个引用。在 Swift 中
String
、Array
和Dictionary
同 Objective-C 中的相应类型不同,都是结构体,而不是类,所以他们都是值类型。这便意味着将这些类型进行传递时,其背后会产生多个不同的值,对任意一个值的修改不会影响其他值,当然如果数组和字典中包含的都是值类型的话。结构体会根据其存储属性默认生成一个初始化函数,但是类不会按自己的属性自动生成初始化函数。 -
在 Swift 中使用 Array 或 Dictionary 等集合类型时,需要注意其作为结构体同 NSArray 或 NSDictionary 的不同。
如下面的代码,向一个部门信息中添加一个员工信息。
//部分代码省略,self.departmentInfo 为部门信息,members 为 Array 类型的集合,保存员工信息 //info 为员工信息 ... if var infos = self.departmentInfo?.members, info != nil { infos.append(info!) }else { let infos = [info!] self.departmentInfo?.members = infos }
这种写法是不可行的,因为 Swift 的优化数据会共享,所以 infos == self.departmentInfo?.members 的结果为 true ,
但是,一旦为 infos 添加成员后,infos 便不再等于 self.departmentInfo?.members 了,即员工信息 info 实际添加到了临时变量 infos 中了,而 members 并没有发生变化。 -
有些情况下,确定一个属性值,需要等到类初始化完毕后,或者该属性计算复杂,那么可以使用
lazy
关键字,将属性值的初始化放在第一次访问时进行。lazy var value:String = getValue() func getValue() -> String { return "" }
但是,这种方式并不是线程安全的,无法保证其在被多个线程访问时,只被初始化一次。
-
属性观察器
willSet
和didSet
只能用来观察存储属性,无法用于计算属性,他们分别含有newValue
和oldValue
两个默认参数。在属性观察器中修改当前属性或在初始化过程中修改该属性,并不会引起属性观察器被调用。 -
对于枚举类型而言,无法定义存储属性,但是可以定义计算属性,而且还可以使用
static
定义类型属性,无论是存储属性还是计算属性均可。 -
对于类而言,其定义的类型属性是可以被子类重写的,并且声明时要使用
class
关键字,如果使用static
关键字则不可以被子类重写,对于类型方法也是如此。而存储属性是不可以用class
进行修饰的,所以可以重写的类型属性只限于使用class
修饰的计算属性。 -
子类在重写父类的实例属性时,可以在子类中为父类中的属性添加属性观察器,无论其是存储属性还是计算属性(计算属性中计算器和属性观察器时不可以同时存在的)。另外,我们虽然可以在子类中将父类中的存储属性重写为计算属性,但是却不可以将父类中的计算属性重写为存储属性。
class A { class var aa:String { get { return "" } set { } } var name = "" var age = 0 var grade:String = "" { didSet { } willSet { } } } class B: A { override var name: String { willSet { } didSet { } } override var age: Int { get { return 0 } set { } } override var grade:String { set { } get { return "" } } }
另外,不能将读写属性重写为只读属性,应在
set
代码快中直接使用super
来调用父类中的设置方法。也不可以为只读属性或常量存储属性添加属性观察器,因为其是不可修改的。也不要只是将父类中的存储属性重写为存储属性,这不允许,也没有意义。
如果想要禁止属性或类被继承,可以使用
final
关键字进行修饰。 -
当为存储型属性设置默认值或者在构造器中为其赋值时,它们的值是被直接设置的,不会触发任何属性观察器。
-
在定义函数时,可以在声明语句前添加
@discardableResult
关键字,表示可以忽略函数的返回值,如此不使用该函数的返回值时也不会产生编译报警。 -
在声明函数时,参数标签是区分不同构造函数的重要信息,所以如果不进行定义,那么会自动生成一个同内部参数名称相同的标签名称,除非使用
_
符号明确省略外部标签名称。另外,需要注意的是内部参数名称并不会作为区分不同函数的依据。func test(str:String) -> String { return "" } func test(str str1:String) -> String { return "" }
这两个函数实际是同一个函数,对于函数的返回值而言,虽然在编译过程中可以区分不同的函数,但是如果直接调用,则会出现无法区分的情况。
func test(str:String) -> String { return str } func test(str:String) -> Void { return }
但是,如果在调用时,明确指定返回值类型,那么则可以这样定义并使用,如果你实在需要的话。
let a = A(a: "a") let _:Void = a.test(str: "str") let str:String = a.test(str: "str")
-
在 Swift 中可以使用
subscript
关键字为类、结构体或枚举类型定义下标访问方法,并且下标个数可以是多个,返回值也可以是包含多个结果的元组。subscript(str:String,index:Int)->(String,Int) { return (str+"1",index+1) }
如上,定义了一个只读的下标访问方法,对给定的下标进行修改,而后返回。鉴于定义的下标的参数可以是任意类型的且个数也是任意的,那么可以通过定义多个下标来进行重载。
-
对于类而言,所有的存储属性在构造结束后都需要有具体值,所以为其设置默认值后,可以不在自定义构造函数,因为有默认
init()
的构造函数生成,结构体也是如此。但不同的是,结构体会默认生成一个包含所有成员作为参数(不包括已经提供了默认值的常量属性)的构造器,所以即便存在存储属性未设置默认值时,不自定义构造函数也是可以的,但是类则必须二选其一,要么提供所有属性的默认值,要么提供一个初始化函数进行初始化赋值。 -
如果自定义构造器的同时,又想保留默认的构造器,那么可以将自定义的构造器写在扩展(extension)中。
-
除了指定构造器、便利构造器(
convenience
)外,还可以定义可失败构造器init?
,如果构造过程中发现构造条件不满足,那么可以通过return nil
来表示构造失败。 -
对于类而言,可以使用
deinit
定义析构器,在类实例释放之前进行一些清理工作。 -
为了避免循环引用,可以使用
weak
关键字修饰一个可选类型的属性,这是一个弱引用,当其指向的实例被释放时,该可选值属性会被置为nil
。此外,还可以使用unowned
关键字修饰一个属性,这是一个无主引用,其所指向的实例的生命周期等于或大于当前实例的生命周期,所以其指向的实例被销毁时,意味着当前实例同时销毁或早已销毁,不会也无需对该无主引用属性进行置nil
操作,所以使用时,要确保无主引用指向的是一个实际存在的实例。当然,如果使用unowned(unsafe)
进行修饰,那么即使其指向实例被销毁,这个不安全的无主引用也会尝试去访问实例所在的内存,如果其还未被回收,或许可以得到想要的结果,但这种赌博是不安全的。 -
对于由闭包捕获实例变量而引发的循环引用,可以通过定义捕获列表来进行解决。
lazy var closure:(String,Int)->Void = { [unowned self, weak delegate = self.delegate!] str,index in print(self,str,index) }
如上,一个类中定义了一个闭包属性,而闭包中又使用了当前类的实例,那么,需要定义捕获列表破解循环引用。捕获的变量放在
[]
中括号内,以,
分隔,并且放在参数列表前。同样的,如果闭包中捕获的实例的生命周期长于闭包本身或者相同,那么使用无主引用,如果捕获的实例可能为
nil
那么要使用弱引用。