juliancadi
juliancadi

Reputation: 1024

Optional field type doesn't conform protocol in Swift 3

I have a class with 1 optional field and 1 non-optional field, both of them with Type AnotherClass and also conform CustomProtocol:

protocol CustomProtocol {}

class CustomClass: CustomProtocol {

    var nonoptionalField: AnotherClass = AnotherClass()
    var optionalField: AnotherClass?

}

class AnotherClass: CustomProtocol {

}

The field nonoptionalField is type AnotherClass and conforms CustomProtocol.

On the other hand, optionalField is actually Optional< AnotherClass> and therefore DOES NOT conform CustomProtocol:

for field in Mirror(reflecting: CustomClass()).children {
    let fieldMirror = Mirror(reflecting: field.value)
    if fieldMirror.subjectType is CustomProtocol.Type {
        print("\(field.label!) is \(fieldMirror.subjectType) and conforms CustomProtocol")
    } else {
        print("\(field.label!) is \(fieldMirror.subjectType) and DOES NOT conform CustomProtocol")
    }
}
// nonoptionalField is AnotherClass and conforms CustomProtocol
// optionalField is Optional<AnotherClass> and DOES NOT conform CustomProtocol

How can I unwrap the Type (not the value) of optionalField property, so that I can associate it with its protocol CustomProtocol?

In other words, how can I get the wrapped Type AnotherClass from Optional< AnotherClass> Type?

LIMITATION:

I really have to use Swift reflection through Mirror and unfortunately the property .subjectType doesn't allow to unwrap the optional wrapped Type of Optional< AnotherClass> so far.

Upvotes: 10

Views: 1055

Answers (2)

Hamish
Hamish

Reputation: 80781

I do not believe there's a simple way to do this, given that we currently cannot talk in terms of generic types without their placeholders – therefore we cannot simply cast to Optional.Type.

Nor can we cast to Optional<Any>.Type, because the compiler doesn't provide the same kinds of automatic conversions for metatype values that it provides for instances (e.g An Optional<Int> is convertible to an Optional<Any>, but an Optional<Int>.Type is not convertible to a Optional<Any>.Type).

However one solution, albeit a somewhat hacky one, would be to define a 'dummy protocol' to represent an 'any Optional instance', regardless of the Wrapped type. We can then have this protocol define a wrappedType requirement in order to get the Wrapped metatype value for the given Optional type.

For example:

protocol OptionalProtocol {
  // the metatype value for the wrapped type.
  static var wrappedType: Any.Type { get }
}

extension Optional : OptionalProtocol {
  static var wrappedType: Any.Type { return Wrapped.self }
}

Now if fieldMirror.subjectType is an Optional<Wrapped>.Type, we can cast it to OptionalProtocol.Type, and from there get the wrappedType metatype value. This then lets us check for CustomProtocol conformance.

for field in Mirror(reflecting: CustomClass()).children {
  let fieldMirror = Mirror(reflecting: field.value)

  // if fieldMirror.subjectType returns an optional metatype value
  // (i.e an Optional<Wrapped>.Type), we can cast to OptionalProtocol.Type,
  // and then get the Wrapped type, otherwise default to fieldMirror.subjectType
  let wrappedType = (fieldMirror.subjectType as? OptionalProtocol.Type)?.wrappedType
    ?? fieldMirror.subjectType

  // check for CustomProtocol conformance.
  if wrappedType is CustomProtocol.Type {
    print("\(field.label!) is \(fieldMirror.subjectType) and conforms CustomProtocol")
  } else {
    print("\(field.label!) is \(fieldMirror.subjectType) and DOES NOT conform CustomProtocol")
  }
}

// nonoptionalField is AnotherClass and conforms CustomProtocol
// optionalField is Optional<AnotherClass> and conforms CustomProtocol

This only deals with a single level of optional nesting, but could easily be adapted to apply to an arbitrary optional nesting level through simply repeatedly attempting to cast the resultant metatype value to OptionalProtocol.Type and getting the wrappedType, and then checking for CustomProtocol conformance.

class CustomClass : CustomProtocol {
    var nonoptionalField: AnotherClass = AnotherClass()
    var optionalField: AnotherClass??
    var str: String = ""
}

/// If `type` is an `Optional<T>` metatype, returns the metatype for `T`
/// (repeating the unwrapping if `T` is an `Optional`), along with the number of
/// times an unwrap was performed. Otherwise just `type` will be returned.
func seeThroughOptionalType(
  _ type: Any.Type
) -> (wrappedType: Any.Type, layerCount: Int) {

  var type = type
  var layerCount = 0

  while let optionalType = type as? OptionalProtocol.Type {
    type = optionalType.wrappedType
    layerCount += 1
  }
  return (type, layerCount)
}

for field in Mirror(reflecting: CustomClass()).children {

  let fieldMirror = Mirror(reflecting: field.value)
  let (wrappedType, _) = seeThroughOptionalType(fieldMirror.subjectType)

  if wrappedType is CustomProtocol.Type {
    print("\(field.label!) is \(fieldMirror.subjectType) and conforms CustomProtocol")
  } else {
    print("\(field.label!) is \(fieldMirror.subjectType) and DOES NOT conform CustomProtocol")
  }
}
// nonoptionalField is AnotherClass and conforms CustomProtocol
// optionalField is Optional<Optional<AnotherClass>> and conforms CustomProtocol
// str is String and DOES NOT conform CustomProtocol

Upvotes: 5

dfrib
dfrib

Reputation: 73176

This is an interesting question, but after fiddling around with it for a while, I previously believed (and was corrected wrong) that this could not be solved using native Swift, which, however, has been shown possibly by @Hamish:s answer.


The goal

We want access, conditionally at runtime, the Wrapped type (Optional<Wrapped>) of an instance wrapped in Any, without actually knowing Wrapped, only knowing that Wrapped possibly conforms to some protocol; in your example CustomProtocol.

The (not insurmountable) obstacles

There are a few obstacles hindering us in reaching a solution to this introspection problem, namely to test, at runtime, whether an instance of Optional<Wrapped> wrapped, in itself, in an instance of Any, holds a type Wrapped that conforms to a given protocol (where Wrapped is not known). Specifically, hindering us from a general solution that is viable even for the case where the value being introspected upon happens to be Optional<Wrapped>.none.

The first problem, as already noted in your question, is that optionals wrapped in Any instances are not covariant (optionals themselves are covariant, but that is in special case present also for e.g. some collections, whereas for custom wrapping types the default behaviour of non-covariance holds). Hence, we cannot successfully test conformance of the type wrapped in Any at its optional level, vs Optional<MyProtocol>, even if Wrapped itself conforms to MyProtocol.

protocol Dummy {}
extension Int : Dummy {}

let foo: Int? = nil
let bar = foo as Any

if type(of: bar) is Optional<Int>.Type {
    // OK, we enter here, but here we've assumed that we actually
    // know the type of 'Wrapped' (Int) at compile time!
}

if type(of: bar) is Optional<Dummy>.Type {
    // fails to enter as optionals wrapped in 'Any' are not covariant ...
}

The second problem is somewhat overlapping: we may not cast an Any instance containing an optional directly to the optional type, or (by noncovariance) to an optional type of a protocol to which the wrapped type conforms. E.g.:

let foo: Int? = 1
let bar = foo as Any
let baz = bar as? Optional<Int>
// error: cannot downcast from 'Any' to a more optional type 'Optional<Int>'
let dummy = bar as? Optional<Dummy>
// error: cannot downcast from 'Any' to a more optional type 'Optional<Dummy>'

Now, we can circumvent this using a value-binding pattern:

protocol Dummy {}
extension Int : Dummy {}

let foo: Int? = 1
let bar = foo as Any
if case Optional<Any>.some(let baz) = bar {
    // ok, this is great, 'baz' is now a concrete 'Wrapped' instance,
    // in turn wrapped in 'Any': but fo this case, we can  test if 
    // 'baz' conforms to dummy!
    print(baz)          // 1
    print(baz is Dummy) // true <--- this would be the OP's end goal
}

// ... but what if 'bar' is wrapping Optional<Int>.none ?

But this is only a workaround that helps in case foo above is non-nil, whereas if foo is nil, we have no binded instance upon which we may perform type & protocol conformance analysis.

protocol Dummy {}
extension Int : Dummy {}

let foo: Int? = nil
let bar = foo as Any
if case Optional<Any>.none = bar {
    // ok, so we know that bar indeed wraps an optional,
    // and that this optional happens to be 'nil', but
    // we have no way of telling the compiler to work further
    // with the actual 'Wrapped' type, as we have no concrete
    // 'Wrapped' value to bind to an instance.
}

I'm been playing around with a few different approaches, but in the end I come back to the issue that for an optional nil-valued instance wrapped in Any, accessing Wrapped (without knowing it: e.g. as a metatype) seems non-possible. As shown in @Hamish:s answer, however, this is indeed not insurmountable, and can be solved by adding an additional protocol layer above Optional.

I'll leave my not-quite-the-finish-line attempts above, however, as the techniques and discussion may be instructive for readers of this thread, even if they didn't manage to solve the problem.

Upvotes: 1

Related Questions