A Swift Tour

https://docs.swift.org/swift-book/GuidedTour/GuidedTour.html

管理来说,使用新的一种语言开始第一个项目在屏幕上打印“hello,world!”。使用swift,一行代码就能搞定。

print("Hello, world!")

如果以前使用C或者Objective-C编写代码,在swift中,这个语法就会很熟悉,这一行代码就是一个完整的项目。不用为了功能导入想input/output或者字符串处理的独立的库。写在全局的代码被用作项目的进入点,所以不需要main()函数。也不需要在每个语句后写分号(;).

这个例子通过展示如何完成一个多样的编程任务告诉你如何用swift开始编写代码。不用担心有不理解的东西,例子余下的部分介绍的所有东西在后面都非常详细的解释了。

获得更好的体验,这部分用xcode打开一个playground。playground可以允许你编辑代码列表,并立刻看到结果

简单的值

用let构造常量,用var构造变量。常量值在编译期不需要知道,但是必须明确分配一个值。也就是说,可以用常量标记只确定一次并在很多地方使用的值。

var myVariable = 42

myVariable = 50

let myConstant = 42

常量或者变量都必须和要分配给他的值相同的类型。但是,不需要每次都明确写出类型。当创建一个常量或者变量的时候,提供一个值,使编译器可以推导出他的类型。上面例子中,编译器推导出myVariable是一个整型值,因为他的初始化值就是一个整型值(42)。

如果初始值没有提供足够的信息(或者没有初始化值),需要把类型写在变量后面,并使用分号分隔开。

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."

使用\()在字符串中引入一个浮点值,在招呼中引入一个人的名字

占用多行的字符串使用三双引号(""")。只要匹配上闭合引号的缩进,每一行开始的缩进都会被移除。例如:

let quotation = """

I said "I have \(apples) apples."

And then I said "I have \(apples + oranges) pieces of fruit."

"""

使用方括号创建数组和字典,通过在方括号中写下标或者键值存取元素。最后一个元素后可以跟一个逗号。

var shoppingList = ["catfish", "water", "tulips"]

shoppingList[1] = "bottle of water"

var occupations = [ "Malcolm": "Captain", "Kaylee": "Mechanic",]

occupations["Jayne"] = "Public Relations"

增添元素时数组自动增长。

shoppingList.append("blue paint")

print(shoppingList)

创建空数组或字典,使用初始化语法

let emptyArray = [String]()

let emptyDictionary = [String: Float]()

如果类型信息能被推导出来,可以写一个空数组跟空字典,例如,当给变量传一个新的值或者给函数传一个参数。

shoppingList = []

occupations = [:]

控制流

用if和switch做条件判断,用for-in、while和repeat-while做循环。条件与循环变量的括号可选。主体的大括号必需的不可省略。

let individualScores = [75, 43, 103, 87, 12]

var teamScore = 0

for score in individualScores {

    if score > 50{

    teamScore += 3

    } else {

    teamScore += 1    

    }

}

print(teamScore)

在if语句中,条件必需是boolean表达式,这意味着像代码if score { ... }是错误的,不能使隐式的判断是否是零。

可以让可能为nil的值结合if和let同时使用。这些值表示为可选类型。可选类型的值包含一个值或者nil来表示值缺失了。在值类型后面加?表示值是可选类型。

var optionalString: String? = "Hello"

print(optionalString == nil)

// Prints "false"

var optionalName: String? = "John Appleseed"

var greeting = "Hello!"

if let name = optionalName {

greeting = "Hello, \(name)"

}

把optionalName改成nil。会得到什么greeting?添加一个else从句,当optionalName是nil的时候把greeting设置成另一个值

如果可选值是nil,条件变成false,大括号中的代码会跳过。否者,可选值解包并且分配给let后的常量,这样块中的代码可以获得解包的值。

还可以通过使用??操作符提供默认值处理可选类型。如果可选值缺失,默认值代替他使用。

let nickName: String? = nil

let fullName: String = "John Appleseed"

let informalGreeting = "Hi \(nickName ?? fullName)"

switches支持任意类型的数据和大量丰富的比较符-不限制整型和相等测试

let vegetable = "red pepper"

switch vegetable {

    case "celery":

    print("Add some raisins and make ants on a log.")

    case "cucumber", "watercress":

    print("That would make a good tea sandwich.")

    case let x where x.hasSuffix("pepper"):

    print("Is it a spicy \(x)?")

    default:

    print("Everything tastes good in soup.")

}

// Prints "Is it a spicy red pepper?"

尝试移除默认case,看会出现什么错误

注意模板中的let如何给匹配的模板中的常量分配值。

代码在相配的switch case中执行完后,程序从switch语句中退出。执行命令不会继续执行到下一个case,所以没有必要在seitch的每一个case代码的最后明确的退出switch。

通过提供成对的名字使用没对key-value对来通过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

        }    

    }

}

print(largest)// Prints "25"

增添另一个变量记录最大数值是哪一类,和哪一个鼠疫最大一样。

使用while重复一个块儿直到状态改变。循环的状态可以在尾部,确保循环至少执行一次。

var n = 2

while n < 100 {

    n *= 2

}

print(n)// Prints "128"

var m = 2

repeat {

    m *= 2

} while m < 100

print(m)// Prints "128"

可以通过使用..<保留一次循环中的索引值来生成一个范围中的索引。

var total = 0

for i in 0..<4 {

    total += i

}

print(total)// Prints "6"

使用..<获取一个忽略最大值得范围,使用...获取一个包括两头值得范围

函数与闭包

使用func声明一个函数。通过跟在函数名称后括号内一列参数来调用函数。使用->分隔参数名称和函数返回类型的类型。

func greet(person: String, day: String) -> String {

    return "Hello \(person), today is \(day)."

}

greet(person: "Bob", day: "Tuesday")

移除参数day。添加一个参数使其在greeting中包含今天午餐。

默认情况,函数使用形参名作为实参名。在形参前写一个自定义的实参标签,或者不用实参标签,只写_。

func greet(_ person: String, on day: String) -> String {

    return "Hello \(person), today is \(day)."

}

greet("John", on: "Wednesday")

使用元祖创建一个组合值-例如,从函数中返回多个值。元祖的元素可以通过名字或者数字引用。

func calculateStatistics(scores: [Int]) -> (min: Int, max: Int, sum: Int) {

    var min = scores[0]

    var max = scores[0]

    var sum = 0

    for score in scores {

        if score > max {

            max = score

        } else if score < min {

            min = score

        }

        sum += score

    }

    return (min, max, sum)

}

let statistics = calculateStatistics(scores: [5, 3, 100, 3, 9])

print(statistics.sum)// Prints "120"print(statistics.2)// Prints "120"

函数可以嵌套。嵌套函数可以调用函数外声明的变量。可以使用嵌套函数来组织很长或者很复杂的函数的代码。

func returnFifteen() -> Int {

    var y = 10

    func add() {

        y += 5

    }
    add()

    return y

}

returnFifteen()

函数是first-class类型。所以函数可以将另一个函数作为返回值返回。

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(list: numbers, condition: lessThanTen)

函数实际是一种特殊的闭包:代码块可以稍后调用。闭包创建的地方可以获取到的变量跟函数可以在比保重使用,及时闭包执行的时候在另一个位置-在内嵌函数中已经见到这种例子了。可以定义一个在大括号中代码周围没有名字的闭包。使用in分隔开实参与返回值类型。

numbers.map({ (number: Int) -> Int in

    let result = 3 * number return result

})

可以通过数字代替名字引用参数。这种方式在非常短的闭包中非常有用。闭包作为函数的最后一个参数时可以紧跟括号出现。当闭包是函数唯一的一个参数时,可以完全忽略括号。

let sortedNumbers = numbers.sorted { $0 > $1 }

print(sortedNumbers)// Prints "[20, 19, 12, 7]"

对象与类

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()

该版本的shape类缺失了一些重要的东西:当创建一个类时使用初始化构造器配置类。使用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."

     }

}

注意在初始化时如何使用self区分name属性和name参数。当创建类的实例时传给初始化器的参数就像调用函数时的参数一样。每一个属性需要分配一个值--在声明的时候或者是在初始化的时候都可以。

如果在对象分解之前需要做一些清理工作可以使用deinit创建分解器。

子类在他们类名后包含了父类名,使用分号分隔。类不是必需要继承自一个标准根类,所以可以根据需求包含或忽略父类。

子类中重写了父类实现的方法使用override标注出来--偶尔重写一个方法,如果没有写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,初始化方法需要一个半径和名字作为参数。在circle类中实现area()与simpleDescription()方法

除了简单的存储属性,属性可以有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 triangle with sides of length \(sideLength)."

    }

}

var triangle = EquilateralTriangle(sideLength: 3.1, name: "a triangle")print(triangle.perimeter)// Prints "9.3"

triangle.perimeter = 9.9

print(triangle.sideLength)// Prints "3.3000000000000003"

perimeter的setter中,新值得潜在名称是newvalue。可以在set后的括号里提供一个明确的名字。

注意EquilateralTriangle的初始化有三个步骤:

    1.为子类声明的属性设置值

    2.调用父类的初始化方法

    3.修改父类实名的属性的值。其他的调用方法、getters或者setters设置工作也在这里执行。

如果不需要计算属性但是需要在设置新的值之前和之后运行一段代码,使用willSet和didSet。值在初始化方法之外改变时,这段代码就会执行。例如,下面的类保证了三角形边长已知与正方形边长相等。

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")print(triangleAndSquare.square.sideLength)// Prints "10.0"print(triangleAndSquare.triangle.sideLength)// Prints "10.0"triangleAndSquare.square = Square(sideLength: 50, name: "larger square")print(triangleAndSquare.triangle.sideLength)// Prints "50.0"

当使用可选类型的值时,在方法、属性和下标等操作前使用?。如果?后的值是nil,?后全部的东西都会忽略,整个表达式的值也是nil。否则,可选类型解包,?后所有的东西在解包的值上进行操作。两种情况下,整个表达式都是可选类型。

枚举与结构体

使用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.rawValue)

            }

        }

}

let ace = Rank.ace

let aceRawValue = ace.rawValue

写一个函数比较rank的raw values的值

使用init?(rawValue:)初始化器生成一个raw value的枚举的实例。会返回一个匹配raw value的枚举case或者如果没有匹配到rank返回nil。

if let convertedRank = Rank(rawValue: 3) {

    let threeDescription = convertedRank.simpleDescription()

}

枚举的case值是真是的值,不仅仅是raw value的另一种方式。实际上,没有实际值的cases不需要一定要给他一个值。

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()方法,给spades和clubs返回"black",给hearts和diamonds返回"red"。

注意上面枚举的hearts两种引用方式:当给hearts常量分配一个值时,枚举case 使用Suit.hearts全名引用,因为常量没有明确指定类型。在switch中,枚举case引用使用.hearts,因为self的值已经被作为外层使用了。在已经知道值类型的任何时候都可以使用简短的形式。

如果枚举有raw values,这些值在声明的时候决定了,意味着每一个特定的实例通常有相同的raw value。枚举的cases的另一个选择是拥有与case关联的值--这些值在获取实例时确定,相同枚举case的每一个实例可以是不同的。可以把枚举case的关联值看做存储属性。服务器可以带着请求信息作出响应或者响应给出一个详细描述的错误。

enum ServerResponse {

    case result(String, String)

    case failure(String)

}

let success = ServerResponse.result("6:00 am", "8:09 pm")

let failure = ServerResponse.failure("Out of cheese.")

switch success {

    case let .result(sunrise, sunset):

        print("Sunrise is at \(sunrise) and sunset is at \(sunset).")

    case let .failure(message):

        print("Failure... \(message)")

}

// Prints "Sunrise is at 6:00 am and sunset is at 8:09 pm."

添加第三个case ServerResponse,并添加到switch中

注意在switch cases中,sunrise和sunset时间如何作为部分的匹配值从ServerResponse中提取出来。

使用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()

写一个可以返回包含所有卡片的数组的函数,每个rank跟suit都组合成一个card。

协议与扩展

使用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

给ExampleProtocol添加另外一个必需实现的方法。你需要对SimpleClass跟SimpleStructure做什么调整使他们仍然遵守协议。

注意在SimpleStructure生命中使用mutating关键字来标记能修改结构体的方法。SimpleClass类声明不需要mutating的标记,因为类的方法通常都改变类。

使用extension给已经存在的类型增添功能,例如一个新方法或者计算属性。可以使用扩展对在任何地方声明的类增加遵守协议,甚至对从库或者包中的类也可以。

extension Int: ExampleProtocol {

    var simpleDescription: String {

        return "The number \(self)"

    }

    mutating func adjust() {

        self += 42

    }

}

print(7.simpleDescription) // Prints "The number 7"

写一个double的扩展,增加absoluteValue属性

可以使用协议名称就像其他命名的类型--例如,创建包含不同类型但都遵从一个协议对象的集合。当你使用一个类型是遵守某个协议的值时,在协议外定义的方法是不可以用的。

let protocolValue: ExampleProtocol = a

print(protocolValue.simpleDescription)

// Prints "A very simple class. Now 100% adjusted."

// print(protocolValue.anotherProperty) // Uncomment to see the error

即使变量protocolValue运行时的类型是SimpleClass,编译器会把它当做给定的类型ExampleProtocol对待。意味着不可以操作该类在协议之外实现的方法和属性。

错误处理

可以使用任何遵循error协议的类代表错误。

enum PrinterError: Error {

    case outOfPaper

    case noToner

    case onFire

}

使用throw来抛出错误,并且throws标记可能会抛出错误的函数。如果在函数中跑出了错误,函数会立马返回,调用函数的代码处理错误。

func send(job: Int, toPrinter printerName: String) throws -> String {

    if printerName == "Never Has Toner" {

        throw PrinterError.noToner

    }

    return "Job sent"

}

有多种处理错误的方式。一种是使用do-catch。在do块中,对可能抛出错误的代码前加try。在catch块中,除非你自定义了名称,不然错误自动分配的名字是error。

do {

    let printerResponse = try send(job: 1040, toPrinter: "Bi Sheng")

    print(printerResponse)

} catch {

    print(error)

} // Prints "Job sent"

将printer的名字改为 "Never Has Toner",则 send(job:toPrinter:)函数会抛出错误。

你可以提供多个catch快来处理特定的错误。可以在catch后像switch中的case一样写模板。

do {

    let printerResponse = try send(job: 1440, toPrinter: "Gutenberg")    

    print(printerResponse)

} catch PrinterError.onFire {

    print("I'll just put this over here, with the rest of the fire.")

} catch let printerError as PrinterError {

    print("Printer error: \(printerError).")

} catch {

    print(error)

} // Prints "Job sent"

在do块中添加一个抛出错误的代码,你需要抛出什么类型的错误才会被第一个catch块处理?对于第二个跟第三个块呢?

另一种处理错误的方式是try?来讲结果转换成可选类型的。如果函数跑出了错误,特殊的错误被抛弃,结果变成nil。否则,结果是包含函数返回值得可选类型。

let printerSuccess = try? send(job: 1884, toPrinter: "Mergenthaler")

let printerFailure = try? send(job: 1885, toPrinter: "Never Has Toner")

使用defer编写一个在函数中所有其他代码都执行完后,函数返回前,才执行的代码块。无论函数是否抛出错误,代码都会执行。可以使用defer来写setup跟cleanup代码,即使需要在不同时间是执行。

var fridgeIsOpen = false

let fridgeContent = ["milk", "eggs", "leftovers"]

func fridgeContains(_ food: String) -> Bool {

    fridgeIsOpen = true

    defer {

        fridgeIsOpen = false

    }

    let result = fridgeContent.contains(food)

    return result

}

fridgeContains("banana")

print(fridgeIsOpen) // Prints "false"

泛型

在扩折号中写一个名字来表示函数或者类的泛型。

func makeArray(repeating item: Item, numberOfTimes: Int) -> [Item] {

    var result = [Item]()

    for _ in 0..

        result.append(item)

    }

    return result

}

makeArray(repeating: "knock", numberOfTimes: 4)

可以使用泛型构成函数和方法,就跟类、枚举、结构体一样。

// Reimplement the Swift standard library's optional type enum OptionalValue {

    case none

    case some(Wrapped)

}

var possibleInteger: OptionalValue = .none

possibleInteger = .some(100)

在主体前使用where指定一些必要条件--例如,需要类实现一个协议,需要两个类是相同的,或者需要类有一个特殊的父类。

func anyCommonElements(_ lhs: T, _ rhs: U) -> Bool where T.Element: Equatable, T.Element == U.Element {

    for lhsItem in lhs {

        for rhsItem in rhs {

            if lhsItem == rhsItem {

                return true

            }

        }

    }

    return false

}

anyCommonElements([1, 2, 3], [3])

修改函数anyCommonElements(_:_:) ,得到一个返回包含两个序列都包含的元素的数组的函数。

你可能感兴趣的:(A Swift Tour)