Lightweight Generics and Nullability
|
This section documents modern Objective-C as implemented by Apple Clang in the current Xcode release — Objective-C 2.0 plus ARC, literals and subscripting, This content was generated with the assistance of AI and should be verified against developer.apple.com before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Lightweight generics and nullability are annotations: they add compile-time information without changing
what happens at run time. Clang introduced both to improve Objective-C’s own diagnostics and — decisively — so that Objective-C APIs import into Swift as properly typed, properly optional declarations rather than as
seas of AnyObject!.
Lightweight Generics
Parameterised Collections
NSArray<NSString *> *names = @[ @"Ada", @"Alan" ];
NSDictionary<NSString *, NSNumber *> *ages = @{ @"Ada": @36 };
NSSet<Item *> *items = [NSSet set];
NSMutableArray<NSURL *> *urls = [NSMutableArray array];
NSString *first = names[0]; // typed: no cast needed
// [names addObject:@42]; // warning: incompatible pointer types
The payoff is at the call site: elements come out correctly typed, and putting the wrong thing in produces a warning.
Nested parameterisation works as expected:
NSDictionary<NSString *, NSArray<Item *> *> *itemsBySection;
NSArray<NSDictionary<NSString *, id> *> *jsonObjects;
These are erased at run time. The type argument exists only for the compiler; NSArray<NSString *> and
NSArray<NSNumber *> are the same class, and nothing stops an untyped code path from inserting the wrong
element. Treat generics as strong documentation plus a good warning, not as a guarantee — validate data
arriving from JSON or from disk regardless.
Annotating a collection type is worth doing everywhere: in properties, in method signatures, and in local variables where it aids readability.
Generic Classes
Your own classes can be parameterised too:
@interface MyStack<ObjectType> : NSObject
- (void)pushObject:(ObjectType)object;
- (nullable ObjectType)popObject;
@property (nonatomic, readonly) NSArray<ObjectType> *allObjects;
@end
ObjectType is a placeholder usable anywhere a type is expected in the interface. The implementation simply
ignores it — there is no specialisation, so inside @implementation the parameter behaves as id:
@implementation MyStack {
NSMutableArray *_storage; // plain: no type parameter needed here
}
- (void)pushObject:(id)object { // ObjectType erases to id
[_storage addObject:object];
}
@end
Usage is what you would expect:
MyStack<NSString *> *stack = [[MyStack alloc] init];
[stack pushObject:@"first"];
NSString *top = [stack popObject]; // typed
// [stack pushObject:@42]; // warning
You may also constrain the parameter and supply a default:
@interface MyBox<ObjectType : id<NSCopying>> : NSObject // must be copyable
@end
@interface MyList<__covariant ObjectType> : NSObject
@end
covariant and contravariant
Variance says how a parameterised type behaves under subtyping:
| Qualifier | Meaning |
|---|---|
|
|
|
|
(neither) |
Invariant: only an exact match is accepted. |
@interface MyProducer<__covariant ObjectType> : NSObject
- (ObjectType)next;
@end
MyProducer<Circle *> *circles = …;
MyProducer<Shape *> *shapes = circles; // allowed: __covariant
@interface MyConsumer<__contravariant ObjectType> : NSObject
- (void)consume:(ObjectType)object;
@end
MyConsumer<Shape *> *anyShape = …;
MyConsumer<Circle *> *circleC = anyShape; // allowed: __contravariant
Rule of thumb: covariant if the parameter appears only in return positions, contravariant if only in
argument positions, and neither if it appears in both.
__kindof
__kindof Type * means "this type or any subclass", and exists to remove the downcast that otherwise clutters
factory- and container-style APIs:
// Without __kindof: the caller writes (MyButton *)[view viewWithTag:7]
- (__kindof UIView *)viewWithTag:(NSInteger)tag;
MyButton *button = [container viewWithTag:7]; // no cast
[button setTitle:@"OK"]; // MyButton's own methods are fine
It also combines with generics — NSArray<__kindof UIView *> * is an array whose elements are `UIView`s or
any subclass, assignable to a more specific variable without a cast. As with everything else on this page, it
is checked at compile time only.
Nullability
The Audited-Region Idiom
Annotating every pointer individually would be unbearable, so Clang provides a region macro. Inside it, every
unannotated object pointer is assumed nonnull, and you annotate only the exceptions:
NS_ASSUME_NONNULL_BEGIN
@interface Downloader : NSObject
@property (nonatomic, copy) NSURL *url; // implicitly nonnull
@property (nonatomic, copy, nullable) NSString *authToken; // explicitly nullable
- (nullable NSData *)cachedDataForURL:(NSURL *)url;
- (void)fetchURL:(NSURL *)url
completion:(void (^)(NSData * _Nullable data, NSError * _Nullable error))completion;
@end
NS_ASSUME_NONNULL_END
Wrap every header in NS_ASSUME_NONNULL_BEGIN/END. It is the house convention across Apple’s own SDKs,
and a header that is entirely unaudited imports into Swift as implicitly-unwrapped optionals everywhere,
which is the worst of both worlds.
The Four Qualifiers
| Property-attribute form | Type form | Meaning |
|---|---|---|
|
|
Never |
|
|
May be |
|
(n/a) |
The getter never returns |
|
|
Deliberately unaudited. Imports into Swift as an implicitly-unwrapped optional. |
The two spellings differ only in position. The nullable form goes in a property’s attribute list or before a
method’s return type; the _Nullable form goes after the *, and is what you must use in C-style contexts — inside a block signature, on a pointer-to-pointer, or in a function declaration:
@property (nonatomic, strong, nullable) NSString *name;
- (nullable NSString *)nameForKey:(NSString *)key;
// In these positions only _Nullable/_Nonnull work:
- (BOOL)loadAndReturnError:(NSError * _Nullable * _Nullable)error;
typedef void (^Handler)(NSData * _Nullable data, NSError * _Nullable error);
extern NSString * _Nullable MyLookup(NSString * _Nonnull key);
That NSError * spelling deserves attention: the *pointer may be NULL (the caller does not want an
error), and the NSError * it points to may be nil. Both levels need annotating. See
Errors and Exceptions.
Nullability is a compile-time warning, not a run-time guarantee. Passing nil to a nonnull parameter
produces a warning, but if it happens anyway — via id, from a dynamic call, or from Swift’s
force-unwrapping — nothing stops it. Keep validating input at trust boundaries.
@available and Availability Attributes
Nullability describes what a value may be; availability describes when an API exists.
@available: the Run-Time Check
if (@available(iOS 15.0, macOS 12.0, *)) {
[self useModernAPI]; // only on new-enough systems
} else {
[self useFallback];
}
The trailing means "on any other platform, assume it is available" and is required. Clang understands
@available well enough to *silence the "only available on iOS 15.0 or newer" warning inside the branch — which a hand-written version check such as
[[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:] cannot do. Always prefer @available.
Note the distinction from #if TARGET_OS_IPHONE: that is a compile-time question about which platform you
are building for. @available is a run-time question about which OS the binary is executing on.
Marking Your Own API
@interface MyService : NSObject
- (void)modernMethod API_AVAILABLE(ios(15.0), macos(12.0));
- (void)oldMethod
API_DEPRECATED("Use modernMethod instead", ios(9.0, 15.0), macos(10.11, 12.0));
- (void)goneMethod API_UNAVAILABLE(tvos, watchos);
- (instancetype)init NS_UNAVAILABLE; // cannot be called at all
@property (nonatomic, readonly) NSInteger legacyCount
__attribute__((deprecated("Use count instead")));
@end
API_DEPRECATED takes the version the API appeared and the version it was deprecated in, plus a message
naming the replacement — which is what makes the resulting compiler warning actionable.
What This Means on the Swift Side
These annotations exist largely for Swift’s benefit, and the mapping is direct:
| Objective-C | Imports into Swift as |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The practical consequence: an Objective-C header that is audited for nullability and parameterised with generics produces a Swift API indistinguishable from a native one, while an unaudited header produces something awkward and unsafe. If your code will be consumed from Swift at all, both annotations are effectively mandatory. See Swift Interoperability.
See Also
-
Swift Interoperability — the full bridging story,
NS_SWIFT_NAMEand the generated headers. -
Collections and Fast Enumeration — the collections these annotations parameterise.
-
Errors and Exceptions — annotating
NSError **out-parameters. -
Basic Syntax and Types —
NS_ENUM,NS_OPTIONSandinstancetype.