为有牺牲多壮志,敢教日月换新天。

Swift5.4 语言指南(九) 闭包

★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
➤微信公众号:山青咏芝(shanqingyongzhi)
➤博客园地址:山青咏芝(https://www.cnblogs.com/strengthen/
➤GitHub地址:https://github.com/strengthen/LeetCode
➤原文地址:https://www.cnblogs.com/strengthen/p/9728063.html 
➤如果链接不是山青咏芝的博客园地址,则可能是爬取作者的文章。
➤原文已修改更新!强烈建议点击原文地址阅读!支持作者!支持原创!
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★

热烈欢迎,请直接点击!!!

进入博主App Store主页,下载使用各个作品!!!

注:博主将坚持每月上线一个新app!!!

是功能完备的功能块,可以在代码中传递和使用。Swift中的闭包类似于C和Objective-C中的块以及其他编程语言中的lambda。

闭包可以从定义它们的上下文中捕获和存储对任何常量和变量的引用。这称为关闭这些常量和变量。Swift为您处理捕获的所有内存管理。

笔记

如果您不熟悉捕获的概念,请不要担心。下面在捕获值中对其进行了详细说明

正如Function中介绍的那样全局和嵌套函数实际上是闭包的特殊情况。闭包采用以下三种形式之一:

  • 全局函数是具有名称且不捕获任何值的闭包。
  • 嵌套函数是具有名称的闭包,可以从其闭包函数捕获值。
  • 闭包表达式是用轻量级语法编写的未命名的闭包,可以从其周围的上下文中捕获值。

Swift的闭包表达式具有简洁明了的样式,其优化功能鼓励在常见情况下使用简洁,简洁的语法。这些优化包括:

  • 从上下文推断参数和返回值类型
  • 单表达式闭包的隐式返回
  • 速记参数名称
  • 尾随闭包语法

闭包表达式

嵌套函数中介绍的嵌套函数是命名和定义自包含代码块作为较大函数的一部分的便捷方法。但是,有时在没有完整的声明和名称的情况下编写类似函数的结构的较短版本有时会很有用。当您使用以函数作为其一个或多个参数的函数或方法时,尤其如此。

闭包表达式是一种以简短,集中的语法编写内联闭包的方法。闭包表达式提供了几种语法优化,可以以缩短的形式编写闭包,而不会失去清晰度或意图。下面的闭包表达式示例通过sorted(by:)在多个迭代中完善方法的单个示例来说明这些优化,每个迭代以更简洁的方式表示相同的功能。

排序方法

Swift的标准库提供了一种称为的方法sorted(by:),该方法根据您提供的排序闭包的输出对已知类型的值数组进行排序。完成排序过程后,该sorted(by:)方法将返回一个与旧数组具有相同类型和大小的新数组,并且其元素的排序顺序正确。sorted(by:)方法未修改原始数组

下面的闭包表达式示例使用该sorted(by:)方法以String反向字母顺序数组进行排序。这是要排序的初始数组:

  1. let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

sorted(by:)方法接受一个闭包,该闭包采用与数组内容相同类型的两个参数,并返回一个Bool值,以说明对这些值进行排序后,第一个值应出现在第二个值之前还是之后。true如果第一个值应出现第二个值之前排序闭包需要返回false否则返回。

本示例正在对String数组进行排序,因此排序闭包必须是type的函数(String, String) -> Bool

提供排序闭包的一种方法是编写正确类型的普通函数,并将其作为sorted(by:)方法的参数传递

  1. func backward(_ s1: String, _ s2: String) -> Bool {
  2. return s1 > s2
  3. }
  4. var reversedNames = names.sorted(by: backward)
  5. // reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

如果第一个字符串(s1)大于第二个字符串(s2),则该backward(_:_:)函数将返回true,指示该字符串s1应出现s2在排序后的数组中。对于字符串中的字符,“大于”表示“在字母表中出现的时间晚于”。这意味着字母"B"“大于”字母"A",并且字符串"Tom"大于string "Tim"这给出了反向的字母排序,并"Barry"放在之前"Alex",依此类推。

但是,这实际上是编写单表达式函数()的一种漫长的方法在此示例中,最好使用闭包表达式语法内联地编写排序闭包。b

闭包表达式语法

闭包表达式语法具有以下一般形式:

  1. { (parameters) -> return type in
  2. statements
  3. }

参数在封闭表达式语法可以在输出参数,但是他们不能有一个默认值。如果您命名可变参数,则可以使用可变参数。元组也可以用作参数类型和返回类型。

下面的示例显示了backward(_:_:)上面函数的闭包表达式版本

  1. reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
  2. return s1 > s2
  3. })

请注意,此内联闭包的参数声明和返回类型与该backward(_:_:)函数的声明相同在这两种情况下,它都写为但是,对于内联闭包表达式,参数和返回类型写花括号内,而不是花括号外。(s1: String, s2: String) -> Bool

闭包主体的开头由in关键字引入此关键字指示闭包的参数和返回类型的定义已完成,并且闭包的主体即将开始。

由于闭包的主体非常短,因此甚至可以将其写在一行上:

  1. reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )

这说明该方法的总体调用sorted(by:)保持不变。一对括号仍然包裹了该方法的整个参数。但是,该参数现在是内联闭包。

从上下文推断类型

因为排序闭包是作为方法的参数传递的,所以Swift可以推断其参数的类型以及它返回的值的类型。sorted(by:)方法是在字符串数组上调用的,因此其参数必须是type的函数这意味着类型不必作为闭包表达式定义的一部分来编写。由于可以推断所有类型,因此还可以省略返回箭头()和参数名称周围的括号:(String, String) -> Bool(String, String)Bool->

  1. reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

当将闭包作为内联闭包表达式传递给函数或方法时,总是可以推断出参数类型和返回类型。因此,当闭包用作函数或方法参数时,您无需编写其完整形式的内联闭包。

尽管如此,您仍然可以根据需要使类型显式,并且如果这样做可以避免代码阅读者产生歧义,则鼓励这样做。在这种sorted(by:)方法的情况下,闭包的目的很明显,那就是发生了排序,读者可以放心地认为闭包很可能与String一起工作,因为它有助于对容器进行排序。字符串数组。

单表达式闭包的隐式收益

单表达式闭包可以通过return从声明中省略关键字来隐式返回其单表达式的结果,如上一示例的该版本中所示:

  1. reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

在这里,sorted(by:)方法参数的函数类型清楚地表明Bool,闭包必须返回一个值。由于闭包的主体包含单个返回值的表达式(,因此没有歧义,可以省略关键字。s1 s2Boolreturn

速记参数名称

雨燕自动提供速记参数名内联闭包,它可以使用的名称,指的是关闭的参数值$0$1$2,等等。

如果在闭包表达式中使用这些速记参数名称,则可以从其定义中忽略闭锁的参数列表,而速记参数名称的数量和类型将从所需的函数类型中推断出来。in关键字也可以被省略,因为封闭件表达是由完全其身体的:

  1. reversedNames = names.sorted(by: { $0 > $1 } )

在这里,$0$1参考闭包的第一个和第二个String参数。

操作员方法

实际上,还有一种更短的方法可以编写上面的闭包表达式。Swift的String类型将大于操作符(>的特定于字符串的实现定义为具有两个type参数的方法String,并返回type的值Bool这与方法所需的方法类型完全匹配sorted(by:)因此,您可以简单地传入大于号运算符,而Swift会推断您要使用其特定于字符串的实现:

  1. reversedNames = names.sorted(by: >)

有关运算符方法的更多信息,请参见运算符方法

尾随闭包

如果您需要将闭包表达式作为函数的最终参数传递给函数,并且闭包表达式很长,那么将其写为尾随闭包可能会很有用您可以在函数调用的括号后面编写尾随闭包,即使尾随闭包仍然是函数的参数。使用尾随闭包语法时,不要在函数调用的过程中为第一个闭包编写参数标签。一个函数调用可以包含多个尾随的闭包。但是,下面的前几个示例使用了单个尾随闭包。

  1. func someFunctionThatTakesAClosure(closure: () -> Void) {
  2. // function body goes here
  3. }
  4. // Here's how you call this function without using a trailing closure:
  5. someFunctionThatTakesAClosure(closure: {
  6. // closure's body goes here
  7. })
  8. // Here's how you call this function with a trailing closure instead:
  9. someFunctionThatTakesAClosure() {
  10. // trailing closure's body goes here
  11. }

上面闭包表达式语法”部分中的字符串排序闭可以sorted(by:)作为结尾的闭包写在方法括号之外

  1. reversedNames = names.sorted() { $0 > $1 }

如果将闭包表达式作为函数或方法的唯一参数提供,并且将该表达式作为尾随闭包提供,则()在调用函数时,无需在函数或方法名称后写一对括号

  1. reversedNames = names.sorted { $0 > $1 }

当闭包足够长而无法在一行中内联写入时,尾随闭包最有用。例如,Swift的Array类型有一个map(_:)方法,该方法将闭包表达式作为其单个参数。对数组中的每个项目调用一次闭包,并为该项目返回一个替代的映射值(可能是其他类型的)。通过在传递给闭包的代码中编写代码,可以指定映射的性质和返回值的类型map(_:)

在将提供的闭包应用于每个数组元素之后,该map(_:)方法将返回一个包含所有新映射值的新数组,其顺序与原始数组中相应值的顺序相同。

这是map(_:)在尾随闭包中使用该方法将Int数组转换为值数组的方法String该数组用于创建新数组[16, 58, 510]["OneSix", "FiveEight", "FiveOneZero"]

  1. let digitNames = [
  2. 0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four",
  3. 5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
  4. ]
  5. let numbers = [16, 58, 510]

上面的代码创建了一个整数数字和其名称的英语版本之间的映射的字典。它还定义了一个整数数组,可以将其转换为字符串。

现在,您可以使用numbers数组来创建String数组,方法是将闭包表达式map(_:)作为尾随闭包传递给数组的方法:

  1. let strings = numbers.map { (number) -> String in
  2. var number = number
  3. var output = ""
  4. repeat {
  5. output = digitNames[number % 10]! + output
  6. number /= 10
  7. } while number > 0
  8. return output
  9. }
  10. // strings is inferred to be of type [String]
  11. // its value is ["OneSix", "FiveEight", "FiveOneZero"]

map(_:)方法为数组中的每个项目调用一次闭包表达式。您无需指定闭包的input参数number的类型,因为可以从要映射的数组中的值推断出该类型。

在此示例中,变量number使用闭包number参数的值初始化,以便可以在闭包主体内修改该值。(函数和闭包的参数始终是常量。)闭包表达式还指定返回类型String,以指示将存储在映射的输出数组中的类型。

闭包表达式在output每次调用时都会构建一个称为的字符串number使用余数运算符(计算的最后一位,并使用该位在字典中查找适当的字符串闭包可用于创建任何大于零的整数的字符串表示形式。number 10digitNames

笔记

digitNames字典下标的调用后会带有一个感叹号(!),因为字典下标会返回一个可选值,以指示如果键不存在,则字典查找可能会失败。在上面的示例中,可以确保始终是字典的有效下标键,因此使用感叹号强制拆开存储在下标的可选返回值中的值。number 10digitNamesString

从检索到的字符串digitNames辞典被添加到前面output,在反向有效建立数字的字符串版本。(该表达式给出了for forfor的值。)number 106168580510

number然后变量除以10因为它是整数,所以在除法过程中会四舍五入,因此16变为158变为5510变为51

重复该过程,直到number等于为止0,此时该output字符串由闭包返回,并通过该map(_:)方法添加到输出数组中

在上面的示例中,使用尾随闭包语法可以在闭包支持的功能之后立即将闭包的功能巧妙地封装起来,而无需将整个闭包包装在map(_:)方法的外部括号内。

如果一个函数使用多个闭包,则可以省略第一个尾随闭包的参数标签,并标记其余的尾随闭包。例如,下面的函数为照片库加载图片:

  1. func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
  2. if let picture = download("photo.jpg", from: server) {
  3. completion(picture)
  4. } else {
  5. onFailure()
  6. }
  7. }

调用此函数加载图片时,将提供两个闭包。第一个关闭是完成处理程序,该处理程序在成功下载后显示图片。第二个闭包是一个错误处理程序,向用户显示错误。

  1. loadPicture(from: someServer) { picture in
  2. someView.currentPicture = picture
  3. } onFailure: {
  4. print("Couldn't download the next picture.")
  5. }

在此示例中,该loadPicture(from:completion:onFailure:)函数将其网络任务分派到后台,并在网络任务完成时调用两个完成处理程序之一。通过这种方式编写函数,可以让您轻松地将负责处理网络故障的代码与成功下载后更新用户界面的代码区分开,而不是仅使用一个处理这两种情况的闭包。

捕捉价值

闭包可以从定义它的周围上下文中捕获常量和变量。然后,闭包可以从其主体内部引用和修改那些常量和变量的值,即使定义常量和变量的原始范围不再存在。

在Swift中,可以捕获值的闭包的最简单形式是嵌套函数,它写在另一个函数的主体内。嵌套函数可以捕获其外部函数的任何自变量,还可以捕获在外部函数内定义的任何常量和变量。

这是一个名为的函数的示例makeIncrementer,其中包含一个名为的嵌套函数incrementer嵌套incrementer()函数从其周围的上下文中捕获两个值runningTotalamount捕获这些值后,将以闭包incrementer形式返回,makeIncrementer闭包将runningTotalamount每次调用时递增

  1. func makeIncrementer(forIncrement amount: Int) -> () -> Int {
  2. var runningTotal = 0
  3. func incrementer() -> Int {
  4. runningTotal += amount
  5. return runningTotal
  6. }
  7. return incrementer
  8. }

的返回类型makeIncrementer这意味着它将返回一个function,而不是一个简单的值。它返回的函数没有参数,并且每次调用时都返回一个值。要了解函数如何返回其他函数,请参见函数类型作为返回类型() -> IntInt

makeIncrementer(forIncrement:)函数定义了一个名为的整数变量runningTotal,用于存储将要返回的增量器的当前运行总计。该变量的初始值为0

makeIncrementer(forIncrement:)函数具有一个Int参数,参数标签为forIncrement,参数名称为amount传递给此参数的参数值指定runningTotal每次调用返回的增量器函数时应增加的量。makeIncrementer函数定义了一个称为的嵌套函数incrementer,该函数执行实际的增量。此函数只是添加amountrunningTotal,并返回结果。

当单独考虑时,嵌套incrementer()函数可能看起来很不寻常:

  1. func incrementer() -> Int {
  2. runningTotal += amount
  3. return runningTotal
  4. }

incrementer()函数没有任何参数,但是它是在函数体内引用runningTotal引用的amount它通过捕获对周围功能引用runningTotalamount从周围功能中引用在其自己的功能体内使用它们做到这一点通过引用捕获可确保调用结束runningTotalamount不会消失makeIncrementer,并且还确保runningTotal下次incrementer调用函数时该引用可用

笔记

作为一种优化,Swift可能会捕获并存储值副本,如果该值未由闭包更改,并且在创建闭包后也未更改该值。

Swift也可以处理不再需要变量时涉及的所有内存管理。

这是一个实际的例子makeIncrementer

  1. let incrementByTen = makeIncrementer(forIncrement: 10)

本示例设置一个常量incrementByTen该常量被称为引用一个增量器函数,10函数在runningTotal每次调用时都会添加到其变量中。多次调用该函数可显示此行为:

  1. incrementByTen()
  2. // returns a value of 10
  3. incrementByTen()
  4. // returns a value of 20
  5. incrementByTen()
  6. // returns a value of 30

如果创建第二个增量器,它将有其自己的对新的单独runningTotal变量的存储引用

  1. let incrementBySeven = makeIncrementer(forIncrement: 7)
  2. incrementBySeven()
  3. // returns a value of 7

incrementByTen再次调用原始的增量器()会继续增加其自己的runningTotal变量,并且不会影响以下变量捕获的变量incrementBySeven

  1. incrementByTen()
  2. // returns a value of 40

笔记

如果将闭包分配给类实例的属性,并且闭包通过引用该实例或其成员来捕获该实例,则将在闭包和实例之间创建一个强大的引用周期。Swift使用捕获列表来打破这些强大的参考周期。有关更多信息,请参见强大的闭包参考循环

闭包是引用类型

在上面的示例中,incrementBySevenincrementByTen是常量,但是这些常量引用的闭包仍然能够增加runningTotal它们已捕获变量。这是因为函数和闭包是引用类型

每当您将函数或闭包分配给常量或变量时,实际上就是在将该常量或变量设置为对该函数或闭包引用在上面的示例中,选择闭包是incrementByTen 常量,而不是闭包本身的内容。

这也意味着,如果将闭包分配给两个不同的常量或变量,则这两个常量或变量都引用同一个闭包。

  1. let alsoIncrementByTen = incrementByTen
  2. alsoIncrementByTen()
  3. // returns a value of 50
  4. incrementByTen()
  5. // returns a value of 60

上面的示例显示调用alsoIncrementByTen与相同incrementByTen因为它们都引用相同的闭包,所以它们都递增并返回相同的运行总计。

逃逸关闭

当闭包作为函数的参数传递给闭包时,闭包被认为是对函数的转义,但是在函数返回后被调用。声明将闭包作为其参数之一的函数时,可以@escaping在参数的类型之前编写,以指示允许转义闭包。

闭包可以逃脱的一种方法是将其存储在函数外部定义的变量中。例如,许多启动异步操作的函数都将闭包参数用作完成处理程序。该函数在开始操作后返回,但是直到操作完成后才调用闭包-该闭包需要转义,稍后再调用。例如:

  1. var completionHandlers = [() -> Void]()
  2. func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
  3. completionHandlers.append(completionHandler)
  4. }

someFunctionWithEscapingClosure(_:)函数将闭包作为其参数,并将其添加到在函数外部声明的数组中。如果未使用标记该函数的参数@escaping,则会出现编译时错误。

self如果self引用的逃逸闭包是引用类的实例,则需要特别考虑捕获self转义的闭包中的内容很容易意外地创建一个强大的参考周期。有关参考循环的信息,请参见“自动参考计数”

通常,闭包通过在闭包主体中使用变量来隐式捕获变量,但是在这种情况下,您需要明确表示。如果要捕获self,请self在使用时明确编写,或包括self在闭包的捕获列表中。self显式编写可以表达您的意图,并提醒您确认没有参考周期。例如,在下面的代码中,传递给的闭包显式地someFunctionWithEscapingClosure(_:)引用self相比之下,传递给的闭包someFunctionWithNonescapingClosure(_:)是一个不冒漏的闭包,这意味着它可以self隐式引用

  1. func someFunctionWithNonescapingClosure(closure: () -> Void) {
  2. closure()
  3. }
  4. class SomeClass {
  5. var x = 10
  6. func doSomething() {
  7. someFunctionWithEscapingClosure { self.x = 100 }
  8. someFunctionWithNonescapingClosure { x = 200 }
  9. }
  10. }
  11. let instance = SomeClass()
  12. instance.doSomething()
  13. print(instance.x)
  14. // Prints "200"
  15. completionHandlers.first?()
  16. print(instance.x)
  17. // Prints "100"

doSomething()是捕获的一个版本,将self其包含在闭包的捕获列表中,然后self隐式引用

  1. class SomeOtherClass {
  2. var x = 10
  3. func doSomething() {
  4. someFunctionWithEscapingClosure { [self] in x = 100 }
  5. someFunctionWithNonescapingClosure { x = 200 }
  6. }
  7. }

如果self是结构或枚举的实例,则始终可以self隐式引用但是,转义的闭包无法捕获对self何时self是结构实例或枚举的可变引用结构和枚举不允许共享的可变性,如“结构和枚举是值类型”中所述

  1. struct SomeStruct {
  2. var x = 10
  3. mutating func doSomething() {
  4. someFunctionWithNonescapingClosure { x = 200 } // Ok
  5. someFunctionWithEscapingClosure { x = 100 } // Error
  6. }
  7. }

someFunctionWithEscapingClosure上面示例中函数的调用是错误的,因为它在mutation方法内部,因此self是可变的。这违反了转义的闭包不能捕获self对结构的可变引用的规则

自动关闭

一个autoclosure是真实自动创建来包装被真实作为参数传递给函数的表达式的封闭件。它不带任何参数,并且在调用它时,它返回包装在其中的表达式的值。这种语法上的便利性使您可以通过编写正则表达式而不是显式闭包来省略函数参数的花括号。

调用具有自动关闭功能的函数很常见,但是实现这种功能并不常见例如,该assert(condition:message:file:line:)函数对其conditionmessage参数进行自动关闭condition仅在调试参数进行评估,并建立其message仅在参数评估conditionfalse

自动闭包可让您延迟评估,因为在调用闭包之前,内部代码不会运行。延迟评估对于具有副作用或计算量大的代码很有用,因为它使您可以控制何时评估该代码。下面的代码显示了闭包如何延迟评估。

  1. var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
  2. print(customersInLine.count)
  3. // Prints "5"
  4. let customerProvider = { customersInLine.remove(at: 0) }
  5. print(customersInLine.count)
  6. // Prints "5"
  7. print("Now serving \(customerProvider())!")
  8. // Prints "Now serving Chris!"
  9. print(customersInLine.count)
  10. // Prints "4"

即使customersInLine数组的第一个元素已由闭包中的代码删除,但只有在实际调用闭包时才删除数组元素。如果从不调用闭包,则闭包内部的表达式永远不会求值,这意味着数组元素不会被删除。请注意,的类型customerProvider不是String而是-没有参数的函数返回字符串。() -> String

将闭包作为函数的参数传递时,您会得到延迟求值的相同行为。

  1. // customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
  2. func serve(customer customerProvider: () -> String) {
  3. print("Now serving \(customerProvider())!")
  4. }
  5. serve(customer: { customersInLine.remove(at: 0) } )
  6. // Prints "Now serving Alex!"

serve(customer:)上面清单中函数采用显式闭包,该闭包返回客户的姓名。下面的版本serve(customer:)执行相同的操作,但不是采取显式的关闭,而是通过使用@autoclosure属性标记其参数的类型来进行自动关闭现在,您可以像调用参数一样使用String函数而不是使用闭包来调用该函数参数会自动转换为闭包,因为customerProvider参数的类型已用@autoclosure属性标记

  1. // customersInLine is ["Ewa", "Barry", "Daniella"]
  2. func serve(customer customerProvider: @autoclosure () -> String) {
  3. print("Now serving \(customerProvider())!")
  4. }
  5. serve(customer: customersInLine.remove(at: 0))
  6. // Prints "Now serving Ewa!"

笔记

过度使用自动关闭功能可能会使您的代码难以理解。上下文和函数名称应清楚表明评估被推迟。

如果要允许自动关闭功能可以转义,请同时使用@autoclosure@escaping属性。@escaping上面在转义闭包中描述了属性

  1. // customersInLine is ["Barry", "Daniella"]
  2. var customerProviders: [() -> String] = []
  3. func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
  4. customerProviders.append(customerProvider)
  5. }
  6. collectCustomerProviders(customersInLine.remove(at: 0))
  7. collectCustomerProviders(customersInLine.remove(at: 0))
  8. print("Collected \(customerProviders.count) closures.")
  9. // Prints "Collected 2 closures."
  10. for customerProvider in customerProviders {
  11. print("Now serving \(customerProvider())!")
  12. }
  13. // Prints "Now serving Barry!"
  14. // Prints "Now serving Daniella!"

在上面的代码中customerProvider,该collectCustomerProviders(_:)函数没有调用传递给它的闭包作为其参数,而是将闭包附加到customerProviders数组中。数组在函数范围之外声明,这意味着可以在函数返回后执行数组中的闭包。结果,customerProvider必须允许参数的值转义函数的范围。

 

 
posted @ 2018-09-30 09:42  为敢技术  阅读(1270)  评论(0编辑  收藏  举报