結構體與類別是 Swift 程式設計的兩大基本積木。兩者都能把相關的資料與函式組成自成一體的功能單位。而當你定義一個結構體或類別時,你就是在定義一個新的型別,供 Swift 使用,就像你已經用過的Int和String這類型別一樣。
Swift 中的結構體與類別有許多相似之處。除了其他相似點之外,兩者都能把值存在_屬性_裡,並透過_方法_提供功能。
它們各自還提供一些額外的功能,但那超出了本練習的範圍。
結構體與類別的定義方式大致相同。它們都以對應的關鍵字開頭,接著是正在定義的型別名稱,然後是結構體或類別的主體,放在大括號之間。
主體可以包含儲存屬性,這些屬性的定義與行為就跟一般的常數或變數沒兩樣。
struct CharacterStats {
var health = 0.0
var speed = 0
var strength = 0
}
class GameCharacter {
var stats = CharacterStats()
var characterClass: String?
var name: String?
var active = false
let id = makeRandomID()
}
如上所述,定義一個結構體或類別只是在定義一個新的_型別_。它只是該型別的值會長什麼樣子的藍圖,並不會真的替你建立任何該型別的值供你使用。
要建立該型別的_實例_,你需要寫出型別名稱,後面接著一對括號。
let someStats = CharacterStats()
let someCharacter = GameCharacter()
這會建立這些型別的值,其中各屬性會填入定義中提供的預設值。請注意,在像 GameCharacter 的name屬性這樣的選用情況下,除非提供值,否則該屬性會預設為 nil,就像定義一般選用型別時若沒有立即提供值一樣。
對於結構體,Swift 會自動提供所謂的_逐成員初始化器_,讓你可以在括號內提供結構體屬性的值,這些值會覆寫定義中的預設值。
let differentStats = CharacterStats(health: 100.0, speed: 6, strength: 18)
結構體與類別的屬性可以用_點標記法_存取,也就是在值的名稱後面加上一個點(.)以及屬性名稱。如果結構體或類別的某個屬性本身也有屬性,也可以用這個點標記法存取這些巢狀屬性。
這種標記法既可以用來取得屬性的值,也可以在允許的情況下用來修改它。
someStats.health
// => 0
someCharacter.name
// => nil
someStats.health = 87.3
someStats.health
// => 87.3
someCharacter.name = "Luther"
someCharacter.name
// => "Luther"
someCharacter.id = "new id"
// Error: Cannot assign to property: 'id' is a 'let' constant
屬性會在結構體與類別中儲存資料,同樣地,你也可以定義_方法_,在結構體或類別中存放函式。
方法的定義方式和一般函式相同,只是寫在結構體或類別的主體內。請注意,如果某個函式會改變結構體中某個屬性的值,它的前面必須加上mutating關鍵字。此外,如果某個屬性可能被方法改變,該屬性就必須用var而非let來定義,就像一般的變數一樣。
struct CharacterStats {
var health = 0.0
var speed = 0
var strength = 0
mutating func takeHit(_ damage: Double) {
health = max(0.0, health - damage)
}
func canLift(_ weight: Int) -> Bool {
weight < strength * 100
}
}
class GameCharacter {
var stats = CharacterStats()
var characterClass: String?
var name: String?
var active = false
let id: String = makeRandomID()
func takesDamage(_ damage: Double) {
stats.takeHit(damage)
if stats.health <= 0 {
active = false
}
}
func sayName() -> String {
return "My name is \(name ?? "no one"), my class is \(characterClass ?? "undetermined")"
}
func lift(_ weight: Int) -> String {
if stats.canLift(weight) {
return "No problem!"
} else {
return "Ooof! No way."
}
}
}
這些方法可以用點標記法呼叫,就跟屬性一樣。
var myChar = GameCharacter()
myChar.stats = CharacterStats(health: 72.8, speed: 19, strength: 6)
myChar.active = true
myChar.lift(750)
// => "Ooof! No way."
myChar.takesDamage(80)
myChar.active
// => false
結構體或類別的每個實例都有一個隱含的屬性,名為self,它指向實例本身。
self最常見的用途,是區分同名的實例屬性與參數:
struct Position {
var x = 0
mutating func move(x: Int) {
// Here, 'self.x' refers to the struct's property,
// while 'x' refers to the parameter passed to the method.
self.x = x
}
}
Swift 型別也提供一個首字母大寫的Self關鍵字,它指的是實例的型別,而不是實例本身。
在這個練習中,你會模擬一個以視窗為基礎的電腦系統。 你會建立一些視窗,它們可以移動、調整大小,並顯示自己的內容。 下圖呈現了你在下面會用到的這些值。
<--------------------- screenSize.width --------------------->
^ ╔════════════════════════════════════════════════════════════╗
| ║ ║
| ║ position.x,_ ║
| ║ position.y \ ║
| ║ \<----- size.width -----> ║
| ║ ^ *──────────────────────┐ ║
| ║ | │ title │ ║
| ║ | ├──────────────────────┤ ║
screenSize.height ║ | │ │ ║
| ║ size.height │ │ ║
| ║ | │ contents │ ║
| ║ | │ │ ║
| ║ | │ │ ║
| ║ v └──────────────────────┘ ║
| ║ ║
| ║ ║
v ╚════════════════════════════════════════════════════════════╝
定義一個名為 Size 的 struct,其中有兩個 Int 屬性 width 和 height,分別儲存視窗目前的寬度和高度。
初始的寬度和高度應分別為 80 和 60。
加入一個方法 resize(newWidth:newHeight:),它接受新的寬度和高度參數,並變更這些屬性以反映新的大小。
let size1080x764 = Size(width: 1080, height: 764)
// Returns Size
var size1200x800 = size1080x764
// Returns Size
size1200x800.resize(newWidth: 1200, newHeight: 800)
size1200x800.height
// Returns 800
定義一個名為 Position 的 struct,其中有兩個 Int 屬性 x 和 y,分別儲存視窗左上角目前的水準和垂直位置。
x 和 y 的初始值都應該是 0。位置 (0, 0) 是螢幕的左上角,x 值在你往右移動時會變大,y 值在你往下移動時會變大。
加入一個方法 moveTo(newX:newY:),它接受新的 x 和 y 參數,並變更這些屬性以反映新的位置。
var point = Position(x: 10, y: 20)
// Returns Position
point.moveTo(newX: 100, newY: -100)
point.y
// Returns -100
定義一個視窗類別,它有下列屬性:
title:String,初始值為「New Window」screenSize:Size,常數值,width = 800、height = 600size:Size,初始值為 Size struct 的預設值position:Position,初始值為 Position struct 的預設值contents:String?,初始值為 nil
你也應該為這個類別定義一個空的初始化器。
let window = Window()
window.title
// Returns "New Window"
resize(to:):(Size) -> (),這個方法以 Size struct 作為輸入,並嘗試將視窗調整為指定的大小。不過,新的大小不能超過特定界限。x = 400、y = 300,而要求調整為 height = 400、width = 300,那麼視窗會被調整為 height = 300、width = 300,因為螢幕在 y 方向上不夠大,無法完全容納這個要求。move(to:):(Position) -> (),這和 resize(to:) 類似,不過這個方法調整的是視窗的_位置_,而不是大小。和 resize 一樣,新的位置不能超過特定限制。x 和 y 的最小位置都是 0。x = 250、y = 100,而要求移動到 x = 600、y = 200,那麼視窗會被移動到 x = 550、y = 200,因為螢幕在 x 方向上不夠大,無法完全容納這個要求。update(title:):(String) -> (),這個方法會將 title 屬性設為傳入字串的值。update(text:):(String?) -> (),這個方法會將 contents 屬性設為傳入的可選字串的值。display():() -> String,這個方法會回傳一個描述視窗目前狀態的字串。例如,如果視窗的 title 是「My First Window」,位置:x = 10、y = 100;大小:width = 200、height = 150;內容:「I 😍 my window」,它就應該回傳字串:"My First Window\nPosition: (10, 100), Size: (200 x 150)\nI 😍 my window\n"。如果 contents 是 nil,最後一行應為「[This window intentionally left blank]」視窗系統應該要有初始化器,讓視窗可以用自訂的輸入來建立。
為 Window 類別建立另一個初始化器。
這個初始化器應該接受下列參數:title、contents,以及兩個可選參數 size 和 position。
size 和 position 參數應預設為 Size 和 Position struct 的預設值。
let window = Window(title: "My First Window", contents: "I 😍 my window")
window.display()
// Returns "My First Window\nPosition: (0, 0), Size: (80 x 60)\nI 😍 my window\n"
let window2 = Window(title: "My Second Window", contents: "I 😍 my window", size: Size(width: 200, height: 150), position: Position(x: 10, y: 100))
window2.display()
// Returns "My Second Window\nPosition: (10, 100), Size: (200 x 150)\nI 😍 my window\n"