Swift 中类和结构体的深入解析
1. 访问控制
在 Swift 中,我们可以通过在实体定义前添加访问级别名称来定义访问级别。以下是一些示例代码:
private struct EmployeeStruct {}
public class EmployeeClass {}
internal class EmployeeClass2 {}
public var firstName = "Jon"
internal var lastName = "Hoffman"
private var salaryYear = 0.0
public func getFullName() -> String {}
private func giveRaise(amount: Double) {}
访问控制存在一些限制,其目的是确保 Swift 中的访问级别遵循一个简单的原则:任何实体都不能基于访问级别更低(更严格)的实体来定义。具体来说,当一个实体依赖于访问级别较低的另一个实体时,我们不能为该实体分配更高(限制更少)的访问级别。例如:
- 当方法的某个参数或返回类型的访问级别为
private
时,我们不能将该方法标记为
public
,因为外部代码无法访问
private
类型。
- 当类或结构体的访问级别为
private
时,我们不能将其方法或属性的访问级别设置为
public
,因为当类为
private
时,外部代码无法访问其构造函数。
2. 继承
继承是面向对象开发的基本概念。它允许一个类定义一组特定的特征,然后其他类可以从该类派生。派生类继承了其基类的所有特征(除非派生类重写了这些特征),并且通常会添加自己的额外特征。
通过继承,我们可以创建类层次结构。在类层次结构中,位于层次结构顶部的类称为基类,派生类称为子类。我们不仅可以从基类创建子类,还可以从其他子类创建子类。子类所派生的类称为父类或超类。在 Swift 中,一个类只能有一个父类,即单继承。
继承是类和结构体的一个基本区别。类可以从父类或超类派生,但结构体不能。子类可以调用和访问其超类的属性、方法和下标,还可以重写这些属性、方法和下标。此外,子类可以为从超类继承的属性添加属性观察器,以便在属性值发生变化时得到通知。
以下是一个示例,展示了继承在 Swift 中的工作方式:
class Plant {
var height = 0.0
var age = 0
func growHeight(inches: Double) {
self.height += inches;
}
}
class Tree: Plant {
private var limbs = 0
func limbGrow() {
self.limbs++
}
func limbFall() {
self.limbs--
}
}
在上述代码中,我们首先定义了一个名为
Plant
的基类,它有两个属性
height
和
age
,以及一个方法
growHeight()
。然后,我们定义了一个名为
Tree
的子类,它继承了
Plant
类的
age
和
height
属性,并添加了一个额外的属性
limbs
,同时继承了
growHeight()
方法,并添加了两个额外的方法
limbGrow()
和
limbFall()
。
我们可以通过以下方式使用
Tree
类:
var tree = Tree()
tree.age = 5
tree.height = 4
tree.limbGrow()
tree.limbGrow()
现在,我们有了一个名为
Plant
的基类和一个名为
Tree
的子类。这意味着
Tree
的超类是
Plant
类,而
Plant
的一个子类是
Tree
类。世界上有很多不同种类的树,我们可以从
Tree
类创建两个子类:
PineTree
类和
OakTree
类:
class PineTree: Tree {
var needles = 0
}
class OakTree: Tree {
var leaves = 0
}
类层次结构如下:
graph TD;
Plant --> Tree;
Tree --> PineTree;
Tree --> OakTree;
3. 重写方法和属性
3.1 重写方法
要重写方法、属性或下标,我们需要在定义前加上
override
关键字。这告诉编译器我们打算重写超类中的某个内容,而不是意外地进行了重复定义。
override
关键字会促使 Swift 编译器验证超类(或其某个父类)是否有可重写的匹配声明。如果在超类中找不到匹配的声明,将会抛出错误。
以下是一个重写方法的示例:
class Plant {
var height = 0.0
var age = 0
func growHeight(inches: Double) {
self.height += inches;
}
func getDetails() -> String {
return "Plant Details"
}
}
class Tree: Plant {
private var limbs = 0
func limbGrow() {
self.limbs++
}
func limbFall() {
self.limbs--
}
override func getDetails() -> String {
return "Tree Details"
}
}
var plant = Plant()
var tree = Tree()
print("Plant: \(plant.getDetails())")
print("Tree: \(tree.getDetails())")
在上述代码中,我们在
Plant
类中添加了一个
getDetails()
方法,然后在
Tree
类中重写了该方法。注意,我们在
Plant
类中不使用
override
关键字,因为它是第一个实现该方法的类;而在
Tree
类中使用了
override
关键字,因为我们正在重写
Plant
类的
getDetails()
方法。运行上述代码,将输出:
Plant: Plant Details
Tree: Tree Details
在
Tree
类内部,我们仍然可以使用
super
前缀来调用其超类的
getDetails()
方法(或任何重写的方法、属性或下标)。例如:
func getDetails() -> String {
return "Height: \(height) age: \(age)"
}
override func getDetails() -> String {
var details = super.getDetails()
return "\(details) limbs: \(limbs)"
}
var tree = Tree()
tree.age = 5
tree.height = 4
tree.limbGrow()
tree.limbGrow()
print(tree.getDetails())
运行上述代码,将输出:
Height: 4.0 age: 5 limbs: 2
我们还可以链式调用重写的方法。例如,在
OakTree
类中添加以下方法:
override func getDetails() -> String {
let details = super.getDetails()
return "\(details) Leaves: \(leaves)"
}
var tree = OakTree()
tree.age = 5
tree.height = 4
tree.leaves = 50
tree.limbGrow()
tree.limbGrow()
print(tree.getDetails())
运行上述代码,将输出:
Height: 4.0 age: 5 limbs: 2 Leaves: 50
3.2 重写属性
我们可以提供自定义的 getter 和 setter 来重写任何继承的属性。重写属性时,我们必须提供要重写的属性的名称和类型,以便编译器验证类层次结构中的某个类是否有匹配的属性可以重写。虽然重写属性不如重写方法常见,但了解如何在需要时进行重写是很有好处的。
以下是一个重写属性的示例:
class Plant {
var description: String {
get {
return "Base class is Plant."
}
}
}
class Tree: Plant {
override var description: String {
return "\(super.description) I am a Tree class."
}
}
在上述代码中,我们在
Plant
类中添加了一个只读属性
description
,然后在
Tree
类中重写了该属性。当我们重写属性时,使用的
override
关键字与重写方法时相同。调用
tree
的
description
属性将返回
Base class is Plant. I am a Tree class.
。
4. 防止重写
为了防止子类重写属性和方法,或者防止整个类被继承,我们可以使用
final
关键字。我们在要防止重写的项的定义前添加
final
关键字,例如
final func
、
final var
和
final class
。任何尝试重写标记为
final
的项都会导致编译时错误。
5. 协议
有时候,我们希望描述一个类的实现(方法、属性和其他要求),而不必实际提供实现。这时,我们可以使用协议。
协议定义了类或结构体的方法、属性和其他要求的蓝图。类或结构体可以提供符合这些要求的实现,提供实现的类或结构体被称为符合该协议。
5.1 协议语法
定义协议的语法与定义类或结构体非常相似。以下是定义协议的示例:
protocol MyProtocol {
//protocol definition here
}
我们通过在类或结构体名称后面加上冒号和协议名称来表明类或结构体符合特定的协议。例如:
class myClass: MyProtocol {
//class implementation here
}
一个类或结构体可以符合多个协议,我们可以用逗号分隔列出它所符合的协议。例如:
class MyClass: MyProtocol, AnotherProtocol, ThirdProtocol {
// class implementation here
}
当一个类需要从超类继承并实现协议时,我们先列出超类,然后是协议。例如:
class MyClass: MySuperClass, MyProtocol, MyProtocol2 {
// Class implementation here
}
5.2 属性要求
协议可以要求符合的类或结构体提供具有指定名称和类型的特定属性。协议不指定属性是存储属性还是计算属性,因为实现细节由符合的类或结构体决定。
在协议中定义属性时,我们必须使用
get
和
set
关键字指定属性是只读还是读写属性。以下是一个定义属性的协议示例:
protocol FullName {
var firstName: String {get set}
var lastName: String {get set}
}
FullName
协议定义了两个属性,任何符合该协议的类或结构体都必须实现这两个属性。这两个属性在
FullName
协议中都是读写属性。如果我们想指定属性为只读,可以只使用
get
关键字,例如:
var readOnly: String {get}
以下是一个符合
FullName
协议的
Scientist
类的示例:
class Scientist: FullName {
var firstName = ""
var lastName = ""
}
如果我们忘记包含
firstName
或
lastName
属性,将会收到
Scientist does not conform to protocol 'FullName'
错误消息。我们还需要确保属性的类型相同。例如,如果我们将
Scientist
类中的
lastName
定义改为
var lastName = 42
,也会收到相同的错误消息,因为协议指定
lastName
属性必须是字符串类型。
5.3 方法要求
协议可以要求符合的类或结构体提供特定的方法。我们在协议中定义方法的方式与在普通类或结构体中定义方法的方式相同,只是不包含花括号或方法体。以下是在
FullName
协议和
Scientist
类中添加
getFullName()
方法的示例:
protocol FullName {
var firstName: String {get set}
var lastName: String {get set}
func getFullName() -> String
}
class Scientist: FullName {
var firstName = ""
var lastName = ""
var field = ""
func getFullName() -> String {
return "\(firstName) \(lastName) studies \(field)"
}
}
结构体也可以像类一样符合 Swift 协议。以下是一个符合
FullName
协议的
FootballPlayer
结构体的示例:
struct FootballPlayer: FullName {
var firstName = ""
var lastName = ""
var number = 0
func getFullName() -> String {
return "\(firstName) \(lastName) has the number \(number)"
}
}
当一个类或结构体符合 Swift 协议时,我们可以确保它实现了所需的属性和方法。这在我们希望确保不同类中实现某些属性或方法时非常有用。
协议在我们希望将代码与特定的类或结构体解耦时也非常有用。以下是使用
FullName
协议、
Scientist
类和
FootballPlayer
结构体解耦代码的示例:
var scientist = Scientist()
scientist.firstName = "Kara"
scientist.lastName = "Hoffman"
scientist.field = "Physics"
var player = FootballPlayer();
player.firstName = "Dan"
player.lastName = "Marino"
player.number = 13
var person: FullName
person = scientist
print(person.getFullName())
person = player
print(player.getFullName())
在上述代码中,我们首先创建了
Scientist
类和
FootballPlayer
结构体的实例。然后,我们创建了一个类型为
FullName
(协议)的
person
变量,并将其设置为刚刚创建的
scientist
实例。接着,我们调用
getFullName()
方法来获取描述信息,这将在控制台输出
Kara Hoffman studies Physics
。然后,我们将
person
变量设置为
player
实例,并再次调用
getFullName()
方法,这将在控制台输出
Dan Marino has the number 13
。
可以看到,
person
变量不关心实际的实现类或结构体是什么。由于我们将
person
变量定义为
FullName
类型,我们可以将其设置为任何符合
FullName
协议的类或结构体的实例。
5.4 可选要求
有时候,我们希望协议定义可选要求,即不需要实现的方法或属性。要使用可选要求,我们首先需要用
@objc
属性标记协议。要将属性或方法标记为可选,我们使用
optional
关键字。
综上所述,Swift 中的类、结构体、继承和协议为我们提供了强大的编程工具,通过合理运用这些特性,我们可以编写出更加灵活、可维护和可扩展的代码。
Swift 中类和结构体的深入解析
6. 协议与解耦的优势总结
协议在代码解耦方面具有显著优势,通过使用协议,我们可以让代码不依赖于具体的类或结构体,提高代码的灵活性和可扩展性。以下是对协议在解耦方面的优势总结:
| 优势 | 描述 |
| ---- | ---- |
| 代码复用性 | 不同的类或结构体可以实现相同的协议,复用协议定义的方法和属性要求,避免代码重复。 |
| 可扩展性 | 当需要添加新的类或结构体时,只需让其符合现有的协议,无需修改原有的代码逻辑。 |
| 松耦合 | 代码不依赖于具体的类或结构体,而是依赖于协议,降低了代码之间的耦合度,提高了代码的可维护性。 |
7. 类和结构体的对比
类和结构体是 Swift 中两种重要的类型,它们有一些相似之处,但也存在一些关键的区别。以下是类和结构体的对比:
| 特性 | 类 | 结构体 |
| ---- | ---- | ---- |
| 继承 | 可以继承其他类 | 不支持继承 |
| 引用类型 vs 值类型 | 引用类型,多个变量可以引用同一个实例 | 值类型,每个变量都有自己的实例副本 |
| 析构函数 | 可以定义析构函数来释放资源 | 不支持析构函数 |
| 初始化 | 可以有多个初始化器,并且可以使用
required
关键字 | 有默认的成员逐一初始化器,也可以自定义初始化器 |
| 方法和属性重写 | 支持方法和属性重写 | 不支持方法和属性重写 |
8. 访问控制的最佳实践
访问控制是 Swift 中一个重要的特性,合理使用访问控制可以提高代码的安全性和可维护性。以下是一些访问控制的最佳实践:
-
最小化访问级别
:尽量将实体的访问级别设置为最小,只暴露必要的接口给外部使用,减少外部代码对内部实现的依赖。
-
遵循访问控制原则
:确保实体的访问级别符合 Swift 的访问控制原则,即不能将一个实体的访问级别设置得比它所依赖的实体更高。
-
使用访问控制来隐藏实现细节
:对于一些内部实现细节,可以将其访问级别设置为
private
或
fileprivate
,避免外部代码直接访问。
9. 继承和重写的使用场景
继承和重写是面向对象编程中的重要概念,在 Swift 中也有广泛的应用。以下是一些继承和重写的使用场景:
-
代码复用
:当多个类有共同的属性和方法时,可以将这些共同的部分提取到一个基类中,然后通过继承来复用这些代码。
-
扩展功能
:子类可以继承基类的功能,并添加自己的额外功能,从而扩展基类的功能。
-
多态
:通过重写基类的方法,子类可以提供不同的实现,实现多态性,提高代码的灵活性。
10. 协议的高级应用
除了基本的协议使用,协议在 Swift 中还有一些高级应用场景,以下是一些常见的高级应用:
-
协议组合
:可以将多个协议组合成一个新的协议,一个类或结构体可以同时符合多个协议。例如:
protocol ProtocolA {
func methodA()
}
protocol ProtocolB {
func methodB()
}
protocol CombinedProtocol: ProtocolA, ProtocolB {
// 可以添加额外的要求
}
class MyClass: CombinedProtocol {
func methodA() {
print("Method A")
}
func methodB() {
print("Method B")
}
}
- 协议扩展 :可以为协议提供默认的实现,让符合协议的类或结构体可以直接使用这些默认实现。例如:
protocol Printable {
func printInfo()
}
extension Printable {
func printInfo() {
print("Default print info")
}
}
class MyPrintableClass: Printable {
// 可以选择不实现 printInfo 方法,使用协议扩展的默认实现
}
let myClass = MyPrintableClass()
myClass.printInfo() // 输出: Default print info
- 关联类型 :协议可以使用关联类型来定义泛型协议,让协议更加灵活。例如:
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
struct IntStack: Container {
typealias Item = Int
private var items = [Int]()
mutating func append(_ item: Int) {
items.append(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Int {
return items[i]
}
}
11. 总结与展望
Swift 中的类、结构体、继承、协议和访问控制等特性为开发者提供了丰富的编程工具,通过合理运用这些特性,我们可以编写出更加灵活、可维护和可扩展的代码。在实际开发中,我们需要根据具体的需求选择合适的类型和特性。
未来,随着 Swift 语言的不断发展,这些特性可能会进一步完善和扩展,为开发者带来更多的便利和可能性。例如,可能会引入更多的访问控制级别或协议特性,让代码的设计更加精细和灵活。同时,我们也可以期待 Swift 在更多的领域得到应用,为不同的开发场景提供更好的支持。
总之,深入理解和掌握 Swift 中的类、结构体、继承和协议等特性,对于提高我们的编程能力和开发效率具有重要的意义。希望本文能够帮助你更好地理解和应用这些特性,在 Swift 开发中取得更好的成果。
2万+

被折叠的 条评论
为什么被折叠?



