學習軌道
/
Swift
Swift
/
練習
/
視窗系統
視窗系統

視窗系統

學習練習

簡介

結構體與類別

結構體與類別是 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,它指向實例本身。 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          ╚════════════════════════════════════════════════════════════╝

1. 定義 Size struct

定義一個名為 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

2. 定義 Position struct

定義一個名為 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

3. 定義 Window 類別

定義一個視窗類別,它有下列屬性:

  • title:String,初始值為「New Window」
  • screenSize:Size,常數值,width = 800、height = 600
  • size:Size,初始值為 Size struct 的預設值
  • position:Position,初始值為 Position struct 的預設值
  • contents:String?,初始值為 nil

你也應該為這個類別定義一個空的初始化器。

let window = Window()
window.title
// Returns "New Window"

4. 新增調整視窗大小的方法

  • resize(to:):(Size) -> (),這個方法以 Size struct 作為輸入,並嘗試將視窗調整為指定的大小。不過,新的大小不能超過特定界限。
  • 允許的最小高度或寬度為 1。 要求的高度或寬度若小於 1,會被裁切為 1。
  • 最大高度和寬度取決於視窗目前的位置,視窗的邊緣不能超出螢幕的邊緣。 超過這些界限的值會被裁切為它們能取的最大大小。 例如,如果視窗的位置在 x = 400、y = 300,而要求調整為 height = 400、width = 300,那麼視窗會被調整為 height = 300、width = 300,因為螢幕在 y 方向上不夠大,無法完全容納這個要求。

5. 新增移動視窗的方法

  • move(to:):(Position) -> (),這和 resize(to:) 類似,不過這個方法調整的是視窗的_位置_,而不是大小。和 resize 一樣,新的位置不能超過特定限制。
  • x 和 y 的最小位置都是 0。
  • 任一方向的最大位置取決於視窗目前的大小;邊緣不能超出螢幕的邊緣。 超過這些界限的值會被裁切為它們能取的最大大小。例如,如果視窗的大小在 x = 250、y = 100,而要求移動到 x = 600、y = 200,那麼視窗會被移動到 x = 550、y = 200,因為螢幕在 x 方向上不夠大,無法完全容納這個要求。

6. 新增更新視窗文字與顯示視窗資訊的方法

  • 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]」

7. 為 Window 類別建立初始化器

視窗系統應該要有初始化器,讓視窗可以用自訂的輸入來建立。 為 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"
透過 GitHub 編輯 連結會在新視窗或分頁中開啟
Swift Exercism

準備好開始 視窗系統 了嗎?

註冊 Exercism,透過 35 個概念116 個練習 和真人引導來學習並精通 Swift,全部免費。