苹果Swift语言官方文档(中文翻译版)

本文档由长沙戴维营教育组织翻译和校对,由于英语水平有限,请大家指正。

长沙戴维营教育还为本教程录制了配套的视频教程在乌班图学院上免费提供,欢迎大家一起学习。

下面的章节,如果为蓝色链接表示以及翻译完毕,可以查看,如果为黑色则表示正在紧张的翻译中。本文档中文版每天都会更新,大家可以随时查看。如由Bug欢迎改正。


Swift简明教程

学习一门新语言的时候,大家都习惯于打印“Hello,world”开始。在Swift中只需要一行代码:

  1. println("Hello, world")

如果你曾经写过C或者Objective-C代码,应该不会对Swift的语法陌生。Swift中上面这一行就是一个完整的程序。你不需要再为输入/输出或者字符串处理功能导入独立的库。程序以全局代码作为入口,因此不再需要main函数了。同样,代码结尾的分号也不会再出现。

再这个简明教程中,你会学习到足够的知识来编写Swift程序。如果看完这个教程后还有什么不理解的,你可以在这本书中找到解释。

Note 最好的体验是在Xcode中将本章的内容作为一个Playground打开。Playground允许你实时预览Swift的运行结果。

简单赋值

在Swift中,使用let定义一个常量,var定义变量。常量的值不需要在编译时确定,但是只能赋值一次。这意味着你可以给一个常量赋值后,多次使用。

  1. var myVariable = 42
  2. myVariable = 50
  3. let myConstant = 42

给常量或者变量赋值的时候,类型必须相同。但是并不需要每次都显式的写出它们的类型,因为编译器默认的确定了一些数据的类型。例如上面的代码中myVariable是一个整数类型。

如果初始值没有提供足够的类型信息(或者没有初始值),可以在变量后增加类型说明符。

  1. let implicitInteger = 70
  2. let implicitDouble = 70.0
  3. let explicitDouble: Double = 70

试验 创建一个常量,指定它的类型为Float并赋值为4。

Swift中的数据类型之间不会进行隐式的转换。如果需要在不同数据类型之间进行转换的话,需要显式的创建一个目标类型的实例。

  1. let label = "The width is "
  2. let width = 94
  3. let widthLabel = label + String(width)

试验 尝试删除最后一行的String,看看会有什么错误。

实际上,还有一种更加简单的方法将值包含到字符串中:把需要包含的值写在圆括号()中,然后在括号前添加反斜线\就可以了,例如:

  1. let apples = 3
  2. let oranges = 5
  3. let appleSummary = "I have \(apples) apples."
  4. let fruitSummary = "I have \(apples + oranges) pieces of fruit."

试验 在字符串中使用\()包含浮点数

Swift使用[]创建和访问数组和字典。

  1. var shoppingList = ["catfish", "water", "tulips", "blue paint"]
  2. shoppingList[1] = "bottle of water"
  3. var occupations = [
  4. "Malcolm": "Captain",
  5. "Kaylee": "Mechanic",
  6. ]
  7. occupations["Jayne"] = "Public Relations"

也可以使用初始化语句创建空的数组和字典。

  1. let emptyArray = String[]()
  2. let emptyDictionary = Dictionary<String, Float>()

如果类型信息能够被推导出来,你可以直接将空数组写为[],空字典写为[:]。例如给函数传递参的时候。

  1. shoppingList = [] // Went shopping and bought everything.

流程控制

使用ifswitch进行条件判断,for-inforwhiledo-while进行循环。条件判断时的圆括号时可选的,但是if或者循环体的花括号{}时必须的。

  1. let individualScores = [75, 43, 103, 87, 12]
  2. var teamScore = 0
  3. for score in individualScores {
  4. if score > 50 {
  5. teamScore += 3
  6. } else {
  7. teamScore += 1
  8. }
  9. }
  10. teamScore

if语句中,判断条件必须时布尔表达式,也就是说if score { ... }的形式是错误的,它并不会隐式的与0进行比较。

你可以使用iflet一起来判断值是否缺失。这些值被看作是选项(Optionals)。一个选项包含一个值或者nil来表示是否缺失内容。在类型后面添加?标记一个值是一个选项。

  1. var optionalString: String? = "Hello"
  2. optionalString == nil
  3. var optionalName: String? = "John Apppleseed"
  4. var greeting = "Hello!"
  5. if let name = optionalName {
  6. greeting = "Hello, \(name)"
  7. }

试验optionalName的值改为nil,看一下能不能得到问候。在if后添加else语句,使得optionalNamenil的时候打印不同的问候语句。

如果选项的值为nil,条件为falseif后的语句被跳过。否则选项的值被赋值给let后的常量,并且该常量的作用域为花括号里面。

Swift里的switch语句支持任意数据类型以及比较操作,而不是被限制为整数的相等。

  1. let vegetable = "red pepper"
  2. switch vegetable {
  3. case "celery":
  4. let vegetableComment = "Add some raisins and make ants on a log."
  5. case "cucumber", "watercress":
  6. let vegetableComment = "That would make a good tea sandwich."
  7. case let x where x.hasSuffix("pepper"):
  8. let vegetableComment = "Is it a spicy \(x)?"
  9. default:
  10. let vegetableComment = "Everything tasts good in soup."
  11. }

试验 尝试移除default语句,看看有什么错

执行完switch的一个case后,程序会从switch语句中跳出,而不会继续执行下一个case语句,因此在Swift中不需要显式的使用break跳出每一个分支。

在使用for-in进行迭代的时候,每次迭代都会返回一个键值对:

  1. let interestingNumbers = [
  2. "Prime": [2, 3, 5, 7, 11, 13],
  3. "Fibonacci": [1, 1, 2, 3, 5, 8],
  4. "Square": [1, 4, 9, 16, 25],
  5. ]
  6. var largest = 0
  7. for (kind, numbers) in interestingNumbers {
  8. for number in numbers {
  9. if number > largest {
  10. largetst = number
  11. }
  12. }
  13. }

largest

试验 增加一个变量,记录最大值所在的分类

while会一直循环执行,直到判断条件发生变化。如果使用do-while,循环体至少会执行一次。

  1. var n = 2
  2. while n < 100 {
  3. n = n * 2
  4. }
  5. n
  6. var m = 2
  7. do {
  8. m = m * 2
  9. } while m < 100
  10. m

for循环中通过索引来控制循环,其中..用来创建一个索引的范围,下面两个循环是一样的效果

  1. var firstForLoop = 0
  2. for i in 0..3 {
  3. firstForLoop += i
  4. }
  5. firstForLoop
  6. var secondForLoop = 0
  7. for var i = 0; i < 3; ++i {
  8. secondForLoop += 1
  9. }
  10. secondForLoop

使用..(两点)创建的范围不包括最大值,而...(三点)创建的范围包含最大值。

函数与闭包

Swift使用func关键字定义函数,然后与C语言一样使用函数名进行调用,而函数返回值类型用->标示。

  1. func greet(name: String, day: String) -> String {
  2. return "Hello \(name), today is \(day)."
  3. }
  4. greet("Bob", "Tuesday")

试验 移除day参数,添加一些别的信息

使用元组从函数中返回多个值。

  1. func getGasPrices() -> (Double, Double, Double) {
  2. return (3.59, 3.69, 3.79)
  3. }
  4. getGasPrices()

Swift的函数可以接受可变参数。

  1. func sumOf(numbers: Int...) -> Int {
  2. var sum = 0
  3. for number in numbers {
  4. sum += number
  5. }
  6. return sum
  7. }
  8. sumOf()
  9. sumOf(43, 597, 12)

试验 编写一个计算参数平均值的函数

Swift的函数可以进行嵌套。被嵌套的函数可以访问外面函数定义的变量。

  1. func returnFifteen() -> Int {
  2. var y = 10
  3. func add() {
  4. y += 5
  5. }
  6. add()
  7. return y
  8. }
  9. returnFifteen()

Swift中的函数也是基本的数据类型,也就是说可以在一个函数中返回另外一个函数。

  1. func makeIncrementer() -> (Int -> Int) {
  2. func addOne(number: Int) -> Int {
  3. return 1 + number
  4. }
  5. return addOne
  6. }
  7. var increment = makeIncrementer()
  8. increment(7)

函数当然也可以作为其它函数的参数进行传递。

  1. func hasAnyMatches(list: Int[], condition: Int -> Bool) -> Bool {
  2. for item in list {
  3. if condition(item) {
  4. return true
  5. }
  6. }
  7. return false
  8. }
  9. func lessThanTen(number: Int) -> Bool {
  10. return number < 10
  11. }
  12. var numbers = [20, 19, 7, 12]
  13. hasAnyMatches(numbers, lessThanTen)

实际上Swift中的函数是闭包的一个特例。我们可以使用没有名字的{}来定义闭包,而闭包内容与返回值类型之间用in进行分隔。

  1. numbers.map({
  2. (number: Int) -> Int in
  3. let result = 3 * number
  4. return result
  5. })

试验 重写上面的闭包,使得所有的奇数都返回0

还有几种方式可以使得闭包的编写更加简洁。当闭包的类型是已知的,比如作为回调或者代理的时候,你可以省略参数类型或者返回值类型。如下:

  1. numbers.map({ number in 3 * number })

在闭包中还可以使用位置来引用参数,这在编写非常短的闭包的时候比较有用。当闭包作为函数的最后一个参数时,可以将它放在函数的圆括号后面。

  1. sort([1, 5, 3, 12, 2]){ $0 > $1 }

类与对象

Swift中使用class关键字定义类。类里面属性的声明与定义变量和常量差不多,而成员方法也与普通函数的写法一样,只是写在类里面。

  1. class Shape {
  2. var numberOfSides = 0
  3. func simpleDescription() -> String {
  4. return "A shape with \(numberOfSides) sides."
  5. }
  6. }

试验 给上面的类用let添加一个常量属性,并且增加一个能够接受一个参数的方法

直接在类名后增加圆括号就可以创建这个类的实例,然后通过点操作符访问它的属性和成员方法。

  1. var shape = Shape()
  2. shape.numberOfSides = 7
  3. var shapeDescription = shape.simpleDescription()

不过上面的Shape类还缺少一些重要的东西:在创建对象的时候进行初始化的初始化器。初始化器用init进行定义。

  1. class NameShape {
  2. var numberOfSides: Int = 0
  3. var name: String
  4. init(name: String) {
  5. self.name = name
  6. }
  7. func simpleDescription() -> String {
  8. return "A shape with \(numberOfSides) sides."
  9. }
  10. }

这里要注意的是init方法中self的用法。初始化方法的参数传递与普通的函数类似。类中的属性都应该进行初始化赋值,不管是在声明的时候还是在初始化器中。

deinit是Swift的析构函数,与dealloc类似,用在对象销毁时进行清理工作。

在类名后使用冒号:和父类的名字表示继承关系,Swift中并不要求每个类都有父类。

子类使用override关键字来重写父类的方法,如果没有写override的话,会出现编译器错误。同样,编译器也会检查带有override关键字的方法是否真的重写了父类的方法。

  1. class Square : NamedShape {
  2. var sideLength: Double
  3. init(sideLength: Double, name: String) {
  4. self.sideLength = sideLength
  5. super.init(name: name)
  6. numberOfSides = 4
  7. }
  8. func area() -> Double {
  9. return sideLength * sideLength
  10. }
  11. override func simpleDescription() -> String {
  12. return "A square with sides of length \(sideLength)."
  13. }
  14. }
  15. let test = Square(sideLength: 5.2, name: "my test square")
  16. test.area()
  17. test.simpleDescription()

试验NamedShape创建一个叫Circle的子类,使它能够接受一个半径和名字作为参数。然后实现areadescribe方法。

与Objective-C类似,Swift还能给属性定义gettersetter

  1. class EquilateralTriangle: NamedShape {
  2. var sideLength: Double = 0.0
  3. init(sideLength: Double, name: String) {
  4. self.sideLength = sideLength
  5. super.init(name: name)
  6. numberOfSides = 3
  7. }
  8. var perimeter: Double {
  9. get {
  10. return 3.0 * sideLength
  11. }
  12. set {
  13. sideLength = newValue / 3.0
  14. }
  15. }
  16. override func simpleDescription() -> String {
  17. return "An equilateral triangle with sides of length \(sideLength)."
  18. }
  19. }
  20. var triangle = EquilateralTraingle(sideLength: 3.1, name: "a triangle")
  21. triangle.perimeter
  22. triangle.perimeter = 9.9
  23. triangle.sideLength

perimeter的设置方法中,新的值默认存放在newValue变量里。当然,也可以显式的在set方法后的圆括号中提供变量名字。

注意EquilateralTriangle类的初始化器里有三步:

  1. 设置子类中定义的属性的值。
  2. 调用父类的初始化器
  3. 改变父类中定义的属性的值。

如果你不需要计算属性的值,但是想要在属性的值发生改变之前或者改变后执行一些任务,可以使用willSetdidSet。例如下面的代码可以保证三角形和四边形的边长总是相同的。

  1. class TriangleAndSquare {
  2. var triangle: EquilateralTraingle {
  3. willSet {
  4. square.sideLength = newValue.sideLength
  5. }
  6. }
  7. var square: Square {
  8. willSet {
  9. triangle.sideLength = newValue.sideLength
  10. }
  11. }
  12. init(size Double, name: String) {
  13. square = Square(sideLength: size, name: name)
  14. triangle = EuilateralTriangle(sideLength: size, name: name)
  15. }
  16. }
  17. var triangleAndSquare = TriangleAndSquare(size: 10, name: "another test shape")
  18. triangleAndSquare.square.sideLength
  19. triangleAndSquare.triangle.sideLength
  20. triangleAndSquare.square = Square(sideLength: 50, name: "larger square")
  21. triangleAndSquare.triangle.sideLength

类的成员方法与函数有一个非常重要的区别。函数里的参数名只能在函数体里面使用,而方法的参数名可以在调用的时候使用。

  1. class Counter {
  2. var count: Int = 0
  3. func incrementBy(amount: Int, numberOfTimes times: Int) {
  4. count += amount * times
  5. }
  6. }
  7. var counter = Counter()
  8. counter.imcrementBy(2, numberOfTimes: 7)

使用选项(Optional)的时候,可以在方法、属性等操作符前使用?。如果选项的值为nil则忽略执行?后的表达式,并且整个表达式的值为nil。否则就执行?后的表达式。

  1. let optionalSquare: Square? = Square(sideLength: 2.5, name: "optinal square")
  2. let sideLength = optionalSquare?.sideLength

枚举与结构体

使用enum关键字创建枚举类型。与类类似,枚举类型中一样可以定义方法。

  1. enum Rank: Int {
  2. case Ace = 1
  3. case Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten
  4. case Jack, Queen, King
  5. func simpleDescription() -> String {
  6. switch self {
  7. case .Ace:
  8. return "ace"
  9. case .Jack:
  10. return "jack"
  11. case .Queen:
  12. return "queen"
  13. case .King:
  14. return "king"
  15. default;
  16. return String(self.toRaw())
  17. }
  18. }
  19. }
  20. let ace = Rank.Ace
  21. let aceRawValue = ace.toRow()

试验 编写一个函数用来比较两个Rank枚举值

在上面的代码中,枚举类型的原始值是Int类型,定义的时候只明确了第一个,后面的依次递增。当然也可以使用浮点数或者字符串作为枚举类型的值。

使用toRawfromRaw可以在枚举类型和原始值之间进行转换。

  1. if let convertedRank = Rank.fromRaw(3) {
  2. let threeDescription = convertedRank.simpleDescription()
  3. }

枚举类型成员的值是一个实际的值,而不仅仅是他们原始值的另外一种写法。事实上,你可以不提供有意义的原始值。

  1. enum Suit {
  2. case Spades, hearts, Diamonds, Clubs
  3. func simpleDescription() -> String {
  4. switch self {
  5. case .Spades:
  6. return "spades"
  7. case .Hearts:
  8. return "hearts"
  9. case .Diamonds:
  10. return "diamonds"
  11. case .Clubs:
  12. return "clubs"
  13. }
  14. }
  15. }
  16. let hearts = Suit.Hearts
  17. let heartsDescription = hearts.simpleDescription()

试验Suit添加一个color方法,当枚举值为Spades或者Clubs的时候返回”black”,否则返回”red”。

有两种方法引用Hearts的成员:给hearts常量赋值的时候,使用了完整的Suit.Hearts名字,这是因为并没有显式的声明该常量的类型。而在switch中,因为已经知道self就是Suit类型的值,直接使用.Hearts的简写就可以了。你可以在任何已经明确数据类型的情况下使用简写。

Swift使用struct关键字创建一个结构体。结构体与类在很多方面都是类似的,包括方法和初始化器。结构体和类最大的不同在于使用结构体传参的时候是按值传递,而对于类来说是按引用传递。

  1. struct Card {
  2. var rank: Rank
  3. var suit: Suit
  4. func simpleDescription() -> String {
  5. return "The \(rank.simpleDescription()) of \(suit.simpleDescription())"
  6. }
  7. }
  8. let threeOfSpades = Card(rank: .Three, suit: .Spades)
  9. let threeOfSpadesDescription = threeOfSpades.simpleDescription()

试验Card添加一个方法用来创建一整套德州扑克,其中每张牌都必须包括花色和点数。

每个枚举成员都可以关联一些值。同一个枚举类型的实例成员都能够关联不同的值。在创建枚举类型的实例时可以给枚举成员关联上一些值。关联的值与原始的值不一样:同一个枚举类型的所有成员中,原始值都是一样的,在定义枚举类型的时候就已经确定了。

例如从服务器上获取日落和日出的实际。服务器会返回正确的信息或者出错。

  1. enum ServerResponse {
  2. case Result(String, String)
  3. case Error(String)
  4. }
  5. let success = ServerResponse.Result("6:00 am", "8:09 pm")
  6. let failure = ServerResponse.Error("Out of cheese")
  7. switch success {
  8. case let .Result(sunrise, sunset):
  9. let serverResponse = "Sunrise is at \(sunrise) and sunset is at \(sunset)."
  10. case let .Error(error):
  11. let serverResponse = "Failure... \(error)"
  12. }

试验ServerResponseswitch添加第三个case分支。

协议与扩展

使用protocol关键字声明协议。

  1. protocol ExampleProtocol {
  2. var simpleDescription: String { get }
  3. mutating func adjust()
  4. }

类、枚举和结构体都能够响应协议。

  1. class SimpleClass : ExampleProtocol {
  2. var simpleDescription: String = "A very simple class."
  3. var anotherProperty: Int = 69105
  4. func adjust() {
  5. simpleDescription += " Now 100% adjusted."
  6. }
  7. }
  8. var a = SimpleClass()
  9. a.adjust()
  10. let aDescription = a.simpleDescription
  11. struct SimpleStructure: ExampleProtocol {
  12. var simpleDescription: String = "A simple structure"
  13. mutating func adjust() {
  14. simpleDescription += " (adjusted)"
  15. }
  16. }
  17. var b = SimpleStructure()
  18. b.adjust()
  19. let bDescription = b.simpleDescription

试验 定义一个枚举类型并响应上面的协议

声明结构体SimpleStructure时使用的mutating关键字标记会对结构体进行修改的方法。由于类中的方法总是可以修改类的对象,因此不需要在SimpleClass中进行标明。

Swift使用extension关键字给一个已经存在的类型添加功能。你可以给任何地方声明的类型使用扩展,使得它响应某个协议,不管它是从框架还是其它地方导入的。

  1. extension Int: ExampleProtocol {
  2. var simpleDescription: String {
  3. return "The number \(self)"
  4. }
  5. mutating func adjust() {
  6. self += 42
  7. }
  8. }
  9. 7.simpleDescription

试验Double类型使用扩展添加一个absoluteValue属性。

可以像其它数据类型一样用协议来声明变量或这常量,例如创建一个对象的集合,使得它可以容纳可以响应同一个协议的不同数据类型的值。当你使用这些值的时候,不能调用协议以外的方法。

  1. let protocolValue: ExampleProtocol = a
  2. protocolValue.simpleDescription
  3. //protocolVAlue.anotherProperty //Uncomment to see the error

尽管protocolValue变量是SimpleClass的对象,但是编译器把它当成是ExampleProtocol类型。这就意味着你只能访问协议里定义的方法和属性。

泛型

使用尖括号可以定义泛型函数或类型。

  1. func repeat<ItemType>(item: ItemType, times: Int) -> ItemType[] {
  2. var result = ItemType[]()
  3. for i in 0..times {
  4. result += item
  5. }
  6. return result
  7. }
  8. repeat("knock", 4)

同样可以创建泛型类、枚举类型和结构体类型。

  1. //重新实现Swift标准库中的optional类型
  2. enum OptionalValue<T> {
  3. case None
  4. case Some(T)
  5. }
  6. var possibleInteger: OptionalValue<Int> = .None
  7. possibleInteger = .Some(100)

使用where关键字可以给泛型添加一个限制列表,例如:两个类型必须相同、必须实现某个协议、必须是某个类的子类等。

  1. func anyCommonElements <T, U where T: Sequence, U: Sequence, T.GeneratorType.Element: Equatable, T.GeneratorType.Element == U.GeneratorType.Element> (lhs: T, rhs: U) -> Bool {
  2. for lhsItem in lhs {
  3. for rhsItem in rhs {
  4. if lhsItem == rhsItem {
  5. return true
  6. }
  7. }
  8. }
  9. return false
  10. }
  11. anyCommonElements([1, 2, 3], [3])

试验 修改anyCommonElements函数,使得它能够返回两个序列的交集。

在简单的情况下,可以省略where,直接写后面的部分就可以了,如<T: Equatable><T where T: Equatable>的作用一样的。