Why are interface methods returning non-nullable reference types being treated as nullable vars?

Viewed 49

I'm new to C#10 and the default enabled nullable NRT functionality. In the following simple example I'm curious why VS2022 tells me (via tooltip) that o is object? when the interface definition explicitly states it is object.

Is this standard behavior and what's the reason for it? Is it because the compiler has no way of telling that the interface implementation is null-safe so has to assume the worst?

Further, s is also nullable (string?) but no warnings or errors are given when I use these nullable types. The compiler confidently informs me that "o is not null here" when I call o.ToString.

I can't tell at this stage if NRTs marks a paradigm shift to how we write modern C# or is "just another language tweak?" What's going on here? I want to 'embrace' it but am struggling with the full understanding of another change to the language.

public interface ITest
{
    Object GetObject();
}

public class Tester
{
    public void Test(ITest i)
    {
        var o = i.GetObject(); //object?
        var s = o.ToString(); //string?
        Console.WriteLine(s);
    }
}
2 Answers

Although an interface implementation written in C# might not be able to return null, there is no requirement that interface implementations be written only in C#. If an interface is implemented by code written in another language, nothing would prevent that code from returning null.

A similar issue arises, incidentally, with the TryGetValue method of IDictionary<TKey,TValue>. If one has a structure with member m whose constructor invokes:

SomeIDictionary.TryGetValue(someKey, ref m);

the C# compiler will blindly assume that the interface method will not return without storing a value into m (e.g. because the key is not found). This may have the surprising effect of causing a statement like:

myStruct = new myStructType(someParameters);

to leave myStruct.m holding whatever value it held beforehand.

FWIW Rider says the type is object not object?

Screenshot of source code in Rider IDE

Also, Rider will refactor the var into object:

Context menu in Rider

Screenshot of Rider refactoring the var into object

Note: the code will compile even if I manually say object?:

Screenshot after manually specifying nullable object

Successful build

Keep in mind nullable reference types are magic sugar sprinkled on top of the type system. Under the hood there's no difference between object and object?; both refer to System.Object. That magical sugar layer is able to deduce that my object? is not null (because its value came from an interface that promises never to return null).


Being pedantic: there's no difference in the type system between a nullable reference type and the same non-nullable reference type. But there is a difference between nullable and non-nullable value types. To be more precise: double is the System.Double struct, but double? is the System.Nullable<System.Double> struct. MS crammed more magic into the language and compiler to support this bifurcation between reference and value types. This would be a great opportunity to start writing glowing things about the type systems in TypeScript or Rust or any of a dozen other languages.

Related