Objc generics, nullability bridged to Swift

I have a legacy Objc class using light-weight generics:

@interface GenericObjcClass <T> : NSObject
- (void)addObserver:(id)obsv callback:(void (^)(T arg))callback;
- (void)postData:(T)data;
@end

@property (nonatomic) GenericObjcClass<NSString *> *testProp;

which I bridge into a modern Swift class while maintaining the generics:

extension Observable {
    public static func observable<T>(with: GenericObjcClass<T>) -> Observable<T> where T: AnyObject {
        ...
    }
}

This is great! However, the compiler won't allow me to add nullability to the Objc generic param. Specifically, this is an error:

@property (nonatomic) GenericObjcClass<NSString * _Nullable> *testProp;
// ERROR: `Type argument 'NSString *' cannot explicitly specify nullability`

Since both Obj-C generics and nullability are just compile-time constructs, it seems there should be no issue with these two interacting together. My current workaround is to expose a wrapper class around an optional that can be used when needed:

@interface Optional <T>: NSObject
@property (nonatomic, nullable) T value;
@end

But this means the caller of postData: has to wrap its parameter, and that I have to create a second Swift Observable extension method just for this wrapper class:

extension Observable {
    public static func observable<T>(with: GenericObjcClass<Optional<T>>) -> Observable<T?> where T: AnyObject {
        ...
    }
}

Asking here because I believe Obj-C generics were added to aid in Swift development. Not sure if there's some solution I'm skipping over, but I believe this might be an unnecessary limitation of the Obj-c generics that should be lifted.

You can't put nullability on a generic argument in Objective-C because that would make a property with explicit nullability nonsensical:

@interface Foo<Wrapped>
@property (nullable) Wrapped wrapped;
@end

void use(Foo<NSString * _Nonnull> *foo); // ???
void use2(Foo<NSString * _Nullable> *foo); // ???

This is different from Swift, where the Optionals stack up, and each level of optionality can mean something different:

class Bar<Wrapped> {
  var wrapped: Wrapped?
}

func use(_ bar: Bar<String>) // okay, 'wrapped' has type 'String?'
func use2(_ bar: Bar<String?>) // okay, 'wrapped' has type 'String??'

It's better to think of an Objective-C generic parameter not as "any type" but as "any class reference, possibly with a set of protocols".

Can Obj-C flatten multiple optionals into a single optional?

Seems intuitive that

void use(Foo<NSString * _Nonnull> *foo);

means wrapped is nullable.

And that to an Obj-C caller,

void use2(Foo<NSString * _Nullable> *foo);

a single optional is as good as double optional, since messaging nil is a no-op.

That's a possible language design, yes. I don't think we consider it desirable for this feature, though.

(I'm not even sure how to respond here in general, since this is ObjC language design, not Swift, and while the Apple folks have some influence over that and transitively the forums have some influence over us, Apple Objective-C is not part of the Swift Evolution Process.)

1 Like