7

This is a question on how to gracefully circumvent the nullability of init in NSObject class.

So here is a classic objective-c implementation:

+ (instancetype)sharedInstance
{
    static dispatch_once_t onceToken;
    static id sharedInstance;
    dispatch_once(&onceToken, ^
    {
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

But now I want to declare it as nonnull, if possible:

+ (nonnull instancetype)sharedInstance;

Unfortunately, init returns a nullable instancetype value. Should I add an NSAssert or something after calling init?

I noticed that some people even document nonnull values as being in reality nullable. Does that make sense?

Should I go bold and simply add NS_ASSUME_NONNULL_BEGIN everywhere, without truly ensuring values are nonnull?

Cœur
  • 37,241
  • 25
  • 195
  • 267
  • 3
    `init` is defined as nullable because it's legal for an object that can't initialize itself to simply return `nil` instead of a valid object. `NSObject` won't do that (in practice), and if your class inherits directly from `NSObject`, you can guarantee that `sharedInstance` will not be `nil` by simply never returning `nil` from your `init` override. – Avi Dec 10 '15 at 06:32
  • 2
    Additional comment: `nullable` and `nonnull` are attributes that define a contract, not reality. They let the compiler know what to expect, so the compiler can insist on correct handling according to the contract. This is obviously important to Swift, whose stated goal is to minimize programmer error. It's not your job to worry about situations such as out-of-memory, which can cause a reference to be `nil` when the contract states otherwise. – Avi Dec 10 '15 at 06:35
  • 2
    Why do you care about nullability? Of course all variables in Objective-C `nullable` even with prefix `nonnull`. It was inventend for interoperability with Swift. There is only one useful thing in Objective-C: mark some arguments in methods, that they should have value and `nil` may lead to crash or unpredicted results. – Cy-4AH Dec 10 '15 at 08:20
  • 1
    @Cy-4AH CLANG_WARN_NULLABLE_TO_NONNULL_CONVERSION – Cœur Dec 11 '15 at 04:28
  • What @Cy-4AH likely wanted to say is, that there is no real advantage of decorating methods with `nonnull` or `nullable` in Objective-C. This is, because Objective-C dispatches all messages dynamically what includes a null check. It is quite usual in Objective-C to deal with null values and take advantages from the messages to nil pattern. It makes sense for interoperating with Swift only. So without interoperation with Swift, simply do not use that. We did not do this in the past 30 years. – Amin Negm-Awad Jan 06 '17 at 11:10
  • @AminNegm-Awad I'm potentially interacting with Swift as it's code for a framework. – Cœur Jan 06 '17 at 11:14
  • I told you what @Cy-4AH wanted to say. Likely he wanted to say that, because you did not mention in your Q, that your code interacts with Swift. – Amin Negm-Awad Jan 06 '17 at 12:21
  • @AminNegm-Awad, @Cy-4AH actually you are both wrong. From my experience *nullability annotations* are very helpful even in an *Objcetive-C* project, once you enable `CLANG_WARN_NULLABLE_TO_NONNULL_CONVERSION` warning in project's settings and wrap even `.m` files with audited regions. – Silmaril May 10 '17 at 14:37
  • @Silmaril Great, that you have this experience. Proof by assertion, that helps. However, I have an experience without nullability support for years. – Amin Negm-Awad May 11 '17 at 04:43
  • @AminNegm-Awad having nullability annotations is different. They help compiler and static analyzer to find hidden bugs. Also with nullability annotations you can remove many unnecessary checks for null. And they are showing expected nullability behavior to the programmer without need to read the documentation. – Silmaril May 11 '17 at 06:20
  • @Silmaril Proof by assertion, that helps. However, I have an experience without nullability support for years. – Amin Negm-Awad May 11 '17 at 07:58
  • Oh, related article: http://indiestack.com/2016/06/nullable-edge-cases/ – Cœur Sep 15 '17 at 17:30

1 Answers1

9

There seem to be two questions here:

  1. How do I ensure that the value produced within my singleton sharedInstance method and returned by that method is actually not nil at run time?

  2. How do I satisfy the system of nullabilty annotations, compiler warnings, and Swift bridging that I'm returning a nonnull pointer?

Ensuring / enforcing nonnull

At some point, every API contract breaks down to a human contract. The compiler can help ensure that, for example, you can't take the result of a nullable call and return it from a method whose return type is nonnull... but somewhere there's usually an original call whose return type is nonnull simply because the programmer who wrote it said, "I promise to never return null, cross my heart, etc."

If you're familiar with Swift, this is similar to the situation with Implicitly Unwrapped Optionals — you use these when you "know" that a value cannot be nil, but can't prove that knowledge to the compiler because that knowledge is external to the source code (something from a storyboard or bundle resource, for example).

That's the situation here — you "know" that init will never return nil, either because you wrote / have source for the initializer in question or because it's just NSObject's init which is documented to return self without doing anything. The only situation where a call to that initializer will fail is because the preceding alloc call failed (and hence you're calling a method on nil, which always returns nil). If alloc is returning nil, you're already in dire straits and your process is not long for this world — this isn't a failure case to be designing API around.

(Nullability annotations are in general for describing intended use of an API, not the more extreme edge and corner cases. If an API call fails only because of a universal error it's not meaningful to annotate it as nullable; likewise, if an API fails only on input that can be ruled out via nonnull parameter annotations, the return value can be assumed nonnull.)

So, long story short: yes, just put NS_ASSUME_NONNULL around your header, and ship your sharedInstance implementation as is.

Returning a nonnull nullable

It's not the case here, but suppose you have a value that's annotated as nullable, but you know (or "know") that it can never be nil and want to return it from your nonnull-annotated method. And you're in a situation where you get a compiler warning for trying.

There's a syntax for that — just cast the value to the expected return type, annotations and all:

return (NSWhatever *_Nonnull)whatever;

In your case this shouldn't be needed — because you're dealing with the id and instancetype special types the compiler is more forgiving about nullability conversion and probably won't warn to begin with.

rickster
  • 124,678
  • 26
  • 272
  • 326
  • Also nice benefit of using nullability annotations is that we can now get rid of `if (self)` check in initializers, when super class is `NSObject` or super initializer returns nonnullable. – Silmaril May 10 '17 at 14:29