Home > Back-end >  How to check if some properties in an object are nil using Mirroring?
How to check if some properties in an object are nil using Mirroring?

Time:01-20

Lets say I have a class that has many properties, and I want to check if most of them are nil...

So, I would like to exclude only two properties from that check (and say, to check against 20 properties).

I tried something like this:

extension MyClass {
    func isEmpty() -> Bool {
        
        let excluded = ["propertyName1", "propertyName2"]
        
        let children = Mirror(reflecting: self).children.filter { $0.label != nil }
        
        let filtered = children.filter {!excluded.map{$0}.contains($0.label)}
        
        let result = filtered.allSatisfy{ $0.value == nil }
        
        return result
        
    }
}

The first thing that bothers me about this code is that, I would have to change excluded array values if I change a property name.

But that is less important, and the problem is, this line:

let result = filtered.allSatisfy{ $0.value == nil }

it doesn't really check if a property is nil... Compiler warns about:

Comparing non-optional value of type 'Any' to 'nil' always returns false

So, is there some better / proper way to solve this?

CodePudding user response:

The Mirror API is pretty rough, and the general reflection APIs for Swift haven't been designed yet. Even if they existed though, I don't think you should be using them for this case.

The concept of an "empty instance" with all-nil fields doesn't actually make sense. Imagine a Person(firstName: nil, lastName: nil, age: nil). you wouldn’t have an “empty person”, you have meaningless nonsense. If you need to model nil, use nil: let possiblePerson: Person? = nil

You should fix your data model. But if you need a workaround for now, I have 2 ideas for you:

Just do it the boring way:

extension MyClass {
    func isEmpty() -> Bool {
        a == nil && b == nil && c == nil
    }
}

Or perhaps:

extension MyClass {
    func isEmpty() -> Bool {
        ([a, b, c] as [Any?]).allSatisfy { $0 == nil }
    }
}

Of course, both of these have the downside of needing to be updated whenever a new property is added

Intermediate refactor

Suppose you had:

class MyClass {
   let propertyName1: Int? // Suppose this doesn't effect emptiness
   let propertyName2: Int? // Suppose this doesn't effect emptiness
   let a: Int?
   let b: Int?
   let c: Int?
}

You can extract out the parts that can be potentially empty:

class MyClass {
   let propertyName1: Int? // Suppose this doesn't effect emptiness
   let propertyName2: Int? // Suppose this doesn't effect emptiness
   let innerProperties: InnerProperties?

   struct InnerProperties { // TODO: rename to something relevant to your domain
       let a: Int
       let b: Int
       let c: Int
   }

   var isEmpty: Bool { innerProperties == nil }
}

If the properties a/b/c are part of your public API, and you can't change them easily, then you can limit the blast radius of this change by just adding some forwarding computed properties:

extension MyClass {
    public var a: Int? { innerProperties?.a }
    public var b: Int? { innerProperties?.b }
    public var c: Int? { innerProperties?.c }
}
  • Related