轨道
/
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 结构体

定义一个名为Size的结构体,它有两个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 结构体

定义一个名为Position的结构体,它有两个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结构体的默认值
  • position:Position,初始值为Position结构体的默认值
  • contents:String?,初始值为nil

你还应该为这个类定义一个空的初始化器。

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

4. 添加一个调整窗口大小的方法

  • resize(to:):(Size) -> () - 该方法接收一个Size结构体作为输入,尝试把窗口调整为指定的大小。不过,新的大小不能超出某些界限。
  • 允许的最小高度或宽度为 1。 请求的高度或宽度小于 1 时会被截断为 1。
  • 最大高度和宽度取决于窗口当前的位置,窗口的边不能越过屏幕的边。 比这些界限更大的值会被截断为它们能取到的最大尺寸。 例如,如果窗口的位置是x = 400、y = 300,而请求把height调整为 400、width调整为 300,那么窗口会被调整为height = 300、width = 300,因为屏幕在y方向上不够大,无法完全容纳这个请求。

5. 添加一个移动窗口的方法

  • move(to:):(Position) -> () - 这与resize(to:)类似,不过该方法把窗口的_位置_调整为请求的值,而不是大小。和resize一样,新位置也不能超出某些限制。
  • 最小的位置是 0,x和y都是如此。
  • 两个方向上的最大位置都取决于窗口当前的尺寸;窗口的边不能越过屏幕的边。 比这些界限更大的值会被截断为它们能取到的最大尺寸。例如,如果窗口的尺寸是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",position 为 x = 10、y = 100,size 为 width = 200、height = 150,contents 为 "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结构体的默认值。

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,全部免费。