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 }
}