Trouble reflecting on lazy properties of an object

reflection

(Gal Cohen) #1

Hi all,

I'm trying to use reflection to collect all the properties on an object of a certain type. I've mostly got it working, but I can't quite figure out how to take lazy properties into account. Any suggestions?

Here is some code to show what I mean.
Mirror.children.count returns 3 indicating it's aware of the uninitialized lazy vars. So I created a function to count the number of Int properties. Notice how it returns 1 instead of 2 until the lazy var is accessed. I guess that makes sense. Question is, how can I modify my function to count those lazy vars as well?

class MyObject {
    let str: String = "this is a string"
    let number: Int = 5
    lazy var lazyNumber: Int = 42
}

func countIntChildren(_ children: Mirror.Children) -> Int {
    var total = 0
    for child in children {
        switch child.value {
        case let optionalObj as Optional<Any>:
            switch optionalObj {
            case .some(let innerObj):
                if innerObj is Int {
                    total += 1
                }
            case .none:
               ()
            }
        }
    }
    return total
}


let obj = MyObject()
let objMirror = Mirror(reflecting: obj)

print(objMirror.children.count)              // prints 3
print(countIntChildren(objMirror.children))  // prints 1, skipping my lazy var int

print(obj.lazyNumber)                        // accessing the lazy var
print(countIntChildren(objMirror.children))  // prints 2

#2

Reflected lazyNumber actually has type Int?, with value nil until it is accessed at the original object. You can then play around with how is works.


(Gal Cohen) #3

I cannot check that child.value is Optional<Int> because I get:
Cannot downcast from 'Any' to a more optional type 'Optional<Int>' error.

It seems if I continue with the path I have now, (checking if it's Optional, and then switching on the optional enum cases won't do either, because the .none case will get it and I'll know I have a lazy var, but not what its type is.


#4

If you decide to count both Int and Int?, you can do:

func countIntChildren(_ children: Mirror.Children) -> Int {
    return children.lazy.filter {
        let value = $0.value 
        return value is Int || type(of: value) == Int?.self
    } .count
}

Due to the way is interact, you can’t just check if value is Int? since it will erraneously include String?.none, that’s why I have it as a separate case.
Caveat is that it won’t include Int?? and its family, but I can’t think of a way to nicely unwrap arbitrary number of wrapping.


(Gal Cohen) #5

Thanks a lot!

Now to complicate things a bit, instead of just counting lazy ‘Int’ properties, I want to create an array holding all the lazy properties who conform to a protocol. Having trouble using as? To cast Child.value to my protocol. Any suggestions? :innocent:


#6

It seems checking for Protocol? is much more elusive compared to concrete types:

protocol Test { }
extension Int: Test { }

let a: Int? = 3
let anyA: Any = a

a is Test // true
a as? Test // 3
type(of: a) == Test?.self // false

anyA is Test // false
anyA as? Test // nil

I’m unsure if Swift provides enough metatype gymnastics to achieve what you want. And if the Protocol has associatedtypes, it’s much less likely—you can’t even express its metatype properly.