Swift 语言快速入门

June 09, 2014

原文链接

几乎每一门语言的入门都是从 "Hello World" 开始的,我们也不例外,用 Swift,只要一行就实现了:

println("Hello World")

如果你曾用过 C 或者 Objective-C,你会发现 Swift 的语法看上去非常的熟悉,上面那一行代码就是一个完整的程序。你不需要为了处理输入/输出或字符串去引入任何独立的方法库;你不再需要 main 函数了,因为所有写到全局作用域的代码都会被当作是入口代码而被执行;同时你也不再需要给每一行代码去加那个分号结束符了。

这篇文章将教会你如何开始用 Swift 完成一个有趣的开发任务。如果你有些地方没有看明白也没有关系,后续的文章中会有更详细的解释。

注意

为了更好的体验 Swift 的魅力,下载下面这个 Playground 压缩包,并在 Xcode 中打开它。Playgrounds 可以让你编辑代码并直接查看结果,而无需编译和运行程序。

下载 Playground 例子

变量和常量

常量的定义是以 let 开头,而变量的定义是以 var 来开头。常量可以在运行时而非编译时被赋值,但你必须为常量赋一个值,而且只能赋值一次。也就是说常量是用于你想定义一个值然后随处都能使用的情况。

var myVariable = 42
myVariable = 50
let myConstant = 42

定义常量或者变量时,其类型必须是和赋给它的值的类型是相同,但你可以不显示的声明其具体类型。只要在定义常量或者变量的时候给它赋一个值,就可以让编译器自己去推断它是什么类型的。在上面的例子中,编译器会推断 myVariable 是一个整数类型,因为其初始值是一个整数。

如果初始值的数据类型并不明确(或者没有初始值)的时候,你需要在常量/变量后面写明其具体类型,并用冒号分隔开。

let implicitInteger = 70
let implicitDouble = 70.0
let explicitDouble: Double = 70

练习题

创建一个 Float 类型的常量,并为其赋值 4

常量和变量一旦被定义后其类型就不能再被修改。如果你想要改变一个常量或变量的类型,只有尝试定义一个新的实例。

let label = "The width is "
let width = 94
let widthLabel = label + String(width)

练习题

尝试删除上面代码中最后一样的 String 转换方法,看看会报什么错?

还有一个非常简单的方法可以把变量加入到字符串里面:用括号把变量名括起来,并在括号前面加上一个反斜杠。例如:

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

练习题

\() 在字符串中包含一个浮点数计算;借用 "Hello World" 的方式向任何人打招呼。

用 ([]) 来创建数组和词典类型,并用下标或键名来访问其元素。

var shoppingList = ["catfish", "water", "tulips", "blue paint"]
shoppingList[1] = "bottle of water"
 
var occupations = [
    "Malcolm": "Captain",
    "Kaylee": "Mechanic",
]
occupations["Jayne"] = "Public Relations"

用对象初始化语法来创建空数组或空词典。

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

你也可以用 [] 来创建一个空数组、用 [:] 来创建一个空词典,因为数据类型是可以被推断出来的。比如,当你为你一个变量赋值的时候,或者给一个函数传参的时候。

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

控制流

关键字 ifswitch 用于控制条件,for-inforwhiledo-while 用于控制循环。条件语句外面的括号是可选的。代码块必须用大括号括起来。

let individualScores = [75, 43, 103, 87, 12]
var teamScore = 0
for score in individualScores {
    if score > 50 {
        teamScore += 3
    } else {
        teamScore += 1
    }
}
teamScore

if 语句中,条件语句必须是一个 Boolean 表达式,也就是说上面的代码里如果有 if score { ... } 是会报错的,0 不会被视为 Boolean 值。

可以在 if 后面使用 let 去定义个可选的常量,可选常量在其值不存在时会返回 nil。在数据类型后面加上一个问号即可将该变量标志成一个可选值。

var optionalString: String? = "Hello"
optionalString == nil
 
var optionalName: String? = "John Appleseed"
var greeting = "Hello!"
if let name = optionalName {
    greeting = "Hello, \(name)"
}

练习题

修改 optionalNamenil,你看到的输出结果是什么?曾加 else 从句,在 optionalName 等于 nil 的时候给它赋一个别的值。

如果一个可选值等于 nil,该条件返回 false,则大括号里面的代码不会被执行。否则,该可选值会被赋给 let 定义的常量,这个常量的作用域仅限于随后的大括号范围。

关键字 switch 支持任意类型的数据,已经各种比较操作,不仅限于 intergers、tests 或 equality。

let vegetable = "red pepper"
switch vegetable {
case "celery":
    let vegetableComment = "Add some raisins and make ants on a log."
case "cucumber", "watercress":
    let vegetableComment = "That would make a good tea sandwich."
case let x where x.hasSuffix("pepper"):
    let vegetableComment = "Is it a spicy \(x)?"
default:
    let vegetableComment = "Everything tastes good in soup."
}

练习题

尝试删除 default 语句,看看会发生什么?

switch 中,一个匹配到的 case 语句执行完后便会退出,不会继续执行下一条 case,所以不用手工去 break 代码。

在用 for-in 遍历词典类型的数据时,只需要提供一对变量名分别代表每一行记录的键名和键值。

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

练习题

增加一个变量来跟踪最大的 number,并记下它的值。

while 可以循环执行一个代码块,直到循环条件改变。循环的条件可以放到代码块的结尾,保证该循环至少被执行一次。

var n = 2
while n < 100 {
    n = n * 2
}
n
 
var m = 2
do {
    m = m * 2
} while m < 100
m

有两种方式可以做 for 循环,一种是使用 .. 来创建一个数值范围;另一种是传统的“初始化、条件、加法计算”的方式。两种方式的效果都是一样的:

var firstForLoop = 0
for i in 0..3 {
    firstForLoop += i
}
firstForLoop
 
var secondForLoop = 0
for var i = 0; i < 3; ++i {
    secondForLoop += 1
}
secondForLoop

用两个点 .. 的循环不会到达那个最大数,而用三个点 ... 的循环会包含最大数。

函数和闭包

函数的定义是以 func 开头的。调用函数的方法和大多数语言一样。函数的返回数据类型是定义在 -> 后面。

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

练习题

把上面的 day 参数换成其它参数,比如告诉用户今天的特价午餐是什么。

函数使用元组(tuple)类型可以有多个返回值。

func getGasPrices() -> (Double, Double, Double) {
    return (3.59, 3.69, 3.79)
}
getGasPrices()

函数可以只用一个参数名来收集可变长度的参数列表,并自动把它们转换成一个数组。

func sumOf(numbers: Int...) -> Int {
    var sum = 0
    for number in numbers {
        sum += number
    }
    return sum
}
sumOf()
sumOf(42, 597, 12)

练习题

编写一个函数可以计算整型参数列表的平均值。

函数是可以被嵌套的。嵌套的函数中可以访问外面一层函数中定义的变量。你可以用嵌套函数来管理函数中复杂的代码逻辑。

func returnFifteen() -> Int {
    var y = 10
    func add() {
        y += 5
    }
    add()
    return y
}
returnFifteen()

函数也是 first-class 类型的。也就是说一个函数可以把另外一个函数当作返回值(有点像JavaScript)。

func makeIncrementer() -> (Int -> Int) {
    func addOne(number: Int) -> Int {
        return 1 + number
    }
    return addOne
}
var increment = makeIncrementer()
increment(7)

一个函数也可以作为参数被传给其它函数。

func hasAnyMatches(list: Int[], condition: Int -> Bool) -> Bool {
    for item in list {
        if condition(item) {
            return true
        }
    }
    return false
}
func lessThanTen(number: Int) -> Bool {
    return number < 10
}
var numbers = [20, 19, 7, 12]
hasAnyMatches(numbers, lessThanTen)

函数其实也是一种闭包。一个闭包可以不定义函数的名字,只需要用({})把代码块包起来,并用 in 来把参数定义、返回类型部分和代码块分割开。

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

练习题

修改上面的代码,判断在出现奇数时返回 0。

闭包还可以被写的更简单灵活一些。当一个闭包的类型是已知的,比如一个简单的回调函数,你可以省略掉参数和返回类型的定义。只要一行代码就可以搞定。

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

你可以用序号去引用参数,代替参数名称 - 这个方法常用在编写超简短的闭包时。最后一个出现的参数会被当作闭包的返回值。

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

对象和类

类的定义是以 class 开头的,紧接着是类的名称。类的属性定义的方法和我们前面讲到的常量和变量的定义方法一样。同样,方法的定义也和函数的定义是一样的。

class Shape {
    var numberOfSides = 0
    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }
}

练习题

let 为上面的类增加一个常量属性,另外增加一个方法包含参数定义。

在类名的后面跟上一个括号就可以创建一个类的实例。用 . 的语法就可以放到实例的各种属性和方法。

var shape = Shape()
shape.numberOfSides = 7
var shapeDescription = shape.simpleDescription()

类的构造函数是 init

class NamedShape {
    var numberOfSides: Int = 0
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }
}

注意看上面 name 参数是如何初始化 self.name 属性的。定义好构造函数后,你就可以像调用函数一样去创建一个类的实例。类的每个属性都需要被初始化,可以是在定义的时候(比如 numberOfSides),也可以是在构造函数里(比如 name)。

类的析构函数是 deinit

定义子类时,用冒号把子类和父类的名字隔开。语言本身没有强制祖先类,所以你可以只在需要的时候才定义父类。

在子类方法定义的前面加上 override 关键字可以重写父类的方法。如果子类中的方法尝试重写父类中的方法,而忘记定义 override 关键字,编译器会发现并抛出错误。同样如果子类方法定义了 override 关键字,而父类中并不存在这个方法,编译也是无法通过的。

class Square: NamedShape {
    var sideLength: Double
    
    init(sideLength: Double, name: String) {
        self.sideLength = sideLength
        super.init(name: name)
        numberOfSides = 4
    }
    
    func area() ->  Double {
        return sideLength * sideLength
    }
    
    override func simpleDescription() -> String {
        return "A square with sides of length \(sideLength)."
    }
}
let test = Square(sideLength: 5.2, name: "my test square")
test.area()
test.simpleDescription()

练习题

另外编写一个 NamedShape 的子类,叫做 Circle,其构造函数的参数是 radiusname。并在 Circle 类中实现 areadescribe 方法。

属性除了可以简单的访问之外,还可以有自己的 getter 和 setter 方法。

class EquilateralTriangle: NamedShape {
    var sideLength: Double = 0.0

    init(sideLength: Double, name: String) {
        self.sideLength = sideLength
        super.init(name: name)
        numberOfSides = 3
    }

    var perimeter: Double {
        get {
            return 3.0 * sideLength
        }
        set {
            sideLength = newValue / 3.0
        }
    }

    override func simpleDescription() -> String {
        return "An equilateral triagle with sides of length \(sideLength)."
    }
}
var triangle = EquilateralTriangle(sideLength: 3.1, name: "a triangle")
triangle.perimeter
triangle.perimeter = 9.9
triangle.sideLength

在 setter 函数中,有一个默认的参数名叫 newValue。你也可以在 set 后面加上括号,然后自己定义参数的名称。

上面 EquilateralTriangle 类的构造函数中做了三件事:

  1. 为子类中定义的属性赋值。
  2. 调用父类的构造函数。
  3. 给父类中定义的属性赋值。可以在这里完成其它任何初始化工作。

还有两个方法可以帮你捕捉属性被修改之前和之后的事件,它们分别是 willSetdidSet。比如,在下面这段代码中,我们可以确保三角形的边长总是和正方形的边长相等。

class TriangleAndSquare {
    var triangle: EquilateralTriangle {
        willSet {
            square.sideLength = newValue.sideLength
        }
    }
    var square: Square {
        willSet {
            triangle.sideLength = newValue.sideLength
        }
    }
    init(size: Double, name: String) {
        square = Square(sideLength: size, name: name)
        triangle = EquilateralTriangle(sideLength: size, name: name)
    }
}
var triangleAndSquare = TriangleAndSquare(size: 10, name: "another test shape")
triangleAndSquare.square.sideLength
triangleAndSquare.triangle.sideLength
triangleAndSquare.square = Square(sideLength: 50, name: "larger square")
triangleAndSquare.triangle.sideLength

函数和类的方法有一个非常重要的区别:在调用时,函数是不能指定给某个参数赋值的,而类的方法就可以(除了第一个参数以外)。方法还支持同名参数,一个用在调用时,你可以指定一个第二名称,只在方法内部使用。

class Counter {
    var count: Int = 0
    func incrementBy(amount: Int, numberOfTimes times: Int) {
        count += amount * times
    }
}
var counter = Counter()
counter.incrementBy(2, numberOfTimes: 7)

在使用可选常量/变量时,你可以在方法、属性等前面加上一个 ?。那么如果 ? 前面的值为 nil 的时候,其后面的代码将会被忽略而不被执行。(好像CoffeeScript)。

let optionalSquare: Square? = Square(sideLength: 2.5, name: "optional square")
let sideLength = optionalSquare?.sideLength

枚举和结构

enum 关键字来定义枚举类型。像类一样,枚举类型也可以有自己的方法。

enum Rank: Int {
    case Ace = 1
    case Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten
    case Jack, Queen, King
    func simpleDescription() -> String {
        switch self {
        case .Ace:
            return "ace"
        case .Jack:
            return "jack"
        case .Queen:
            return "queen"
        case .King:
            return "king"
        default:
            return String(self.toRaw())
        }
    }
}
let ace = Rank.Ace
let aceRawValue = ace.toRaw()

练习题

编写一个函数来比较两个 Rank 的值是否完全一样。

在上面的例子中,因为枚举的值类型为 Int,你只需要给第一个值赋值,其余值会自动按顺序递增的赋值(像Golang)。枚举的值类型也可以是字符串或者浮点数等。

toRawfromRaw 方法可以在枚举的键名和键值间做转换。

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

有时候你也可以不给枚举的值指定具体的类型。

enum Suit {
    case Spades, Hearts, Diamonds, Clubs
    func simpleDescription() -> String {
        switch self {
        case .Spades:
            return "spades"
        case .Hearts:
            return "hearts"
        case .Diamonds:
            return "diamonds"
        case .Clubs:
            return "clubs"
        }
    }
}
let hearts = Suit.Hearts
let heartsDescription = hearts.simpleDescription()

练习题

Suit 增加一个 color 方法,当值为 SpadesClubs 时返回 black,当值为 HeartsDiamonds 时返回 red

有两种方式可以访问 Hearts 的枚举成员:在定义尝试 hearts 并为其赋值时,我们用了完整的 Suit.Hearts 因为常量的类型在这里是不可知的;在 switch 里枚举,self 的类型是已知的,所以我们可以用缩写 .Hearts 来访问变量。在知道变量类型的时候就可以使用缩写的方式。

结构的定义是以 struct 开头的。结构和类一样,可以定义方法,也有自己的初始化方法。结构和类最大的区别是结构变量在赋值和传参的时候总是被完整复制一份,而类只是专递其引用地址。

struct Card {
    var rank: Rank
    var suit: Suit
    func simpleDescription() -> String {
        return "The \(rank.simpleDescription()) of \(suit.simpleDescription())"
    }
}
let threeOfSpades = Card(rank: .Three, suit: .Spades)
let threeOfSpadesDescription = threeOfSpades.simpleDescription()

练习题

Card 增加一个方法用来创建一叠牌,每张牌都有自己的点数和花色。

从枚举成员的实例可以获取到与该实例相关联的值。同样的枚举成员的实例可以有与之关联的不同的值。你在创建实例的时候提供其关联的值。关联值和原始的值是不一样的。原始值是在定义枚举类型时指定的,所有枚举类型的实例都有相同的原始值。

举例来说,想要从服务器端获取日出和日落的时间,服务器返回的信息可以是时间信息,也可以是错误信息。

enum ServerResponse {
    case Result(String, String)
    case Error(String)
}
 
let success = ServerResponse.Result("6:00 am", "8:09 pm")
let failure = ServerResponse.Error("Out of cheese.")
 
switch success {
case let .Result(sunrise, sunset):
    let serverResponse = "Sunrise is at \(sunrise) and sunset is at \(sunset)."
case let .Error(error):
    let serverResponse = "Failure...  \(error)"
}

练习题

再为 ServerResponse 增加一个 case,然后加到 switch 中。

注意看日出和日落时间是如何从 ServerResponse 匹配和提取的。

接口和扩展

接口的定义是以 protocol 开头的。

protocol ExampleProtocol {
    var simpleDescription: String { get }
    mutating func adjust()
}

类、枚举和结构都可以实现接口。

class SimpleClass: ExampleProtocol {
    var simpleDescription: String = "A very simple class."
    var anotherProperty: Int = 69105
    func adjust() {
        simpleDescription += "  Now 100% adjusted."
    }
}
var a = SimpleClass()
a.adjust()
let aDescription = a.simpleDescription
 
struct SimpleStructure: ExampleProtocol {
    var simpleDescription: String = "A simple structure"
    mutating func adjust() {
        simpleDescription += " (adjusted)"
    }
}
var b = SimpleStructure()
b.adjust()
let bDescription = b.simpleDescription

练习题

编写一个枚举类型实现上面的接口。

extension 关键字可以为已有的类型添加功能,比如新的方法和属性。

extension Int: ExampleProtocol {
    var simpleDescription: String {
    return "The number \(self)"
    }
    mutating func adjust() {
        self += 42
    }
}
7.simpleDescription

练习题

Double 类型添加一个扩展,并增加 absoluteValue 属性。

你可以像使用其它任何类型一样的使用接口名称。比如你可以创建一大堆对象,它们有不同的类型,但它们都实现了一个统一的接口。当你访问的变量类型是一个接口时,接口以外的方法是不能被访问到的。

let protocolValue: ExampleProtocol = a
protocolValue.simpleDescription
// protocolValue.anotherProperty  // Uncomment to see the error

即便是变量 protocolValue 的运行时类型为 SimpleClass,但编译器仍会把它当成接口 ExampleProtocol。也就是说,你访问不到该接口定义以外的方法和属性。

泛型

用尖括号把名字括起来用于定义一个泛型函数或是泛型类型。

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

泛型可以用于函数、方法、类、枚举以及结构。

// Reimplement the Swift standard library's optional type
enum OptionalValue<T> {
    case None
    case Some(T)
}
var possibleInteger: OptionalValue<Int> = .None
possibleInteger = .Some(100)

在类型名字后面加上 where 关键字来描述一个需求列表。比如,要求类型实现一个接口;要求两个相同的类型;或要求一个类有一个特殊的父类。

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

练习题

修改 anyCommonElements 函数,返回任何两个序列具有共同元素的数组。

在这个简单例子中,你可以忽略 where,只是简单的在冒号后面加上接口或是类型的名称。写 <T: Equatable> 和写 <T where T: Equatable> 是一样的。