zekel
zekel

Reputation: 9467

Can a closure property of a struct refer to the struct instance, without an explicit reference?

This is what I'd like to do, but quantity property isn't available.

struct MyStruct {
    var quantity: Int
    let valueFn: () -> (Int)
}
var s1 = MyStruct(quantity: 2) { () -> (Int) in
    return quantity * 2 // error: can't use `quantity`
}

It doesn't work with classes either:

class MyClass {
    var quantity: Int
    let valueFn: () -> (Int)

    init(valueFn: () -> (Int)) {
        self.valueFn = valueFn
    }
}
var c1 = MyClass { () -> (Int) in
    return quantity // error: can't use quantity or self
}

This one works, but I have to explicitly pass in the struct or struct property. My question is this: Can I do this without having a third property to wrap and pass in the reference? Is there a more Swifty approach?

struct MyOtherStruct {
    var quantity: Int
    private let valueFn: (Int) -> (Int)
    var value: Int {
        return valueFn(quantity)
    }
}
var s3 = MyOtherStruct(quantity: 2) { (quantity) -> (Int) in
    return quantity * 2
}
s3.value // -> 4

Upvotes: 2

Views: 703

Answers (1)

dfrib
dfrib

Reputation: 73186

I've fibbled with this the last 20 minutes now, and I don't believe this is possible to achieve in the initialization of of your MyStruct instances; since at the call to the initializer (which you do with the trailing closure above), the instance does not yet exist. I will show below, however, a workaround for classes; one much like your third approach above, and one with more versatile usage (but horribly worse class implementation), making use of subclassing NSObject.

Anyway, possibly the above answers your question (as "no" for structs, and "possible, but overly complicated" for classes) with regard of use directly with the initializer in a versatile manner.


Below follows

  1. A closure "wrapping" method much your third solution: wrapping a lazy closure over the closure you send at initialization. The drawback with this method is that you can't choose, for different class instances, which class properties you would like to use in the closure, as the wrapper (calling the mutable initialized closure) is already set at compile time.

  2. Subclassing NSObject: a method that is probably overly complicated for any practical use (and also quite non-swifty), that however give you more versatile control over which class properties to use in the closure. By subclassing NSObject you get access to the method .valueForKey to apply to self. Added for the technical discussion/curiosity.

Method 1

class MyClass {
    var quantity: Int
    private let closure: (Int) -> (Int)

    init(quantity: Int, closure: (Int) -> (Int)) {
        self.quantity = quantity
        self.closure = closure
    }

    lazy var valueFn : () -> Int = {
        [unowned self] () -> Int in
        return self.closure(self.quantity)
    }
}

/* Example usage */
var c1 = MyClass(quantity: 2) { (a) -> (Int) in
    return a * 2
}

c1.valueFn() // 4
c1.quantity = 4
c1.valueFn() // 8

Method 2

Class setup (...):

class MyClass : NSObject {
    var quantity: Int = 0
    var anotherQuantity: Int = 0
    private var keyExists : Bool = true
    private let key : String
    private let foo: (Int) -> Int

    init(operateClosureOn: (propertyWithKey: String, withInitialValue: Int),
        closure: (Int) -> Int) {
        key = operateClosureOn.propertyWithKey
        foo = closure

        super.init()

        let val = operateClosureOn.withInitialValue
        if let _ = (Mirror(reflecting: self).children.filter{ $0.label == key }).first {
            self.setValue(val, forKey: key)
        }
        else { keyExists = false }
    }

    lazy var valueFn: () -> Int = {
        [unowned self] () -> Int in

        if !self.keyExists {
            return 0
        }

        guard let a = self.valueForKey(self.key) as? Int else {
            print("Unexpected: property for key '\(self.key)' is not if type 'Int'.")
            return 0
        }

        return self.foo(a)
    }
}

Example usage:

/* Example usage */
var c2 = MyClass(operateClosureOn: ("quantity", 2)) {
    (val) -> Int in
    return 2 * val
}

c2.valueFn() // 4
c2.quantity = 4
c2.valueFn() // 8

var c3 = MyClass(operateClosureOn: ("anotherQuantity", 20)) {
    (val) -> Int in
    return val / 2
}

c3.valueFn() // 10
c3.anotherQuantity = 40
c3.valueFn() // 20


var c4 = MyClass(operateClosureOn: ("aTypo", 20)) {
    (val) -> Int in
    return val / 2
}

c4.valueFn() // 0, OK, at least no runtime exception with this non-safe non-swifty solution :0)

Upvotes: 1

Related Questions