1

I have some classes defined in Objective-C similar to this:

@interface Type: NSObject {
    
}
@end

@interface SubType1: Type {
    
}
@end

@interface SubType2: Type {
    
}
@end

@interface Parent <T: __kindof Type *> : NSObject
@property (nonatomic, strong) NSArray <T> *anArray;
@property (nonatomic, strong) T anObject;
@end

@interface SubParent1: Parent<SubType1 *> {
    
}
@end

@interface SubParent2: Parent<SubType2 *> {
    
}
@end

And I am trying to make a Swift function that can take any subclass of Parent. I tried the following:

func callFunc(parent: Parent<Type>) {
                
}
callFunc(parent: SubParent1())

And I get the error: Cannot convert value of type 'Parent<SubType1> to expected argument type 'Parent<Type>'

Also tried:

func callFunc<T>(parent: T) where T: Parent<Type>{

}
callFunc(parent: SubParent1())

And I get the error Type of expression is ambiguous without more context.

In general I want a method that can handle any type of sub-parent class (SubParent1, SubParent2) which has a subtype of the parent class type (SubType1, SubType2) since the method only needs to access properties defined on the Parent. Switching the Objective-C classes is not a possible option due to some other limitations so I am looking for a solution that keeps the classes defined in Objective-C. Not sure if possible but if it is possible can I downcast afterwards if the parameter is expected as the Parent?

Later Edit: Also how can I allow a function to return any sub-parent type in Swift for example:

enum AnEnum {
    case subParent1(_ : SubParent1)
    case subParent2(_ : SubParent2)
    
    func subParent<T: Type>() -> Parent<T>? {
        switch self {
        case .subParent1(let sub1):
            return sub1
        case .subParent2(let sub2):
            return sub2
        }
    }
}

The compiler complains about sub1 and sub2 that 'Type of expression is ambiguous without more context'.

Bogdan
  • 402
  • 2
  • 8
  • 18

2 Answers2

2

You're parameterizing on Parent rather than Type, which the thing you're actually changing. You mean:

func callFunc<T>(parent: Parent<T>) { ... }

While you could explicitly call out T: Type, this isn't really necessary, since Parent already enforces that.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Just an observation - in Objective-C you'd work at the highest, most abstract level and it will end up making the deepest call possible, even if that breaks the system. In Swift's safe environment you have to work at the lowest most specific level possible and Swift will call as shallow as possible to stay safe. – skaak Mar 17 '21 at 09:50
  • Thank you for the solution. Can you also help me with the "Later Edit" part? – Bogdan Mar 17 '21 at 09:54
  • 1
    That edit should be a new question, but the answer is that it's impossible in the general case. Using only information known at compile-time, what is the precise return type of subParent()? In this specific case, it's even more impossible, since I could pass *any* Type. I could conform `CBPeripheral` to `Type` elsewhere in the program, and you'd have to return a `Parent`. How would you do that? What you may want here is a protocol or a type-eraser, but it depends on the actual use case. You should open a question that explains what the calling code looks like. – Rob Napier Mar 17 '21 at 13:33
  • @skaak I'm not quite certain what you mean here. It's common in both ObjC and Swift to pass around variables of either very high or very low specificity. Swift tends to have far more abstract code than ObjC. Consider code written against very abstract protocols like Collection vs what's done in ObjC. – Rob Napier Mar 17 '21 at 13:36
  • @RobNapier I am actually thinking of a specific example I constructed for myself based on https://stackoverflow.com/questions/66606803/objective-c-protocol-conformance-using-subclass-in-method-signature and which looks a lot like this answer. It is difficult to continue this discussion without me posting that example but my (specific, not general) conclusion was that Swift forces you to work as specific as possible (to stay safe?) and even then it errs on the safe side. Oh well, maybe I'll post that example as a question or maybe I can show it to you in a chat if you really want to pursue this – skaak Mar 17 '21 at 16:10
  • @RobNapier have a look at this https://stackoverflow.com/questions/66720314/why-swift-call-too-shallow-here not my full example but a juicy portion. I think also in general Swift tries to be specific and Objective-C tries to be general e.g. in Objective-C working with ```NSArray * a``` is pretty common whereas in Swift you have to give the type. – skaak Mar 20 '21 at 10:10
  • ObjC didn't have generics until very recently, and it still mostly exists to assist in bridging to Swift (the compiler doesn't help enforce generics nearly as much as I'd like it to). The only thing you *could* work with was an NSArray. – Rob Napier Mar 20 '21 at 13:56
  • Glancing over the question, I think you have a lot of good answers already. But I think you've confused "specific" and "general" types with "static" and "dynamic" dispatch. Swift definitely does strive to make dispatch static. It's dramatically faster (especially when optimized). Objective-C is incredibly dynamic, and this has long been a barrier to certain kinds of optimizations. ObjC has never been a language for high-performance code. Swift is trying to compete with C++ in this regard (which is what you'd usually use for performance-critical code in a Cocoa project). – Rob Napier Mar 20 '21 at 14:01
1

The problem here is that you expect covariance, but Swift generics don't generally support covariance.

There are many explanations of covariance on the web, so I won't explain it here. But I will show exactly how covariance would be unsound with your example types. Consider this code:

let sp1 = SubParent1()
let p = sp1 as Parent<Type>  // (1) Swift forbids this cast...
let t: Type = SubType2()
p.anObject = t               // (2) ...because this assigment would be unsound.

Suppose Swift allowed the forbidden cast (1). Then the assignment (2) would assign a SubType2 to a property that can only hold a SubType1.

You may be able to work around this by making your callFunc function generic over both the Parent subtype and the Type subtype:

func callFunc<T, P>(parent: P) where T: Type, P: Parent<T> { }

EDIT: Rob Napier's answer correctly shows that you only need to be generic over T, not over P.

rob mayoff
  • 375,296
  • 67
  • 796
  • 848