欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Swift4学习笔记-1.3 Swift之旅

程序员文章站 2024-03-23 12:40:58
...

1.3 Swift之旅

传统表示,新语言的第一个程序应该在屏幕上打印出“Hello,world!”这个词。 在Swift中,这可以在一行中完成:

print("Hello, world!")

在哪运行这句代码?首先你要有Mac系统的电脑,Windows系统不行,在Mac系统里的App Store里搜索安装Xcode。打开Xcode,点击以下图标打开Playground:

Swift4学习笔记-1.3 Swift之旅

接下来选择如下图标,点击Next:

Swift4学习笔记-1.3 Swift之旅

然后输入上述代码,Playground会自动运行,在底部输出结果

Swift4学习笔记-1.3 Swift之旅

如果您写过C或Objective-C代码,则这种语法在Swift中看起来很熟悉,这一行代码就是一个完整的程序。您不需要为输入/输出或字符串处理等功能导入单独的库。在全局范围内编写的代码用作程序的入口点,因此不需要main()函数。 您也不需要在每个语句的末尾写分号。

这篇文章为您提供了足够的信息以便你开始在Swift中编写代码,我们会向您展示如何完成各种编程任务。 不要担心,如果你不明白的东西,本文中介绍的一切都将在本书的其余部分中详细解释。

注意,为了获得最佳体验,请在Xcode中打开本章作为Playground。Playground允许您编辑代码列表并立即查看结果:

下载Playground

简单的值

使用let定义一个常量,var定义一个变量。 常量的值在编译时不需要知道,但是您必须为其分配一次值。 这意味着您可以使用常量来命名一次您确定并且在许多地方使用的值。

var myVariable = 42
myVariable = 50
let myConstant = 42

常数或变量必须与要分配给它的值具有相同的类型。 但是,您并不总是必须明确地编写该类型。 在创建常量或变量时提供值可以让编译器推断其类型。 在上面的示例中,编译器推断myVariable是一个整数,因为它的初始值是一个整数。

如果初始值没有提供足够的信息(或者没有初始值),则通过在变量之后写入该冒号来指定该类型。

let implicitInteger = 70
let implicitDouble = 70.0
let explicitDouble: Double = 70
实验
创建一个具有显式类型的浮点数和值为4的常量。

Swift4学习笔记-1.3 Swift之旅

值不会隐式转换为其他类型。 如果需要将值转换为其他类型,请明确地创建一个所需类型的实例。

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

Swift4学习笔记-1.3 Swift之旅

实验

尝试从最后一行删除转换为String的函数。 你有什么错误?

Swift4学习笔记-1.3 Swift之旅

有一个更简单的方法用来来在字符串中插入数字:将数字写在括号中,并在括号之前写入反斜杠(\)。 例如:

let apples = 3
let oranges = 5
let appleSummary = "I have \(apples) apples."
let fruitSummary = "I have \(apples + oranges) pieces of fruit."
Swift4学习笔记-1.3 Swift之旅
实验
使用\()在字符串中包含浮点计算,并在某个问候语中包含某人的名字。

Swift4学习笔记-1.3 Swift之旅

对于占用多行的字符串,使用三个双引号("""),只要与引用的缩写匹配,每个引用行开始处的缩进就会被删除,例如:

let quotation = """
Even though there's whitespace to the left,
the actual lines aren't indented.
Except for this line.
Double quotes (") can appear without being escaped.
 
I still have \(apples + oranges) pieces of fruit.
"""
Swift4学习笔记-1.3 Swift之旅

使用括号([])创建数组和字典,并通过将索引或键写入括号中来访问它们的元素。 在最后一个元素之后允许使用逗号。

var shoppingList = ["catfish", "water", "tulips", "blue paint"]
shoppingList[1] = "bottle of water"
 
var occupations = [
    "Malcolm": "Captain",
    "Kaylee": "Mechanic",//最后一个允许使用逗号
]
occupations["Jayne"] = "Public Relations" //可以用字符换作为键

要创建一个空数组或字典,请使用initializer语法。

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语句中条件必须是一个布尔表达式 - 这意味着代码,如 if score{...} 是一个错误,它不会以score是否为0去作为判断。

您可以使用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)"
}

实验


将optionalName改成nil,你会得到什么greating? 添加一个else子句,如果optionalName为nil则设置不同的问候语。

Swift4学习笔记-1.3 Swift之旅


Swift4学习笔记-1.3 Swift之旅

从上面两个运行结果可知,当optionalName为nil时,如果没有else则执行if里面的内容,有else语句就执行else里的内容

如果可选值为nil,则条件为false,并且跳过大括号中的代码。 否则,可选值被解包并分配给let之后的常量,这使得代码块内的解包值可用。

处理可选值的另一种方法是使用??操作。 如果缺少可选值,则使用默认值。

let nickName: String? = nil
let fullName: String = "John Appleseed"
let informalGreeting = "Hi \(nickName ?? fullName)"
Swift4学习笔记-1.3 Swift之旅

Switch支持任何类型的数据和各种比较操作 - 它们不限于整数和相等判断。

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.")
}
Swift4学习笔记-1.3 Swift之旅

实验
尝试删除default。 你会得到什么报错?
Swift4学习笔记-1.3 Swift之旅

这告诉我们,let如何在模式中将与模式匹配的值分配给常量。

在执行匹配的switch case内的代码之后,程序自动退出switch语句。 执行不会继续到下一种case,所以在每个案例的代码结尾都不需要明确指出switch的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)
Swift4学习笔记-1.3 Swift之旅
实验:添加另一个变量来跟踪哪个数字是最大的,以及最大的数字是多少。

使用while重复一段代码,直到一个条件改变。 循环的条件可以在最后,确保循环至少运行一次。

var n = 2
while n < 100 {
    n *= 2
}
print(n)
 
var m = 2
repeat {
    m *= 2
} while m < 100
print(m)
Swift4学习笔记-1.3 Swift之旅

您可以通过使用.. <来创建索引范围来保持索引的循环。

var total = 0
for i in 0..<4 {
    total += i
}
print(total)
Swift4学习笔记-1.3 Swift之旅

使用.. <创建一个省略其上限值的范围,并使用...来创建一个包含这两个值的范围。
Swift4学习笔记-1.3 Swift之旅

函数和闭包

使用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 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)
Swift4学习笔记-1.3 Swift之旅

函数可以嵌套。 嵌套函数可以访问在外部函数中声明的变量。 您可以使用嵌套函数在长或复杂的函数中组织代码。

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
})
Swift4学习笔记-1.3 Swift之旅

实验
重写闭合,为所有奇数返回零。

Swift4学习笔记-1.3 Swift之旅

您有几个选项可以更简洁地写入闭包。 当闭包类型已知时,例如代理的回调,您可以省略其参数的类型,其返回类型或两者都省略。 单个语句闭包隐式地返回它们唯一的语句的值。

let mappedNumbers = numbers.map({ number in 3 * number })
print(mappedNumbers)
Swift4学习笔记-1.3 Swift之旅

您可以通过数字而不是名称来引用参数 - 这种方法在非常短的闭包中特别有用。 作为函数的最后一个参数传递的闭包可以在括号后立即显示。 当闭包是函数的唯一参数时,可以完全省略括号。

let sortedNumbers = numbers.sorted { $0 > $1 }
print(sortedNumbers)
Swift4学习笔记-1.3 Swift之旅
对象和类

使用类后跟类的名称来创建一个类。 类中的属性声明的写法与常量或变量声明相同,只不过它在类的上下文中。 同样,方法和函数声明也是一样的。

class Shape {
    var numberOfSides = 0
    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }
}
通过在类名后添加括号来创建类的实例。 使用"点"语法访问实例的属性和方法。

var shape = Shape()
shape.numberOfSides = 7
var shapeDescription = shape.simpleDescription()
Swift4学习笔记-1.3 Swift之旅
这个版本的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参数区分开来。 当您创建类的实例时,初始化程序的参数就像函数调用一样传递。 每个属性都需要赋值-----在其声明中(如numberOfSides)或初始化(与name一样)。
如果需要在释放对象之前执行一些清理,请使用deinit创建一个deinitializer。

子类在其类名后面包含它们的超类名称,用冒号分隔。 不需要类来对任何标准的根类进行子类化,因此您可以根据需要添加或省略一个超类。

覆盖超类实现的子类上的方法标记为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()
除了存储的简单属性之外,属性可以有一个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)

在setter的周边,新值具有隐含名称newValue。 您可以在设置后的括号中提供明确的名称。

请注意,EquilateralTriangle类的初始化程序有三个不同的步骤:
设置子类声明的属性值。
调用超类的初始化器。
更改由超类定义的属性的值。 此时也可以使用方法,getter或setter的任何其他设置工作。

如果您不需要计算属性,但仍需要提供在设置新值之前和之后运行的代码,请使用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。 否则,可选值被解开,?之后的所有操作都将作用于展开的值。 在这两种情况下,整个表达式的值是可选的值。
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)
        }
    }
}
let ace = Rank.ace
let aceRawValue = ace.rawValue
默认情况下,Swift会分配从零开始的原始值,并每次增加一个,但是您可以通过显式指定值来更改此行为。 在上面的例子中,Ace明确地给出了一个原始值1,其余的原始值按顺序分配。 您也可以使用字符串或浮点数作为枚举的原始类型。 使用rawValue属性访问枚举大小写的原始值。

使用init?(rawValue:)初始化程序从原始值创建枚举的实例。 它返回与原始值匹配的枚举情况,如果没有匹配的Rank,则返回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()
实验
添加一个color()方法到Suit对于spadesclubs返回“black”,并为heartsdiamonds返回“red”。

Swift4学习笔记-1.3 Swift之旅
请注意上述枚举的hearts情况的两种方法:在向hearts常量赋值时,枚举情况Suit.hearts由其全名引用,因为常量没有指定的显式类型。 在switch中,枚举情况由缩写形式.hearts提及,因为self值的类型已知。 只要值的类型已知,您可以使用缩写形式。

如果枚举具有原始值,则这些值将作为声明的一部分,这意味着特定枚举大小写的每个实例始终具有相同的原始值。 枚举案例的另一个选择是具有与案例相关联的值 - 在创建实例时确定这些值,并且对于枚举情况的每个实例可能会有所不同。 您可以将关联的值看作是枚举情况实例的存储属性。 例如,考虑从服务器请求日出和日落时间的情况。 服务器响应所请求的信息,或者响应错误的描述。

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)")
}
Swift4学习笔记-1.3 Swift之旅

注意如何从ServerResponse值中提取日出和日落时间,作为将值与switch case相匹配的一部分.

使用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的声明中使用mutating关键字来标记修改结构的方法。 SimpleClass的声明不需要任何标记为mutation的方法,因为类上的方法可以随时修改类。

使用extension将功能添加到现有类型,如新方法和计算属性。 您可以使用扩展来将协议一致性添加到其他地方声明的类型,甚至是从库或框架导入的类型。

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

Swift4学习笔记-1.3 Swift之旅

您可以像任何其他命名类型一样使用协议名称 - 例如,创建具有不同类型但都符合单个协议的对象集合。 当使用类型为协议类型的值时,协议定义之外的方法不可用。

let protocolValue: ExampleProtocol = a
print(protocolValue.simpleDescription)
// 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)
}

实验
将printer名称更改为“Never Has Toner”,以使send(job:toPrinter:)函数抛出一个错误。

Swift4学习笔记-1.3 Swift之旅
您可以提供处理特定错误的多个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)
}
处理错误的另一种方法是使用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)

泛型

在尖括号内写一个名字来形成通用的功能或类型。

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)
在body之前使用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])
<T: Equatable> 和 <T> ... where T: Equatable.用法是一样的