C# 8.0 Nullable Reference in Practice
.NET Oxford
The most significant new feature of C# 8.0 is its support for non-nullable references. In a bid to mitigate what is often described as the "billion dollar mistake" of allowing references to be null, C#'s language designers have attempted to retrofit the ability to write code that declares unambiguously that it doesn't want nulls, and for the compiler to attempt to enforce that for you. Adding such a feature almost two decades into the life of the language and its supporting runtime was always going to be challenging, so this feature has quite a few quirks (such as its apparently inverted name).
In this talk, Ian Griffiths will describes the ins and outs of the new feature, with particular reference to his employer, endjin's experience with applying this new feature to existing code. In addition to an in-depth description of the technical details of nullable references, this talk will also describe strategies for migrating an existing codebase into the world of nullability-awareness.
Transcript
Introduction
Ian Griffiths: Thank you all very much for coming. This is a talk on perhaps the most ambitious new aspect of C# 8, which is nullable references. And the key thing about this talk, because I know you had, I think Jon Skeet come a while ago to tell you about this when it was still in preview. The unique selling point of this talk is I've actually been using it.
So this is not just a theoretical talk. I'm gonna talk initially—first half is gonna be all about what the feature is, what it offers, how it works. And then the second half is what it's like to use in practice, things you will need to know to be able to migrate to it successfully, the problems you're likely to come across, and ways of working around them or dealing with them.
This means that unusually the criteria for self-promotion slide is slightly more relevant than normal because this is that the talk is based on stuff that I actually do. So I'm Ian Griffiths. I am a technical fellow at a company called endjin.
We are a smallish consulting firm. We specialize in cloud-based transformation and all sorts of data processing. Our unofficial thing is we help small teams achieve big things, but basically I am in charge of technical oversight of all of our work. So one of the things we've been spearheading is the application of nullable types across all of the IP that we develop internally, all the libraries that we've written, many of which we've open sourced.
So you can go and take a look at some of this stuff if you want to. And so that's what I'm gonna be talking about tonight. Also, I have a book to plug. In fact, I've brought two copies of the book with me and I will be giving them out to people who would like to ask questions of the form.
Please can I have a free copy of your book will not count. But if you'd like a book please ask a question. If you don't want a book, but want to ask a question anyway, then don't let that stop you. The book is not mandatory, but yes I am here to plug my book. So I may mention it a few times. So it is Programming C#. It's about C#, does what it says in the title.
The Problem with Null References
The first half of this talk prior to pizza is gonna be the theory. What is the problem that this new language feature is designed to solve? What is nullable references for?
How do they help? What are the new language features that comprise this new thing? And then after the break, we'll look at how you can tackle the task of migrating an existing code base that does not use this feature onto using this feature, because it's not totally straightforward. This feature is actually switched off by default because it is not entirely backwards compatible.
So we'll look at how that works in practice. So what is this all about? There is a thing that a computer scientist, British computer scientist called Tony Hoare invented, which he now describes as his billion dollar mistake. This is an estimate. He reckons it's somewhere between a hundred million and $10 billion. A large amount of money has been caused by his decision to allow references to refer to things that aren't there.
So this dates back to the 1960s. He was working on a language called ALGOL W, and because it was easy and convenient, he said variables that refer to a thing can also be set into a state where they don't refer to a thing and that's null. And it's propagated throughout loads of languages. C# has this, many of the C family languages have this, anything that was influenced by ALGOL, which is a whole raft of languages have this feature. Many languages don't by the way. A popular joke amongst F# developers is what can C# do that F# can't?
And the answer is crash with a null reference exception. So some languages, it's not strictly true. You can do it in F# because it interops with .NET, but the default is things are not nullable unless you say they can be nullable. Haskell has the same characteristic and it means you can't make mistakes like this.
What is wrong with this code? What does this do wrong? Object reference not set to an instance of an object. Thank you very much. It will go bang here when we try and dereference this. Piece of string. It will go bang if we're passing in null. It will fail. So that is not good. And it's very easy to make these mistakes.
Enabling Nullable Reference Types
It's fairly easy to spot on this slide because this fits on a slide. Not all enterprise applications fit on one slide. So there might sometimes be difficulties in working out whether the thing here has a possibility of being null. And this is what the new language feature in C# 8 known technically as nullable references is there to try and solve.
So the idea is that if you turn this feature on, this is how you turn it on, by the way, you go into your project file and add a nullable property and set that to enable. That will fully turn the feature on. And when you do this, you'll get complaints. If you do things you're not supposed to, we get a squiggly underneath the null here saying that's not right.
<PropertyGroup>
<Nullable>Enable</Nullable>
</PropertyGroup>
You should not be passing in a null here because that method can't cope with nulls. That method expects there definitely to be a string there. So this is essentially the promise. The compiler is going to tell us, we hope, when we've made mistakes of this kind.
Benefits Beyond Error Detection
This is not a net win necessarily. There are—there's one obvious benefit, which is it will start to tell you about null-related errors. There are less obvious benefits. Actually, one of the big benefits that we have found as we've moved code over to this mechanism is that it improves the expressiveness of your code base because your code base tells you.
So today, if you've got a .NET library that's got some property or some object, you don't necessarily know whether it's gonna be null or not. You can't tell. But once you've gone through the process of upgrading your code to use this stuff, it's unambiguous. Either it says, yes, this definitely can be null.
Or it says, no, this will not be null. You should not see null. And I'll come back to that in a minute. Could versus won't turns out to be a bit of a sticky detail of all of this. So it means there's a lot more information in your code base. And we were surprised actually, by how big a difference that made in practice. We knew in theory this would be the case, but the reality has been a pleasant surprise, I would say so far.
The Challenges
So what's the problem? The first thing that will happen when you turn this on is it generates an enormous amount of work for you. So you'll turn it on and suddenly thousands of new warnings, or depends how big your project. Maybe a handful of warnings, maybe hundreds, possibly thousands, depending on how you structure your code, of warnings saying this either isn't right or needs some work.
And in a lot of cases, it's because you need to go and add the information that the compiler needs to do its job. For example, if you use optional arguments in C#, if you declare any function that takes optional arguments and the default value that you specify is null, you are going to have to go and modify all of those to say this is nullable.
Because the default is non-nullable. So you're gonna make changes all over the place just to get past the point where it will compile. And then it will give you a bunch of warnings, many of which will actually be maybe not real problems at all. As we're gonna see, this is not perfect, because fundamentally what the C# team have tried to do here is retrofit a fundamental language feature to a language and runtime that's already about 20 years old, which is courageous. I would say it's not been straightforward. And so there are plenty of rough edges. And actually the biggest issue that we have encountered is that certain common practices in .NET just don't fit that well with this new approach. The big one being the serialization scenarios of one kind or another.
Now there are ways around this and I will talk about them, but this is a big problem that you will run into in practice. Not everything actually likes being in this sort of world. Because this was retrofitted, they can't guarantee anything. So the first thing to be aware of is that nothing is guaranteed.
Working with Third-Party Libraries
Audience: So what happens if you're using a third party library that doesn't subscribe to this by default? Could you then end up having nulls? What would happen if null was returned from a method in a library then?
Ian Griffiths: Yes. So what would happen if you are using—if you've turned on this language feature, but you are using a library that does not support it, which is going to be the case, right?
Because most things don't support it, right? Someone has to start. And unless the entire world agrees the strict dependency tree of all .NET code ever written and upgrades it in order, which probably won't happen, then yes, this is a scenario you'll definitely come across. One of the issues is that C# might simply not know about certain expressions, whether they're meant to be null or not. As we're gonna see, it categorizes expressions. And one of the categorizations is what's called oblivious, which is basically this thing doesn't even know what null is. So null—sorry, it doesn't know what the distinction means.
And so you just can't infer anything. And in those cases, it simply disables the warnings. So you don't get the warnings, but you also don't get to know that there's a problem. So that's the answer—is that it's essentially a bit of a black hole. And they had to do it that way, because the alternative would be either somehow you would have to be able to layer annotations on top of someone else's component, which you—there's no way of doing that. They haven't provided a mechanism for that. Or you just get loads of warnings and would have to then do things to suppress them one way or another, which would be equally annoying. So they've taken the decision that there is such a thing as being oblivious to whether something is null or not.
And that sort of spreads outwards, unless you take steps to stop it doing that, which I'll also talk about. So other components will not necessarily have been upgraded to this. Also, the runtime has not changed. This is not a .NET feature in the sense that the runtime's type system, the CTS, has not been augmented to understand this.
I think the .NET team would argue with me on this. They say it is a .NET feature, because C# is a .NET language and the .NET tool chain supports this. But it's certainly not part of the CLR. It is part of the .NET ecosystem, but it's not part of the CLR itself. And so the CLR can't tell you when these things have gone wrong. All of the analysis that this new language feature enables happens at—it's compile time, there's no runtime checks.
Subverting the System
And one of the consequences of this is it's really easy to subvert them. In fact, let me write—I'm gonna type it and hope the PowerPoint is back to life by the time I return. So if I write:
int HowLongIsAPieceOfString(string piece)
{
return piece.Length;
}
So far no errors because I have not turned on the language feature, but if I turn on the language feature, so I come into my project file and somewhere in a property group, I say <Nullable>Enable</Nullable>
. Come back here and wait for a little bit. There we go. Cannot convert null literal to non-nullable reference type. So basically this is the point of this, right? This is why we are doing this. So this is the goal—is to be told when we've done something wrong. So we obviously have to pass in something that at least resembles a meaningful value.
I foresee a future in which lots of people just pass empty strings instead of null strings, thus completely defeating the entire purpose of the exercise. Hey, we've got two kinds of null. Let's just use the other one. Don't do that. But like I said, it's quite easy to subvert this. If I am just a little bit more emphatic about it and stick an exclamation mark on the end, then it goes, oh, that's fine.
Then this—the exclamation mark is called the null forgiving operator, also known informally as the dammit operator. So rather than this, you're saying this dammit, and that's an assertion that this is definitely not null, despite I would say quite strong evidence to the contrary that I think that probably is null.
Flow Analysis
So might be pretty basic, but if you define a string and set its value as null and then pass it in, which level does that flag as I'm not happy with it? If I simply have a string and set that to null and then pass that in, is that a devious way of bypassing this?
So if I have:
string definitelyNotNull = "";
And I pass that in, then it's happy. If I set this to null, the problem now is underscored here. So when you declare a variable, you express whether it's allowed to be null and actually it's smarter than that. It turns out the C# compiler—notice what I've done, because as I'm gonna talk about in a minute, the C# compiler doesn't just take our word for it.
It actually does a certain amount of flow analysis to work out what it thinks is the status of things. So it will go, well, hang on, you may have declared that as non-nullable. I know I've not introduced this yet, but that definitely means non-nullable. If I wanted to be nullable I would write:
string? definitelyNotNull = null;
Now it's no longer complaining about the initialization. Cause it's like, what's fine. I'm allowed to put null into this variable because it says it's nullable, but now I'm not allowed to pass it as that argument. Unless I decide I do want to, in which case I can bypass it. I'll explain. I'm gonna get to why that's useful.
New Syntax and Semantics
Okay. Let's look at what we've actually got, because I've just shown you a random smattering of what happens. Let's get a little bit more formal about this. Here is how things look when you've turned the new feature on. You have explicit nullability for reference types, such as string, in kind of the same way as you have with value types.
So with value types, since C# 2, you have been able to stick a question mark on them to say, actually this might not be here. So with value types, it was always a case of this thing's definitely present unless I say explicitly it's nullable. And so the theory is that we now get to do the same thing with references.
However, what they haven't done is introduced a new NullableRef<T>
that's the reference type equivalent of Nullable<T>
, because this is backed by a generic type called Nullable<T>
. And you can do nullable int, nullable any value type. And that says, okay, this is that value type, but possibly not. For all the Haskell programmers in the room, it's like the Maybe type. It's a thing that can be one of two states.
The intention is the same with this. And if they were designing this in from the start of .NET, they probably would've made it work in a much more similar way. These would've been the same thing. The problem is they can't really do that because all the .NET code already out there doesn't know what this hypothetical NullableRef<T>
would be.
Type System Changes
The only things, the only kind of type you can use, the only thing that's able to work with existing code is the good old reference we've been using since .NET 1. And so this is just a syntactic thing. This is a—in the C# compiler's head, both of these types string
or string?
, both of them actually end up meaning the same thing to the CLR.
And that thing is exactly what string always used to mean before this language feature came along. One of the reasons that's important is that if that wasn't the case, turning this on would break all of your code more or less straight away, because it would completely change the meaning. So they just decided this is not going to be completely consistent with how nullable value types have typically worked in .NET.
This also means that even though they've tried to keep things as similar as possible for backwards compatibility, nevertheless, it means that this is quite a dramatic change turning it on, because I've had to do—what this used to mean before and how it's changed columns there. The value types haven't changed. Normal value types and nullable value types, nothing new here for that.
But with reference types, notice this "may be null" has moved down a line. So before, if you wrote string—reference to a string or null, that's what that meant. It was either reference to a string or was a reference to nothing. Whereas now we have a new syntax that means the old thing, which by the way, explains the rather cryptic name for this technology.
So it's called nullable references. It's—hang on. Haven't we always had them? References have been nullable since .NET 1. That's always been a feature. How is this a new feature? The new thing is the new syntax for that. And rather more dramatically, the fundamental change in what the old syntax means.
Now, the old syntax means I don't want this ever to be null please.
Flow Analysis and Inference
So that's the new syntax, but there's also some new features because the compiler is able to infer whether things are null or not. Take a look at this code. We've got our old HowLongIsAPieceOfString down at the bottom, which still requires a non-nullable string.
But here I've got a wrapper function called—method called HowManyCharacters, which takes a nullable string. And this is saying if I've been passed in null, then I'm just gonna say you've not given me a string.
int HowManyCharacters(string? piece)
{
if (piece == null)
{
return 0; // or throw, or whatever
}
return HowLongIsAPieceOfString(piece);
}
And what's interesting is this doesn't cause an error. You might think it would because here the function being called says gotta have a non-nullable string. And the variable I'm passing in is declared as nullable. How can that work? And the answer is the compiler does flow control analysis—flow analysis, sorry.
In using basically the same system that it uses to check that you've initialized your variables. It's basically the same inference engine that has always been used for definite assignment has now been repurposed to do this.
Question—can reflection APIs discover nullability? Yes, it's all done through custom attributes.
So you can find out that way. Now you can see what the compiler has inferred because if you mouse over any expression that is a reference type—actually strictly any variable reference, it will tell you whether it thinks it might be null. So you can see in the popup there, it says piece is not null here, even though it was declared as nullable. The compiler goes, well, hang on, you've tested it for null.
We could only end up here if it's not null. And so I now know that in this line it's not null. And so it's okay to pass it as an argument. So we have the declared nullability, but we also have the inferred nullability. And those are not always the same.
Library Support and Attributes
Now you can help it here. Here's an interesting case. This is almost exactly the same code as the last slide, but rather than comparing for null, I am using string.IsNullOrWhiteSpace
, which is a library function.
And if you compile this code on .NET Core 3.0 or later, or .NET Standard 2.1 or later, this will continue to work. It will continue to tell you that piece is not null here and therefore it will let you pass it as an argument. If however you compile this against .NET Standard 2.0, it is unable to make that inference.
What's the difference? The library function in question, IsNullOrWhiteSpace, has been annotated with an attribute telling the compiler what it's allowed to infer. So this says, if this function returns false, you can safely conclude that this argument is not null.
So in essence, it enables it to infer the same thing that it would've inferred if I'd explicitly tested for null. So it's basically says I'm doing a null test for you so that you could flow that out.
There's a bunch of attributes you can add to your code and the .NET framework class—the .NET class libraries have also been annotated with these in .NET Core 3/.NET Standard 2.1 or later.
And if you've got those things, then the inference gets a whole lot better. So we have the type system and also the inferred information. They're two separate things that compiler has access to. So the types are just, how is this variable or argument or property or field declared? Was it declared as non-nullable?
Nullable Contexts
You may find that causes a terrifying cascade of warnings and looks completely intractable. And you might go, ah, turn off. So there are other ways you can do this. You don't have to turn everything on at once. You can, for example, say, I am not ready to annotate my code. So I would like all of my code to remain in an oblivious state, but I'd still like to see warnings that tell me that I have misused other people's fully null-aware code.
So if I'm using .NET Standard 2.1, I'd like to be told if I've messed up, but I'm not ready to annotate my own code yet. That's what you get if you say just warnings, please. Conversely, you can say I'd like to annotate my code now, but I get a terrifying cascade of warnings when I turn that on. So I'd just like to add the annotations and I'll deal with the warnings later, in which case you can say, just turn on the annotations and you don't have to do this at the entire project level.
If you don't want to, you can set this at every single line of code. If you want to, there is a #nullable
directive where you can enable or disable or undo the last thing you did with the restore, either the annotations or the warnings. Now the terminology around this is horrible, but I've been unable to think of a way they could have named it better.
So they've had this really bizarre language. Each line of code exists in two nullable contexts. It is in a nullable warning context and it is in a nullable annotation context. The nullable warning context can be either enabled or disabled and so can the nullable annotation context. This is how they've defined it.
Comparison with Other Languages
Audience: Given that you've used this extensively, don't you—C# is the first language to try and bring in this concept of having—I did some Swift development a few years back, which also introduced that as a concept. Doesn't this feel really complicated because the way that I'd see it done in Swift was you didn't have to change the entire project landscape in order to enable it.
You could do it on a per basically property definition from that perspective. And then you essentially able to mix your nullable and non-null from the same code base without having all these errors and warnings and scary stuff going on. So you can sequentially change your code base to that.
Ian Griffiths: So I'm unfamiliar with this bit of Swift. So does Swift have like different syntaxes for I'm using this and I'm not?
Audience: So it would—the exclamation character that you put in, which then tells you bring it around non-null, which was then fooling the—that it actually is not, they would—you don't have to declare that on the actual property itself. So you've got string exclamation mark so now they would've string exclamation mark. And then string question mark.
Ian Griffiths: Okay. This is what I was wondering. They got three syntaxes. They have a different syntax for oblivious. So the old syntax means oblivious, right? I think there's a good reason they've not done this.
Because they did explore this. They did explore putting an exclamation mark on the end to mean definitely not null. So that was one of the options that was considered. The reason they've not done this is that in practice, most places where your code today defines an existing reference type property, field, argument, whatever, probably doesn't want to be nullable.
Why, and this has certainly been our experience. The vast majority, we actually don't change the code. We accept the change in meaning, because that accurately expresses what was already implicitly the case. And the number that we change is relatively small.
Audience: So I guess, this just feels a lot more scary than that mechanism.
Ian Griffiths: I understand where you're coming from. This feels more scary because you switch it on and everything changes. The reason they're doing this is they want this to be the new default. The problem with the Swift way of doing it is there's a very strong incentive to leave everything in the oblivious state.
And that's the problem.
Future Default Status
Audience: Will it be enabled by default in the future?
Ian Griffiths: Will it be enabled by default in the future? Microsoft has said they would like to do that. I am not convinced they will ever be able to do that. I don't know. I suspect they might try turning it on with new project templates. I think that would be the obvious first step. And then maybe it will become a default, but unless there's some big breaking change and maybe four years time, they'll decide to reinvent the project system again.
And that will be the big opportunity to do this, but let's hope not.
Attributes for Nullability
So let's just quickly go through the attributes since I'm talking about what you can do. There's—so you can constrain inputs to functions. Why on earth would you need an attribute to do that? Why don't I just write string or string question mark, depending on whether I want nulls or not?
Yes. Unless you're using generics. Prove to be a massive headache in all of this, because the compiler doesn't even know whether the T you're looking at is a value type or a reference type. And the syntax that we use with reference types all just goes to pieces as soon as you've introduced the possibility that it could be anything there.
And so we fall back to using attributes. If you want to write a function which is able to take a value and which can't take a non-null reference. Actually you can constrain the type argument with the new notnull constraint. That's one option. But there are situations where you might possibly not wanna do that because you might wanna say this argument's allowed to be null, that one's not, and they're both of type T.
Now you can't do that with a generic constraint. So then you can do it with AllowNull. And there's also DisallowNull to say, no matter what the person tries to pass in here as the type argument, I don't want to allow null in this particular place. So generally you only ever use these in generics because if you're not using generics, you can just specify it directly with the type question mark.
There are also, as you've just seen, attributes that influence the inference system:
MaybeNullWhen
- for conditional nullability based on return valuesNotNullWhen
- this is definitely not null if and only if the return value is thisNotNullIfNotNull
- this thing will not be null if this other thing is not nullnotnull
generic constraint - I don't mind if T is a value type or a reference type, but whatever it is, I don't want it to be null
Multi-Targeting Support
Question. What sort of scale have you been doing this on? How much code is it? How long, how many developers, what kind of—just give us a sense of the—it's, I wouldn't say it's massive. We've probably had six developers involved on the project, move things over to this so far.
So this is not what we haven't done is a kind of 200 person major project.
Migration Strategy
Now I'm gonna talk about some of the practical issues that we have encountered, actually trying to use these nullable language features in practice. So when you turn this feature on, you are gonna see something of this kind. Big pile of either warnings or in our case errors, because we generally run with warnings as errors turned on. The default is there'll be warnings rather than errors, but yes, loads and loads of things saying this doesn't look right. Now, the first thing you may have to ask yourself is actually, where do I even start with this?
We found that if there's various ways you can tackle this, you can—with any library, it doesn't have to be transitive. It's not like async was. So the async language features were one of these things where once you change it here, it rattles all the way up through the stack. So it's all or nothing.
So this is not like that. It is entirely possible to turn on the nullable references feature at the leaves of your dependency tree and not do anything else. It's also entirely possible to turn them on at the top as well. Anywhere in the food chain, you can switch this on at any time. We tried various things.
We actually tried a few different approaches. What happens if we just dive right into the middle? Is that better or worse than going right to the bottom? But typically we tend to find, unsurprisingly, that if you dive right into the middle, you get this sort of picture, whereas projects that down at the leaves of the dependency hierarchy tend to be slightly easier to cope with.
.NET Standard 2.0 Support
So what does work? What if you need to target .NET Standard 2.0, and most of our libraries do, either because they were already being used in projects that were targeting .NET Core 2.1 or even .NET Framework, or just because we've got customers who aren't ready to move to .NET Core 3.1 yet. Even now there are problems deploying web apps to Azure App Service with .NET Core 3.1.
So fortunately, you can do it. You can turn on the feature. You can say I'm using C# version 8. You will have to say, I'd like to use the latest version of C# in your project settings, and also enable nullable feature as well. But it lets you do this.
You can stick the question mark on types that are meant to be null. That'll work and you can even apply the attributes. As I mentioned earlier, there is this Nullable library, that's his name. Nullable, not something nullable, just Nullable in NuGet, which adds in definitions of all the attributes we talked about in the previous half, making it possible to do this.
The Serialization Problem
Here's the big one. Properties of serializable types. You will probably have quite a lot of this warning in your future. CS8618. It's actually a warning about a constructor. It's telling you that your constructor has not initialized all of your fields.
public class Class1
{
public string Name { get; set; }
public string? FavoriteColor { get; set; }
}
So one of those gets the squiggly. The other one doesn't and the squiggly in this case says non-nullable property Name is uninitialized. Consider declaring the property as nullable. That is not a helpful suggestion. So yeah, sure. Let's just make everything nullable. There we go. All your problems will go away.
Except now you'll get lots of errors when you try and use it and you'll have no information about what the intent was. Thus, throwing away one of the big benefits of this.
Solutions for Serialization
Solution 1: Make Properties Nullable
The very first thing you should do is say, is it actually nullable? Is that what I meant? When I wrote this, did I intend for this to be nullable? So one of the things we find ourselves doing is click on the symbol, Shift+F12, find all references. Is anything testing this to see if it's null? Oh, yes. Loads of code tests. Everything that uses this is either doing .? or == null or ?.
Obviously this must be a nullable thing, because everything expects it to be null. That's almost certainly a no-brainer. Just stick a question mark on there.
Solution 2: Constructor Initialization
So one solution to this is shown here. I've made all my properties read only, and I force them all to be initialized through the constructor. Actually, you don't have to do both those things. You can just say all the properties that are non-nullable have to be initialized through the constructor.
public class Configuration
{
public Configuration(string name, string connectionString)
{
Name = name;
ConnectionString = connectionString;
}
public string Name { get; }
public string ConnectionString { get; }
}
This keeps the compiler happy. Good, because that's the most important thing. And the good thing about this is it is actually relatively well supported in popular frameworks.
Json.NET is perfectly happy to use this. You can just give it the constructor. As long as the names match, it will use the constructor. Entity Framework also supports this, by the way. It's perfectly happy to initialize all of your properties through a constructor rather than through property initialization.
However, this is not the universal answer to this problem. For one thing, there are things that don't support this. If you are using Microsoft.Extensions.Configuration.Binder, the thing that can read stuff out of setting sources and plug them into properties of an object, that does not currently support constructor initialization.
Solution 3: The Long Hard Slog
We can make the field nullable and the property non-nullable. This is a means by which we can avoid the compiler error because the compiler is perfectly happy that this is not initialized because we said that the field is nullable.
private string? _name;
public string Name
{
get => _name ?? throw new InvalidOperationException("Name has not been initialized");
set => _name = value ?? throw new ArgumentNullException(nameof(value));
}
So that's an acceptable solution. And the flow control analysis will correctly determine that this just won't return if the field is null.
Solution 4: Abandon Hope
You can just say, you know what, this is too difficult for this file or there's something about this that means that I'm using this in a context where nullability awareness is just never gonna fly.
#nullable disable
That's always an option that might actually be the most sensible thing to do in certain cases.
Solution 5: Initialize with Defaults (Not Recommended)
It's really tempting to set—it's just, I'm gonna set that to an empty string, because I know the real one will be along in a minute. It works. And actually in cases where it definitely will be initialized, it works relatively well.
public string Name { get; set; } = "";
The problem is you lose the ability to detect that you've made a mistake. This is just the same thing. It's just the non-useful value now has a different name. It's string.Empty or "", rather than null. So this is not really better than disabling nullability.
Generics and Nullability
Generics, as I mentioned earlier, are problematic in this world because the reality of retrofitting this 20 years after the runtime was invented mean that there's a certain lack of symmetry here.
Nullable things are different if they are value types compared to how they are if they're nullable reference types. So what's T?
? It depends on whether the T is a class or struct. The code the compiler generates is different depending on whether it's nullable reference or nullable value.
// This doesn't work as you might expect
public T? GetValueOrDefault<T>()
{
return default(T);
}
A nullable type parameter must be known to be a value type or non-nullable reference type. Consider adding a struct or type constraint, another example of the compiler telling you to do the wrong thing.
Practical Considerations
Equality Comparisons
Sometimes you'll implement comparison. It's not just IEquatable<T>
, it's also just this .Equals
. Do you make it nullable or not?
public override bool Equals(object? obj)
{
// Implementation
}
And the answer is yes, you do, because the general presumption throughout all existing .NET code is that you're allowed to pass in null when you're calling something .Equals
.
Prefer Cast Over As
Also finally, I have a reason for insisting on one of my pet peeves. So when you cast—prefer old fashioned cast with parentheses over as
foo. So don't use an as
cast, unless you mean an as
cast.
// Prefer this when you expect the cast to succeed
var myObject = (MyType)someObject;
// Over this
var myObject = someObject as MyType;
myObject.DoSomething(); // Might throw NullReferenceException
So if you are writing as
today for casts you fully require to succeed, stop now, because you'll have fewer problems. When you come to port your code over.
Summary
So to sum up when you are considering moving over, decide what targets are, because that actually has an impact on how things are gonna work.
And if you are gonna target—do you need to target .NET Standard 2.0? You might be targeting it today. Do you still need to? Will you still need to in six months time? It might actually be worth just deferring this until you can say our entire world is .NET Core 3.1. If you are able to do that, life will be simpler.
So that's a consideration as to the timing of this sort of project. Then once you've decided you are going to go ahead and do it, I would recommend mostly starting from the dependency tree, but be prepared to revisit. As you learn more about what you should have done as you start to use the results, find the things that truly are nullable and mark them as such. Use the attributes to enable the compiler to infer as much as it possibly can about what is null and what is not. With properties, correct by construction is often preferable, but there are alternatives if you need them. Explicit implementation, maybe the most flexible fallback.
Okay. Thanks very much.