Using a Swift PropertyWrapper to ensure a closure is only called once

栏目: IT技术 · 发布时间: 4年前

内容简介:Want to skip to the answer? Variables marked with this property wrapper will be destroyed after they are read once. Use with caution!Now lets see how we got here.

Learn : how to write a property wrapper that avoids boilerplate for your asynchronous callback code.

The Solution Code

Want to skip to the answer? Variables marked with this property wrapper will be destroyed after they are read once. Use with caution!

@propertyWrapper
struct ReadableOnce<T> {
    var wrappedValue: T? {
        mutating get {
            defer { self._value = nil }
            return self._value
        }
        set {
            self._value = newValue
        }
    }
    private var _value: T? = nil
}

Now lets see how we got here.

Avoiding multiple invocations of a completion block

Let’s say we have a class that does some work, and then invokes its completion block.

class NumberChecker {
    private var completion: ((Error?) -> Void)
    
    init(completion: @escaping ((Error?) -> Void)) {
        self.completion = completion
    }
    
    public func check() {
        if someCondition {
            handleFailure(NSError(domain: "MyDomain", code: 1))
        }
        handleSuccess()
    }
    
    private func handleSuccess() {
        self.completion(nil)
    }
    private func handleFailure(_ error: Error) {
        self.completion(error)
    }
}

This code has a secret bug in its check method. Did you notice it?

    public func check() {
        if someCondition {
            handleFailure(NSError(domain: "MyDomain", code: 1))
        }
        handleSuccess()
    }

If someCondition gets evaluated as false , the completion block will be invoked twice – once by handleFailure() , and again by handleSuccess() . This is a bug – the completion block should only get called once.

Now obviously in this trivial example you could fix this by just using an else statement. But in The Real World , your code may be much more complicated, and it may be harder to make the calls of the completion block mutually exclusive.

Perhaps your code is asynchronous, and completes when the first of two operations returns a result, for example.

I’m not judging how you have gotten yourself into this problem – just providing a neat solution.

How to solve this problem

Our approach here is going to be to ensure the completion block can only be called once by destroying it once it has been used. One perfectly reasonable approach to this solution is as follows:

class NumberChecker {
    // Make the completion closure optional
    private var completion: ((Error?) -> Void)?
    
    init(completion: @escaping ((Error?) -> Void)) {
        self.completion = completion
    }
    
    public func check() {
        if someCondition {
            handleFailure(NSError(domain: "MyDomain", code: 1))
        }
        handleSuccess()
    }
    
    private func handleSuccess() {
        self.completion?(nil)
        self.completion = nil
    }
    private func handleFailure(_ error: Error) {
        self.completion?(error)
        self.completion = nil
    }
}

Here we are simply setting the completion closure to nil after we use it. This is fine, but requires us to add extra boilerplate around each usage of the callback. It is also error-prone, since we could easily forget to include the self.completion = nil and end up with a partial solution.

So instead, lets create a property wrapper that changes the behaviour of the get ter for that variable.

What is a property wrapper?

Property wrappers are structs that use Generics to define extra behaviour around the usage of some arbitrary property. They are defined separately from the property they act on, and can be re-used easily for different properties, even of different types.

NSHipster has a great write-up on the background and purpose of this Swift feature if you haven’t seen it before so I won’t go into more of those details here.

The ReadableOnce property wrapper:

I included this at the top, but lets look in more detail at our property wrapper:

@propertyWrapper
struct ReadableOnce<T> {
    var wrappedValue: T? {
        mutating get {
            // This defer is the magic. It lets us 
            // erase the value AFTER it is returned.
            defer { self._value = nil }
            return self._value
        }
        set {
            self._value = newValue
        }
    }
    // Use a private backing variable that we can modify here.
    private var _value: T? = nil
}

The property wrapper definition is just a struct marked with @propertyWrapper . It has a special var called wrappedValue , which is the place we have to implement whatever our custom logic is.

In our case, we want to put our magic into the get method of the value, so that the value is destroyed after being returned the first time. The set method should behave as normal, so we just add a private backing variable, here _value .

The defer keyword

Deferred closures are executed at the end of the scope they are declared in. In our case, the defer keyword allows us to execute our cleanup code after the return value is returned.

Downsides of this approach

The biggest concern with this is precisely the magic that makes it useful. It is unintuitive that you can only read once, and that reading could mutate the value.

The following code demonstrates the most likely pitfall:

private func handleSuccessPoorly() {
    if self.completion != nil {
        // The completion was just read (and set to nil).
        // So the next line will silently do nothing!
        self.completion?(nil)
    }
}

Overall, I think this property wrapper is very useful, as long as it is clear to the developer that it is being used.

So basically:

With great power comes great responsibility.

Uncle ben. yes. from spiderman.

Possible Improvements

There are a number of things you might continue to experiment with to improve this solution:

  • Use a different property wrapper to accomplish the same task
  • Implement something like a callThenDestroy() method on the type you want to store
  • Try @dynamicCallable (see this Hacking With Swift post for info)
  • Thread safety has not been considered in this implementation. It would be possible for two threads to both get access to the variable before either of them set it to nil .

This article is based on a code sample shared with me by Josh Caswell (https://github.com/woolsweater)

Have you tried any of the above? Do you have a better solution? Please leave a comment below!


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

轻资产创业

轻资产创业

蔡余杰 / 广东人民出版社 / 2017-11 / 45.00元

在互联网时代,资金和资源已经不是制约创业的关键因素。如今即便没有充足的资金和资产做后盾,创业梦依旧可以成为现实。相信轻资产创业模式能够帮助众多经营管理者和创业者实现管理与创业的梦想。 轻资产创业存在误区,如何跨过? 如何巧用四大模式让自媒体创业落地? 如何用一个点子引发创意型创业? 如何利用电商平台实现流量为王的营销型创业? 如何巧用知识节点做好知识产型创业? ......一起来看看 《轻资产创业》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

随机密码生成器
随机密码生成器

多种字符组合密码

SHA 加密
SHA 加密

SHA 加密工具