第5章 类型
本章教学录像:1小时37分钟
Swift语言存在两种类型:命名型类型和复合型类型。命名型类型是指定义时可以给定名字的类型,包括类、结构体、枚举和协议。比如,一个用户定义的类MyClass的实例拥有类型MyClass。除了用户定义的命名型类型,Swift标准库也定义了很多常用的命名型类型,包括那些表示数组、字典和可选值的类型。
本章要点(已掌握的在方框中打勾)
□ 数制
□ 数据类型字面值
□ 高级数据类型
□ 类型继承、推断别名
5.1 计算机如何识数——数制
本节视频教学录像:22分钟
对数据进行操作,还有不少硬件设备与计算机通信,都是通过一组二进制数控制和反映硬件的状态的。在表示一个数时,二进制形式位数多,八进制和十六进制比二进制书写方便些,它们都是计算机中计算常用的数制。
5.1.1 二进制
二进制是逢二进一的数制,目前的计算机全部都是采用二进制系统。0和1是二进制数字符号,运算规则简单,操作方便,因为每一位数都可以用任何具有两个稳定状态的元件表示,所以二进制易于用电子方式实现。
1.二进制运算规则
加法:0+0=0,0+1=1,1+0=1,1+1=10
减法:0-0=0,1-0=1,1-1=0,10-1=1
乘法:0×0=0,0×1=0,1×0=0,1×1=1
除法:0÷1=0,1÷1=1
例如,(1100)2 + (0111)2的计算如下。
1100
+ 0111
10011
2.二进制转换为十进制
十进制是逢十进一,由数字符号0、1、2、3、4、5、6、7、8、9组成,可以这样分析十进制数。
(1234)10 = 1 * 10 3 + 2 * 10 2 + 3 * 10 1 + 4 * 10 0 = 1000 + 200 +30 + 4 =(1234)10
可采用同样的方式转换二进制为十进制。
(1101)2 = 1 * 2 3 + 1 * 2 2 + 0 * 2 1 + 1 * 2 0 = 8 + 4 + 0 + 1 = (13)10
(10.01)2 = 1 * 2 1 + 0 * 2 0 + 0 * 2 -1 + 1 * 2 -2 = 2 + 0 + 0 + 0.25 = (2.25)10
3.十进制转换为二进制
⑴十进制整数转换为二进制:方法是除以2取余,逆序排列,以(89)10为例,具体如下。
89÷ 2 余1
44÷ 2 余0
22÷ 2 余0
11÷ 2 余1
5 ÷ 2 余1
2 ÷ 2 余0
1 余1
(89)10 = (1011001)2
(5)10 = (101)2
(2)10 = (10)2
⑵十进制小数转换为二进制:方法是乘以2取整,顺序排列,以(0.625)10为例,具体如下。
0.625 * 2= 1.25 取整1
0.25 * 2= 0.5 取整0
0.5 * 2= 1 取整1
(0.625)10= (0.101)2
(0.25)10= (0.01)2
(0.5)10= (0.1)2
5.1.2 八进制
八进制是逢八进一的数制,由0~7等8个数字组成。八进制比二进制书写方便,也常用于计算机计算。需要注意的是,在Swift语言中,八进制数以00开头,比如004、0017等。
1.八进制转换为十进制
和二进制转换为十进制的原理相同,如:(64)8 = 6 * 81+ 4 * 80= 48 + 4 =(52)10。
2.二进制转换为八进制
整数部分从最低有效位开始,以3位二进制数为一组,最高有效位不足3位时以0补齐,每一组均可转换成一个八进制的值,转换结果就是八进制的整数。小数部分从最高有效位开始,以3位为一组,最低有效位不足3位时以0补齐,每一组均可转换成一个八进制的值,转换结果就是八进制的小数。例如:(11001111.01111)2 = (011 001 111.011 110)2 = (317.36)8。
5.1.3 十六进制
十六进制就是逢十六进一的数制,由0~9和A~F共16个数字组成(A代表10,F代表15),也常用于计算机计算。在Swift语言中,十六进制数以数字0x开头,比如0x1A、0xFF等。
1.十六进制转换为十进制
和二进制转换为十进制的原理相同,举例如下。
(2FA)16 = 2 * 16 2 + F * 16 1 + A * 16 0 = 512 + 240 + 10 =(762)10
2.二进制转换为十六进制
与二进制转换为八进制相似,这里是以4位为一组,每一组转换为一个十六进制的值。
(11001111.01111)2 = (1100 1111.0111 1000)2 = (CF.78)16
5.2 数据类型字面值
本节视频教学录像:13分钟
字面值的用法是一种非常直观的,以易于阅读的格式表示的固定值。在Swift语言中提供了整型字面值、浮点型字面值、字符型字面值、字符串型字面值以及布尔型字面值。
5.2.1 整型字面值
整型字面值常用的有如下4种形式。
(1) 二进制数,前缀用0b表示。如0b10000,表示16。
(2) 十进制数,没有前缀。如16,表示16。
(3) 八进制数,前缀用0o表示。如0o20,表示16。
(4) 十六进制数,前缀用0x表示。如0x10,表示16。
【范例5-1】整型字面值。
本例将分别为整型赋值不同进制的字面值。
import Foundation
let a=19
let b=0b10011
let c=0o23
let d=0x13
println(a)
println(b)
println(c)
println(d)
运行结果如下。
5.2.2 浮点型字面值
浮点型字面值能用十六进制和十进制来表示,并且它们必须在小数点的两侧。此外,浮点数还可以使用科学计数法表示,其中,使用大写或者小写的e表示十进制的浮点数,使用大写或者小写的p来表示十六进制的浮点数。其语法如下。
n.ne+/-P //十进制的浮点数
n.np+/-P //十六进制的浮点数
其中,P表示小数点移动的位数,+表示向右移动,-表示向左移动。举例如下。
3.14e2 //十进制的浮点数,表示314.0
3.14 e-2 //十六进制的浮点数,表示0.0314
0xFp2 //十六进制的浮点数,表示60
0xFp-2 //十六进制的浮点数,表示3.75
5.2.3 字符型字面值
字符型字面值通常使用双引号表示,如"X"、"Y"等。其语法如下。
字符名="值"
【范例5-2】用字符字面值为一个字符常量赋值,然后输出。
本例是"X"字面值赋值给字符常量char。
import Foundation
let char="X"
println(char)
运行结果如下。
5.2.4 字符串字面值
字符串字面值是由一对双引号包围的固定顺序的文本字符。字符串字面值可以用来提供一个常量或者变量的初始值,举例如下。
Let someString="some String literal value"
在此代码中,Swift推断somestring常量为字符串类型,因为是使用一个字符串初始化的。使用字符串字面值对大多数打印字符都是有效的,但是对一些非打印字符就可以直接表示它们了,例如空格、回车等,所以Swift提供了一些特性的字符去表示它们——转义序列。其中,转义序列是由反斜杠和字符组合在一起的字符串。转义字符的种类以及功能如下。
【范例5-3】用“\n”换行转义序列让字符串“abcd”进行换行,然后输出。
import Foundation
let char="\nabcd" //这里有一个输出换行
println(char)
运行结果如下。
5.2.5 布尔型字面值
布尔型的字面值比较简单,只有true和false两种值。
【范例5-4】用布尔型的字面值为常量赋值,然后输出。
import Foundation
let char=true
println(char)
运行结果如下。
5.3 Swift类型
本节视频教学录像:4分钟
在Swift中除了简单数据类型外,还有高级数据类型,高级数据类型包括类、结构、函数以及元组等。Swift将它自己的数据类型分为两种:命名类型和复合类型。其中,命名类型中包括类、结构、枚举和协议以及简单数据类型等。复合类型是一种没有名字,由Swift语言定义的类型。此类型分为两种,分别是函数类型和元组类型。
5.3.1 类型注解
类型注解显式地指定一个变量或表达式的类型。类型注解始于冒号(:),终于类型,比如下面两个例子。
01 let someTuple:(Double, Double) = (3.14159, 2.71828)
02 func someFunction(a: Int){/* ... */}
在第一个例子中,表达式 someTuple 的类型被指定为(Double,Double)。在第二个例子中,函数someFunction 的参数a的类型被指定为Int。
类型注解可以在类型之前包含一个类型特性(type attributes)的可选列表。
5.3.2 类型标识符
类型标识符是引用命名型类型或者是命名型/复合型类型的别名。
大多数情况下,类型标识符引用的是同名的命名型类型。例如类型标识符Int引用命名型类型Int,同样,类型标识符Dictionary<String, Int>引用命名型类型 Dictionary<String,Int>。
在两种情况下类型标识符引用的不是同名的类型。情况一,类型标识符引用的是命名型/复合型类型的类型别名。比如,在下面的例子中,类型标识符使用 Point 来引用元组(Int,Int)。
01 typealias Point = (Int, Int)
02 let origin: Point = (0, 0)
情况二,类型标识符使用dot(.)语法来表示在其他模块(modules)或其他类型嵌套内声明的命名型类型。例如,下面例子中的类型标识符引用在 ExampleModule 模块中声明的命名型类型MyType。
var someValue: ExampleModule.MyType
5.4 高级数据类型——元组类型
本节视频教学录像:10分钟
将多个值放到一起,组合成一个元素,便构成了元组。
5.4.1 元组类型字面量
元组类型是使用逗号隔开并使用括号括起来的0个或多个类型组成的列表。其语法如下。
(元组值1,元组值2,元组值3,元组值4……)
其中,值可以是任意类型的数据类型,如:
(a,3.14,"Hello Swift")
可以使用元组类型作为一个函数的返回类型,这样就可以使函数返回多个值。也可以命名元组类型中的元素,然后用这些名字来引用每个元素的值。元素的名字由一个标识符和“:”组成。
void 是空元组类型 () 的别名。如果括号内只有一个元素,那么该类型就是括号内元素的类型。比如,(Int) 的类型是 Int 而不是 (Int)。所以,只有当元组类型包含两个元素以上时才可以标记元组元素。
5.4.2 声明元组类型的常量/变量
常量/变量除了可以对简单类型进行声明,还可以声明为元组类型。元组类型的常量/变量的语法如下。
let 常量名=元组类型的字面量
var 变量名=元组类型的字面量
【范例5-5】声明元组类型的常量和变量,然后输出。
import Foundation
let cha=("a",3.14,"Hello Swift")
var chb=("x","y","Hello Swift")
println(cha)
println(chb)
运行结果如下。
元组类型的常量和变量除了可以单独对一组进行声明,还可以同时将多组常量或变量声明为元组类型的字面值。其语法如下。
let (常量名1,常量名2,常量名3,常量名4,…)=元组类型的字面量
var (变量名1,变量名2,变量名3,变量名4,…)=元组类型的字面量
例如:同时声明一组类型为元组类型的常量或者变量。
let (x,y,z)=(1,3.14,"Hello Swift1")
var (a,b,c)=(x,y,"Hello Swift2")
提示
在声明时,左边的常量或者变量的个数必须和右边元组类型字面量的个数一致,否则会报错。
let (x,y,z)=(1,3.14,"Hello Swift1",ab)
在该例中由于左边的常量是3个,而右边的字面值是4个,这样变量名和变量值就不能一一对应,所以运行时便会报错。
5.5 其他数据类型
本节视频教学录像:36分钟
5.5.1 函数类型
函数类型是表示一个函数、方法或闭包的类型,它由一个参数类型和返回值类型组成,中间用箭头->隔开。
01 parameter type -> return type
由于参数类型和返回值类型可以是元组类型,所以函数类型可以让函数与方法支持多参数与多返回值。
可以对函数类型应用带有参数类型()并返回表达式类型的autoclosure属性。一个自动闭包函数捕获特定表达式上的隐式闭包而非表达式本身。下面的例子使用autoclosure属性来定义一个很简单的assert函数。
01 func simpleAssert(condition:@autoclosure () -> Bool, message: String
){
02 if !condition(){
03 println(message)
04 }
05 }
06 let testNumber = 5
07 simpleAssert(testNumber % 2 == 0, "testNumber isn't an even number.")
08 // prints "testNumber isn't an even number."
函数类型可以拥有一个可变长参数作为参数类型中的最后一个参数。从语法角度上讲,可变长参数由一个基础类型名字和...组成,如 Int...。可变长参数被认为是一个包含了基础类型元素的数组,即Int...就是 Int[]。
为了指定一个 in-out 参数,可以在参数类型前加 inout 前缀,但是不可以对可变长参数或返回值类型使用 inout。
柯里化函数(curried function)的类型相当于一个嵌套函数类型。例如,下面的柯里化函数addTwoNumber()()的类型是 Int -> Int -> Int。
01 func addTwoNumbers(a: Int)(b: Int) -> Int{
02 return a + b
03 }
04 addTwoNumbers(4)(5) // returns 9
柯里化函数的函数类型从右向左组成一组。例如,函数类型 Int -> Int -> Int 可以被理解为Int ->(Int -> Int),也就是说,一个函数传入一个 Int 然后输出作为另一个函数的输入,然后又返回一个Int。例如,你可以使用如下嵌套函数来重写柯里化函数addTwoNumbers()()。
01 func addTwoNumbers(a: Int) -> (Int -> Int){
02 func addTheSecondNumber(b: Int) -> Int{
03 return a + b
04 }
05 return addTheSecondNumber
06 }
07 addTwoNumbers(4)(5) // Returns 9
5.5.2 数组类型
Swift语言使用中括号内包含类型名[type]的形式来简化标准库中定义的命名型类型Array<T>。换句话说,下面两个声明是等价的。
01 let someArray: [String] = ["Alex", "Brian", "Dave"]
02 let someArray: Array<String>= ["Alex", "Brian", "Dave"]
上面两种情况中,常量 someArray 都被声明为字符串数组。数组的元素也可以通过[]获取访问:someArray[0]是指第0个元素“Alex”。
上面的例子同时显示,可以使用[]作为初始值构造数组,空的[]则用来构造指定类型的空数组。
01 var emptyArray:[Double]=[]
也可以使用链接起来的多个[]集合来构造多维数组。例如,下例使用三个[]集合来构造三维整型数组:
01 var array3D:[[[Int]]]=[[[1,2],[3,4]],[[5,6],[7,8]]]
访问一个多维数组的元素时,最左边的下标指向最外层数组的相应位置元素。接下来往右的下标指向第一层嵌入的相应位置元素,依次类推。这就意味着,在上面的例子中,array3D[0]是指[[1,2],[3,4]],array3D[0][1]是指[3,4],array3D[0][1][1]则是指值4。
5.5.3 可选类型
Swift定义后缀?来作为标准库中的定义的命名型类型Optional<T>的简写。换句话说,下面两个声明是等价的。
01 var optionalInteger:Int?
02 var optionalInteger:Optional<Int>
在上述两种情况中,变量optional Integer 都被声明为可选整型类型。注意,在类型和?之间没有空格。
类型Optional<T>是一个枚举,有两种形式:None和Some(T),用来代表可能出现或可能不出现的值。任意类型都可以被显式的声明(或隐式的转换)为可选类型。当声明一个可选类型时,确保使用括号给?提供合适的作用范围。比如说,声明一个整型的可选数组,应写作(Int[])?,写成Int[]?的话则会出错。
如果你在声明或定义可选变量或特性的时候没有提供初始值,它的值则会自动赋成默认值nil。
可选符合 Logic Value 协议,因此可以出现在布尔值环境下。此时,如果一个可选类型 T? 实例包含有类型为 T 的值(也就是说值为 Optional.Some(T)),那么此可选类型就为 true,否则为false。
如果一个可选类型的实例包含一个值,那么就可以使用后缀操作符!来获取该值,正如下面描述的。
01 optionalInteger = 42
02 optionalInteger! // 42
使用!操作符获取值为 nil 的可选项会导致运行时错误(runtime error)。
也可以使用可选链和可选绑定来选择性地执行可选表达式上的操作。如果值为 nil,不会执行任何操作,因此也就没有运行错误产生。
5.5.4 隐式解析可选类型
Swift 语言定义后缀!作为标准库中命名类型 ImplicitlyUnwrappedOptional<T>的简写。换句话说,下面两个声明是等价的。
01 var implicitlyUnwrappedString: String!
02 var implicitlyUnwrappedString: ImplicitlyUnwrappedOptional<String>
上述两种情况中,变量 implicitlyUnwrappedString被声明为一个隐式解析可选类型的字符串。注意,类型与“!”之间没有空格。
可以在使用可选的地方同样使用隐式解析可选。比如,你可以将隐式解析可选的值赋给变量、常量和可选特性,反之亦然。
有了可选,在声明隐式解析可选变量或特性的时候就不用指定初始值,因为它有默认值nil。由于隐式解析可选的值会在使用时自动解析,所以没必要使用操作符!来解析它。也就是说,如果使用值为nil的隐式解析可选,就会导致运行错误。
使用可选链会选择性地执行隐式解析可选表达式上的某一个操作。如果值为 nil,就不会执行任何操作,因此也不会产生运行错误。
5.5.5 协议合成类型
协议合成类型是一种符合每个协议的指定协议列表类型。协议合成类型可能会用在类型注解和泛型参数中。协议合成类型的形式如下。
01 protocol<Protocol 1, Procotol 2>
协议合成类型允许指定一个值,其类型可以适配多个协议的条件,而且不需要定义一个新的命名型协议来继承其他想要适配的各个协议。比如,协议合成类型 protocol<ProtocolA, Protocol B, Protocol C>等效于一个从Protocol A、Protocol B、Protocol C继承而来的新协议Protocol D,很显然这样做有效率得多,甚至不需引入一个新名字。
协议合成列表中的每项必须是协议名或协议合成类型的类型别名。如果列表为空,它就会指定一个空协议合成列表,这样每个类型都能适配。
5.5.6 元类型
元类型是指所有类型的类型,包括类、结构体、枚举和协议。
类、结构体或枚举类型的元类型是相应的类型名紧跟.Type。协议类型的元类型——并不是运行时适配该协议的具体类型——是该协议名字紧跟.Protocol。比如,类SomeClass的元类型就是SomeClass.Type,协议SomeProtocol的元类型就是SomeProtocal.Protocol。
可以使用后缀self表达式来获取类型。比如,SomeClass.self返回SomeClass本身,而不是SomeClass 的一个实例。同样,SomeProtocol.self 返回 SomeProtocol 本身,而不是运行时适配SomeProtocol的某个类型的实例。还可以对类型的实例使用dynamicType表达式来获取该实例在运行阶段的类型,如下所示。
class SomeBaseClass {
class func printClassName() {
println("SomeBaseClass")
}
}
class SomeSubClass: SomeBaseClass {
override class func printClassName() {
println("SomeSubClass")
}
}
let someInstance: SomeBaseClass = SomeSubClass()
// someInstance is of type SomeBaseClass at compile time, but
// someInstance is of type SomeSubClass at runtime
someInstance.dynamicType.printClassName()
运行结果如下图所示。
5.6 类型继承子句
本节视频教学录像:5分钟
类型继承子句被用来指定一个命名型类型继承哪个类且适配哪些协议。类型继承子句开始于冒号(:),紧跟由“,”隔开的类型标识符列表。
类可以继承单个超类,适配任意数量的协议。当定义一个类时,超类的名字必须出现在类型标识符列表首位,然后跟上该类需要适配的任意数量的协议。如果一个类不是从其他类继承而来,那么列表可以以协议开头。
其他命名型类型可能只继承或适配一个协议列表。协议类型可能继承于其他任意数量的协议。当一个协议类型继承于其他协议时,其他协议的条件集合会被集成在一起,然后其他从当前协议继承的任意类型必须适配所有这些条件。
枚举定义中的类型继承子句可以是一个协议列表,或是指定原始值的枚举,一个单独的指定原始值类型的命名型类型。
5.7 类型推断
本节视频教学录像:4分钟
Swift广泛地使用类型推断,从而允许你可以忽略很多变量和表达式的类型或部分类型。比如,对于var x: Int = 0,可以完全忽略类型而简写成var x = 0——编译器会正确地推断出x的类型Int。类似地,当完整的类型可以从上下文推断出来时,也可以忽略类型的一部分。比如,如果写了let dict:Dictionary = ["A":1],编译器也能推断出dict的类型是Dictionary<String,Int>。
在上面的两个例子中,类型信息从表达式树(expression tree)的叶子节点传向根节点。也就是说,var x: Int = 0 中 x 的类型首先根据 0 的类型进行推断,然后将该类型信息传递到根节点(变量x)。
在Swift中,类型信息也可以反方向流动——从根节点传向叶子节点。在下面的例子中,常量eFloat上的显式类型注解(:Float)导致数字字面量2.71828的类型是Float而非Double。
01 let e = 2.71828 // The type of e is inferred to be Double.
02 let eFloat: Float = 2.71828 // The type of eFloat is Float.
Swift 中的类型推断在单独的表达式或语句水平上进行。这意味着所有用于推断类型的信息必须可以从表达式或其某个子表达式的类型检查中获取。
5.8 类型别名
本节视频教学录像:3分钟
类型别名就是为现有类型定义的替代名称。类型别名可以帮助开发者使用更加符合开发场景的名字来指定一个已存在的类型,可以使用typealias关键字实现对于类型别名的定义。其语法如下。
typealias 类型别名=数据类型名称
其中,typealias是关键字,类型别名表示标识符,数据类型名称表示整型、字符型等。当一个类型别名被声明后,你可以在程序的任何地方使用别名来代替已存在的类型。已存在的类型可以是已经被命名的类型或者是混合类型。类型的别名不产生新的类型,它只是简单地和已存在的类型做名称替换。
【范例5-6】定义类型别名。
本例使用typealias关键字定义一个类型别名,并使用Max输出该类型最大的值范围。代码如下。
import Foundation
typealias AudioSample=Int
var maxAudioSample=AudioSample.max
println(maxAudioSample)
运行结果如下。
在上例中,可以看到AudioSample的最大值实际上就是lnt类型的值。由于这里使用的是64位计算机,所以这里出现的是64位整型的最大值。
5.9 习题
一、选择
1.十进制的数字25转换成二进制的数字为( )。
A. 10101
B. 11001
C. 11101
D. 10110
2.下列选项中,表示十六进制的选项是( )。
A. 0s
B. 0x
C. 0b
D. 0o
3.数值0o307使用print()函数输出的结果是( )。
A. 307
B. 263
C. 463
D. 199
4.以下表达式中,正确的是( )。
A. 111 2 <75 10
B. 36 8 >1F 16
C. 24 10 ==31 8
D. 1011011 2 <43 16
5.下列选项能输出双引号的是( )。
A. println(""")
B. println("/"")
C. println("\"")
D. println(")
二、填空
1.二进制的数字101101转换成十进制的数字是______。
2.八进制的100转换为十进制是______,十六进制的100转换为十进制是______。
3.写出下列语句中声明的变量的类型:
varchar1=1 变量类型______
varchar2="swift" 变量类型______
varchar3=true 变量类型______