结构体和类是 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 结构体定义一个名为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
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
Window 类定义一个窗口类,它包含以下属性:
title:String,初始值为 "New Window"screenSize:Size,常量,width = 800,height = 600size:Size,初始值为Size结构体的默认值position:Position,初始值为Position结构体的默认值contents:String?,初始值为nil
你还应该为这个类定义一个空的初始化器。
let window = Window()
window.title
// Returns "New Window"
resize(to:):(Size) -> () - 该方法接收一个Size结构体作为输入,尝试把窗口调整为指定的大小。不过,新的大小不能超出某些界限。x = 400、y = 300,而请求把height调整为 400、width调整为 300,那么窗口会被调整为height = 300、width = 300,因为屏幕在y方向上不够大,无法完全容纳这个请求。move(to:):(Position) -> () - 这与resize(to:)类似,不过该方法把窗口的_位置_调整为请求的值,而不是大小。和resize一样,新位置也不能超出某些限制。x和y都是如此。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",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]"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"