Tysac
Tysac

Reputation: 257

Is it a code smell to use private global variables as extension 'properties'?

Because 'Extensions may not contain stored properties' I've seen people getting around this by using getter / setter with objc_getAssociatedObject / objc_setAssociatedObject

(See) How to have stored properties in Swift, the same way I had on Objective-C?

I find the solutions discussed there very 'unswifty' but still like to keep the variables close to where they are used.

That's why I recently started doing the following when I want to use a 'property' in an extension.

private var lastValue: Int = 0


extension ViewController {

    func checkIfBiggerThanLastNumber(_ number: Int) -> Bool {

        let savedLast = lastValue
        lastValue = number
        return number > savedLast
    }

}

Because I didn't find this solution elsewhere, I wonder if that is a code smell or what drawbacks come with it.

That other extensions in that source file can access lastValue is a thing I can live with because the extension is in it's own source file.

Upvotes: 2

Views: 471

Answers (2)

Duncan C
Duncan C

Reputation: 131426

Global variables are best avoided. I would recommend making your var a private instance variable rather than a global.

It's reasonable to have private variables that are added to the object to support "native" extensions. (An extension that is defined in the same source file as the original object is really just a way of grouping code, since it is "baked into" the base object.

Using objc_setAssociatedObject for adding storage to NSObject subclasses is an option for extensions that extend other classes. You need to be aware that there is a small time penalty, but unless you're referencing an associated object repeatedly in a loop, it's not likely to be an issue. It also requires that the object be an Objective-C object, which also has a small time penalty.

Using objc_setAssociatedObject does also make your Swift code Apple-only. if you are building code that you intend to use on Linux, it's not really an option.

Upvotes: 1

Rob Napier
Rob Napier

Reputation: 299355

This is commonly done with a dictionary. ("Commonly" meaning "when the ObjC runtime isn't available." As Charles Srstka notes, the correct tool here for a view controller is definitely objc_getAssociatedObject. There is no particular virtue in avoiding the runtime once you've already inherited from NSObject.)

It leaks some memory (since there's no automatic way to clean out unused values), but the cost is usually small (or you have to add a mechanism to "garbage collect" which isn't too hard).

private var lastValues: [ObjectIdentifier: Int] = [:]

extension ViewController {
    private var lastValue: Int {
        get {
            return lastValues[ObjectIdentifier(self)] ?? 0
        }
        set {
            lastValues[ObjectIdentifier(self)] = newValue
        }
    }

    func checkIfBiggerThanLastNumber(_ number: Int) -> Bool {

        let savedLast = lastValue
        lastValue = number
        return number > savedLast
    }
}

I haven't tested this extensively, but this an example of how you might build an auto-garbage collecting version of this to clean up memory if you had a lot of objects that come and go. It garbage collects every time you modify it, but you could do it other ways depending on need:

// A simple weak dictionary. There are probably better versions out there. 
// I just threw this together.
struct WeakDict<Key: AnyObject, Value>: ExpressibleByDictionaryLiteral {
    struct Box<T: AnyObject>: Hashable {
        let identifier: ObjectIdentifier
        weak var value: T?
        init(_ value: T) {
            self.identifier = ObjectIdentifier(value)
            self.value = value
        }

        static func ==(lhs: Box<T>, rhs: Box<T>) -> Bool {
            return lhs.identifier == rhs.identifier
        }
        var hashValue: Int { return identifier.hashValue }
    }

    private var dict: [Box<Key>: Value]

    init(dictionaryLiteral elements: (Key, Value)...) {
        dict = Dictionary(uniqueKeysWithValues: elements.map { (Box($0), $1) })
    }

    private mutating func garbageCollect() {
        dict = dict.filter { (key, _) in key.value != nil }
    }

    subscript(_ key: Key) -> Value? {
        get {
            return dict[Box(key)]
        }
        set {
            garbageCollect()
            dict[Box(key)] = newValue
        }
    }
}

With that, usage is almost identical:

private var lastValues: WeakDict<ViewController, Int> = [:]

extension ViewController {
    private var lastValue: Int {
        get { return lastValues[self] ?? 0 }
        set { lastValues[self] = newValue }
    }

    func checkIfBiggerThanLastNumber(_ number: Int) -> Bool {
        let savedLast = lastValue
        lastValue = number
        return number > savedLast
    }
}

Upvotes: 2

Related Questions