Swift入门指南(iOS9 Programming Fundamentals With swift)
第三章 变量与简单类型
深入介绍变量的声明和初始化,介绍所有主要的Swift內建简单类型;
这里的简单是相对‘集合’说的,第四章会介绍主要的内建集合类型;
3.1 变量作用域与声明周期
变量就是个类型明确的具名盒子;
每个变量都必须要显示声明;为了将对象放到盒子中,即让变量名引用该对象,你需要将对象赋给变量;
除了给引用赋予一个名字,根据所声明的位置,变量还会对所引用的对象赋予一个特定的作用域(可见性)与生命周期;
将某个对象赋给变量可以确保它能被所需的代码看到,并且持续足够长的时间来满足这个目的;
在Swift文件结构中,变量实际上可以在任何地方声明;不过,区分变量作用域与生命周期的层次还是必要的:
1.全局变量:
全局变量指的是声明在Swift文件顶层的变量;
全局变量的生命周期与文件一样长,这意味着它会一直存在(严格说来,至少是在程序运行时它会存在);
全局变量在任何地方可见,这正是“全局”一词的由来;相同文件中的所有代码都会看到他;因为位于顶层,因此相同文件中的任何其他代码都会位于顶层或是更低的层次,这都是作用域所包含的层次;
默认,相同模块中的任何其他文件中的代码也可以看到它,因为同一个模块中的Swift文件会自动看到彼此,因此也会看到彼此顶层的内容;
2.属性:
属性指的是声明在对象类型声明顶层的变量(枚举 结构体 类);有两种:实例属性与静态/类属性;
1)实例属性:
默认,属性就是实例属性;其值对于该对象类型的每个实例来说都是不同的;其生命周期与实例的生命周期相同;
实例随后的生命周期取决于该实例所赋予的变量的生命周期;
2)静态/类属性:
通过关键字static或class生命的属性就是静态/类属性(第四章会详细介绍);
其生命周期与对象类型的生命周期相同;
如果对象类型生命在文件顶层,或是声明在另一个对象类型的顶层,而该对象类型又声明在顶层,那么这就意味着它会一直存在;
属性对于对象中的所有代码都是可见的,代码可以通过self加上点符号来引用属性,在没有歧义的情况下,也可以省略;
默认,在持有对象实例的情况下,实例属性对其他代码也是可见的;可以通过实例引用与点符号来引用属性;
默认,只要其他代码能够看见该对象类型的名字,静态/类属性也是可见的;可以通过对象类型与点符号来引用属性;
3.局部变量:
局部变量指的是声明在函数体中的变量;局部变量的生命周期取决于外围花括号的生命周期;
局部变量也叫做自动变量,表示他会自动产生和消亡;
3.2 变量声明
变量是通过let或var声明的;
let声明的变量是常量,其值在首次赋值(初始化)之后就不会再变化了;
var声明的变量才是真正的变量,其值可以被后续的赋值所改变;
变量的类型是绝对不可以改变的;
声明变量时可以显示或隐式的指定类型:
显示变量类型声明:在变量名后,添加一个冒号和类型名;
通过初始化创建隐式变量类型:将变量初始化作为声明的一部分,并且没有提供显示的类型,那么Swift就会根据初始化值推断其类型;
显示声明变量类型也是必要的:
1.Swift无法推断出类型:
这种,显示变量类型,可以让Swift推断出初始值的类型;
var opts:UIViewAnimationOptions = [.autoreverse,.`repeat`]
这个例子中,如果不指定opts的类型,编译是通不过的;
2.程序员无法推断出类型:
显示声明变量类型是为了提醒自己;
使用显示变量声明,变量在声明时无需初始化,但不是个好习惯:Swift编译器会阻止你使用从未赋过值的变量;
有时,你希望调用的Cocoa方法会立即返回一个值,然后再传递给相同方法的函数中使用该值
var bti :UIBackgroundTaskIdentifier = 0
bti = UIApplication.shared.beginBackgroundTask(expirationHandler: {
UIApplication.shared.endBackgroundTask(bti)
})
Swift安全规则不允许你使用一个代码声明持有数字的变量,然后在匿名函数中使用它;
因此,你需要提前声明好变量,这里还会有一个错误:要求被闭包捕获的变量需要初始化;
解决办法就是提前声明好变量,然后为其赋予一个假的初始值作为占位符;
对象的实例属性(在枚举 结构体 或 类声明的顶层)可以在对象的初始化器函数中进行初始化,而不必在声明中赋值;
对应属性,具有显示类型而不直接赋予初始值是合法且常见的;
3.3 计算初始化器
有时,你希望通过运行几行代码来计算出变量的初始值;
完成这件事简单且紧凑的方式就是使用匿名函数,然后立即调用;
在初始化实例属性时也可以这么做:通过定义和调用一个匿名函数来声明并初始化该属性;
事实上,定义与调用匿名函数常常是通过多行代码来计算出实例属性初始值的唯一合法方式;
原因在于,当初始化实例属性时是无法调用实例方法的,因为这个时候实例还不存在;毕竟,实例正在创建过程中;
self.bgImage;
let bgImage :UIImage? = {
return imageOfSize(size: CGSize.zero, whatToDraw: {
//绘制
})
}()
3.4 计算变量
目前为止,介绍的变量都是存储下来的变量;
变量还是可以计算的,这意味着变量不再持有值,而是持有函数;
在给变量赋值时,函数setter会被调用;当引用变量时,另一个函数getter会被调用;
self.nowReadWrite = "hua"
print(self.nowReadWrite)
var nowReadWrite:String {
set{
print(newValue)
}
get{
return "haha"
}
}
声明计算变量的语法:
1.变量必须是个var(不能是let);其类型必须要显式声明,后跟一对花括号;
2.getter函数叫做get,主要到这里并没有正式的函数声明;单词get后跟一对花括号,里面是函数体;
3.getter函数必须要返回与变量类型相同的值;
4.setter函数叫做set,这里并没有正式的函数声明;单词set后跟一对花括号,里面是函数体;
5.setter的行为就像是一个接收参数的函数;默认情况下,参数通过局部名newValue进入setter函数;
set函数可以存储值,不过他无法将其存储到计算变量中,计算变量是不可以存储的,他只是调用getter与setter函数的便捷方法;
语法的几个变种:
1.set函数的参数名可以指定:将其放到单词set后面的圆括号中即可;
self.nowReadWriteVal = "HQ"
print(self.nowReadWriteVal)
var nowReadWriteVal:String{
set(val){
print(val)
}
get{
return "qiang"
}
}
2.不一定要有setter,如果省略setter,那么变量就变成只读的了;没有setter的计算变量是Swift中创建只读变量的主要方式;
3.一定要有getter,若没有setter,get与后面的花括号就可以省略了;
print(self.nowReadOnly)
var nowReadOnly:String {
return "DM"
}
计算属性经常用到的地方:
1.只读变量:
计算变量是创建只读变量最简单的方式,只需要在声明中省略setter即可,通常是全局变量或属性,局部的只读变量意义不大;
2.函数门面:
如果每次需要的一个值,是有函数计算出来的,那么代码可以通过简单方式将其表示一个只读的计算变量;
3.其他变量门面:
计算变量位于存储变量之前,作为一个守护者,来确定如何设置和获取那些存储变量;
这是相比于OC的访问器方法;在极端情况下,公开的计算变量是由一个私有的存储变量所维护的;
print(self.culCount)
print(self.culCount)
print(self.culCount)
private var cul = countAdderOperation()
var culCount:Int {
get{
return self.cul()
}
}
func countAdderOperation() -> () -> Int {
var ct = 0
return {
ct = ct + 1
print("count is \(ct)")
return ct
}
}
计算实例属性函数可以引用其他实例属性,还可以调用实例方法;这是很重要的,因为一般说来,存储属性的初始化器这两件事都做不了;计算属性之所以可以,是因为直到实例存在了才可以调用它的函数;
3.5 setter观察者
计算变量并不需要成为存储变量的门面,这一点与你想的可能不同;
这是因为Swift提供了另一个漂亮的特性,可以让你将功能注入存储变量的setter中,即setter观察者;
这些代码会在其他代码设置存储变量后被调用;
声明具有setter观察者变量的语法,非常类似于声明计算变量的语法;
你可以编写一个willSet函数,一个didSet函数,二者也可以同时提供:
self.s = "hua"
var s = ""{
willSet{
print(newValue)
}
didSet{
print(oldValue)
self.s = "DM"//不会再触发willSet
}
}
1.变量必须是var(不能是let),必须为他赋初值,后跟一对花括号;
2.willSet函数,如果有,那就是willSet,后跟一对花括号,里面是函数体;当其他代码设置该变量时他会被调用,就在变量接收到新值之前;
3.默认,willSet函数会将接收到的新值设为newValue;你可以在单词willSet后面的圆括号中提供不同的名字来改变这一点,旧值依然位于存储变量中,willSet可以访问它;
4.didiSet函数,如果有,那就是didSet,后跟花括号,里面是函数体;当其他代码设置改变量时他会被调用,就在变量接收到新值之后;
5.默认,didSet会就收到旧值,他已经被变量值所代替,名字为oldValue,你可在单词didSet后面的圆括号中提供不同的名字来改变这一点;新值已经位于存储变量中,didSet函数可以访问到他;此外,didSet函数也可以将存储变量设为不同的值;
注意:
存储变量被初始化或是didSet函数修改存储变量的值,那么Setter观察者函数就不会被调用,因为这是个循环;
相对计算变量来说,我更倾向于使用Setter观察者;
3.6 延迟初始化
如果存储变量在声明时被赋予一个初始值,并且使用了延迟初始化,那么直到运行着的代码访问了该变量的值时才会计算初始值并完成赋值;
Swift中,3种类型的变量可以做到延迟初始化:
1.全局变量:
全局变量自动就是延迟初始化的,当应用启动时,文件与顶层代码都会执行,这时初始化全局变量是没有意义的,因为应用甚至还没有运行;
全局初始化必须延迟到后面的某个有意义的时间点处;因此,全局变量初始化直到其他代码首次引用它们时才会发生;在底层,该行为由dispatch_once保护,这使得初始化只会执行一次并且是线程安全的;
2.静态属性:
类似于全局变量,原因同;
Swift中不存储类属性,因此类属性是无法初始化的,也不能做到延迟初始化;
3.实例属性:
默认,实例属性不是延迟初始化的,不过声明中通过关键字lazy让他变成延迟加载;
该属性必须通过var声明;如果在代码获取属性值之前有其他代码对该属性赋值,那么属性的初始化器就永远都不会执行;
延迟初始化器,通常实现单例;单例是一种设计模式,所有代码都可以访问该类的一个共享实例:
let myClass = MyClass.shareMyClassSingleton
print(myClass)
class MyClass {
static let shareMyClassSingleton = MyClass()//静态属性:延迟初始化器的使用
}
其他代码可以通过MyClass.shareMyClassSingleton获取对MyClass单例的引用;
直到其他代码首次调用时,单例实例才会创建出来;
延迟初始化器可以做到正常的初始化器做不到的事情;特别地,它可以引用到实例,正常的初始化器却做不到这一点,因为在正常的初始化器运行时,实例还不存在;与之相反,延迟初始化器直到实例已经创建出来后的某个时间点才会运行,因此可以引用到实例;
常用的方式还有:通过一个定义与调用匿名函数来初始化延时实例属性(lazy var myview:UIView = {}()类似这种方式,之前写过类似的);
语言中有一些小陷阱:
延时实例属性不能拥有Setter观察者
手工实现延迟属性:
print(self.lazyFront)
private var lazyBacker:Int = 0
var lazyFront:Int {
get{
self.lazyBacker = 42
return self.lazyBacker
}
set{
//willset
self.lazyBacker = newValue
//didiset
}
}
原则是只有lazyFront可以被外界访问;lazyBacker是底层存储;
lazyFront现在只是一个普通的计算变量(这是计算变量观察的一种方式),可以在设置时观察它:在其Setter函数中加入额外代码,位于“willset”与“didset”注释处,也可以将整个setter删除,设为只读;
3.7 内建简单类型
下面是Swift提供的主要的简单类型,以及适合于这些内建类型的实例方法、全局函数与运算符(集合类型下一章讲);
3.7.1 Bool
Bool对象类型(结构体)只有两个值,真或假(是与非);通过关键字true和false来表示这些值;
Bool变量通常也叫作标识;
Bool在条件判断中很有用,没我们直接将Bool变量作为条件;写成if comp == true 这种是愚蠢且错误的,因为如果判断成立,那就没必要显示测试他为true还是false;条件表达式本身已经通过测试了;
与其他计算机语言不同,Swift中没有任何东西何以隐式转换或被当做Bool,比如C中,boolean实际上是数字,0是false;但是Swift中,除了false,没有任何东西是false,true也如此;
布尔代数提供逻辑运算,Bool值可以应用这些操作:
!:
非,在布尔值前,反转布尔值;
&&:
逻辑与,如果第一个操作数为false,那么第二个操作数甚至都不会计算(避免可能产生的副作用);
||:
逻辑或,如果第一个操作数为true,那么第二个操作数甚至都不会计算(避免可能产生的副作用);
如果逻辑运算复杂,对子表达式加上圆括号有助于理清运算逻辑与顺序;
3.7.2 数字
主要的数字类型(结构体)是Int和Double,这表示你应该使用这两种类型;
其他数字存在的主要目的就是与C和OC API兼容,因为在编写iOS程序时,Swift需要与它们通信;
1.Int
Int对象类型(结构体)表示介于Int.max与Int.min(包含首尾两个数字)之间的一个整数;实际的限定值取决于应用运行的平台与架构,因此不能完全依赖它们;
表示一个Int最简单的方式就是将其作为一个数字字面值;可以在数字间使用下划线,增加可读性;前导0也是合法的,有助于填补与对齐代码中的值;
在数字前加上0b 0o 0x 分别表示二进制 八进制 十六进制数;
2.Double
Double对象类型(结构体)表示一个精度大约为小数点后15位的浮点数(64位存储);
表示一个Double最简单的方式就是将其作为一个数字字面值,可以使用数字间下划线和前导0;
Double字面值不能以小数点开头,这一点与OC有明显区别;
科学计数法表示Double值,字母e后面内容就是10的指数;小数部分为0可以省略小数点;3e2表示3乘以10的平方 = 300;
十六进制(乘方)表示Double值,0x开头,字母p后内容就是2的指数;如0x10p2表示16*2的2次方 = 64;
除了其他属性,Double还有一个静态属性Double.infinity和一个实例属性isZero;
print(Double.infinity)//Double无穷大
print(2.0.isZero)
3.强制类型转换
指将一种数字类型的值转换为另一种;
显示转换:
Swift没有提供显示类型转换,不过可以通过实例化来达到相同的目的;比如使用在圆括号中使用Int来实例化一个Double;
let e = 10//隐式转换
let x = Double(e)//显示转换
print(x)
隐式转换:
在将数字值赋给变量或作为参数传递给函数时,Swift只会执行字面值的隐式转换;
由于Swift只会执行隐式转换,算数运算合并数字值时或是赋值时需要进行显示转换;
4.其他数据类型
单纯使用Swift,只会用到Int与Double;而Cocoa中还有很多其他数值类型;
对于Swift来说,这些类型只是类型别名而已,这意味着他们是其他类型的别名;
之前说过:不能通过变量赋值、传递或组合不同数值类型的值,你只能显示将这些值转换为正确的类型才行;
有时,你需要赋值或传递一种整形类型,但目标需要的却是另一种整形类型,而你也不知道到底需要哪一种整形类型;
这是可以调用numericCast让Swift进行动态类型转换;
var ii:Int = 0
let jj:Int8 = 1
ii = numericCast(jj)
print(ii)
5.算术运算
%:余号运算符
浮点操作数是合法的;-修改为一个实例方法truncatingRemainder;
let aa = 9.9.truncatingRemainder(dividingBy: 0.5)
print( aa)
let dmA = 9
let dmB = 2
let bb = dmA % dmB
print( bb)
整形类型可以看做二进制,因此可以进行二进制的位运算:
& | ^(异或) ~(按位取反)<<(左移) >>(右移),右移:将第一个操作数向右移动第二个操作数所指定的位数;
let dmC = 101
let dmD = 011
let dmE = dmC & dmD
print(type(of: dmD))
print(type(of: dmE))
print(dmE)
从技术上说,如果整形是无符号的,那么位运算符会执行逻辑位移;如果整形是有符号的,那么他会执行算数位移;(有符号数的位运算逻辑忘了 大家自己查查吧 原码反码补码 和符号关系很大);
整形的上下溢出是运行时错误,会崩溃;
若对是否上下溢出的情况不在乎,可以使用特殊的算数运算符来消除错误:&+ &- &*
let dmH = Int.max &+ 2
print(Int.max)
print(dmH)
简便(复合)赋值算数运算符有+= -= *= /= %= &= |= ^= ~= <<= >>=;
Swift提供一元增加与减少运算符++和--;
前缀使用:增加/减少值1,并存储到相同的变量中,然后用于外部表达式中;
后缀使用:当前值用于外部表达式中,然后增加/减少值1,并存储到相同的变量中;
全局函数包含了:abs(绝对值)、max、min
print( abs(-7))
print(max(7, 8))
其他数学函数来自C标准库
print(sqrt(4))
print(arc4random()%100)
6.比较
== != < > <= >=
请记住,基于计算机存储数字的方式,Double值的相等性比较可能会与你期望的不一致,想要判断两个Double是否相等,更可靠的方式是将他们的差值与一个非常小的值进行比较;
let isEqualDouble = abs(5.00001 - 5.00005) < 0.0000001
print(isEqualDouble)
3.7.3 String
String对象类型(结构体)标示文本;标示String值最简单的方式是使用字面值;
在底层,是Unicode,在字符串字面值可以包含任意字符串;
let leftTripleArrow = "\u{21DA}"
print(leftTripleArrow)
比较重要的转义字符:
1.\n:UNIX换行符;
2.\t:制表符;
3.\":引号(这里的转义是表示他并非字符串字面值的结束)
4.\\:反斜杠(因为单独的一个反斜杠是转义字符)
Swift最酷的特性之一就是字符串插入,使用转义圆括号\(...?);
let s = "I am \(leftTripleArrow)"
print(s)
圆括号中的内容不一定非得是变量的名字;他可以是Swift中任何合法的表达式;
注意:转义圆括号中不能有双引号,哪里都不能有;
字符串拼接:
想要拼接两个字符串,最简单的方式是使用 + 运算符(或 +=),因为该运算符已经被重载了;
作为+=的代替,还可以调用appendContentsOf实例方法:
var dmJ = "hello"
dmJ.append(contentsOf: " dm")
print(dmJ)
还可以使用 joinWithSeparator方法;通过一个带拼接的字符串数组调用它,并将插入其中的字符串传递给该数组;
let dmK = "hello"
let dmM = "world"
let space = " "
let greeting = [dmK,dmM].joined(separator: space)
print(greeting)
比较运算符也重载了,作用于String操作数,相同的文本表示相等,如果一个String按照字母表顺序位于另一个之前,那么前一个就小于后一个;
Swift还提供了一些附加的便捷实例方法与属性;
isEmpty:字符串是否为空串"";
hasPrefix、hasSuffix:判断字符串是否以另一个字符串开始或结束;
uppercaseString、lowercaseString:大小写转换;
可以在String和Int之间进行强行类型转换;字符串还可以通过其他进制来表示Int:提供一个radix,参数表示进制;
let dmN = 31
let dmO = String(dmN , radix:16)
print(dmO)
实际上,String的强制类型转换是字符串插值与使用print在控制台打印的基础;你可以将任何对象转换为String;方式是让其遵循如下3个协议之一:Streamable、CustomStringConvertible与CustomDebugStringConvertible;
可以通过characters属性的count方法获得String的字符长度;
print(greeting.characters.count)
String并没有一个简单意义上的长度概念,String是Unicode编码序列(有多个Ubicode编码构成一个字符的情况),为了知道一个序列标示多少字符,我们需要遍历序列,将其解析为所表示的字符;
for c in greeting.characters {
print("\(c) type is \(type(of: c))")
}
更深层次可以通过utf8与utf16属性将String分解为Utf-8编码与Utf-16编码:
for c in greeting.utf16 {
print("\(c) type is \(type(of: c))")
}
还有一个unicodeScalars属性:它将String的UTF-32编码集合表示为一个UnicodeScalar结构体;
想要从数字编码构造字符串,请通过数字实例化一个unicodeScalars并将它append到String上;
print("\(greeting.unicodeScalars) type is \(type(of: greeting.unicodeScalars))")
print( flag(country: "DE"))
func flag(country:String) -> String {
let base:UInt32 = 127397
var s = ""
for c in country.unicodeScalars {
let v = String.init(UnicodeScalar(base + c.value)!)
s.append(v)
}
return s
}
上边这个辅助函数会将一个两字母的国家缩写转换为其国家国旗的表情符号;
String并没有提供更多关于标准字符串操作的方法,这是因为Foundation框架所提供的特性的缺失,实际开发总会导入这个框架;
Swift String桥接了Foundation NSString,这意味着很大程度上,真正使用的是Foundation 的NSString方法;
如:
var dmP = "hello world"
print( dmP.capitalized)//大写
由于桥接的存在,如下range是Range类型(实质是Foundation的NSRange转换而来的),注意 并不是所有的都是通过桥接来的;
但有时不希望这种转换,只想使用Foundation并接收NSRange;可以使用as运算符显示将字符串转换为NSString;
let range = (dmP as NSString).range(of: "ll")
print("\(range) type is \(type(of: range))")
下面的这个例子也是通过类型转换让Swift使用Foundation框架的;
let dmPP = (dmP as NSString).substring(with: NSMakeRange(1, 3))
print(dmPP)
3.7.4 Character
Character对象类型(结构体)表示一个(或多个)Unicode字符,即字符串中的一个字符;
可以通过character属性将String对象分解为一系列Character对象;
形式上,它是String.CharacterView结构体;不过习惯上称为字符序列;可以通过for in循环遍历;
在字符序列之外遇到Character对象的情况并不多,甚至都没有创建Character字面值的方式;
想要从头创建一个Character,请通过单字符的String进行初始化;
let dmQ = Character.init("s")
print("\(dmQ) type is \(type(of: dmQ))")
Swift与Cocoa对字符串包含的元素有着不同的理解;Swift涉及字符,NSString则涉及UTF-16编码;所以会出现String与NSString元素的失配;虽然并不多见;
可以比较Character;
字符序列(characters)有许多属性与方法,因为是集合类型;
dmP = "hua qiang"
print(dmP.characters.first ?? "HDM-nil")
print(dmP.characters.last ?? "HDM-nil")
indexOf方法会在序列中找到给定字符首次出现的位置并返回其索引,这里的索引值并不是int;
let loc1 = dmP.index(of: "q")
print("\(String(describing: loc1)) type is \(type(of: loc1))")
还有诸如:contains方法,表示序列中是否包含某个字符;
contains还可以接收一个函数(indexOf也可以),如下代码判断目标字符串是否包含元音;
dmP = "hello world"
print(dmP.characters.contains(where: {"aeiou".contains($0)}))
字符序列可以使用filter方法,实现过滤,结果仍是一个字符序列,可以强制类型转换为String;
print(String(dmP.characters.filter({"aeiou".characters.contains($0)})))//过滤出字符串中所有的元音;
dropFirst、dropLast方法分别返回排除掉第一个、最后一个字符之后的新字符序列;
prefix与suffix会从初始字符序列的起始和末尾处提取出给定长度的字符序列;
Split会根据一个函数将字符序列转换为一个数组;
print(dmP.split(separator: "o"))
print(dmP.characters.split(whereSeparator: {"aeiou".contains($0)}).map{String($0)})
还可以像操作数组那样操作String(实际上是底层的字符序列),但不是这样string[0];
原因在于:String的索引类型是一种特殊的嵌套类型String.Index(实际上是String.CharacterView.Index的类型别名);
创建该类型并不容易:首先使用String(或字符序列)的startIndex或endIndex,或indexOf的返回值;接下来调用advancedBy方法获取所需要的索引(该方法已经废弃);
dmP = "hello"
var ix = dmP.characters.index(dmP.startIndex, offsetBy: 4)
var ic = dmP[ix]
print("\(ic) type is \(type(of: ic))")
ix = dmP.index(before: ix)//index 后一个/前一个index偏移
ic = dmP[ix]
print("\(ic) type is \(type(of: ic))")
字符串还可以在指定索引处插入和删除;
值得注意的是,我们可以将字符序列直接转换为Character对象数组,因为数组的索引是Int,使用容易;
3.7.5 Range
Range对象类型(结构体)表示一对端点;有两个运算符可以构造一个Range字面值;提供一个起始值,一个终止值,中间是一个Range运算符;
...
..<
可以在Range运算符两侧使用空格;
不存在反向Range,Range的起始值不能大于终止值;
Range端点的类型通常是某种数字;
Range的常用法是for in中遍历数字;
还可以使用Range的contains实例方法来判断某个值是否在给定的范围内(这种情况下,Range实际上是个间隔)
Range另一个使用场景是对序列进行索引;我们将String的character转换为一个Array;接下来将Int Range作为该数组的索引,然后在将其转换为String;
let dmR = "hello"
let arrR = Array(dmR.characters)
let resultR = arrR[1..<3]
print(String(resultR))
此外,可以直接将Range作为String(或其底层字符序列)的索引,不过这时他必须是String.Index的Range;如前示例所讲,这样做很笨拙;更好的方式是让Swift将从Cocoa方法调用中得到的NSRange转换为Swift Range;
一种优雅的便捷方式是从序列的indices属性开始;
let dmRIndices = dmR.characters.indices
print("\(dmRIndices) type is \(type(of: dmRIndices))")
var dmS = dmR[dmRIndices.index(after: dmRIndices.startIndex)..<dmRIndices.index(before: dmRIndices.endIndex)]
print(dmS)
dmS = dmR[dmRIndices.startIndex..<dmRIndices.endIndex]
print(dmS)
replaceRange方法会拼接为一个范围;
removeRange方法会删掉一系列字符;
Swift Range(由两个端点组成)与Cocoa NSRange差别很大(由起始点和长度组成),但两者可以在一定条件下互相转换;
let dmT = NSMakeRange(1, 2);
let dmU = Range.init(1..<3)
print((dmT as NSRange).toRange()!)
print(dmU)
3.7.6 元组
元组是轻量级、自定义、有序的多值集合;作为一种类型,它是通过一个圆括号,里面是所包含值的类型,逗号间隔;
let pair:(Int,String) = (1,"dm")
print(pair)
元组纯粹是Swift语言特性,与Cocoa和OC不兼容,因此只能放在Cocoa无法触及之处;
元组也是函数返回多值的方式;
可以赋值给变量名元组,以此作为同时给多个变量赋值的一种方式;
var dmhA:String
var dmhB:String
(dmhA,dmhB) = ("hua","one")
print((dmhA,dmhB))
安全的实现变量互换;(全局Swap能更加通用的实现这个操作)
(dmhA,dmhB) = (dmhB,dmhA)
print((dmhA,dmhB))
要忽略掉其中一个赋值,请在接收元组中使用下划线;
enumerate方法可以通过for in遍历序列,然后在每次迭代中接收到每个元素的索引号与元素本身;这两个结果是以元组的形式返回的:
let dmhC = "hello"
for (index,value) in dmhC.characters.enumerated() {
print("Index is \(index) - Value is \(value)")
}
直接引用元组的每个元素有两种方式:
1.通过索引号:将字面数字(不是变量值)作为消息名发送给元组,并使用点符号;如:pair.0;也可以用这种方式为其赋值;
2.给元祖命名:类似函数参数,需要作为显示或隐式类型声明的一部分;
let pairA :(a:String,b:String) = ("hua","qiang")//显示声明
let pairB = (a:"hua",b:"qiang")//隐式声明
print(pairA.a)
print(pairB.a)
名字现在是该值类型的一部分,并且要通过随后的赋值来访问;
可以将没有名字的元组赋值给相应的有名字的元组,反之亦然;
在传递或是从函数返回一个元组时可以省略元组名;
若程序中一贯使用元组,可以使用typealias关键字,设置别名;
拥有元素名的元组与函数列表的相似性并非巧合,参数列表就是一个元组;
每个函数实际上都接收一个元组参数并返回一个元组;如:func f(i1:Int,i2:Int) -> (){}
调用f时可以将元组作为实参传递进去;(有外部参数的,可以传一个带有具名元素的元组);
之所以这样讲是因为有助理解,现在这种方式已经不支持了;
Void(不返回值的函数所返回的值类型)实际上是空元组的类型别名;
3.7.7 Optional
Optional对象类型(枚举)用于包装任意类型的其他对象;
单个Optional对象只能包装一个对象;一个Optional对象还可能不包含任何对象;这正是Optional这个名字的由来-可选;
他可以包装其他对象,也可以不包装;可以看做是一个盒子,他可以是空的;
使用Optional初始化器:
let opA = Optional("howdy")
print(opA ?? "error")
在生命和初始化之后,opA就拥有了类型,它既不是String,也不是简单的Optional,实际它是包装了String的Optional;这意味着只能将包装了String的Optional付给他;
创建Optional常规的方法并不是使用Optional初始化器,而是将某个类型的值赋给或是传递给包装该类型的Optional引用;(被符的值会被自动包装到Optional中)
我们还需要一种方式能够显示地将某个变量声明为包装了String的Optional;否则无法声明Optional类型的变量了,同时也无法声明Optional类型的参数了;
本质上,Optional是个泛型,因此包装了String的Optional其实是Optional<String>(后边会介绍该语法);不过,也不用非得这默写,Swift支持Optional类型表示的语法糖:使用包装类型名,后跟一个问号;
let opB:String? = "hondy"
print(opB!)
此外,在需要包装某个类型值的Optional时,你可以将包装类型的值传进去;
这是因为参数传递就像赋值:未包装的值会被隐式包装;
optionalExpecter(s: "hua")
func optionalExpecter(s:String?) {
print(s ?? "optioanal-nil")
}
1.展开Optional
展开Optional,一种方式是使用展开运算符(或是强制展开运算符),他是个后缀感叹号;该语法表示进入Optional值中,获取被包装的值,然后在该处使用这个值;如果Optional包装了某个类型,那么是无法向其发送该类型所允许的消息;首先需要展开他;
每次都使用展开运算符,代码会比较长:
一种方式是:将展开值赋给包装类型的一个变量,然后使用该变量;(就是将解包的值存起来,当做正常类型的变量来使用,就不举例了)
2.隐式展开Optional
可以将Optional类型声明为隐式未包装的;这其实是一种类型,即ImplicitlyUnwrappedOptional;
这也是一种Optional,不过编译器允许他使用一些特殊的魔法操作:在需要包装类型时,可以直接使用它;
你可以显示展开,但不必,因为它可以隐式展开;
let opC:ImplicitlyUnwrappedOptional<String> = "nil"
realStringExpecter(s: opC)
func realStringExpecter(s:String){
print(s)
}
因为是可选值,所以如果是nil的话,隐式解包也会crush;
就像包装了String的Optional可以表示为String?一样;
包装了String的隐式展开String可以表示为String!;
所以上边的例子也可以写成:
let opD: String! = "qiang"
realStringExpecter(s: opD)
请记住,隐式展开的Optional也是个Optional,只是个便捷写法而已;隐式展开的Optional,也就告诉了编译器,如果在需要被包装类型的地方使用了它,编译器能够将其展开;
3.魔法词nil
Optional会包含一个包装值;也可以不包含任何包装值;两种情况构成了完整的Optional;
你需要通过一种方式来判断Optional是否包含了包装值:通过关键字nil;
1)判断一个Optional是否包含了包装值:
测试Optional是否与nil相等;
2)指定没有包装值的Optional;(使用nil对Optional进行变量赋值);
魔法词nil表达:
一个Optional包装了恰当的类型,但实际上不包含该类型的任何对象;
Swift中nil既不是对象,也不是值,他只不过是一个简便写法而已;你可以认为他是真实存在的;但实际上,没有什么东西是nil,只是相当于;
由于类型为Optional的变量可能为nil,所以Swift使用了一种特殊的初始化规则:
如果变量var的类型为Optional,那么值默认为nil:
var opE:String?
print(opE)
--好吧,这样其实并不好,搞了一堆警告;
在Swift中,类型为Optional的变量(var)是唯一一种会被隐式初始化的类型变量;
现在来说一条Swift中最为重要的一个原则:不能展开不包含任何东西的Optional;
4.Optional链
有时,想向被Optional所包装的值发送信息,可以解包,这种使用可选值解包之后发送消息的形式的代码,叫做Optional链;
Optional本身并不会响应任何消息(实际会响应一些,不过很少,也不是Optional里面对象所要响应的消息);
Swift提供了一个特殊的简写形式:通过问号后缀运算符而非感叹号将Optional展开,这样可以达到安全地向可能为空的Optional发送消息的目的;
var stringMayBe:String?
var upper = stringMayBe?.uppercased()
print(upper ?? "NIL")
这个Optional链,?展开,表示有条件展开;不为nil则展开并发送消息,nil的话,就不要展开它,当然也不会发消息(这行代码相当于没做什么);
Swift有一个特殊的原则:如果一个Optional链包含了可选的展开Optional,并且如果该Optional链生成了一个值,那么该值本身就会被包含到Optional中;
print("\(upper) type is \(type(of: upper))")//nil type is Optional<String>
upper = "upper"
print("\(upper) type is \(type(of: upper))")//Optional("upper") type is Optional<String>
可以看到,可选链返回的upper的类型是Optional<String>;请仔细阅读这条原则的两个条件,理解准确;
更长的Optional链也是合法的:无论链中要展开多少个Optional,如果其中一个被展开了,那么整个表达式就会生成一个Optional,包装正常展开后所得到的类型,并且这个过程会安全的失败(返回nil);
这种情况并没有使用和生成嵌套的Optional,不过,出于一些原因,可以生成包装到另一个Optional中的Optional,后续会介绍;
如果涉及可选展开Optional的Optional链生成了一个结果,那么你可以通过检查结果来判断链中所有Optional是否可以安全展开:如果不为nil,就表示一切都可以成功展开;
但如果可选链并不是会得到结果的表达式呢?
self.view.window?.rootViewController = self//Void? 类型
要知道该Optional链是否展开成功,我们通过一个技巧:
在Swift中,不返回值的语句都会返回一个Void,因此对拥有可选展开的Optional的赋值会返回一个包装了Void的Optional(这句很重要);
我们可以捕获这个Optional,并判断他是否为nil;如果不为nil,那么赋值就成功了;
let dmm:Void? = self.myClass?.flag = "HADM"
print("\(dmm) type is \(type(of: dmm))")//Optional(()) type is Optional<()>
if dmm != nil {
print("it works")
}else{
print("it not works")
}
var myClass:MyClass? = {
MyClass()
}()
class MyClass {
var flag:String = "hadm"
}
如果函数调用返回一个Optional,那么你可以展开结果并使用,无需先捕获结果,可以直接展开,方式是在函数调用后使用一个感叹号或问号;
!和?后缀运算符:分别表示无条件和有条件展开Optional;
表示Optional类型与类型名搭配使用的!和?语法糖:分别表示String的Optional,String隐式展开的Optional;
两者表面上相似,实质没有关系;
5.与Optional的比较
let dmo:String? = "handy"
if dmo == "handy" {
print("value equals")
}
Swift会自动(且安全)将其包装值(如果有)与“handy”比较;
也可以与nil进行比较,在所有情况下的比较都可以正确的进行;
如果Optional包装了可以使用大于和小于运算符类型的值,那么这些运算符也可以直接应用到Optional上;
6.为何使用Optional
Optional一个非常重要的目的就是提供可以与OC交换的对象值;OC中任何对象引用都可能为nil,需要通过一种方式向OC发送nil并接受来自OC的nil;
Swift会帮助正确使用CocoaAPI中的恰当类型;
请记住,将被包装值赋给Optional是合法的,因为系统会将其包装起来;
使用Optional的另一个重要目的在于推断出实例属性的初始化:
如果变量类型的Optional,那么即便没有对其初始化,他也会有一个值,即nil;如果你知道某个对象将会具有值,但不是现在,那么Optional就非常方便了;一个典型事例就是 插座变量(@IBOutlet),它指向界面中某个东西(如UIButton)的一个引用;
属性button在ViewController实例首次创建出来之后还没有值,但是视图控制器的视图加载之后,button值会被设定好;
@IBOutlet var myButton:UIButton!
该变量的类型是一个隐式展开Optional:
之所以是可选值,是因为在VC首次创建出来之后,myButton需要一个占位符值;
之所以隐式展开,是因为这样代码就可以将self.myButton当做对实际的UIButton的引用,不必强调它是个Optional了;
另一种相关情况是当一个变量(通常是实例变量)所表示的数据需要一些时间才能获取到:
具体的数据属性可以是Optional类型(隐式展开的最好),在获取到数据之前,它们都是nil,当数据获取到后,他们才会被赋予真实的值;
最后,Optional最重要的用处之一就是可以将值标记为空或使用不正确的值(和上一点有些类似);
很多内建的Swift函数都以类似的方式使用Optional,如之前的String转换为Int,Int(s)//Optional(31) 举例而已
在OC中对于可能找不到给定的子字符串位置(索引)的情况,一般会返回一个特殊值,比如NSNotFound,他实际是一个非常大的负数:
对于这种情况Swift会返回Optional,将其表示为nil;
在Swift-Cocoa桥接的场景中,Swift多数都会进行转换,但有些还是需要自己来判断,如果只是使用Swift的话,Optional是很好的选择;