Try out Nullable Reference Types

Phillip Carter

Phillip

Try out Nullable Reference Types

With the release of .NET Core 3.0 Preview 7, C# 8.0 is considered "feature complete". That means that the biggest feature of them all, Nullable Reference Types, is also locked down behavior-wise for the .NET Core release. It will continue to improve after C# 8.0, but it is now considered stable with the rest of C# 8.0.

At this time, our aim is to collect as much feedback about the process of adopting nullability as possible, catch any issues, and collect feedback on further improvements to the feature that we can do after .NET Core 3.0. This is one of the largest features ever built for C#, and although we’ve done our best to get things right, we need your help!

It is at this junction that we especially call upon .NET library authors to try out the feature and begin annotating your libraries. We’d love to hear your feedback and help resolve any issues you come across.

Familiarize yourself with the feature

We recommend reading some of the Nullable Reference Types documentation before getting started with the feature. It covers essentials like:

  • A conceptual overview
  • How to specify a nullable reference type
  • How to control compiler analysis or override compiler analysis

If you’re unfamiliar with these concepts, please give the documentation a quick read before proceeding.

Turn on Nullable Reference Types

The first step in adopting nullability for your library is to turn it on. Here’s how:

Make sure you’re using C# 8.0

If your library explicitly targets netcoreapp3.0, you’ll get C# 8.0 by default. When we ship Preview 8, you’ll get C# 8.0 by default if you target netstandard2.1 too.

.NET Standard itself doesn’t have any nullable annotations yet. If you’re targeting .NET Standard, then you can use multi-targeting for .NET Standard and netcoreapp3.0, even if you don’t need .NET Core specific APIs. The benefit is that the compiler will use the nullable annotations from CoreFX to help you get your own annotations right.

If you cannot update your TFM for some reason, you can set the LangVersion explicitly:

Note that C# 8.0 is not meant for older targets, such as .NET Core 2.x or .NET Framework 4.x. So some additional language features may not work unless you are targeting .NET Core 3.0 or .NET Standard 2.1

From here, we recommend two general approaches to adopting nullability.

Opt in a project, opt out files

This approach is best for projects where you’ll be adding new files over time. The process is straightforward:

  1. Apply the following property to your project file:

  2. Disable nullability in every file for that project by adding this to the top of every existing file in the project:

  3. Pick a file, remove the #nullable disable directive, and fix the warnings. Repeat until all #nullable disable directives are gone.

This approach requires a bit more up front work, but it means that you can continue working in your library while you’re porting and ensure that any new files are automatically opted-in to nullability. This is the approach we generally recommend, and we are currently using it in some of our own codebases.

Note that you can also apply the Nullable property to a Directory.build.props file if that fits your workflow better.

Opt in files one at a time

This approach is the inverse of the previous one.

  1. Enable nullability in a file for a project by adding this to the top of the file:

  2. Continue adding this to files until all files are annotated and all nullability warnings are addressed.

  3. Apply the following property to your project file:

  4. Remove all #nullable enable directives in source.

This approach requires more work at the end, but it allows you to start fixing nullability warnings immediately.

Note that you can also apply the Nullable property to a Directory.build.props file if that fits your workflow better.

What’s new in Nullable Reference Types for Preview 7

The most critical additions to the feature are tools for working with generics and more advanced API usage scenarios. These were derived from our experience in beginning to annotate .NET Core.

The notnull generic constraint

It is quite common to intend that a generic type is specifically not allowed to be nullable. For example, given the following interface:

It may be desirable to only allow non-nullable reference and value types. So substituting with string or int should be fine, but substituting with string? or int? should not.

This can be accomplished with the notnull constraint:

This will then generate a warning if any implementing class does not also apply the same notnull constraints:

To fix it, we need to apply the same constraints:

And when creating an instance of that class, if you substitute it with a nullable reference type, a warning will also be generated:

It also works for value types:

This constraint is useful for generic code where you want to ensure that only non-nullable reference types can be used. One prominent example is Dictionary<TKey, TValue, where TKey is now constrained to be notnull, which disallows using null as a key:

However, not all nullability problems with generics can be solved in this way. This is where we’ve added some new attributes to allow you to influence nullable analysis in the compiler.

The issue with T?

So you have have wondered: why not "just" allow T? when specifying a generic type that could be substituted with a nullable reference or value type? The answer is, unfortunately, complicated.

A natural definition of T? would mean, "any nullable type". However, this would imply that T would mean "any non-nullable type", and that is not true! It is possible to substitute a T with a nullable value type today (such as bool?). This is because T is already an unconstrained generic type. This change in semantics would likely be unexpected and cause some grief for the vast amount of existing code that uses T as an unconstrained generic type.

Next, it’s important to note that a nullable reference type is not the same thing as a nullable value type. Nullable value types map to a concrete class type in .NET. So int? is actually Nullable<int>. But for string?, it’s actually the same string but with a compiler-generated attribute annotating it. This is done for backwards compatibility. In other words, string? is kind of a "fake type", whereas int? is not.

This distinction between nullable value types and nullable reference types comes up in a pattern such as this:

This would mean that the parameter is the nullable version of T, and T is constrained to be notnull. If T were a string, then the actual signature of M would be M<string>([NullableAttribute] T t), but if T were an int, then M would be M<int>(Nullable<int> t). These two signatures are fundamentally different, and this difference is not reconcilable.

Because of this issue between the concrete representations of nullable reference types and nullable value types, any use of T? must also require you to constrain the T to be either class or struct.

Finally, the existence of a T? that worked for both nullable reference types and nullable value types does not address every issue with generics. You may want to allow for nullable types in a single direction (i.e., as only an input or only an output) and that is not expressible with either notnull nor a T and T? split unless you artificially add separate generic types for inputs and outputs.

Nullable preconditions: AllowNull and DisallowNull

Consider the following example:

This might have been an API that we supported prior to C# 8.0. However, the meaning of string now means non-nullable string! We may wish to actually still allow null values, but always give back some string value with the get. Here’s where AllowNull can come in and let you get fancy:

Since we always make sure that we get no null value with the getter, I’d like the type to remain string. But we want to still accept null values for backwards compatibility. The AllowNull attribute lets you specify that the setter accepts null values. Callers are then affected as you’d expect:

Note: there is currently a bug where assignment of null conflicts with nullable analysis. This will be addressed in a future update of the compiler.

Consider another API:

In this case, MyHandle refers to some handle to a resource. Typical use for this API is that we have a non-null instance that we pass by reference, but when it is cleared, the reference is null. We can get fancy and represent this with DisallowNull:

This will affect any caller by emitting a warning if they pass null, but will warn if you attempt to "dot" into the handle after the method is called:

These two attributes allow us single-direction nullability or non-nullability for those cases where we need them.

More formally:

The AllowNull attribute allows callers to pass null even if the type doesn’t allow it. The DisallowNull attribute disallows callers to pass null even if the type allows it. They can be specified on anything that takes input:

  • Value parameters
  • in parameters
  • ref parameters
  • fields
  • properties
  • indexers

Important: These attributes only affect nullable analysis for the callers of methods that are annotated with them. The bodies of annotated methods and things like interface implementation do not respect these attributes. We may add support for that in the future.

Nullable postconditions: MaybeNull and NotNull

Consider the following example API:

Here we have another problem. We’d like Find to give back default if nothing is found, which is null for reference types. We’d like Resize to accept a possibly null input, but we want to ensure that after Resize is called, the array value passed by reference is always non-null. Again, applying the notnull constraint doesn’t solve this. Uh-oh!

Enter [MaybeNull] and [NotNull]. Now we can get fancy with the nullability of the outputs! We can modify the example as such:

And these can now affect call sites:

The first method specifies that the T that is returned could be a null value. This means that callers of this method must check for null when using its result.

The second method has a trickier signature: [NotNull] ref T[]? array. This means that array could be null as an input, but when Resize is called, array will not be null. This means that if you "dot" into array after calling Resize, you will not get a warning. But after Resize is called, array will no longer be null.

More formally:

The MaybeNull attribute allows for a return type to be null, even if its type doesn’t allow it. The NotNull attribute disallows null results even if the type allows it. They can be specified on anything that produces output:

  • Method returns
  • out parameters (after a method is called)
  • ref parameters (after a method is called)
  • fields
  • properties
  • indexers

Important: These attributes only affect nullable analysis for the callers of methods that are annotated with them. The bodies of annotated methods and things like interface implementation do not respect these attributes. We may add support for that in the future.

Conditional postconditions: MaybeNullWhen(bool) and NotNullWhen(bool)

Consider the following example:

Methods like this are everywhere in .NET, where the return value of true or false corresponds to the nullability (or possible nullability) of a parameter. The MyQueue case is also a bit special, since it’s generic. TryDequeue should give a null for result if the result is false, but only if T is a reference type. If T is a struct, then it won’t be null.

So, we want to do three things:

  1. Signal that if IsNullOrEmpty returns false, then value is non-null
  2. Signal that if TryParse returns true, then version is non-null
  3. Signal that if TryDequeue returns false, then result could be null, provided it’s a reference type

Unfortunately, the C# compiler does not associate the return value of a method with the nullability of one of its parameters! Uh-oh!

Enter NotNullWhen(bool) and MaybeNullWhen(bool). Now we can get even fancier with parameters:

And these can now affect call sites:

This enables callers to work with APIs using the same patterns that they’ve used before, without any spurious warnings from the compiler:

  • If IsNullOrEmpty is true, it’s safe to "dot" into value
  • If TryParse is true, then version was parsed and is safe to "dot" into
  • If TryDequeue is false, then result might be null and a check is needed (example: returning false when the type is a struct is non-null, but false for a reference type means it could be null)

More formally:

The NotNullWhen(bool) signifies that a parameter is not null even if the type allows it, conditional on the bool returned value of the method. The MaybeNullWhen(bool) signifies that a parameter could be null even if the type disallows it, conditional on the bool returned value of the method. They can be specified on any parameter type.

Nullness dependence between inputs and outputs: NotNullIfNotNull(string)

Consider the following example:

In this case, we’d like to return a possibly null string, and we should also be able to accept a null value as input. So the signature accomplishes what I’d like to express.

However, if path is not null, we’d like to ensure that we always give back a string. That is, we want the return value of GetFileName to be non-null, conditional on the nullness of path. There’s no way to express this as-is. Uh-oh!

Enter NotNullIfNotNull(string). This attribute can make your code the fanciest, so use it with care! Here’s how we’ll use it in my API:

And this can now affect call sites:

More formally:

The NotNullIfNotNull(string) attribute signifies that any output value is non-null conditional on the nullability of a given parameter whose name is specified. They can be specified on the following constructs:

  • Method returns
  • ref parameters

Flow attributes: DoesNotReturn and DoesNotReturnIf(bool)

You may work with multiple methods that affect control flow of your program. For example, an exception helper method that will throw an exception if called, or an assertion method that will throw an exception if an input is true or false.

You may wish to do something like assert that a value is non-null, and we think you’d also like it if the compiler could understand that.

Enter DoesNotReturn and DoesNotReturnIf(bool). Here’s an example of how you could use either:

When ThrowArgumentNullException is called in a method, it throws an exception. The DoesNotReturn it is annotated with will signal to the compiler that no nullable analysis needs to happen after that point, since that code would be unreachable.

When MyAssert is called and the condition passed to it is false, it throws an exception. The DoesNotReturnIf(false) that annotates the condition parameter lets the compiler know that program flow will not continue if that condition is false. This is helpful if you want to assert the nullability of a value. In the code path following MyAssert(value != null); the compiler can assume value is not null.

DoesNotReturn can be used on methods. DoesNotReturnIf(bool) can be used on input parameters.

Evolving your annotations

Once you annotate a public API, you’ll want to consider the fact that updating an API can have downstream effects:

  • Adding nullable annotations where there weren’t any may introduce warnings to user code
  • Removing nullable annotations can also introduce warnings (e.g., interface implementation)

Nullable annotations are an integral part of your public API. Adding or removing annotations introduce new warnings. We recommend starting with a preview release where you solicit feedback, with aims to not change any annotations after a full release. This isn’t always going to be possible, but we recommend it nonetheless.

Current status of Microsoft frameworks and libraries

Because Nullable Reference Types are so new, the large majority of Microsoft-authored C# frameworks and libraries have not yet been appropriately annotated.

That said, the "Core Lib" part of .NET Core, which represents about ~20% of the .NET Core shared framework, has been fully updated. It includes namespaces like System, System.IO, and System.Collections.Generic. We’re looking for feedback on our decisions so that we can make appropriate tweaks as soon as possible, and before their usage becomes widespread.

Although there is still ~80% CoreFX to still annotate, the most-used APIs are fully annotated.

Roadmap for Nullable Reference Types

Currently, we view the full Nullable Reference Types experience as being in preview. It’s stable, but the feature involves spreading nullable annotations throughout our own technologies and the greater .NET ecosystem. This will take some time to complete.

That said, we’re encouraging library authors to start annotating their libraries now. The feature will only get better as more libraries adopt nullability, helping .NET become a more null-safe place.

Over the coming year or so, we’re going to continue to improve the feature and spread its use throughout Microsoft frameworks and libraries.

For the language, especially compiler analysis, we’ll be making numerous enhancements so that we can minimize your need to do things like use the null-forgiveness (!) operator. Many of these enhancements are already tracked on the Roslyn repo.

For CoreFX, we’ll be annotating the remaining ~80% of APIs and making appropriate tweaks based on feedback.

For ASP.NET Core and Entity Framework, we’ll be annotating public APIs once some new additions to CoreFX and the compiler are added.

We haven’t yet planned how to annotate WinForms and WPF APIs, but we’d love to hear your feedback on what kinds of things matter!

Finally, we’re going to continue enhancing C# tooling in Visual Studio. We have multiple ideas for features to help using the feature, but we’d love your input as well!

Next steps

If you’re still reading and haven’t tried out the feature in your code, especially your library code, give it a try and please give us feedback on anything you feel ought to be different. The journey to make unanticipated NullReferenceExceptions in .NET go away will be lengthy, but we hope that in the long run, developers simply won’t have to worry about getting bitten by implicit null values anymore. You can help us. Try out the feature and begin annotating your libraries. Feedback on your experience will help shorten that journey.

Cheers, and happy hacking!

Phillip Carter
Phillip Carter

Program Manager, .NET and Languages

Follow Phillip   

81 comments

  • Avatar
    Kyle Brown

    Can you expand on “Note that C# 8.0 is not meant for older targets, such as .NET Core 2.x or .NET Framework 4.x. So some additional language features may not work unless you are targeting .NET Core 3.0 or .NET Standard 2.1”?
    What parts of C# 8 are expected to work on .NET Framework and which aren’t?

    • Phillip Carter
      Phillip Carter

      Hey Kyle,

      There are a number of features that won’t work on .NET Framework. First is Async Streams, backed by IAsyncEnumerable, which is unavailable on .NET Framework. Next is ranges, which use the Range and Index types, which are also unavailable on .NET Framework. Most prominently is support for default implementations in interfaces, which is a brand-new runtime concept that is not supported on .NET Framework. And none of the nullable annotations for first-party .NET APIs are available for .NET Framework. Everything else can technically run on .NET Framework, but since that’s only a subset of the language (and missing all the big features), we’re making it clear where the lines are drawn to avoid confusion about why feature X would appear to work, but feature Y would not.

  • Avatar
    . .

    thank you for a great article!
    > 1. Signal that if IsNullOrEmpty returns true, then value is non-null
    I think the above sentense should be “Signal that if IsNullOrEmpty returns false, then value is non-null".

  • Avatar
    Steven He

    Great article, however, what about tuples?
    “`
    public async Task<(string, T)> SomeMethodAsync(string x)
    {
        //…
    }
    “`

    How can I make only the second item in the tuple which is in the Task `MaybeNull`? (async method cannot use an `out` parameter)

  • Avatar
    Tomy Black

    Would it be possible to have not nullable string properties initialized as empty strings on classes, just like int is initialized to 0 by default? So you don’t have initialize every string in the constructor, especially in asp net classes? Or it could be forced in object initializer syntax.

  • Олександр Ляхевич
    Олександр Ляхевич

    Great article, thanks!
    I have a question about those attributes. Can I use them somehow, if I have library targeting multiple frameworks? I see that those are available only for dotnetcore 3.0. I hope I won’t need to put each of those to #if precompiler block?

    • Phillip Carter
      Phillip Carter

      Thanks!

      It’s currently a bit tricky when multitargeting. We’ve updated the specification for .NET Standard 2.1 to include all of the more advanced attributes so that you can share that code across .NET Core and Xamarin. But the .NET Standard 2.0 spec won’t be updated, so if you use that then you won’t have access to these types. Similarly, since they aren’t on .NET Framework, any specific target for net4X will not see these types.

  • Avatar
    x x

    It’s going to be a nightmare to apply all these required attributes and generic constraints in every single class and every single method. The spaghetti code will be harder and harder to read and maintain. Additional 10-20% of time for the project to just fight NRE using language patches – NRE which we already know how to deal with. Also juniors will have difficult time to learn all this. Do not ever remove a way to disable current behavior. I was a big supporter of new features in C# but now thinking more and more about switching back to Java… All the mess with .NET Framework, .NET Core, .NET Standard, Portable .NET, .NET 5 and beyond, multiplatform feature imparity, contradicting ideas every year. You’ll not clean this in next 10 years, I’m worried you’ll only proliferate it further.

    • Avatar
      Immo Landwerth

      > It’s going to be a nightmare to apply all these required attributes and generic constraints in every single class and every single method.

      Based on our experience annotating .NET Core’s corlib, I don’t think this will be true. The vast majority of our APIs didn’t need any custom attributes as the types are either fully generic or not-null.

      > Additional 10-20% of time for the project to just fight NRE using language patches – NRE which we already know how to deal with.

      One can certainly make an argument that nullable reference types doesn’t free you from thinking about null references; however, the goal is to make null-states part of the design and thus part of the public API signature. The cost for doing this will certainly not be zero, but the hope is that it’s less time that you — and your consumers — spend today with debugging and fixing null reference exceptions.

  • Avatar
    Chun Er ON

    This is definitely great move probably biggest since LINQ
    I think it’s time to make a revolutionary step after many years of evolution
    MS should develop new language without burden of legacy code compatibility and with full natural support of nullable types whether ref or val
    It should be just <Type> or <Type>? with compilation errors and no special attributes to remember and soft warnings
    Kotlin does it and new C# can do it
    It might be 2 diff languages or compiler settings: 1 for legacy and another for future

  • Avatar
    Ian Good

    Thanks for the writeup!I’m not sure if I missed it in this article, but are there options for enumerable results?For instance, `myList.OfType<Foo>()` will only yield non-null results;`myList.Cast<Foo>()` may yield null elements.

    • Avatar
      Immo Landwerth

      We haven’t annotated Linq yet. However, custom attributes cannot be applied to nested generics, so we can’t express that OfType() will never return null values, so it will inherit the nullability of the underlying enumerable.

  • Avatar
    dopefu@euroweb.email

    Phillip,
    Can you explain why with these changes, this method is still not possible?

    public T? Get<T>() where T : notnull{    return null;}

    • Avatar
      Immo Landwerth

      Because T could either be a value type or a referene type. But the representation between the two would be quote different: T? for a value type means that you’re effectively replacing T? with Nullable<T>. But for a reference type the underlying type would just be T.

  • Avatar
    Eugene Ivanoff

    I just wonder how this “feature” adds up to code readability? You now have to track SEVERAL possibilities in the code:
    1. Old behavior – nullable reference types feature is not used at all.
    2. The nullable reference types feature is set globally in project settings.
    3. The nullable reference types feature is set globally in project settings and occasionally is turned off in some places.
    4. The nullable reference types feature is NOT set globally, but code occasionally uses #nullable enable/disable in some places.
    Now a reader of the code needs to keep this zoo in mind when reading any part of the code. What is correct behavior in one file can be incorrect for the other file.
    So, just imagine you’re reading some code and you see no check for null. Your thoughts? “Well, may be developer forgot to check for null? Oh, wait! We have nullable reference types feature! Lemme check #nullable enable. H-m-m… Can’t find it… Oh! Maybe it’s in project settings? Huh! No setting! I thought so – a developer forgot to check for null!!!!” Whatta mess…

    • Avatar
      Immo Landwerth

      I hear you.

      > Now a reader of the code needs to keep this zoo in mind when reading any part of the code. What is correct behavior in one file can be incorrect for the other file.

      That’s why I wouldn’t recommend shipping a zoo. To me, #nullable enable/disable should be only be interim states while your migrating existing code to use nullable reference types. The end result should be that it’s globally turned on and no source files contain any #nullable directives.

      • Avatar
        Eugene Ivanoff

        Yes, I agree with you. If to exclude #nullable enable/disable, then it’d be much easier! But what is meant to be used for migrations could be abused for regular code – not for migrations.

    • Avatar
      Jon McGuire

      I’m glad my retirement is within reach. I hate to imagine how lowest-bidder contract devs are going to wreck corporate codebases with this “zoo” of features (great description, Eugene). It’s easy to call down from the Ivory Tower and “not recommend shipping a zoo” but your true base, corporate America, hires low-experience contractors by the thousands and has billions of lines of legacy code in null-using libraries that may continue to be used for 10 or 15 years to come. It’s just a fact of life. Do these factors result in NREs? Absolutely. Daily. But NREs are generally easy to fix. Adding new complex ways to use nulls will only make the problem worse, because the use of null isn’t going away, no matter how hard you try. That horse has left the barn.

  • Avatar
    Tyler Brinkley

    nameof support for the parameter names specified in the NotNullIfNotNullAttribute would be nice though I understand it would be low priority.

  • Avatar
    Dan ny

    If I understand correctly, that horrible list of attributes is because a genericized ‘T’ can be nullable or non-nullable, but ‘T?’ can only be nullable?  Wouldn’t it have been simpler to introduce syntax for “non-nullable T”?
    I get that you don’t want “non-nullable” syntax because you want “non-nullable by default” behavior, but since that’s not true for generic parameters, this option is definitely worse lol

    If it’s too late to make that a possibility, can you at least combine “NotNull” and “DisallowNull” so we don’t have to memorize which is which (also “MaybeNull” and “AllowNull”)?

    • Phillip Carter
      Phillip Carter

      > because a genericized ‘T’ can be nullable or non-nullable, but ‘T?’ can only be nullable?

      Not quite. What I also wrote was that even if T and T? had a more natural definition, they still wouldn’t solve all the kinds of problems people have. For example, one-way nullability (either an input or an output, but not both) without forcing you to use two generic type parameters.

      • Avatar
        Dan ny

        I think an explicit “non-nullable T” syntax would have solved all the cases above.  For instance, if T! means “non-nullable T” and T? means “nullable T”, then
        [NotNullIfNotNull(“input”)] T? MyFunc(T? input) { … }
        could be rewritten as an overload:
        T? MyFunc(T? input) { … }T! MyFunc(T! input) { … }

  • Avatar
    Jason Bock

    For the notnull constraint, where is that information stored? For a library I’m writing, I’m using GenericParameterAttributes off of the generic parameter type along with the GetGenericParameterConstraints() extension methods. Is the data to determine that the “notnull” constraint within these two members? Or does it have to be gleaned through other metadata, similiar to NullableAttribute and NullableContextAttribute?

  • Marcos Meli
    Marcos Meli

    Thanks a ton Phillip for this article.
    Maybe you need to clarify a bit more when the attributes are needed to avoid some people get scared.
    1) If you are working on a project and you use <Nullable>enable</Nullable> you don’t need any attribute at all because your code has the flag enabled and backward compatibility is not an issue (of course if no other project uses it)
    2) Most of the explanations are valid for library authors or internal reusable code but most of the devs will never know that these attributes exist at all, just the warnings are relevant
    3) Some attributes aim to make the CoreFx analyzers and compiler errors/warnings better, but maybe the library authors will never use them
    Keep Rocking with .Net Core !!

  • Avatar
    Richard Szalay

    What’s the tooling story for this preview from a library author’s perspcetive? Is VS 16.2 + Compilers 3.3.0-beta1-final enough + LangVersion 8 enough for nullables to work with a netstandard2.0 output?

  • Avatar
    Richard Szalay

    Here’s my experience attempting to apply nullable types to MockHttp
    The library targets pcl328, net40, net45, netstandard 1.1, and netstandard 2.0. I use a Shared Code project and individual class library projects for everything except netstandard (which uses multitargeting). On all projects, I:
    1. Installed “Microsoft.Net.Compilers 3.3.0-beta1-final”2. Set LangVersion and Nullable props in the csproj
    I had to annotate a few types and add a guard in one case, but otherwise it was quite smooth.
    My only real issue is that I’m seeing CS6832: The annotation for nullable reference types should only be used in code within a context on all my framework projects but not my netstandard ones.
    The branch with these changes are here, if that’s useful. https://github.com/richardszalay/mockhttp/tree/feature/charp-8

  • Avatar
    Michael Ketting

    Do you have plans to add NRT annotations to a .NET 4.8.1 service release for those projects that are likely end their life cycle 10-20 years down the road without ever migrating to .NET Core?

  • Avatar
    Petr Onderka

    Have you considered having a tiny tool that helps with the recommended migration paths? E.g. “dotnet nullable enable-project-disable-files” would set Nullable to enable in the csproj and add “#nullable disable” to all .cs files in the project. And “dotnet nullable enable-project-clear-files”, which would remove “#nullable enable” from files.

    • Phillip Carter
      Phillip Carter

      Hey Petr,

      We’ve definitely thought about things like this (and better VS tooling to help with migration). But the tools support is pretty close to being locked for the .NET Core 3.0/VS 16.3 release. I anticipate we’ll do something like this both in the CLI and in the VS tools in a future tools update, in addition to other things (e.g., adding Nullable enable to the project file triggers a suggested removal of all “#nullable enable” directives in source files).

  • Avatar
    Daniel Smith

    Nullable reference types has certainly grown some arms and legs.  So glad that you guys have examinied this in depth, as I don’t think anyone realised the full implications at the start of the process.
    My main concerns are that some of these new attributes could potentially become redundant when the static analysis becomes more advanced, and they are currently limited to quite a narrow set of scenarios.
    Could any of the prior work done by MS research for Spec# (https://en.wikipedia.org/wiki/Spec_Sharp) be useful here e.g. the “requires” and “ensures” preamble that can be added at the start of methods seems like it would allow for much more expressive and flexible checks.

    • Phillip Carter
      Phillip Carter

      Hey Alex, good post! There’s definitely a little give and take here. If a component takes a non-nullable reference type as input and doesn’t check for null, that _should_ be fine. So you could argue that it’s the fault of the caller for passing null there (and indeed, they would get a warning if they tried!). A different camp of people feel that most components shouldn’t fail, and thus check for null even they they are using a non-nullable reference type as input. I tend to be in the first camp, but I can see it going either way. There isn’t a truly “right” answer here.

    • Avatar
      Tyler Brinkley

      From my perspective, NullReferenceException’s are the responsibility of where they are thrown so as a library author any publicly facing input parameters that shouldn’t be null should be marked as non-nullable for compile-time safety but should be checked and throw an ArgumentNullException when null as opposed to throwing a NullReferenceException in my code.

  • Avatar
    Jeff Jones

    I fail to see why this is such a big deal, or even needed in comparision to the resources used to create it.  I have yet to see examples of why this new feature set provides any significant advantages over how nullable types work now.  An article that realistically comnpares the value of using this new feature over how it is done now would be helpful.

    • Avatar
      Immo Landwerth

      That’s fair feedback. To me, the biggest advantage of the feature is that nulls are expressed at the API level, which shows up in documentation and in IntelliSense. This avoids surprises down the road.

      Rewriting a large code base to use this feature certainly has a cost and whether that’s worth it depends on various factors. I’ve started to migrate some of my own projects but it’s a bit early for me to have a great working model for when it makes sense vs. when it doesn’t.

      However, one thing I do want to make clear is that we’ve thought hard about the design from the point of view what the world needs to look like to turn this on by default. For example, if you were to write a brand new library in the .NET 5 time frame, our goal is to enable nullable by default. That’s also why we annotate what can be null, rather than what cannot be null (we believe the vast majority of reference type usage assumes non-null). In that world, nullable annotations are a very much a tiny increment (in that they require additional annotations) but the benefit is that compiler gives you static validation for when you accidentally dereference something that’s null (and you always tend to have more usage sites than definitions).

    • Avatar
      Immo Landwerth

      There certainly is overlap with the code contracts feature. However, the code contracts in .NET requried additional tooling outside the compiler. Also, since code contracts were based around generalized predicates they also required a theorem prover-style tool, which isn’t exactly cheap to run. In .NET, we found the vast amount of code contract usage was about declaring non-null states. By incoropating this into the language (and thus the compiler and IDE editing experience) we believe we get something that delivers most of its valuable with the added advantage of rapid feedback and expressiveness in the language.

  • Avatar
    Dominik Jeske

    What about times when we have class that cannot be ready after constructor call and we have to call initialization in some method (Initialize, OnStarted etc.). Now I have ‘Non-nullable field ‘fieldName’ is uninitialized’. I want this field to be not nullable because after init it should behave like that. Is there any language support in c# 8 for this kind of cases?

    • Avatar
      Immo Landwerth

      Yeah, we’ve seen cases like this in the BCL. The workaround we use is an explicit null-assignment with a bang in the field initializer:

      public class Test
      {
      // I promise this will be initialized.
      private string _number = null!;

      public Test()
      {
      Init();
      }

      private void Init()
      {
      _text = “data”;
      }
      }

      • Avatar
        Dominik Jeske

        Tnx. for info. Introducing this kind of functionality is very challenging but I think that this is only a beginning and you will end up with something great!

  • Avatar
    Jon McGuire

    It’s ironic that a plan to eliminate “the null problem” requires an explosion of new ways to use null. The fact that you recognized a need for this virtually proves that fundamental language changes (non-nullable by default) this late in the C# lifecycle is a Really Bad Idea. Nobody likes NREs but they aren’t the Great Disaster they’re made out to be, otherwise there would already be a lot more code that worked harder to avoid the use of nulls altogether.

    • Avatar
      Immo Landwerth

      On the one hand, I understand your concerns. This blog post shows all the wrinkles in one place, which gives the appearance of a complex feature. At the same time, I don’t agree with the sentiment that if only we were willing to be bolder the feature would be simpler. One cannot eliminate null, because sometimes values are just missing. We could have decided to let users annotate what must not be null (i.e. by using a ! suffix, like string!). This would make the feature more “backwards compatible” in that string means nullable string and string! means non-nullable string but we felt that this creates inconsistencies between value types and reference types but it also doesn’t gel with how we see the world, i.e. that most reference type usages assume non-null.

      I certainly see the complexities in the corners of our type system, especially when generics and out parameters are involved, but I don’t think it’s as complex as you make it sound. But the proof is in the pudding, so we’d love for you to try the feature and give us feedback!

  • Avatar
    Joshua Schaeffer

    All I came here to find out is how to disable the useless feature.
    <PropertyGroup><Nullable>HONK HONK</Nullable></PropertyGroup>
    I’m going to rewrite all my code so I can participate in C#8? No no no, you must be joking. You will simply be redefining null as a proxy sentinel object, one of every type. Your code will still crash on errors but crash differently and downwind. I’m not changing one line of code for this busybody academic crap. I worked in C++ for years and I have now seen it all. Stop wasting time on things that should never leave Microsoft Research before the bean counters send the whole C# team to Azure pitchfork purgatory like the rest of our heroes.

  • Boris Vetrov
    Boris Vetrov

    Hi, maybe c# team need to look for implementations of null in other languages (forexample Scala [Nothing, Null, Option[T], Any], or as Vb.Net MayBe) – since in order to keep those rules in mind it’s not easy at all, this mess of rules doesn’t gives to developer opportunity to use their code in full manner of their code abilities.

  • Avatar
    Dominik Jeske

    I really like new approach to nullability but one thinks bothers me. We have to tread differently nullable reference types and nullable value types. It would be great if we could have some common thing like Maybe from functional world and we can check for nullability in common way.

  • Avatar
    Nathan Zahra

    Regarding the two flow attributes: DoesNotReturn and DoesNotReturnIf(bool)
    1) Naming of [DoesNotReturn(If(bool))?] as per the example given seems unclear. [DoesNotContinue(If(bool))?] or [DoesNotProceed(If(bool))?] fits better in such scenario, though return value enforcement must be the priority.
    2) The reasoning behind specifying [DoesNotReturn] is for control flow and semantics. But why would one ever use this attribute on a method returning void? or throwing an exception? As I see it, it's the compiler's job to determine whether to continue nullable analysis or not, and maybe handle these cases automatically. Maybe I'm unaware of the possible challenges such a feature would require.
    Need to delve into this further to have an opinion. I hope the forecasted benefits won’t lead to much boilerplate code. Most of us are here for an alternative to bloated frameworks such as Java Spring and its annotation hell. But if this is a step forward and is actually effective, it’s a more functional shift in mindset every programmer should learn adopt.

  • Avatar
    MgSam

    What has always been missing for me from the inception of doing this in C# has been usage statistics of the non-null features that TypeScript added. In my anecdotal experience, the feature seems sparsely used outside Microsoft. Though that could be completely wrong.  Either way, that data should have been collected to make an informed decision about whether this monumental undertaking was worth the cost. 

  • Avatar
    paul louth

    Just installed to continue the migration of language-ext to C#8 nullable refs and had to stop after a minute.  The performance is appalling.  Every key press is taking at least .25 of a second sometimes longer, it’s totally unusable.  A real dissappointment. 

    • Phillip Carter
      Phillip Carter

      Hey Paul,

      Sorry that you’re seeing such awful perf problems here. Could you file a bug with some diagnostic info over on the Roslyn repo? VS version, compiler version, etc.

      We haven’t seen any perf regressions like you mention in our own codebases (example: .NET project system), but there could be an issue we just haven’t caught yet. Theoretically it shouldn’t have a noticable impact on editor performance since autocompletion is an asynchronous operation, but if there’s an unaccounted for case that your codebase is exercising we’ll get that fixed ASAP.

      Thanks!

  • Avatar
    John Landheer

    It should now be possible for the runtime to throw an exception upon assignment of NULL to a non-nullable type. Is that an option or would that be too costly?

    • Phillip Carter
      Phillip Carter

      This would just shift the problem. For backwards compat reasons, we’d still need to allow null assignment, but instead of getting an NRE when you ‘.’ into a type, you’d get a different exception that wasn’t guarded effectively at compile time.

  • Avatar
    Aaron Kunz

    Great post with lots of details, the latter maybe being partially the source of some of the worries brought up in the replies. I can understand the thoughts that this feature makes the language a bit more complex and thus less approachable, as not all C# users work with it daily (IT ops, inhouse corp. devs etc.) and keeping all those corner cases with its appropriate “special” attribute in mind can feel overwhelming at first. IMHO “special” attributes (e.g. CAS in the past) always feel like a weird/magic interplay of language/framework/runtime which should present themselves more prominently (e.g. as language keyword, like co/variance did) than hiding as a “normal” attribute in disguise – but that’s obviously just my feeling.All in all I think this is a sensible approach considering such an effort at this point of the ecosystem and widespread usage of the language. I guess most of the time only library devs should be concerned with those corner cases and its attributes and thus the impact for “part time” C# users is hopefully restricted of just thinking more carefully about their nulls (with the compiler’s help) and expressing it with appropriate types.It is a bit like with TypeScript – you can leverage it without knowing all the details of its type system (which IMHO is way more complex than what we are talking about here).

  • Avatar
    Tore Nestenius

    When is the support for the generic notnull constraint available? I can’t seem to get it to be accepted in the 16.3.0 preview 2.0 interface IDoStuff<TIn, TOut> where TIn : notnull where TOut : notnull

  • Avatar
    endofunk

    Why was the value constraint on Nullable not removed, i.e. to “simply” support both value and reference types?  The issue is that the “fake” nullable boxing and separate need to type constrain value & reference types makes it rather difficult to conform nullable to FP algebras like: monoid, functor, applicative functor, monad, traversable, etc. 
    .
    .
    A good example of this increasing complexity with generic extension methods is a monadic lifter function like LiftM (function application with monadic arguments) with multiple arities (1 to 9, or more).
    public static Maybe<R> LiftM<A, B, C, D, E, F, G, H, I, J, R>(this Func<A, B, C, D, E, F, G, H, I, J, R> @this, Maybe<A> a, Maybe<B> b, Maybe<C> c, Maybe<D> d, Maybe<E> e, Maybe<F> f, Maybe<G> g, Maybe<H> h, Maybe<I> i, Maybe<J> j) => a.FlatMap(xa => b.FlatMap(xb => c.FlatMap(xc => d.FlatMap(xd => e.FlatMap(xe => f.FlatMap(xf => g.FlatMap(xg => h.FlatMap(xh => i.FlatMap(xi => j.FlatMap(xj => Just(@this(xa, xb, xc, xd, xe, xf, xg, xh, xi, xj))))))))))));
    .
    To implement the same functionality for Nullable generic types that can either be constrained to value or reference makes this akwardly type constrained complex to implement.
    .
    The same complexity similarly plays out with implementing extensions for the alternative using the SelectMany function i.e. monadic application using Linq query syntax e.g.
    public static Maybe<R> SelectMany<A, B, R>(this Maybe<A> @this, Func<A, Maybe<B>> fn, Func<A, B, R> select) => @this.FlatMap(a => fn(a).FlatMap(b => select(a, b).ToMaybe()));

    from _1 in …
    from _2 in …

    select f(_1, _2, …)

  • Avatar
    Shawn Clabough

    For the “Opt in files one at a time” option, I think you meant for the setting to be <Nullable>disable</Nullable> then use #nullable enable at the top of the files you want to be checked.

      • Avatar
        Shawn Clabough

        You have both “Opt in a project, opt out files” and “Opt in files one at a time” specified as <Nullable>enable</Nullable> which for me starts showing warnings for every file. After setting <LangVersion>8.0</LangVersion>, I had to use <Nullable>disable</Nullable> and #nullable enable at the top of files to get it to work one file at a time.

        • Phillip Carter
          Phillip Carter

          Hey Shawn,

          Not quite. In the post, I mention starting with #nullable enable, then setting the project file once issues are addressed, then removing the file directives. The post doesn’t say to do both at the same time.

          • Avatar
            Shawn Clabough

            Ah, thanks. My <Nullable>disable</Nullable> with #nullable enable is accomplishing the same thing as not having the <Nullable> setting in the project. 

  • Avatar
    Viktor Nikolaev

    Consider this code:

    public IEnumerable Test(IEnumerable strs)
    {
    return strs.Where(x => !string.IsNullOrEmpty(x));
    }

    After compiling I get such error: [CS8619] Nullability of reference types in value of type ‘IEnumerable<string?>’ doesn’t match target type ‘IEnumerable<string>’

    How can I fix this?

Leave a comment