zoukankan      html  css  js  c++  java
  • Swift5.3 语言指南(九) 闭包

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

    闭包是独立的功能块,可以在代码中传递和使用。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(_:)方法为数组中的每个项目调用一次闭包表达式。您无需指定闭包的输入参数number的类型,因为可以从要映射的数组中的值推断出该类型。

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

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

    注意

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

    从检索到的字符串digitNames辞典被添加到前面output,在反向有效建立数字的字符串版本。(该表达式给出了for for for的值。)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必须允许参数的值转义函数的范围。

  • 相关阅读:
    格律詩
    React获取视频时长
    ant 入门级详解
    OpenShift证书批准及查询证书过期时间 wang
    kubeadm快速部署kubernetes集群(v1.22.3) wang
    OpenShift中SDN核心知识点总结 wang
    kubeadm快速部署kubernetes集群(v1.22.3)(二) wang
    Prometheus Operator使用ServiceMonitor自定义监控 wang
    Prometheus Operator配置k8s服务自动发现 wang
    Ceph RBD Mirroring wang
  • 原文地址:https://www.cnblogs.com/strengthen/p/9728063.html
Copyright © 2011-2022 走看看