Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Optional field type doesn't conform protocol in Swift 3

Tags:

swift

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.

like image 813
juliancadi Avatar asked Mar 12 '17 19:03

juliancadi


People also ask

How do you write Optional protocol in Swift?

Swift protocols on their side do not allow optional methods. But if you are making an app for macOS, iOS, tvOS or watchOS you can add the @objc keyword at the beginning of the implementation of your protocol and add @objc follow by optional keyword before each methods you want to be optional.

Can struct conform to protocol Swift?

In Swift, protocols contain multiple abstract members. Classes, structs and enums can conform to multiple protocols and the conformance relationship can be established retroactively.

How do I add a protocol in Swift?

To create a protocol, use the protocol keyword followed by the name you want and defined by the curly braces. Protocols can be of 2 types: read-only/read-write. Read-only means you can only get the variable, but you cannot set it. Read-write means you can both set and get properties.

Is it possible to prevent the adoption of a protocol by a struct?

The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements. But there would be a time when you want to restrict protocols to be adopted by a specific class. In Swift 5, you can do just that.


2 Answers

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
like image 155
Hamish Avatar answered Sep 28 '22 08:09

Hamish


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.

like image 44
dfrib Avatar answered Sep 28 '22 09:09

dfrib