作者 Cosmin Pupăză
从结构中学习到了命名类型。在本章中你将学习另一种具有属性和方法的命名类型,类,它与结构非常像。
类是引用类型,与值类型相反,类有不同于值类型的优点和缺点。你通常会使用结构去表示值,用类去表示对象。
那么,什么是值,什么是对象呢?
创建类
思考下面类的定义:
class Person {
var firstName: String
var lastName: String
init(firstName: String, lastName: String) {
self.firstName = firstName
self.lastName = lastName
}
var fullName: String {
return "\(firstName) \(lastName)"
}
}
let john = Person(firstName: "Johnny", lastName: "Appleseed")
是不是很简单!类几乎与结构完全相同。关键字 class 在类名的前面,花括号内都是类的成员。
但是你可以看到类与结构的不同,上面的类定义了一个初始化方法,它为firstName和lastName设置初始值。与结构不同的是,一个类并不会自动地提供一个成员初始化方法,这意味着如果你需要的话,你必须自己编写它。
如果你忘记编写初始化方法,swift编译器会提醒你:
QQ20180522-103212@2x.png
除了初始化方法之外,类和结构的初始化规则页非常类似。类初始化方法是名为init的函数,所有存储属性必须在init结束前分配初始值。
实际上类还有更多的初始化方式,但是你必须等到下一章“高级类”,它将向你介绍继承的概念以及它对初始化规则的影响。现在,通过使用基本类初始化方法,你可以轻松地使用Swift的类。
引用类型
在Swift中,结构的实例是不可变的值。另一方面,类的实例是一个可变对象。因为类是引用类型,类型的变量不存储实际实例,而是引用存储实例在内存中的位置。如果你要创建一个SimplePerson类实例:
class SimplePerson {
let name: String
init(name: String) {
self.name = name
}
}
var var1 = SimplePerson(name: "John")
它在内存中是这样的:
QQ20180522-104707@2x.png
如果你要创建一个新的变量var2并将其赋值为var1:
var var2 = var1
然后,var1和var2中的引用将在内存中引用相同的位置。
QQ20180522-105009@2x.png
相反,作为值类型的结构存储实际值,提供对其的直接访问。将SimplePerson类实现替换为这样的结构:
struct SimplePerson {
let name: String
}
在内存中,变量不会引用内存中的某个位置,但是该值将只属于var1:
QQ20180522-105120@2x.png
赋值var var2 = var1在本例中复制var1的值:
QQ20180522-105154@2x.png
值类型和引用类型各有各自的优缺点。在本章后面,你将考虑在给定的情况下使用哪种类型。现在,让我们来看看类和结构是如何工作的。虽然下面的描述并不适用于所有情况,但是要记住它是一个很好的一阶模型。
堆&堆栈
当你创建一个引用类型(如类)时,系统将实际的实例存储在一个称为堆的内存区域中。值类型的实例(例如结构体)驻留在称为堆栈的内存区域中,除非该值是类实例的一部分,在这种情况下,该值将与类实例的其余部分一起存储在堆中。
堆和堆栈在执行任何程序时都有重要的作用。对它们是什么以及它们如何工作的一般理解将帮助你可视化类和结构之间的功能差异:
•系统使用堆栈来存储任何立即执行的线程;它受到CPU的严格管理和优化。当函数创建一个变量时,堆栈存储该变量,然后在函数退出时销毁它。由于这个堆栈组织得很好,所以它非常高效,因此也非常快。
•系统使用堆来存储引用类型的实例。堆通常是一个大内存池,系统可以请求并动态地分配内存块。生命是灵活多变的。堆不会像堆栈那样自动销毁它的数据;这样做需要额外的工作。这使得在堆上创建和删除数据比在堆栈上更慢。
也许你已经知道了它与结构和类的关系。看看下面的图表:
QQ20180522-114312@2x.png
•当你创建一个类的实例时,你的代码请求堆上的一个内存块来存储实例本身;如图右侧的实例中的第一个名称和最后一个名称。它将该内存的地址存储在堆栈上的命名变量中;右侧是对存储在图左侧的引用。
•当你创建一个结构的实例(它不是类实例的一部分)时,实例本身就存储在堆栈中,而堆从未涉及到。
这只是简单介绍了堆和堆栈的动态,但你现在已经知道了足够的知识来理解用于与类工作的引用语义。
使用引用
在“结构”中,你看到了在使用结构和其他值类型时所涉及的复制语义。这里有一个小提示,利用这一章的Location和DeliveryArea结构:
struct Location {
let x: Int
let y: Int }
struct DeliveryArea {
var range: Double
let center: Location
}
var area1 = DeliveryArea(range: 2.5,center: Location(x: 2, y: 4))
var area2 = area1
print(area1.range) // 2.5
print(area2.range) // 2.5
area1.range = 4
print(area1.range) // 4.0
print(area2.range) // 2.5
当你将area1的值赋给area2时,area2将收到area1值的副本。当area1.range 收到一个新的值4,这个数字只反映在area1中,而area2仍然具有原来的2.5的值。
由于类是一个引用类型,当你分配给变量给类时,系统不会复制实例;只复制引用。
将前面的代码与下面的代码进行对比:
var homeOwner = John
john.firstName = "John" // John wants to use his short name!
john.firstName // "John"
homeOwner.firstName // "John"
正如你所看到的,john和homeOwner的值是一样的!
这种在类实例之间的隐含共享导致了一种新的思维方式。例如,如果john对象发生了变化,那么任何引用john的东西都会自动看到更新。如果你使用的是一个结构,那么您必须单独更新每个副本,否则它仍然具有“Johnny”的旧值。
对象id
在前面的代码示例中,可以看到john和homeOwner都指向同一个对象。两个引用都是命名变量。如果你想知道变量后面的值是否是John怎么办?
你可能会想要检查firstName的值,但是你怎么知道它是你要找的John而不是冒名顶替者呢?更糟的是,如果john又改了名字会怎么样?
在Swift中,==操作符让你检查一个对象的id是否等于另一个对象的id:
john === homeOwner // true
当==操作符检查两个值是否相等时,===身份操作符比较两个引用的内存地址。它告诉你引用的值是否相同;也就是说,它们指向堆上的同一块数据。
这意味着这个===操作符可以区分出你要找的john和一个假冒的john。
let imposterJohn = Person(firstName: "Johnny", lastName: "Appleseed")
john === homeOwner // true
john === imposterJohn // false
imposterJohn === homeOwner // false
// Assignment of existing variables changes the instances the variables reference.
homeOwner = imposterJohn
john === homeOwner // false
homeOwner = John
john === homeOwner // true
当您不能依赖于常规的等式(==)来比较和识别你关心的对象时,这一点特别有用:
// Create fake, imposter Johns. Use === to see if any of these imposters
are our real John.
var imposters = (0...100).map { _ in
Person(firstName: "John", lastName: "Appleseed")
}
// Equality (==) is not effective when John cannot be identified by his
name alone
imposters.contains {
$0.firstName == john.firstName && $0.lastName == john.lastName
} // true
通过使用id操作符,你可以验证引用本身是否相等,并将我们真正的John从人群中分离出来:
// Check to ensure the real John is not found among the imposters.
imposters.contains {
$0 === John
} // false
// Now hide the "real" John somewhere among the imposters.
imposters.insert(john, at: Int(arc4random_uniform(100)))
// John can now be found among the imposters.
imposters.contains {
$0 === John
} // true
// Since `Person` is a reference type, you can use === to grab the real
John out of the list of imposters and modify the value.
// The original `john` variable will print the new last name!
if let indexOfJohn = imposters.index(where: { $0 === john }) {
imposters[indexOfJohn].lastName = "Bananapeel"
}
john.fullName // John Bananapeel
你可能会发现,你不会在日常的Swift中使用identity操作符==。重要的是了解它的功能,以及它对引用类型的属性的说明。
方法和可变性
正如您以前所读到的,类的实例是可变对象,而结构的实例是不可变的值。下面的例子说明了这个区别:
struct Grade {
let letter: String
let points: Double
let credits: Double
}
class Student {
var firstName: String
var lastName: String
var grades: [Grade] = []
init(firstName: String, lastName: String) {
self.firstName = firstName
self.lastName = lastName
}
func recordGrade(_ grade: Grade) {
grades.append(grade)
}
}
let jane = Student(firstName: "Jane", lastName: "Appleseed")
let history = Grade(letter: "B", points: 9.0, credits: 3.0)
var math = Grade(letter: "A", points: 16.0, credits: 4.0)
jane.recordGrade(history)
jane.recordGrade(math)
注意,recordGrade(_:)可以通过向末尾添加更多的值来改变数组grades。尽管该方法会改变当前对象,但不需要关键字mutating。
如果你用一个struct尝试过这样做,那么你将会得到一个失败,因为结构是不可变的。记住,当你改变一个结构的值时,你不是在修改它的值,而是在创造一个新的值。关键词mutating标记方法用一个新值替换当前值。使用类时,这个关键字不会被使用,因为实例本身是可变的
可变性和常量
前面的例子可能让你困惑,jane被定义为一个常量,却被修改啦。
当你定义一个常数时,常数的值是不能改变的。如果你回想一下关于值类型和引用类型的讨论,请记住,使用引用类型时,值是引用。
QQ20180522-164021@2x.png
红色中“reference1”的值是存储在jane中的值。这个值是一个引用,因为jane被声明为一个常量,这个引用是常量。如果你试图将另一个学生分配给jane,你会得到一个构建错误:
// Error: jane is a `let` constant
jane = Student(firstName: "John", lastName: "Appleseed")
如果你将jane声明为一个变量,那么你就能够为它分配另一个在堆上的学生实例:
var jane = Student(firstName: "Jane", lastName: "Appleseed")
jane = Student(firstName: "John", lastName: "Appleseed")
在另一个学生被指派给jane后,jane后面的引用的值会被更新,以指向新的Student对象。
QQ20180522-164303@2x.png
因为没有任何东西引用原始的“Jane”对象,它的内存将被释放到其他地方。你将在“异步闭包和内存管理”中了解更多。
一个类的任何单个成员都可以通过使用常量来保护,但是由于引用类型本身并不是作为值,它们不能作为一个整体从变化中得到保护。
网友评论