一次Swift之旅

作者: BoomLee | 来源:发表于2017-03-17 19:16 被阅读543次

    在具体学习Swift语法之前,我们有必要对Swift有一个整体的宏观的印象。

    引子

    对于一门新语言的第一个程序,通常的建议是在屏幕上输出Hello, world!。在Swift中,可以一行搞定:

    print("Hello, world!")
    

    如果你使用过C或者Objective-C,以上语法会看起来非常熟悉,在Swift中,这一行代码就是一个完整的程序。无需为了输入输出或者字符串处理引入一个单独的库。写在全局作用域的代码被当做程序的入口,因此无需main()函数。同样也不需要在每个语句的结尾写分号。
    本篇会通过展示如何完成不同的编程任务来给你足够的信息去开始使用Swift编程。无需担心无法理解接下来的内容,所有内容都会在之后的文章中做详细介绍。

    NOTE
    为了最好的体验,打开Xcode中的playground。Playgrounds允许你编辑展示的代码并且能即刻显示结果。

    目录

    • 简单值
    • 控制流
    • 函数和闭包
    • 对象和类
    • 枚举和结构体
    • 协议和扩展
    • 错误处理
    • 泛型

    简单值(Simple Values)

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

    小测试
    在一段字符中使用\()插入一段浮点数计算表达式,并插入某人的名字

    使用方括号创建数组和字典,通过下标或者键获取对应值。最后一个元素可以以逗号结尾。

    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 = [:]
    

    控制流(Control Flow)

    使用ifswitch进行条件操作,使用for-in, for, whilerepeat-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条件中,条件必须是个布尔表达式,这意味着像if score { ... }之类的代码是错误的,这类条件判断并不会默认的和0进行比较。
    你可以同时使用iflet来判断可选型。一个可选值或者包含一个值,或者包含nil,nil代表值不存在。在值类型后写一个问号代表值是可选型的。

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

    小测试
    optionalName置为nil,查看结果。添加else分句设置不同的问好方式。

    如果这个可选值是nil,判断条件则为false,大括号中的代码将会跳过。如果不为nil,可选值会被解包并被赋值给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.")
    }
    

    小测试
    尝试移除default条件看看发生了什么。

    注意观察let是如何把符合条件的值赋值给一个常量的。
    当在一个case中执行了符合条件的代码,程序会从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
            }
        }
    }
    print(largest)
    

    小测试
    添加一个变量去定位哪个number是最大的,并找出最大值。

    使用while重复执行一块代码直到条件变化。一个循环的条件同样也可以在后边,这样可以保证循环至少执行一次。

    var n = 2
    while n < 100 {
        n = n * 2
    }
    print(n)
     
    var m = 2
    repeat {
        m = m * 2
    } while m < 100
    print(m)
    

    可以通过使用..<创建一个索引区间来在循环中设置一个索引。

    var total = 0
    for i in 0..<4 {
        total += i
    }
    print(total)
    

    使用..<创建的区间不包括最大值,使用...创建的区间包括最大值。

    函数和闭包(Functions and Closures)

    使用func声明一个函数。通过包含一系列参数的函数名来调用函数。使用->来将参数名、参数类型与函数返回值类型区分。

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

    小测试
    删除day参数。添加一个参数代表今天的午餐。

    默认的,函数使用内部参数名作为外部参数名。也可以在内部参数名之前自定义一个外部参数名,或者用_代表没有外部参数名。

    func greet(_ person: String, on day: String) -> String {
        return "Hello \(person), today is \(day)."
    }
    greet("John", on: "Wednesday")
    

    使用元组(tuple)可以创建混合值,例如一个函数需要返回多个值。元组的元素可以通过名字或者序号获取到。

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

    函数是一等类型。这意味着一个函数可以返回另一个函数作为它的返回值。

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

    小测试
    写一个闭包对所有的奇数返回0。

    你有多种方式将一个闭包书写的更为简洁。当一个闭包类型已知时,例如代理的回调,你可以忽略参数类型和返回值类型,或者二者都忽略。单语句的闭包隐式的返回了唯一声明的值。

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

    你可以使用数值(位置)取代名字索引参数,这种范式在极短的的闭包中极其有用。当一个闭包作为一个函数的最后一个参数时,可以直接跟在圆括号后面。当一个闭包作为一个函数的唯一参数时,你可以忽略整个圆括号。

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

    对象和类(Objects and Classes)

    在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类缺失了一些重要的东西:当一个类被创建的时候,需要一个构造器(initializer)去构建这个类。答案是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创建一个析构器(deinitializer)。
    子类通过在类名后添加父类名继承父类,以冒号分隔。Swift没有必须强制继承的根类,所以可以根据需要继承或者忽略父类。
    子类想要复写父类的方法需要标记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()
    

    小测试
    创建一个名为CircleNamedShape的子类,这个子类的构造器包括半径和名字参数。实现area()simpleDescription()方法。

    除了存储型的属性外,属性也可以有gettersetter方法。

    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)
    

    perimetersetter方法中,新值有默认的名字newValue。可以在set关键字后提供一个显示的名字。
    注意观察,EquilateralTriangle类的构造器有重要的三步:

    1. 给子类声明的属性赋值
    2. 调用父类的构造器方法
    3. 修改父类定义的属性值。此时任何额外的方法调用、getters或者setters的设置工作也会完成。

    如果你不需要计算这个属性但是仍然需要在新值被设置之前或者之后调用代码,使用willSetdidSet。构造器之外值的每次变化都会调用你的代码。例如,以下类的triangle的边长总是等于square的边长。

    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。同样的,如果可选值被成功解包,?之后的所有操作都会使用解包值。在以上两种情况下,整个表达式的值是一个可选值。

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

    枚举和结构体(Enumerations and Structures)

    使用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的原始值来比较它们的值。

    默认的,Swift的原始值(raw values)是从0开始的并递增,你也可以显示的自定义原始值。在上面的例子中,Ace显示的被赋值为1,其它的原始值按序递增。你也可以使用字符或者浮点型作为枚举的原始值类型。使用rawValue属性获取一个枚举的原始值。
    使用init?(rawValue:)构造方法创建一个枚举的实例。

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

    一个枚举的case值是实际值,并不是原始值的另一种表达方式。实际上,当一个case不存在一个有意义的原始值时,你无需提供一个case值。

    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常量赋值时,因为常量没有被明确的指定类型,所以使用了全名Suit.hearts。在枚举内部,因为self值已经被知道是一个suit,所以使用了缩写.hearts。任何时候只要值类型已知便可以使用缩写。
    如果一个枚举有原始值,这些值会被当做声明的一部分,这意味着一个特定枚举的任何实例都有相同的原始值。另一方面,枚举的每个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)")
    }
    

    小测试
    ServerResponse添加第三种状态,并添加到switch中。

    注意观察日升和日落的时间是如何从ServerResponse值中获取的,并作为满足switch cases值的一部分。
    使用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()
    

    协议和扩展(Protocols and Extensions)

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

    小测试
    创建一个实现以上协议的枚举

    注意观察在SimpleStructure声明中使用了mutating关键字创建了一个修改结构体的方法。SimpleClass的声明中不需要mutating关键字标记方法,因为类的方法总是可以修改类本身。
    使用extension对现有的类型添加功能,例如新的方法和计算型属性。你可以使用扩展对一个定义在任何地方的类型添加协议,甚至一个引入的库或者框架的类型。

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

    小测试
    扩展Double类型并添加absoluteValue属性。

    可以像使用任何其它指定类型一样使用一个协议名,例如,创建一个不同类型但是遵循相同协议的对象的集合。当要使用一个协议类型的值时,协议定义之外的方法是不可用的。

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

    尽管protocolValue变量有一个运行时的SimpleClass类型,编译器还是会当做指定的ExampleProtocol类型。这意味着你获取不到这个类实现的除它遵循的协议之外的方法和属性。

    错误处理(Error Handling)

    你可以使用任何遵循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)
    }
    

    小测试
    printer name改为"Never Has Toner", send(job:toPrinter:)会抛出一个错误。

    你可以使用多个catch块来处理指定的错误。在catch后书写表达式就像在switchcase后书写的一样。

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

    小测试
    添加代码使得在do块内可以抛出错误。你抛出什么样的错误会让错误被第一个,第二个或者第三个catch捕获?

    另一种处理错误的方式是使用try?将结果转化为可选型。如果函数抛出一个错误,指定的错误会被忽略,结果会是nil。如果没有抛出错误,结果是一个包括函数返回值的可选型。

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

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

    泛型(Generics)

    在尖括号内写一个名字来创建泛型函数或者类型。

    func makeArray<Item>(repeating item: Item, numberOfTimes: Int) -> [Item] {
        var result = [Item]()
        for _ in 0..<numberOfTimes {
            result.append(item)
        }
        return result
    }
    makeArray(repeating: "knock", numberOfTimes:4)
    

    你可以创建函数、方法、类、枚举、结构体的泛型形式。

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

    在函数体之前使用where关键字去限定一系列要求,例如,限定类型实现某一个协议,限定两个类型是相同的,或者限定一个类有特定的父类。

    func anyCommonElements<T: Sequence, U: Sequence>(_ 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
                    }
                }
            }
            return false
    }
    anyCommonElements([1, 2, 3], [3])
    

    小测试
    修改anyCommonElements(_:_:)函数返回一个数组,这个数组的元素是两个序列的共有元素。

    <T: Equatable><T> ... where T: Equatable>的写法是等价的。

    接下来

    Swift之旅到此结束,接下来会具体介绍Swift语法。
    上一篇:关于Swift
    下一篇:Swift-基础部分

    相关文章

      网友评论

        本文标题:一次Swift之旅

        本文链接:https://www.haomeiwen.com/subject/dhhjnttx.html