作者 Cosmin Pup?z?
從結(jié)構(gòu)中學(xué)習(xí)到了命名類型。在本章中你將學(xué)習(xí)另一種具有屬性和方法的命名類型眯分,類误甚,它與結(jié)構(gòu)非常像缚甩。
類是引用類型,與值類型相反窑邦,類有不同于值類型的優(yōu)點和缺點擅威。你通常會使用結(jié)構(gòu)去表示值,用類去表示對象冈钦。
那么郊丛,什么是值,什么是對象呢?
創(chuàng)建類
思考下面類的定義:
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")
是不是很簡單宾袜!類幾乎與結(jié)構(gòu)完全相同。關(guān)鍵字 class 在類名的前面驾窟,花括號內(nèi)都是類的成員庆猫。
但是你可以看到類與結(jié)構(gòu)的不同,上面的類定義了一個初始化方法绅络,它為firstName和lastName設(shè)置初始值月培。與結(jié)構(gòu)不同的是,一個類并不會自動地提供一個成員初始化方法恩急,這意味著如果你需要的話杉畜,你必須自己編寫它。
如果你忘記編寫初始化方法衷恭,swift編譯器會提醒你:
除了初始化方法之外此叠,類和結(jié)構(gòu)的初始化規(guī)則頁非常類似。類初始化方法是名為init的函數(shù)随珠,所有存儲屬性必須在init結(jié)束前分配初始值灭袁。
實際上類還有更多的初始化方式,但是你必須等到下一章“高級類”窗看,它將向你介紹繼承的概念以及它對初始化規(guī)則的影響∪灼纾現(xiàn)在,通過使用基本類初始化方法显沈,你可以輕松地使用Swift的類软瞎。
引用類型
在Swift中,結(jié)構(gòu)的實例是不可變的值拉讯。另一方面涤浇,類的實例是一個可變對象。因為類是引用類型遂唧,類型的變量不存儲實際實例芙代,而是引用存儲實例在內(nèi)存中的位置。如果你要創(chuàng)建一個SimplePerson類實例:
class SimplePerson {
let name: String
init(name: String) {
self.name = name
}
}
var var1 = SimplePerson(name: "John")
它在內(nèi)存中是這樣的:
如果你要創(chuàng)建一個新的變量var2并將其賦值為var1:
var var2 = var1
然后盖彭,var1和var2中的引用將在內(nèi)存中引用相同的位置纹烹。
相反,作為值類型的結(jié)構(gòu)存儲實際值召边,提供對其的直接訪問铺呵。將SimplePerson類實現(xiàn)替換為這樣的結(jié)構(gòu):
struct SimplePerson {
let name: String
}
在內(nèi)存中,變量不會引用內(nèi)存中的某個位置隧熙,但是該值將只屬于var1:
賦值var var2 = var1在本例中復(fù)制var1的值:
值類型和引用類型各有各自的優(yōu)缺點片挂。在本章后面,你將考慮在給定的情況下使用哪種類型。現(xiàn)在音念,讓我們來看看類和結(jié)構(gòu)是如何工作的沪饺。雖然下面的描述并不適用于所有情況,但是要記住它是一個很好的一階模型闷愤。
堆&堆棧
當(dāng)你創(chuàng)建一個引用類型(如類)時整葡,系統(tǒng)將實際的實例存儲在一個稱為堆的內(nèi)存區(qū)域中。值類型的實例(例如結(jié)構(gòu)體)駐留在稱為堆棧的內(nèi)存區(qū)域中讥脐,除非該值是類實例的一部分遭居,在這種情況下,該值將與類實例的其余部分一起存儲在堆中旬渠。
堆和堆棧在執(zhí)行任何程序時都有重要的作用俱萍。對它們是什么以及它們?nèi)绾喂ぷ鞯囊话憷斫鈱椭憧梢暬惡徒Y(jié)構(gòu)之間的功能差異:
?系統(tǒng)使用堆棧來存儲任何立即執(zhí)行的線程;它受到CPU的嚴(yán)格管理和優(yōu)化。當(dāng)函數(shù)創(chuàng)建一個變量時告丢,堆棧存儲該變量枪蘑,然后在函數(shù)退出時銷毀它。由于這個堆棧組織得很好芋齿,所以它非常高效腥寇,因此也非常快觅捆。
?系統(tǒng)使用堆來存儲引用類型的實例赦役。堆通常是一個大內(nèi)存池,系統(tǒng)可以請求并動態(tài)地分配內(nèi)存塊栅炒。生命是靈活多變的掂摔。堆不會像堆棧那樣自動銷毀它的數(shù)據(jù);這樣做需要額外的工作。這使得在堆上創(chuàng)建和刪除數(shù)據(jù)比在堆棧上更慢赢赊。
也許你已經(jīng)知道了它與結(jié)構(gòu)和類的關(guān)系乙漓。看看下面的圖表:
?當(dāng)你創(chuàng)建一個類的實例時释移,你的代碼請求堆上的一個內(nèi)存塊來存儲實例本身;如圖右側(cè)的實例中的第一個名稱和最后一個名稱叭披。它將該內(nèi)存的地址存儲在堆棧上的命名變量中;右側(cè)是對存儲在圖左側(cè)的引用。
?當(dāng)你創(chuàng)建一個結(jié)構(gòu)的實例(它不是類實例的一部分)時玩讳,實例本身就存儲在堆棧中涩蜘,而堆從未涉及到。
這只是簡單介紹了堆和堆棧的動態(tài)熏纯,但你現(xiàn)在已經(jīng)知道了足夠的知識來理解用于與類工作的引用語義同诫。
使用引用
在“結(jié)構(gòu)”中,你看到了在使用結(jié)構(gòu)和其他值類型時所涉及的復(fù)制語義樟澜。這里有一個小提示误窖,利用這一章的Location和DeliveryArea結(jié)構(gòu):
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
當(dāng)你將area1的值賦給area2時叮盘,area2將收到area1值的副本。當(dāng)area1.range 收到一個新的值4霹俺,這個數(shù)字只反映在area1中柔吼,而area2仍然具有原來的2.5的值。
由于類是一個引用類型丙唧,當(dāng)你分配給變量給類時嚷堡,系統(tǒng)不會復(fù)制實例;只復(fù)制引用。
將前面的代碼與下面的代碼進行對比:
var homeOwner = John
john.firstName = "John" // John wants to use his short name!
john.firstName // "John"
homeOwner.firstName // "John"
正如你所看到的艇棕,john和homeOwner的值是一樣的!
這種在類實例之間的隱含共享導(dǎo)致了一種新的思維方式。例如串塑,如果john對象發(fā)生了變化沼琉,那么任何引用john的東西都會自動看到更新。如果你使用的是一個結(jié)構(gòu)桩匪,那么您必須單獨更新每個副本打瘪,否則它仍然具有“Johnny”的舊值。
對象id
在前面的代碼示例中傻昙,可以看到j(luò)ohn和homeOwner都指向同一個對象闺骚。兩個引用都是命名變量。如果你想知道變量后面的值是否是John怎么辦?
你可能會想要檢查firstName的值妆档,但是你怎么知道它是你要找的John而不是冒名頂替者呢?更糟的是僻爽,如果john又改了名字會怎么樣?
在Swift中,==操作符讓你檢查一個對象的id是否等于另一個對象的id:
john === homeOwner // true
當(dāng)==操作符檢查兩個值是否相等時贾惦,===身份操作符比較兩個引用的內(nèi)存地址胸梆。它告訴你引用的值是否相同;也就是說,它們指向堆上的同一塊數(shù)據(jù)须板。
這意味著這個===操作符可以區(qū)分出你要找的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
當(dāng)您不能依賴于常規(guī)的等式(==)來比較和識別你關(guān)心的對象時,這一點特別有用:
// 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
你可能會發(fā)現(xiàn),你不會在日常的Swift中使用identity操作符==甜奄。重要的是了解它的功能柠横,以及它對引用類型的屬性的說明。
方法和可變性
正如您以前所讀到的贺嫂,類的實例是可變對象滓鸠,而結(jié)構(gòu)的實例是不可變的值。下面的例子說明了這個區(qū)別:
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(_:)可以通過向末尾添加更多的值來改變數(shù)組grades糜俗。盡管該方法會改變當(dāng)前對象,但不需要關(guān)鍵字mutating。
如果你用一個struct嘗試過這樣做悠抹,那么你將會得到一個失敗珠月,因為結(jié)構(gòu)是不可變的。記住楔敌,當(dāng)你改變一個結(jié)構(gòu)的值時啤挎,你不是在修改它的值,而是在創(chuàng)造一個新的值卵凑。關(guān)鍵詞mutating標(biāo)記方法用一個新值替換當(dāng)前值庆聘。使用類時,這個關(guān)鍵字不會被使用勺卢,因為實例本身是可變的
可變性和常量
前面的例子可能讓你困惑伙判,jane被定義為一個常量,卻被修改啦黑忱。
當(dāng)你定義一個常數(shù)時宴抚,常數(shù)的值是不能改變的。如果你回想一下關(guān)于值類型和引用類型的討論甫煞,請記住菇曲,使用引用類型時,值是引用抚吠。
紅色中“reference1”的值是存儲在jane中的值常潮。這個值是一個引用,因為jane被聲明為一個常量楷力,這個引用是常量蕊玷。如果你試圖將另一個學(xué)生分配給jane,你會得到一個構(gòu)建錯誤:
// Error: jane is a `let` constant
jane = Student(firstName: "John", lastName: "Appleseed")
如果你將jane聲明為一個變量弥雹,那么你就能夠為它分配另一個在堆上的學(xué)生實例:
var jane = Student(firstName: "Jane", lastName: "Appleseed")
jane = Student(firstName: "John", lastName: "Appleseed")
在另一個學(xué)生被指派給jane后垃帅,jane后面的引用的值會被更新,以指向新的Student對象剪勿。
因為沒有任何東西引用原始的“Jane”對象贸诚,它的內(nèi)存將被釋放到其他地方。你將在“異步閉包和內(nèi)存管理”中了解更多厕吉。
一個類的任何單個成員都可以通過使用常量來保護酱固,但是由于引用類型本身并不是作為值,它們不能作為一個整體從變化中得到保護头朱。