0

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?

HangarRash
  • 7,314
  • 5
  • 5
  • 32
Whirlwind
  • 14,286
  • 11
  • 68
  • 157
  • 1
    What is the whole purpose of this code? This kind of type check at runtime is pretty *unswifty*. – vadian Jan 19 '23 at 15:23
  • Well the only purpose is just to check certain properties (and it can be a lot of them) if they are all nil @vadian What would be a swifty way for this? I mean I can do something like `[property1, property2...].allSatisfy {$0 == nil}` ... But I was trying to do something more automatic :) aka, to just exclude few that I don't need, and check others if they are `nil` – Whirlwind Jan 19 '23 at 15:28
  • I think you can use keypaths to have a type-safe exclusion list, which will be renamed when you refactor your properties (and give a compilation error if they're edit manually and go out-of-sync). – Alexander Jan 19 '23 at 15:32
  • 1
    @Whirlwind I would argue that there's a fundamental issue here: "empty instances" with all-nil fields _simply don't make any sense_, and you should just avoid them to begin with. Have a look at the comments I posted on [this question](https://stackoverflow.com/q/75138664/3141234). – Alexander Jan 19 '23 at 15:34
  • @Alexander I agree, but it is not up to me really. It should be avoided, but that kind of a refactor is not possible at the moment. I can't change logic too much. – Whirlwind Jan 19 '23 at 15:39
  • Mirror is very limited, and rarely is the tool you want for more than just debug printing. Make your items Encodable, and inspect the resulting JSON. You can decode the output back into a JSON object if you like, and then apply easy operations like `.count` on it. For an example of how to make a JSON object, see https://github.com/rnapier/RNJSON/blob/main/Sources/RNJSON/JSON.swift or the very short and simple one here: https://stackoverflow.com/a/65902852/97337 or https://stackoverflow.com/a/68367281/97337 – Rob Napier Jan 19 '23 at 15:44

1 Answers1

2

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 }
}
Alexander
  • 59,041
  • 12
  • 98
  • 151
  • Ok thanks for insights about mirroring...Ig I will skip it and go with a boring `allSatisfy` way for now. It seems easiest and better than big `if` statement (and it its easiest when it comes to refactor). – Whirlwind Jan 19 '23 at 17:27
  • I'd strongly recommend you to consider the intermediate refactoring approach. It's a pretty localized changed should be pretty easy to pull off, and puts you on the road to improvement – Alexander Jan 19 '23 at 18:01