扩展可以给现有类型添加计算型实例属性和计算型类属性。这个例子给 Swift 内建的 Double 类型添加了五个计算型实例属性,从而提供与距离单位相关工作的基本支持:
1 2 3 4 5 6 7 8 9 10 11 12 13
extensionDouble { var km: Double { returnself*1_000.0 } var m: Double { returnself } var cm: Double { returnself/100.0 } var mm: Double { returnself/1_000.0 } var ft: Double { returnself/3.28084 } } let oneInch =25.4.mm print("One inch is \(oneInch) meters") // 打印“One inch is 0.0254 meters” let threeFeet =3.ft print("Three feet is \(threeFeet) meters") // 打印“Three feet is 0.914399970739201 meters”
// 嵌套的 Suit 枚举 enumSuit: Character { case spades ="♠", hearts ="♡", diamonds ="♢", clubs ="♣" }
// 嵌套的 Rank 枚举 enumRank: Int { case two =2, three, four, five, six, seven, eight, nine, ten case jack, queen, king, ace structValues { let first: Int, second: Int? } var values: Values { switchself { case .ace: returnValues(first: 1, second: 11) case .jack, .queen, .king: returnValues(first: 10, second: nil) default: returnValues(first: self.rawValue, second: nil) } } }
// BlackjackCard 的属性和方法 let rank: Rank, suit: Suit var description: String { var output ="suit is \(suit.rawValue)," output +=" value is \(rank.values.first)" iflet second = rank.values.second { output +=" or \(second)" } return output } }
Suit 枚举用来描述扑克牌的四种花色,并用一个 Character 类型的原始值表示花色符号。
Rank 枚举用来描述扑克牌从 Ace~10,以及 J、Q、K,这 13 种牌,并用一个 Int 类型的原始值表示牌的面值。(这个 Int 类型的原始值未用于 Ace、J、Q、K 这 4 种牌。)
let theAceOfSpades =BlackjackCard(rank: .ace, suit: .spades) print("theAceOfSpades: \(theAceOfSpades.description)") // 打印“theAceOfSpades: suit is ♠, value is 1 or 11”
第一个代码片段定义了一个新的基类 MediaItem。这个类为任何出现在数字媒体库的媒体项提供基础功能。特别的,它声明了一个 String 类型的 name 属性,和一个 init(name:) 初始化器。(假定所有的媒体项都有个名称。)
1 2 3 4 5 6
classMediaItem { var name: String init(name: String) { self.name = name } }
下一个代码段定义了 MediaItem 的两个子类。第一个子类 Movie 封装了与电影相关的额外信息,在父类(或者说基类)的基础上增加了一个 director(导演)属性,和相应的初始化器。第二个子类 Song,在父类的基础上增加了一个 artist(艺术家)属性,和相应的初始化器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
classMovie: MediaItem { var director: String init(name: String, director: String) { self.director = director super.init(name: name) } }
classSong: MediaItem { var artist: String init(name: String, artist: String) { self.artist = artist super.init(name: name) } }
最后一个代码段创建了一个数组常量 library,包含两个 Movie 实例和三个 Song 实例。library 的类型是在它被初始化时根据它数组中所包含的内容推断来的。Swift 的类型检测器能够推断出 Movie 和 Song 有共同的父类 MediaItem,所以它推断出 [MediaItem] 类作为 library 的类型:
1 2 3 4 5 6 7 8
let library = [ Movie(name: "Casablanca", director: "Michael Curtiz"), Song(name: "Blue Suede Shoes", artist: "Elvis Presley"), Movie(name: "Citizen Kane", director: "Orson Welles"), Song(name: "The One And Only", artist: "Chesney Hawkes"), Song(name: "Never Gonna Give You Up", artist: "Rick Astley") ] // 数组 library 的类型被推断为 [MediaItem]
在幕后 library 里存储的媒体项依然是 Movie 和 Song 类型的。但是,若你迭代它,依次取出的实例会是 MediaItem 类型的,而不是 Movie 和 Song 类型。为了让它们作为原本的类型工作,你需要检查它们的类型或者向下转换它们到其它类型,就像下面描述的一样。
若当前 MediaItem 是一个 Movie 类型的实例,item is Movie 返回 true,否则返回 false。同样的,item is Song 检查 item 是否为 Song 类型的实例。在循环结束后,movieCount 和 songCount 的值就是被找到的属于各自类型的实例的数量。
下面的例子,迭代了 library 里的每一个 MediaItem,并打印出适当的描述。要这样做,item 需要真正作为 Movie 或 Song 的类型来使用,而不仅仅是作为 MediaItem。为了能够在描述中使用 Movie 或 Song 的 director 或 artist 属性,这是必要的。
在这个示例中,数组中的每一个 item 可能是 Movie 或 Song。事前你不知道每个 item 的真实类型,所以这里使用条件形式的类型转换(as?)去检查循环里的每次下转:
1 2 3 4 5 6 7 8 9 10 11 12 13
for item in library { iflet movie = item as?Movie { print("Movie: \(movie.name), dir. \(movie.director)") } elseiflet song = item as?Song { print("Song: \(song.name), by \(song.artist)") } }
// Movie: Casablanca, dir. Michael Curtiz // Song: Blue Suede Shoes, by Elvis Presley // Movie: Citizen Kane, dir. Orson Welles // Song: The One And Only, by Chesney Hawkes // Song: Never Gonna Give You Up, by Rick Astley
当向下转型为 Movie 应用在两个 Song 实例时将会失败。为了处理这种情况,上面的例子使用了可选绑定(optional binding)来检查可选 Movie 真的包含一个值(这个是为了判断下转是否成功。)可选绑定是这样写的“if let movie = item as? Movie”,可以这样解读:
“尝试将 item 转为 Movie 类型。若成功,设置一个新的临时常量 movie 来存储返回的可选 Movie 中的值”
若向下转型成功,然后 movie 的属性将用于打印一个 Movie 实例的描述,包括它的导演的名字 director。相似的原理被用来检测 Song 实例,当 Song 被找到时则打印它的描述(包含 artist 的名字)。
注意
转换没有真的改变实例或它的值。根本的实例保持不变;只是简单地把它作为它被转换成的类型来使用。
Any 和 AnyObject 的类型转换
Swift 为不确定类型提供了两种特殊的类型别名:
Any 可以表示任何类型,包括函数类型。
AnyObject 可以表示任何类类型的实例。
只有当你确实需要它们的行为和功能时才使用 Any 和 AnyObject。最好还是在代码中指明需要使用的类型。
这里有个示例,使用 Any 类型来和混合的不同类型一起工作,包括函数类型和非类类型。它创建了一个可以存储 Any 类型的数组 things:
things 数组包含两个 Int 值,两个 Double 值,一个 String 值,一个元组 (Double, Double),一个 Movie 实例“Ghostbusters”,以及一个接受 String 值并返回另一个 String 值的闭包表达式。
你可以在 switch 表达式的 case 中使用 is 和 as 操作符来找出只知道是 Any 或 AnyObject 类型的常量或变量的具体类型。下面的示例迭代 things 数组中的每一项,并用 switch 语句查找每一项的类型。有几个 switch 语句的 case 绑定它们匹配到的值到一个指定类型的常量,从而可以打印这些值:
for thing in things { switch thing { case0asInt: print("zero as an Int") case0asDouble: print("zero as a Double") caselet someInt asInt: print("an integer value of \(someInt)") caselet someDouble asDoublewhere someDouble >0: print("a positive double value of \(someDouble)") caseisDouble: print("some other double value that I don't want to print") caselet someString asString: print("a string value of \"\(someString)\"") caselet (x, y) as (Double, Double): print("an (x, y) point at \(x), \(y)") caselet movie asMovie: print("a movie called \(movie.name), dir. \(movie.director)") caselet stringConverter as (String) -> String: print(stringConverter("Michael")) default: print("something else") } }
// zero as an Int // zero as a Double // an integer value of 42 // a positive double value of 3.14159 // a string value of "hello" // an (x, y) point at 3.0, 5.0 // a movie called Ghostbusters, dir. Ivan Reitman // Hello, Michael
注意
Any 类型可以表示所有类型的值,包括可选类型。Swift 会在你用 Any 类型来表示一个可选值的时候,给你一个警告。如果你确实想使用 Any 类型来承载可选值,你可以使用 as 操作符显式转换为 Any,如下所示:
funcnourish(withitem: String) throws { do { try vendingMachine.vend(itemNamed: item) } catchisVendingMachineError { print("Invalid selection, out of stock, or not enough money.") } }
do { try nourish(with: "Beet-Flavored Chips") } catch { print("Unexpected non-vending-machine-related error: \(error)") } // 打印“Invalid selection, out of stock, or not enough money.”
可以使用 try? 通过将错误转换成一个可选值来处理错误。如果是在计算 try? 表达式时抛出错误,该表达式的结果就为 nil。例如,在下面的代码中,x 和 y 有着相同的数值和等价的含义:
1 2 3 4 5 6 7 8 9 10 11 12
funcsomeThrowingFunction() throws -> Int { // ... }
let x =try? someThrowingFunction()
let y: Int? do { y =try someThrowingFunction() } catch { y =nil }
如果 someThrowingFunction() 抛出一个错误,x 和 y 的值是 nil。否则 x 和 y 的值就是该函数的返回值。注意,无论 someThrowingFunction() 的返回值类型是什么类型,x 和 y 都是这个类型的可选类型。例子中此函数返回一个整型,所以 x 和 y 是可选整型。
iflet roomCount = john.residence?.numberOfRooms { print("John's residence has \(roomCount) room(s).") } else { print("Unable to retrieve the number of rooms.") } // 打印“Unable to retrieve the number of rooms.”
iflet roomCount = john.residence?.numberOfRooms { print("John's residence has \(roomCount) room(s).") } else { print("Unable to retrieve the number of rooms.") } // 打印“John's residence has 1 room(s).”
classResidence { var rooms = [Room]() var numberOfRooms: Int { return rooms.count } subscript(i: Int) -> Room { get { return rooms[i] } set { rooms[i] = newValue } } funcprintNumberOfRooms() { print("The number of rooms is \(numberOfRooms)") } var address: Address? }
使用前面定义过的类,创建一个 Person 实例,然后像之前一样,尝试访问 numberOfRooms 属性:
1 2 3 4 5 6 7
let john =Person() iflet roomCount = john.residence?.numberOfRooms { print("John's residence has \(roomCount) room(s).") } else { print("Unable to retrieve the number of rooms.") } // 打印“Unable to retrieve the number of rooms.”
上面代码中的赋值过程是可选链式调用的一部分,这意味着可选链式调用失败时,等号右侧的代码不会被执行。对于上面的代码来说,很难验证这一点,因为像这样赋值一个常量没有任何副作用。下面的代码完成了同样的事情,但是它使用一个函数来创建 Address 实例,然后将该实例返回用于赋值。该函数会在返回前打印“Function was called”,这使你能验证等号右侧的代码是否被执行。
1 2 3 4 5 6 7 8 9 10
funccreateAddress() -> Address { print("Function was called.")
let someAddress =Address() someAddress.buildingNumber ="29" someAddress.street ="Acacia Road"
如果在可选值上通过可选链式调用来调用这个方法,该方法的返回类型会是 Void?,而不是 Void,因为通过可选链式调用得到的返回值都是可选的。这样我们就可以使用 if 语句来判断能否成功调用 printNumberOfRooms() 方法,即使方法本身没有定义返回值。通过判断返回值是否为 nil 可以判断调用是否成功:
1 2 3 4 5 6
if john.residence?.printNumberOfRooms() !=nil { print("It was possible to print the number of rooms.") } else { print("It was not possible to print the number of rooms.") } // 打印“It was not possible to print the number of rooms.”
if (john.residence?.address = someAddress) !=nil { print("It was possible to set the address.") } else { print("It was not possible to set the address.") } // 打印“It was not possible to set the address.”
iflet firstRoomName = john.residence?[0].name { print("The first room name is \(firstRoomName).") } else { print("Unable to retrieve the first room name.") } // 打印“Unable to retrieve the first room name.”
iflet firstRoomName = john.residence?[0].name { print("The first room name is \(firstRoomName).") } else { print("Unable to retrieve the first room name.") } // 打印“The first room name is Living Room.”
访问可选类型的下标
如果下标返回可选类型值,比如 Swift 中 Dictionary 类型的键的下标,可以在下标的结尾括号后面放一个问号来在其可选返回值上进行可选链式调用:
下面的例子尝试访问 john 中的 residence 属性中的 address 属性中的 street 属性。这里使用了两层可选链式调用,residence 以及 address 都是可选值:
1 2 3 4 5 6
iflet johnsStreet = john.residence?.address?.street { print("John's street name is \(johnsStreet).") } else { print("Unable to retrieve the address.") } // 打印“Unable to retrieve the address.”
iflet johnsStreet = john.residence?.address?.street { print("John's street name is \(johnsStreet).") } else { print("Unable to retrieve the address.") } // 打印“John's street name is Laurel Street.”
iflet buildingIdentifier = john.residence?.address?.buildingIdentifier() { print("John's building identifier is \(buildingIdentifier).") } // 打印“John's building identifier is The Larches.”
如果要在该方法的返回值上进行可选链式调用,在方法的圆括号后面加上问号即可:
1 2 3 4 5 6 7 8 9
iflet beginsWithThe = john.residence?.address?.buildingIdentifier()?.hasPrefix("The") { if beginsWithThe { print("John's building identifier begins with \"The\".") } else { print("John's building identifier does not begin with \"The\".") } } // 打印“John's building identifier begins with "The".”
这是一个析构器实践的例子。这个例子描述了一个简单的游戏,这里定义了两种新类型,分别是 Bank 和 Player。Bank 类管理一种虚拟硬币,确保流通的硬币数量永远不可能超过 10,000。在游戏中有且只能有一个 Bank 存在,因此 Bank 用类来实现,并使用类型属性和类型方法来存储和管理其当前状态。
每个 Player 实例在初始化的过程中,都从 Bank 对象获取指定数量的硬币。如果没有足够的硬币可用,Player 实例可能会收到比指定数量少的硬币。
Player 类定义了一个 win(coins:) 方法,该方法从 Bank 对象获取一定数量的硬币,并把它们添加到玩家的钱包。Player 类还实现了一个析构器,这个析构器在 Player 实例释放前被调用。在这里,析构器的作用只是将玩家的所有硬币都返还给 Bank 对象:
1 2 3 4 5
var playerOne: Player? =Player(coins: 100) print("A new player has joined the game with \(playerOne!.coinsInPurse) coins") // 打印“A new player has joined the game with 100 coins” print("There are now \(Bank.coinsInBank) coins left in the bank") // 打印“There are now 9900 coins left in the bank”
创建一个 Player 实例的时候,会向 Bank 对象申请得到 100 个硬币,前提是有足够的硬币可用。这个 Player 实例存储在一个名为 playerOne 的可选类型的变量中。这里使用了一个可选类型的变量,是因为玩家可以随时离开游戏,设置为可选使你可以追踪玩家当前是否在游戏中。
playerOne!.win(coins: 2_000) print("PlayerOne won 2000 coins & now has \(playerOne!.coinsInPurse) coins") // 打印“PlayerOne won 2000 coins & now has 2100 coins” print("The bank now only has \(Bank.coinsInBank) coins left") // 打印“The bank now only has 7900 coins left”
在这里,玩家已经赢得了 2,000 枚硬币,所以玩家的钱包中现在有 2,100 枚硬币,而 Bank 对象只剩余 7,900 枚硬币。
1 2 3 4 5
playerOne =nil print("PlayerOne has left the game") // 打印“PlayerOne has left the game” print("The bank now has \(Bank.coinsInBank) coins") // 打印“The bank now has 10000 coins”
玩家现在已经离开了游戏。这通过将可选类型的 playerOne 变量设置为 nil 来表示,意味着“没有 Player 实例”。当这一切发生时,playerOne 变量对 Player 实例的引用被破坏了。没有其它属性或者变量引用 Player 实例,因此该实例会被释放,以便回收内存。在这之前,该实例的析构器被自动调用,玩家的硬币被返还给银行。
structFahrenheit { var temperature: Double init() { temperature =32.0 } } var f =Fahrenheit() print("The default temperature is \(f.temperature)° Fahrenheit") // 打印“The default temperature is 32.0° Fahrenheit”
这个结构体定义了一个不带形参的构造器 init,并在里面将存储型属性 temperature 的值初始化为 32.0(华氏温度下水的冰点)。
Color 提供了一个构造器,为红蓝绿提供三个合适 Double 类型的形参命名。Color 也提供了第二个构造器,它只包含名为 white 的 Double 类型的形参,它为三个颜色的属性提供相同的值。
1 2 3 4 5 6 7 8 9 10 11 12 13
structColor { let red, green, blue: Double init(red: Double, green: Double, blue: Double) { self.red = red self.green = green self.blue = blue } init(white: Double) { red = white green = white blue = white } }
两种构造器都能通过为每一个构造器形参提供命名值来创建一个新的 Color 实例:
1 2
let magenta =Color(red: 1.0, green: 0.0, blue: 1.0) let halfGray =Color(white: 0.5)
classSurveyQuestion { var text: String var response: String? init(text: String) { self.text = text } funcask() { print(text) } }
let cheeseQuestion =SurveyQuestion(text: "Do you like cheese?") cheeseQuestion.ask() // 打印“Do you like cheese?” cheeseQuestion.response ="Yes, I do like cheese."
你可以修改上面的 SurveyQuestion 示例,用常量属性替代变量属性 text,表示问题内容 text 在 SurveyQuestion 的实例被创建之后不会再被修改。尽管 text 属性现在是常量,我们仍然可以在类的构造器中设置它的值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
classSurveyQuestion { let text: String var response: String? init(text: String) { self.text = text } funcask() { print(text) } } let beetsQuestion =SurveyQuestion(text: "How about beets?") beetsQuestion.ask() // 打印“How about beets?” beetsQuestion.response ="I also like beets. (But not with cheese.)"
默认构造器
如果结构体或类为所有属性提供了默认值,又没有提供任何自定义的构造器,那么 Swift 会给这些结构体或类提供一个默认构造器。这个默认构造器将简单地创建一个所有属性值都设置为它们默认值的实例。
iflet valueMaintained =Int(exactly: wholeNumber) { print("\(wholeNumber) conversion to Int maintains value of \(valueMaintained)") } // 打印“12345.0 conversion to Int maintains value of 12345”
let valueChanged =Int(exactly: pi) // valueChanged 是 Int? 类型,不是 Int 类型
if valueChanged ==nil { print("\(pi) conversion to Int does not maintain value") } // 打印“3.14159 conversion to Int does not maintain value”
下例中,定义了一个名为 Animal 的结构体,其中有一个名为 species 的 String 类型的常量属性。同时该结构体还定义了一个接受一个名为 species 的 String 类型形参的可失败构造器。这个可失败构造器检查传入的species 值是否为一个空字符串。如果为空字符串,则构造失败。否则,species 属性被赋值,构造成功。
1 2 3 4 5 6 7 8 9
structAnimal { let species: String init?(species: String) { if species.isEmpty { returnnil } self.species = species } }
你可以通过该可失败构造器来尝试构建一个 Animal 的实例,并检查构造过程是否成功:
1 2 3 4 5 6 7
let someCreature =Animal(species: "Giraffe") // someCreature 的类型是 Animal? 而不是 Animal
iflet giraffe = someCreature { print("An animal was initialized with a species of \(giraffe.species)") } // 打印“An animal was initialized with a species of Giraffe”
如果你给该可失败构造器传入一个空字符串到形参 species,则会导致构造失败:
1 2 3 4 5 6 7
let anonymousCreature =Animal(species: "") // anonymousCreature 的类型是 Animal?, 而不是 Animal
if anonymousCreature ==nil { print("The anonymous creature could not be initialized") } // 打印“The anonymous creature could not be initialized”
let fahrenheitUnit =TemperatureUnit(symbol: "F") if fahrenheitUnit !=nil { print("This is a defined temperature unit, so initialization succeeded.") } // 打印“This is a defined temperature unit, so initialization succeeded.”
let unknownUnit =TemperatureUnit(symbol: "X") if unknownUnit ==nil { print("This is not a defined temperature unit, so initialization failed.") } // 打印“This is not a defined temperature unit, so initialization failed.”
因此上面的 TemperatureUnit 的例子可以用原始值类型的 Character 和进阶的 init?(rawValue:) 构造器重写为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
enumTemperatureUnit: Character { caseKelvin="K", Celsius="C", Fahrenheit="F" }
let fahrenheitUnit =TemperatureUnit(rawValue: "F") if fahrenheitUnit !=nil { print("This is a defined temperature unit, so initialization succeeded.") } // 打印“This is a defined temperature unit, so initialization succeeded.”
let unknownUnit =TemperatureUnit(rawValue: "X") if unknownUnit ==nil { print("This is not a defined temperature unit, so initialization failed.") } // 打印“This is not a defined temperature unit, so initialization failed.”
classVehicle { var currentSpeed =0.0 var description: String { return"traveling at \(currentSpeed) miles per hour" } funcmakeNoise() { // 什么也不做——因为车辆不一定会有噪音 } }
classTandem: Bicycle { var currentNumberOfPassengers =0 }
Tandem 从 Bicycle 继承了所有的属性与方法,这又使它同时继承了 Vehicle 的所有属性与方法。Tandem 也增加了一个新的叫做 currentNumberOfPassengers 的存储型属性,默认值为 0。
如果你创建了一个 Tandem 的实例,你可以使用它所有的新属性和继承的属性,还能查询从 Vehicle 继承来的只读属性 description:
1 2 3 4 5 6
let tandem =Tandem() tandem.hasBasket =true tandem.currentNumberOfPassengers =2 tandem.currentSpeed =22.0 print("Tandem: \(tandem.description)") // 打印:“Tandem: traveling at 22.0 miles per hour”
let automatic =AutomaticCar() automatic.currentSpeed =35.0 print("AutomaticCar: \(automatic.description)") // 打印“AutomaticCar: traveling at 35.0 miles per hour in gear 4”
防止重写
你可以通过把方法,属性或下标标记为 final 来防止它们被重写,只需要在声明关键字前加上 final 修饰符即可(例如:final var、final func、final class func 以及 final subscript)。
任何试图对带有 final 标记的方法、属性或下标进行重写的代码,都会在编译时会报错。在类扩展中的方法,属性或下标也可以在扩展的定义里标记为 final。
可以通过在关键字 class 前添加 final 修饰符(final class)来将整个类标记为 final 。这样的类是不可被继承的,试图继承这样的类会导致编译报错。
structTimesTable { let multiplier: Int subscript(index: Int) -> Int { return multiplier * index } } let threeTimesTable =TimesTable(multiplier: 3) print("six times three is \(threeTimesTable[6])") // 打印“six times three is 18”
structPoint { var x =0.0, y =0.0 funcisToTheRightOf(x: Double) -> Bool { returnself.x > x } } let somePoint =Point(x: 4.0, y: 5.0) if somePoint.isToTheRightOf(x: 1.0) { print("This point is to the right of the line where x == 1.0") } // 打印“This point is to the right of the line where x == 1.0”
structPoint { var x =0.0, y =0.0 mutatingfuncmoveBy(xdeltaX: Double, ydeltaY: Double) { x += deltaX y += deltaY } } var somePoint =Point(x: 1.0, y: 1.0) somePoint.moveBy(x: 2.0, y: 3.0) print("The point is now at (\(somePoint.x), \(somePoint.y))") // 打印“The point is now at (3.0, 4.0)”
上面的 Point 结构体定义了一个可变方法 moveBy(x:y :) 来移动 Point 实例到给定的位置。该方法被调用时修改了这个点,而不是返回一个新的点。方法定义时加上了 mutating 关键字,从而允许修改属性。
注意,不能在结构体类型的常量(a constant of structure type)上调用可变方法,因为其属性不能被改变,即使属性是变量属性,详情参见 常量结构体的存储属性:
classPlayer { var tracker =LevelTracker() let playerName: String funccomplete(level: Int) { LevelTracker.unlock(level +1) tracker.advance(to: level +1) } init(name: String) { playerName = name } }
Player 类创建一个新的 LevelTracker 实例来监测这个用户的进度。它提供了 complete(level:) 方法,一旦玩家完成某个指定等级就调用它。这个方法为所有玩家解锁下一等级,并且将当前玩家的进度更新为下一等级。(我们忽略了 advance(to:) 返回的布尔值,因为之前调用 LevelTracker.unlock(_:) 时就知道了这个等级已经被解锁了)。
你还可以为一个新的玩家创建一个 Player 的实例,然后看这个玩家完成等级一时发生了什么:
1 2 3 4
var player =Player(name: "Argyrios") player.complete(level: 1) print("highest unlocked level is now \(LevelTracker.highestUnlockedLevel)") // 打印“highest unlocked level is now 2”
player =Player(name: "Beto") if player.tracker.advance(to: 6) { print("player is now on level 6") } else { print("level 6 has not yet been unlocked") } // 打印“level 6 has not yet been unlocked”