类和结构体是人们构建代码所用的一种通用且灵活的构造体。可以使用完全相同的语法规则来为类和结构体定义属性(常量、变量)和添加方法,从而扩展类和结构体的功能。
不同于其他语言,Swift不需要为类和结构体创建独立的接口和实现文件。
你所要做的是在一个单一文件中定义一个类或者结构体,系统将会自动生成面向其它代码的外部接口。
注意: 通常一个类
的实例被称为对象(object)
。然而在Swift 中,类和结构体的关系要比在其他语言中更加的密切,本章中所讨论的大部分功能都可以用在类和结构体的实例上。因此,我们会主要使用实例(instance)
而不是对象
。
1、类和结构体的比较
相同点:
- 定义属性(properties)用于存储值
- 定义方法(methods)用于提供功能
- 定义附属下标(subscripts)用于访问值
- 定义构造器(initializers)用于建立初始化状态
- 通过扩展(extend)以增加默认实现的功能
- 实现协议(protocols)以提供某种标准功能
与结构体相比,类还有如下的附加功能:
- 继承(Inheritance),允许一个类继承另一个类的特征
- 类型转换(Type casting),允许在运行时检查和解释一个类实例的类型
- 解构器(Deinitializers),允许一个类实例释放任何其所被分配的资源
- 引用计数(Reference counting ),允许对一个类的多次引用
注意: 结构体总是通过被复制的方式在代码中传递,没有用到引用计数。
2、定义语法
1 struct Resolution { 2 var width = 0 3 var height = 0 4 } 5 class VideoMode { 6 var resolution = Resolution() 7 var interlaced = false 8 var frameRate = 0.0 9 var name: String? 10 }
注意,定义类和结构,实际上是定义一种新的Swift类型。因此,用UpperCamelCase
这种方式来命名(如 SomeClass
和SomeStructure
等)。相反的,用lowerCamelCase
这种方式为属性和方法命名(如framerate
和incrementCount
),以便和类区分。
在上面的示例中我们定义了一个名为Resolution
的结构体,用来描述一个显示器的像素分辨率。这个结构体包含了两个名为width
和height
的存储属性。存储属性(Stored properties)是捆绑和存储在类或结构体中的常量或变量。当这两个属性被初始化为整数0
的时候,它们会被推断为Int
类型。
在上面的示例中我们还定义了一个名为VideoMode
的类,用来描述一个视频显示器的特定模式。这个类包含了四个存储属性。第一个是分辨率
,它被初始化为一个新的Resolution
结构体的实例,具有Resolution
的属性类型。新VideoMode
实例同时还会初始化其它三个属性,它们分别是,初始值为false
(意为“非隔行扫描视频”)的interlaced
,初始值为0.0
的回放帧率frameRate
和值为可选String
的name
。name
属性会被自动赋予一个默认值nil
,意为“没有name
值”,因为它是一个可选类型。
3、类和结构体实例
Resolution
结构体和VideoMode
类的定义仅描述了什么是Resolution
和VideoMode
。它们并没有描述一个特定的分辨率(resolution)或者视频模式(video mode)。为了描述一个特定的分辨率或者视频模式,我们需要生成一个它们的实例。
结构体和类都使用构造器语法来生成新的实例。
1 let someResolution = Resolution() 2 let someVideoMode = VideoMode()
4、访问属性
使用点语法:
1 print("The width of someResolution is (someResolution.width)") 2 // prints "The width of someResolution is 0"
也可以访问子属性,如VideoMode
中Resolution
属性的width
属性:
1 print("The width of someVideoMode is (someVideoMode.resolution.width)") 2 // prints "The width of someVideoMode is 0"
使用点语法赋值:
1 someVideoMode.resolution.width = 1280 2 print("The width of someVideoMode is now (someVideoMode.resolution.width)") 3 // prints "The width of someVideoMode is now 1280"
注意:与Objective-C不同,Swift 允许直接设置结构体属性的子属性。上面的最后一个例子,就是直接设置了someVideoMode
中resolution
属性的width
这个子属性,以上操作并不需要重新设置resolution
属性。
5、结构体的成员逐一构造器
所有结构体都有一个自动生成的成员逐一构造器(Memberwise Initializers),用于初始化新结构体实例中成员的属性。新实例中各个属性的初始值可以通过属性的名称传递到成员逐一构造器之中:
1 let vga = Resolution( 640, height: 480)
与结构体不同,类实例没有默认的成员逐一构造器。
6、结构体和枚举是值类型
值类型被赋予给一个变量、常量或者本身被传递给一个函数的时候,实际上操作的是其的拷贝。
在 Swift 中,所有的结构体和枚举类型都是值类型。这意味着它们的实例,以及实例中所包含的任何值类型属性,在代码中传递的时候都会被复制。
1 let hd = Resolution( 1920, height: 1080) 2 var cinema = hd
将hd
赋予给cinema
的时候,实际上是将hd
中所存储的值(values)
进行拷贝,然后将拷贝的数据存储到新的cinema
实例中。结果就是两个完全独立的实例碰巧包含有相同的数值。由于两者相互独立,因此将cinema
的width
修改为2048
并不会影响hd
中的width
的值。
1 cinema.width = 2048 2 print("cinema is now (cinema.width) pixels wide") 3 // prints "cinema is now 2048 pixels wide" 4 print("hd is still (hd.width) pixels wide") 5 // prints "hd is still 1920 pixels wide"
枚举也是一样:
1 enum CompassPoint { 2 case North, South, East, West 3 } 4 var currentDirection = CompassPoint.West 5 let rememberedDirection = currentDirection 6 currentDirection = .East 7 if rememberedDirection == .West { 8 print("The remembered direction is still .West") 9 } 10 // prints "The remembered direction is still .West"
7、类是引用类型
与值类型不同,引用类型在被赋予到一个变量、常量或者被传递到一个函数时,操作的是引用,并不是拷贝。因此,引用的是已存在的实例本身而不是其拷贝。
下面是一个类的实例tenEighty:
1 let tenEighty = VideoMode() 2 tenEighty.resolution = hd 3 tenEighty.interlaced = true 4 tenEighty.name = "1080i" 5 tenEighty.frameRate = 25.0
tenEighty
被赋值给新常量alsoTenEighty,同时对alsoTenEighty
的帧率进行修改:
1 let alsoTenEighty = tenEighty 2 alsoTenEighty.frameRate = 30.0
因为类是引用类型,所以tenEight
和alsoTenEight
实际上引用的是相同的VideoMode
实例。换句话说,它们是同一个实例的两种叫法。
1 print("The frameRate property of tenEighty is now (tenEighty.frameRate)") 2 // prints "The frameRate property of tenEighty is now 30.0"
需要注意的是tenEighty
和alsoTenEighty
被声明为常量((constants)而不是变量。然而你依然可以改变tenEighty.frameRate
和alsoTenEighty.frameRate
,因为这两个常量本身不会改变。它们并不存储
这个VideoMode
实例,在后台仅仅是对VideoMode
实例的引用。所以,改变的是被引用的基础VideoMode
的frameRate
参数,而不改变常量的值。
8、恒等运算符(Identity Operators)
因为类是引用类型,有可能有多个常量和变量在后台同时引用某一个类实例。(对于结构体和枚举来说,这并不成立。因为它们作为值类型,在被赋予到常量、变量或者传递到函数时,其值总是会被拷贝。)
如果能够判定两个常量或者变量是否引用同一个类实例将会很有帮助。为了达到这个目的,Swift 内建了两个恒等运算符:
- 等价于 ( === )
- 不等价于 ( !== )
运用这两个运算符检测两个常量或者变量是否引用同一个实例:
1 if tenEighty === alsoTenEighty { 2 print("tenEighty and alsoTenEighty refer to the same VideoMode instance.") 3 } 4 // prints "tenEighty and alsoTenEighty refer to the same VideoMode instance."
请注意“等价于"
(用三个等号表示,===) 与“等于"
(用两个等号表示,==)的不同:
- “等价于”表示两个类类型(class type)的常量或者变量引用同一个类实例。
- “等于”表示两个实例的值“相等”或“相同”,判定时要遵照类设计者定义定义的评判标准,因此相比于“相等”,这是一种更加合适的叫法。
当在定义自定义类和结构体的时候,你有义务来决定判定两个实例“相等”的标准。
9、指针
如果你有 C,C++ 或者 Objective-C 语言的经验,那么你也许会知道这些语言使用指针来引用内存中的地址。一个 Swift 常量或者变量引用一个引用类型的实例与 C 语言中的指针类似,不同的是并不直接指向内存中的某个地址,而且也不要求你使用星号(*)来表明你在创建一个引用。Swift 中这些引用与其它的常量或变量的定义方式相同。
10、类和结构体的选择
在代码中,可以使用类和结构体来定义自定义数据类型。
但是,结构体实例总是通过值传递,类实例总是通过引用传递。这意味两者适用不同的任务。
按照通用的准则,当符合一条或多条以下条件时,请考虑构建结构体:
- 结构体的主要目的是用来封装少量相关简单数据值。
- 有理由预计一个结构体实例在赋值或传递时,封装的数据将会被拷贝而不是被引用。
- 任何在结构体中储存的值类型属性,也将会被拷贝,而不是被引用。
- 结构体不需要去继承另一个已存在类型的属性或者行为。
举例来说,以下情境中适合使用结构体:
- 几何形状的大小,封装一个
width
属性和height
属性,两者均为Double
类型。 - 一定范围内的路径,封装一个
start
属性和length
属性,两者均为Int
类型。 - 三维坐标系内一点,封装
x
,y
和z
属性,三者均为Double
类型。
在其它情况下,定义一个类,生成一个它的实例,并通过引用来管理和传递。实际中,这意味着绝大部分的自定义数据构造都应该是类,而非结构体。
11、字符串(String)、数组(Array)、和字典(Dictionary)类型的赋值与复制行为
Swift 中字符串(String)
,数组(Array)
和字典(Dictionary)
类型均以结构体的形式实现。这意味着String,Array,Dictionary类型数据被赋值给新的常量或变量,或者被传入函数或方法中时,它们的值会发生拷贝行为(值传递方式)。
Objective-C中字符串(NSString)
,数组(NSArray)
和字典(NSDictionary)
类型均以类的形式实现,这与Swfit中以值传递方式是不同的。NSString,NSArray,NSDictionary在发生赋值或者传入函数(或方法)时,不会发生值拷贝,而是传递已存在实例的引用。
注意: 以上是对于字符串、数组、字典和其它值的拷贝
的描述。 在代码中,拷贝好像确实是在有拷贝行为的地方产生过。实际上,在 Swift 的后台中,只有确有必要,实际(actual)
拷贝才会被执行。Swift 管理所有的值拷贝以确保性能最优化的性能,所以没有必要去避免赋值以保证最优性能。