swift - 快速组合声明式语法

标签 swift variadic combine

Swift Combine的声明式语法对我来说似乎很奇怪,并且似乎有很多事情是不可见的。

例如,以下代码示例在Xcode游乐场中构建并运行:

[1, 2, 3]

.publisher
.map({ (val) in
        return val * 3
    })

.sink(receiveCompletion: { completion in
  switch completion {
  case .failure(let error):
    print("Something went wrong: \(error)")
  case .finished:
    print("Received Completion")
  }
}, receiveValue: { value in
  print("Received value \(value)")
})

我看到我假设是使用[1、2、3]创建的数组文字实例。我想这是一个数组文字,但是我不习惯在不将其分配给变量名或常量或使用_ =的情况下“声明”它。

之后,我放了一个故意的新行,然后是.publisher。 Xcode是否忽略空格和换行符?

由于这种样式,或者是我从视觉上解析这种样式的新颖性,我误认为“receiveValue:”是可变参数或某些新语法,但后来意识到它实际上是.sink(...)的参数。

最佳答案

首先清理代码

格式化

首先,如果格式正确,则阅读/理解此代码会容易得多。因此,让我们开始:

[1, 2, 3]
    .publisher
    .map({ (val) in
        return val * 3
    })
    .sink(
        receiveCompletion: { completion in
            switch completion {
            case .failure(let error):
                print("Something went wrong: \(error)")
            case .finished:
                print("Received Completion")
            }
        },
        receiveValue: { value in
            print("Received value \(value)")
        }
    )

清理map表达式

我们可以通过以下方法进一步清理地图:
  • 使用隐式返回
    map({ (val) in
        return val * 3
    })
    
  • 使用隐式返回
    map({ (val) in
        val * 3
    })
    
  • 删除参数声明周围不必要的括号
    map({ val in
        val * 3
    })
    
  • 删除不必要的换行符。有时它们对于在视觉上分离事物很有用,但这是一个足够简单的封闭,它只会增加不必要的噪音
    map({ val in val * 3 })
    
  • 使用隐式参数,而不是val,该参数无论如何都是非描述性的
    map({ $0 * 3 })
    
  • 使用结尾闭包语法
    map { $0 * 3 }
    

  • 最后结果

    带有编号的行,因此我可以轻松地返回。
    /*  1 */[1, 2, 3]
    /*  2 */    .publisher
    /*  3 */    .map { $0 * 3 }
    /*  4 */    .sink(
    /*  5 */        receiveCompletion: { completion in
    /*  6 */            switch completion {
    /*  7 */            case .failure(let error):
    /*  8 */                print("Something went wrong: \(error)")
    /*  9 */            case .finished:
    /* 10 */                print("Received Completion")
    /* 11 */            }
    /* 12 */        },
    /* 13 */        receiveValue: { value in
    /* 14 */            print("Received value \(value)")
    /* 15 */        }
    /* 16 */    )
    

    通过它。

    第1行,[1, 2, 3]
    第1行是数组文字。这是一个表达式,就像1"hi"truesomeVariable1 + 1一样。像这样的数组不需要分配任何东西就可以使用。

    有趣的是,这不一定意味着它是一个数组。相反,Swift具有ExpressibleByArrayLiteralProtocol。可以从数组文字中初始化任何符合条件的类型。例如,Set符合要求,因此您可以编写:let s: Set = [1, 2, 3],您将获得一个包含Set123。在没有其他类型信息的情况下(例如,上面的Set类型注释),S​​wift使用Array作为首选数组文字类型。

    第2行,.publisher
    第2行正在调用数组文字的publisher属性。这将返回Sequence<Array<Int>, Never>。那不是一个普通的 Swift.Sequence ,它不是一个通用的协议,而是在Publishers模块的Combine命名空间(无大小写的枚举)中找到的。因此,其完全限定类型为 Combine.Publishers.Sequence<Array<Int>, Never>

    这是一个Publisher,其OutputInt,并且Failure类型为Never(即,由于无法创建Never类型的实例,因此不会出现错误)。

    第3行,map
    第3行正在调用上面map值的Combine.Publishers.Sequence<Array<Int>, Never>实例函数(又称方法)。每当元素通过此链传递时,都会通过给map的闭包进行转换。
  • 1将进入,3将出现。
  • 然后2进入,6出来。
  • 最终3进入,6出来。

  • 到目前为止,此表达式的结果是另一个Combine.Publishers.Sequence<Array<Int>, Never>
    第4行,sink(receiveCompletion:receiveValue:)
    第4行是对 Combine.Publishers.Sequence<Array<Int>, Never>.sink(receiveCompletion:receiveValue:) 的调用。有两个闭包参数。
  • 提供{ completion in ... }闭包作为标有receiveCompletion:的参数的参数
  • 提供{ value in ... }闭包作为标有receiveValue:的参数的参数

  • Sink正在为我们上面的Subscription<Array<Int>, Never>值创建一个新订户。当元素通过时,receiveValue闭包将被调用,并作为参数传递给它的value参数。

    最终,发布者将完成操作,调用receiveCompletion:闭包。 completion参数的参数将是 Subscribers.Completion 类型的值,该值是.failure(Failure)大小写或.finished大小写的枚举。由于Failure类型是Never,因此实际上不可能在此处创建.failure(Never)的值。因此,完成将始终为.finished,这将导致print("Received Completion")被调用。语句print("Something went wrong: \(error)")是死代码,永远无法到达。

    关于“声明式”的讨论

    没有任何语法元素可使此代码符合“声明性”的要求。声明式风格是与“命令式”风格的区别。在命令式中,您的程序由一系列命令或要完成的步骤组成,通常具有非常严格的顺序。

    以声明的方式,您的程序由一系列声明组成。实现这些声明所必需的细节都被抽象出来,例如CombineSwiftUI之类的库。例如,在这种情况下,您声明每当从print("Received value \(value)")传入数字时,将打印该数字的[1, 2, 3].publisher。发布者是一个基本的示例,但是您可以想象一个发布者正在从文本字段发出值,而该文本字段在未知时间发生事件。

    我最喜欢伪装命令式和声明式样式的示例是使用Array.map(_:)之类的函数。

    您可以这样写:
    var input: [InputType] = ...
    var result = [ResultType]()
    
    for element in input {
        let transformedElement = transform(element)
        result.append(result)
    }
    

    但是有很多问题:
  • 您最终会重复很多代码,只是有些微妙的差异。
  • 读起来比较棘手。由于for是这样的常规构造,因此在这里有很多可能。为了确切地了解会发生什么,您需要研究更多细节。
  • 通过不调用Array.reserveCapacity(_:),您错过了优化机会。这些对append的重复调用可以达到result数组缓冲区的最大容量。在那时候:
  • 必须分配新的更大缓冲区
  • 需要将result的现有元素复制到
  • 需要释放旧缓冲区
  • ,最后,必须在
  • 中添加新的transformedElement
    这些操作可能会变得昂贵。并且,随着添加越来越多的元素,您可能会数次耗尽容量,从而导致许多此类重新生成操作。通过调用result.reserveCapacity(input.count),您可以告诉数组预先分配一个大小合适的缓冲区,这样就不需要进行增长操作了。
  • 即使您在构造后可能不需要对其进行变异,result数组也必须是可变的。

  • 可以将此代码编写为对map的调用:
    let result = input.map(transform)
    

    这有很多好处:
  • 较短(尽管并不总是一件好事,在这种情况下,较短的长度不会丢失任何内容)
  • 更清楚。 map是一个非常特定的工具,只能做一件事。一看到map,您就会知道input.count == result.count,并且结果是transform函数/ closure的输出数组。
  • 它是经过优化的,内部map调用reserveCapacity,它永远不会忘记这样做。
  • result可以是不可变的。

  • 调用map遵循一种更具声明性的编程风格。您无需摆弄数组大小,迭代,附加等内容。如果您有input.map { $0 * $0 },那么您将说“我希望输入的元素平方”。 map的实现必须具有for循环,append等等。虽然它以命令式实现,但该函数将其抽象化,并允许您以更高的抽象级别编写代码,而不必在乎诸如for循环之类的无关紧要的东西。

    关于swift - 快速组合声明式语法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59779962/

    相关文章:

    macos - 单击时,Swift 中的托盘图标会变灰。

    ios - 转换任何对象?到 [String :AnyObject? ] 总是失败

    c++ - 对 C++ 11 可变模板参数进行操作,存储到元组中

    swift - "PassthroughSubject"似乎是线程不安全的,这是错误还是限制?

    swiftui - 在 ObservableObject 中引用 EnvironmentObject

    swift - 为什么静态属性的闭包在赋值时执行?

    ios - 警告 : Unexpected type for now playing key kMRMediaRemoteNowPlayingInfoDuration

    c - C可变参数宏是否能够递归扩展##__VA_ARGS__?

    c - 是否可以迭代可变参数宏中的参数?

    swift - 何时不使用 eraseToAnyPublisher()