Swift 4 - A Swift Tour

快速浏览

按照传统,一个新的编程语言的的第一个程序是在屏幕上打印 “Hello, world!”。在Swift当中,我们可以通过下面这行代码来打印:

print("Hello, world!")

如果你曾经使用过C语言或者Objective-C语言,您会从这种句式中感受到友好——在Swift中,这便是这个 “Hello, world!” 程序的全部了。你不需要为了输入/输出或者字符串的句柄去专门引用一个库。代码将在全局范围内被执行,所以您也不需要使用main()方法作为入口。同时,您不需要再语句的结尾处使用分号进行分隔标记。

这份手册将提供给您全面的信息,告诉您如何使用Swift进行程序开发。不要担心有不明白的地方,这本手册后面的内容将解答您的每一个问题。

简单赋值

用 let 声明一个常量,用 var 声明一个变量。在声明一个常量时可以不对它赋值,但在使用它之前,一定要对它进行一次赋值。也就是说,常量是可以一次赋值,在多个场景中重复使用的。

var myVariable = 42

myVariable = 50

let myConstant = 42

常量或变量的声明的数据类型必须和其真实的数据类型相同。当然,您并不是必须总是声明这些常量或变量的数据类型。编译器将自动在进行赋值时对这些没有声明类型的常量或变量确认它们的数据类型。在上面的例子当中,编译器会根据赋值认为myVariable的数据类型是整型。

当初始化时的值不足以确定常量或变量的类型(或者没有进行初始化赋值)时,可以把数据类型明确的写到常量或变量的后面,用冒号隔开。

let implicitInteger = 70

let implicitDouble = 70.0

let explicitDouble: Double = 70

不同数据类型之间无法互相赋值,如果您想把一个值赋给一个数据类型不同变量,需要进行显式的类型转换。

let label = "The width is "

let width = 94

let widthLabel = label + String(width)

Swift中提供一种非常简单的方式进行将数据拼接到字符串中,在需要插入数值的地方,使用 \( "变量或常量" ) 的方式进行插入拼接,比如

let apples = 3

let oranges = 5

let appleSummary = "I have \(apples) apples."

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

Swift中使用 [ ] 创建数组和字典,并在 [ ] 中写明读写操作的标识,在赋值时,允许在最后一个元素后添加逗号。如下:

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 = [String: Float]()

当无法确定数组或字典中的数据类型时,使用如下方式进行初始化:

shoppingList = []

occupations = [:]


控制流

使用 if 和 switch 语句进行条件分支控制,使用for-in, for, 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 { ... } 的形式将会报错,程序不会隐式的将 score 与 0 比较并得到一个 Boolean 值。

您可以在 if 语句的条件区域中使用 let 声明一个常量,这个常量相当于一个可选类型值。一个可选类型值代表这个值可以是这种数据类型的值也可以是 nil 。声明为可选类型值得常量或者变量需要在写明数据类型后再加上一个 ? 作为标记。

var optionalString: String? = "Hello"

print(optionalString == nil)

var optionalName: String? = "John Appleseed"

var greeting = "Hello!"

if let name = optionalName {

        greeting = "Hello, \(name)"

}

当可选类型值为 nil 时,条件判断的结果为 false,反之为 true。需要注意的是,程序将在对使用 let 声明的变量赋值之前对可选类型值进行拆包,以得到一个确定的值,在block语句中同样如是。

另一个使用可选类型值进行判断的方法是使用 ?? 语句,当前面的可选类型值为 nil 时,则使用后面的默认值进行操作。

let nickName: String? = nil

let fullName: String = "John Appleseed"

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

switch 语句可以支持任何数据的判断和多种多样的判断表达式——在Swift中,不会被限制在整形数据和等价判断中。

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

}

请注意此处 let 的使用方式,通过 let 将表达式的值赋值给常量。

当程序按照条件匹配执行完一条case分支的逻辑后,将退出 switch 语句,故而不需要再 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

                }

        }

}

print(largest)

使用 while 语句重复执行代码块中的逻辑,直到条件表达式的结果发生变化(判断结果为假)

var n = 2

while n < 100 {

        n *= 2

}

print(n)

var m = 2

repeat {

        m *= 2

} while m < 100

print(m)

你也可以使用 ..< 确定一个整数范围来控制循环。

var total = 0

for i in 0..<4 {

        total += i

}

print(total)

当使用 ...< 时,确定的取值范围不包括右边界值;使用 ... 时,包括两端的边界值。


函数和闭包

使用关键字 func 来声明一个函数。通过函数名加圆括号——括号内添加需要的参数——来调用一个函数。 使用 -> 符号分隔参数列表和返回值类型。如下:

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

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

}

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

在默认情况下,使用参数名作为参数的说明标签,我们也可以在参数名前自定义参数标签,或者使用 _ 表示无说明标签。

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

print(statistics.2)

函数可以拥有一组元素个数可变的参数,这些参数将会被存储到一个数组中。

func sumOf(numbers: Int...) -> Int {

        var sum = 0

        for number in numbers {

                sum += number

        }

        return sum

}

sumOf()

sumOf(numbers: 42, 597, 12)

函数中可以套嵌函数,这些被套嵌的函数可以被外部函数调用。你可以在长串的或者复杂的代码中使用套嵌函数来组织规划代码。

func returnFifteen() -> Int {

        var y = 10

        func add() {

                y += 5

        }

        add()

        return y

}

returnFifteen()

在Swift中,函数是 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)

函数是闭包的一种特殊形式:可以延时调用的代码块。 闭包中的代码可以访问自身代码范围中的可用的变量和函数,即使闭包在执行时处于不同的范围 - 您已经使用嵌套函数看到了一个示例。在函数的参数或是返回值中,你可以使用 {} 直接定义一个闭包进行使用,而不需要对这个闭包进行命名。

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

        let result = 3 * number

        return result

})

你可以通过几种方式来更加简洁的书写闭包。当一个闭包的类型已知时,比如实现一个代理协议的回调,你可以省略其中的参数或者返回值类型,也可以将其全部省略。当闭包中只有一行语句时,该闭包隐式的返回这一行语句的计算结果。

let mappedNumbers = numbers.map({ number in 3 * number })

print(mappedNumbers)

在闭包中,你可以通过参数位置而不是参数名来使用参数,这种方式在短小的闭包中非常实用。作为函数的最后一个参数传递的闭包可以在括号后立即显示。 当闭包是函数的唯一参数时,可以完全省略括号。

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

print(sortedNumbers)


对象和类

在 class 关键字后加上类名来声明一个类。在类中声明属性的写法和声明常量或变量的写法是相同的,区别只在于属性声明于类的上下文环境中。同样的,方法和函数的声明方式也相同。

class Shape {

        var numberOfSides = 0

        func simpleDescription() -> String {

                return "A shape with \(numberOfSides) sides."

        }

}

通过在类名后添加 () 来创建一个类的实例对象。使用点语法调用对象属性和方法。

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 的用法:用来区分属性和参数。在创建实例时,向初始化方法中传递参数的方式和调用方法时相似。每个属性都需要一个初始值,或者在声明时赋值,或者在初始化时赋值。

使用 deinit 方法名来创建一个析构方法,该方法将在实例被释放前执行,用来进行内存清理的工作。

声明一个子类时,将它的父类类名写在子类类名后面,用冒号隔开。子类的声明不要求继承于任何标准根类,所以父类类名可以根据需要添加或忽略。

在子类中重写父类的方法,需要使用 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()

在子类中增加的属性同样可以拥有 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)

triangle.perimeter = 9.9

print(triangle.sideLength)

在 perimeter 的 setter 方法中,使用 newValue 隐式调用新传入的值(赋值时传入的参数)。也可以在 setter 方法后添加括号对参数进行命名。

需要注意子类中初始化方法对实例对象的赋值步骤(以 EquilateralTriangle 为例):

1. 给子类中添加的属性赋值

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

3. 修改父类中的属性值,或执行其他的逻辑操作

如果你不需要对属性值进行计算,但仍需要在赋值前或复制后对对属性值进行处理,使用 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)

print(triangleAndSquare.triangle.sideLength)

triangleAndSquare.square = Square(sideLength: 50, name: "larger square")

print(triangleAndSquare.triangle.sideLength)

当使用可选值时,在调用方法、属性、逻辑前写上 ?。这样,如果 ?前的值为 nil,则 ?后的逻辑将被忽略,整个表达式的值为 nil 。如果不为 nil,则可选值将被拆包,并执行后续逻辑。在这两种情况中,表达式的到的值也都是一个可选值。

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

                }

        }

}

letace=Rank.ace

letaceRawValue=ace.rawValue

在默认情况下,Swift 从 0 开始,将新的值分配给所有的情况,每次加 1 ,但是你可以通过直接赋值的方式改变这种形式。在上面的例子当中,Ace 被直接赋值为 1 , 其它原始值将按照顺序继续分配。 你也可以使用字符串或浮点类型的数字作为原始值分配给一个枚举值。使用 rawValue 属性可以取得一个枚举值的原始值。

使用 构造函数 init?(rawValue:) 可以新建一个指定原始值的枚举值。该函数将返回一个与原始值匹配的枚举值,如该 rawValue 不存在(无法在指定的枚举中匹配到),则返回 nil 。

if let convertedRank = Rank(rawValue: 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()

注意上面关于 hearts 这个枚举值的两种书写方式: 当给常数 hearts 赋值时,使用完全书写的方式,因为此时,常数 hearts 的类型未声明(不确定);而在 switch 方法内部,当前枚举值类型已经确定,此时可以使用枚举值得简写形式。

如果一个枚举值有关联的原始值,并将这个原始值作为对外声明的一部分,这意味着每一个实例当中,其特定的枚举值总会有一个相同的原始值。在枚举值的使用当中,你还可以选择在为某个特定的枚举值在创建时才关联一个原始值,这样,对于特定的某个枚举值,是可以拥有不同的关联原始值的。你可以将关联的值视为枚举值得存储(注册)属性。比如,当我们需要从服务器请求日出和日落的时间时,服务器可以返回我们需要的信息,或者返回一个错误的描述,具体如下:

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

}

此处要注意我们是怎么从枚举值中取出日出和日落的时间信息的。

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


协议和扩展

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

在上面的例子中,SimpleStructure 中声明的 adjust() 方法使用了 mutating 关键字,这个关键字表明,该方法将会修改当前结构体的值。而在 SimpleClass 中并没有使用这个关键字,这是因为,在类中,方法是一直被允许进行修改动作的。

使用 extension 关键字向已声明的类型中添加功能,比如新的方法或者新的计算属性。你可以使用扩展 (extension) 的方式对一个在其它地方 (任意地方) 声明的类型添加一致性协议,甚至这个类型是在引用的库或框架中声明的

extension Int: ExampleProtocol {

        var simpleDescription: String {

                return"The number\(self)"

        }

        mutating func adjust() {

                self+=42

        }

}

print(7.simpleDescription)

你可以像使用其它类型名那样使用协议的名字 —— 比如,创建一个由不同类型的对象组成的集合,但是集合中的这些对象都遵守同一个协议。当你使用声明的类型为协议的变量或常量时,在协议外定义的方法将无法使用。

let protocolValue: ExampleProtocol = a

print(protocolValue.simpleDescription)

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

即使 protocolValue 在运行时是 SimpleClass 类型变量,但是在编译阶段,编译器会将其视作已声明的 ExampleProtocol 类型 (协议类型) 的变量。这意味着,你不能突然地 ( accidentally:看似无心实则有意地 )调用在协议外实现的方法或属性。(原句:This means that you can’t accidentally access methods or properties that the class implements in addition to its protocol conformance.)


异常处理

你可以使用任意一种类型来面熟异常,只要这种类型遵守了 Error 协议

enum PrinterError: Error {

        case outOfPaper

        case noToner

        case onFire

}

使用 throw 关键字 来跑出一个异常,同时使用 throws 关键字 来标识该方法会抛出异常。如果你在一个方法中抛出了异常,那么在抛出异常的同时,该方法会直接返回并调用异常处理的逻辑。

func send(job: Int, toPrinterprinterName: 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)

}

你可以使用多个 catch 代码块来具体地处理不同 (类型) 的异常。你可以像在 switch 中的 case 后书写表达式那样,在 catch 后书写表达式。

do {

        let printerResponse=trysend(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)

}

另一种进行异常处理的方法是使用 try? 将结果转变为可选值。那么,当方法抛出异常时,计算结果为 nil ,或者方法将返回一个包含计算结果的可选类型值。

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

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

使用 defer 书写的代码块将在其所在方法的其它代码都执行后,在结果返回前执行。defer 代码块的执行不区分方法是否抛出了异常。你可以在新的逻辑被执行前,使用 defer 书写一些设置或清理代码,即使这需要在不同时时间被执行 (但这是有效和可行的)。

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)


泛型

在名称后的尖括号中添加类型名来创建一个泛型的 (指定类型的) 方法或类型,支持泛型 (指定类型) 的包括 函数、方法、类、枚举和结构体。

func makeArray(repeatingitem: 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.Iterator.Element: Equatable, T.Iterator.Element == U.Iterator.Element {

                for lhsItem in lhs {

                        for rhsItem in rhs {

                                if lhsItem == rhsItem {

                                        return true

                                }

                        }

                }

                returnfalse

}

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

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