Nullable strawman #790
Replies: 66 comments
-
So there's a conscious decision here that if you null-check a property that you can then treat that property as non-null despite the fact that the value could certainly be updated to I mean, it does make sense, it just immediately raises a red flag in my mind. |
Beta Was this translation helpful? Give feedback.
-
This feels wrong. If p.FirstName can be null then we should go where it's declared and say Isn't the principle that we're going to assume the code does not have any null-bugs and then the compiler will take a second look and let us know what it found? Are we saying here that we may know "better" than the compiler? |
Beta Was this translation helpful? Give feedback.
-
That's the point of the |
Beta Was this translation helpful? Give feedback.
-
But assuming the compiler is correct, doesn't that mean "just let me add this bug?" |
Beta Was this translation helpful? Give feedback.
-
In that case, yes. The assumption is that the compiler won't always be correct and that it's easier to give the developer a tool to override the check rather than to force the developer to have to explicitly eliminate the possibility of |
Beta Was this translation helpful? Give feedback.
-
The way I see it, the expression The only one I can really think of is a boundary-case: //Assembly A.dll:
[assembly: NotNull] //strawman for the attribute indicating that this assembly has the nullability analysis on
public static async Task Foo(object optional) //oops, this parameter was accidentally not adorned when this version of the lib shipped, but the implementation can gracefully handle it if it's 'null'
{
//....
}
//Assembly B.dll:
[assembly: NotNull]
await Foo(null); //would now be a false-positive warning And even this might be incredibly contrived. |
Beta Was this translation helpful? Give feedback.
-
Famous last words... 🙈 There are already mechanisms for suppressing warnings. If |
Beta Was this translation helpful? Give feedback.
-
Apple Swift already has such an operator (as well as a third "oblivious" type) which I believe was added largely to address compatibility with the existing Cocoa libraries. So we should have plenty of evidence as to whether this operator poses more of a problem than not. let possibleString : String? = SomeFunc()
let forcedString : String = possibleString! One big difference in Apple Swift is that it definitely does throw if the value is |
Beta Was this translation helpful? Give feedback.
-
That's a good point. If |
Beta Was this translation helpful? Give feedback.
-
Some more detailed scenarios I can think of. Scenario 1 I am a library writer that moves to C# 8.0. My library both accepts and returns values that can or cannot be null. To help people that use my library avoid mistakes, I annotate the types of nullable values with I'll indicate the version of the library without an opt-in flag Version A and the other one Version B. Scenario 2 I am a library consumer that moves to C# 8.0. I have no idea what all these nullable reference types are about, so if I upgrade the library to either Version A or Version B, I should not get any warnings. Scenario 3 I am a library consumer that moves to C# 8.0. I read a bit about these nullable reference types and start adding If I upgrade to Version B, though, I expect to receive warnings when I try to pass a Scenario 4 I am a library consumer that moves to C# 8.0. I read a lot about these nullable reference types and have finally decided to flip the opt-in switch. I haven't upgraded any libraries, so anything I receive from an external method is... implicitly nullable. I don't want to treat everything as explicitly nullable, since that would mean I would have to mark every type with a I upgrade to Version A, and now all these nullable returns are starting to trigger warnings in my code. It's okay, I opted in, I know what I'm doing. I can still pass a I upgrade to Version B, and now all the checks are on. To summarize (in actions, the client is on the left, the library is on the right):
|
Beta Was this translation helpful? Give feedback.
-
The term that the team has been using in these past few issues for this scenario is "null-oblivious", which is essentially the current behavior in that the compiler has no special knowledge about passing or receiving In the case of a return value, you could be explicit about it at your own call-site: string? s = obj.MaybeGetSomething(); |
Beta Was this translation helpful? Give feedback.
-
You refer to using I appreciate the team have to balance keeping the feature simple on the one hand, with making it have as little impact on existing code as possible on the other. But at the moment, they seem to be compromising the feature far too much in pursuit of minimal impact on existing code. |
Beta Was this translation helpful? Give feedback.
-
@DavidArno as far as I understood the LDM examples, yes. Marking types as nullable will always be allowed. Opting in will only change the meaning of unadorned types. |
Beta Was this translation helpful? Give feedback.
-
My proposal in #727 is intended to eliminate the need to use
This statement is a major red flag for me, as it intentionally undermines the soundness of the feature right from the start. If some users are going to want flow analysis to consider null check against values potentially aliased by other threads, I have a hard time seeing a solution that doesn't involve analyzers. The compiler can expose the declared types of all symbols in the API available to analyzers. Everyone is essentially in agreement regarding what this means for exposed symbols, and the remaining case of Moving the flow analysis rules and reporting to a set of analyzers has massive advantages for this feature:
¹ Even if |
Beta Was this translation helpful? Give feedback.
-
I suspect I'm repeating myself here, but having |
Beta Was this translation helpful? Give feedback.
-
The point is to help catch and eliminate a large though not 100% set of cases that people encounter today. Don't let perfect be the enemy of the good. |
Beta Was this translation helpful? Give feedback.
-
The benefit is incremental. As more code becomes compliant and adopts the nullability metadata the more consuming code will be informed as to when those API boundaries can be null. Given that currently you have no way to know this it can only be a net positive.
You can only avoid the warning by ensuring that the value isn't No, it's not perfect. It's not possible to make it perfect. But it's possible to make it better than nothing and to guard against the common causes of NullReferenceExceptions. I kind of look at it like Java generic erasure: purely compiler candy and can very easily defeated via pathological code, but it offers guardrails that most of the time gets things right.
The syntax means "nullable". That will be true of both reference types and value types. Yes, the mechanism is different, but most of the time that won't matter. I'll note that you're commenting on an issue opened specifically to explore how these changes affect real-world code as well as to pivot to make it more effective and less obnoxious. If you feel that you have scenarios where this feature will not fit the team will certainly be interested in examples. |
Beta Was this translation helpful? Give feedback.
-
So, in a brave new C# world without NullReference exceptions you open up a C# code and see some class declared as |
Beta Was this translation helpful? Give feedback.
-
The team would agree with you. Dialects are a bad thing and require massive amounts of justification. And given that |
Beta Was this translation helpful? Give feedback.
-
I really don't understand. This feature is for compiler and Visual Studio warnings only? Maybe we should make better compiler or code analysis in Visual Studio to give us these potential null reference exceptions warnings for all reference types (without these question and exclamation marks that you want to add)? Resharper can do this quite good for a long time. Why do you need to change the language and introduce such a great confusion? Does java community have similar plans? You know what will be the most obvious consequence of this feature - developers will use String.Empty instead of null, will create static singleton Empty objects for classes etc. This will give us even more strange errors. |
Beta Was this translation helpful? Give feedback.
-
Resharper requires that the developer use attributes all over the place to explicitly mark parameters as nullable/not-nullable. That's a heckuva lot more verbose than a simple |
Beta Was this translation helpful? Give feedback.
-
If you take a look at your code example on SharpLab, you'll see that it's not a valid syntax when NRTs are turned on. It warns that For it to be valid C# 8 code, it needs changing to: class A { public string? SomeProp {get;set;} } at which point, it's obvious that it's C# 8+ code. Without that |
Beta Was this translation helpful? Give feedback.
-
So as I mentioned before most of the code will require changes because property object initialization are one of the most common things used. |
Beta Was this translation helpful? Give feedback.
-
That seems like a great thing for those developers to do.
Why would it give them strange errors. This is the standard 'introduce null object' pattern, and it's something that's really good for people to do (and which these types of language features will help push people toward). You make this sound like a bad thing, but it sounds like a great outcome if that actually happens! |
Beta Was this translation helpful? Give feedback.
-
Why? TypeScript has already demonstrated that this approach works quite well and can be added incrementally to an ecosystem. Having the feature be embedded in the language is also super nice so that you don't have to do things like add This is something that will be used a ton. When something is used that much, and is involved in so many parts of the language (honestly, this likely touches about every bit of teh language), having dedicated syntax makes a lot of sense. Note: one of the other really good things here is that the language changes are forward thinking as well. Specifically, the hope is that far in the future |
Beta Was this translation helpful? Give feedback.
-
In my experience NullReferenceExceptions always occur well... unexpectedly. Most of them happen when we deal with external source, like databases, values from xml or json etc. That's why it is so hard to find these errors during testing. And that's why it is the most common mistake in production code. This feature assumes that we know beforehand what variables, fields, properties will be null at runtime and what will be not. You can't forbid setting null to a reference type. So in production you can get null, well..., in any member of reference type. And this feature can't stop it. It just protects only some parts of your code from this error. But you must and will deal with nulls in c# and java, because they lie in the heart of type system of these languages. You can't just forbid to use them and think that it solves all the problems. |
Beta Was this translation helpful? Give feedback.
-
Your own experience doesn't match the experience of others. For example, even in Roslyn itself, null ref exceptions are likely the most common thing we run into. And it commonly happens even with code we have total control over.
Indeed. However, that's why this feature helps. It draws attention to the location where these may occur and it motivates the developer to update the code accordingly to the prevent that from happening.
No. it does not assume that. Indeed, that's the very point of it. It helps call out the issues it sees, and it lets you then make decisions about it. If it says: here's a potential problem, it then passes the job to you to figure things out. You don't hae to know beforehand. You can figure things out when necessary. And, if you can't figure things out, you can just say things like:
etc. etc.
And, importantly, this feature will still limit that to a lower set than you get today. As mentioned before, "Perfect is the enemy of good". Note: it is likely the case that if you want perfect, you could get it as well with some additional out of band analyzers that then require explicit steps to be taken to totally ensure no null ref exceptions at all. For the core language itself, such a feature would likely make things too onerous. But you could certainly invest in that yourself if you found it valuable.
Great! That's the idea. And now that that part of your code is protected, you have less code that will have these problems.
No one thinks this. This feature is about helping with classes of these issues, and making things less likely. It's been known since day one by everyone involved in the design here that this will not solve all the problems. |
Beta Was this translation helpful? Give feedback.
-
Serialization libraries can take advantage of the nullability metadata to now enforce non-nullability at the point of deserialization rather than allowing partially constructed and invalid data to sneak half-way through your application wreaking havoc. That sounds like a win to me. Yes, this feature is a compromise. The perfect is the enemy of the good, and in this case the perfect is entirely unattainable anyway. You're welcome to ignore this feature and not use it within your codebases. |
Beta Was this translation helpful? Give feedback.
-
Sure. But having some parts protected is better than having none protected. I'm really struggling to follow the logic from some here of, "this feature isn't perfect therefore this feature is bad". It's better than we had before and - if the journey through to "Bestest Betterness" (#98) is anything to go by - the language team will continue to improve the feature over the course of future releases. |
Beta Was this translation helpful? Give feedback.
-
When I'm designing and implementing a new type, deciding whether those variables, fields and properties should permit null is absolutely a part of my design process. After this feature ships, the additional expressiveness of C# will let me concisely indicate where nulls are expected and where they aren't. Using that information, the compiler (and by extension, my IDE) becomes my ally, pointing out areas where I haven't paid enough attention, such as where a potentially null value is being treated as definitely not null. |
Beta Was this translation helpful? Give feedback.
-
Nullable strawman
Update: The strawman has been updated as of Aug 24, 2017, based on design decisions up until this date.
For a number of design meetings we've been working off of a strawman proposal for nullable reference types, to see that we get through all the design issues. I'm posting the original strawman here, with the expectation that it will evolve significantly over the next couple of days, as I work in the LDM decisions.
That means that you comment at your own risk; the strawman will change and your comment may no longer apply. Consider commenting on the design note announcements instead, as they come out. Also, there is already a discussion of key issues in #788.
As the strawman stabilizes, I'll fold it into the original proposal (#36).
The strawman breaks reference nullability tracking into a couple of feature proposals:
?
postfix on reference types, and uses flow analysis to guard (with warnings) such references from being directly or indirectly dereferenced without a null check. This is a breaking change insofar as nullable reference types may be implicitly introduced through type inference on existing code, which therefore need to be guarded by an opt-in mechanism.!
operator)Goals
On top of the actual feature value, the following are goals:
Feature: Nullable reference types
Nullable reference types are reference types annotated with a postfix
?
to indicate that null is an intentional part of their domain.Warning on dereference
In source code, dereferencing a nullable reference leads to a warning:
Warning on conversion
Similarly, implicitly converting a nullable reference to a reference type that is not nullable yields a warning
This way, the nullable reference is protected from being unduly dereferenced even indirectly without warning.
Flow analysis
The compiler tracks variables through code flow to see when they can be considered non-null, based on surrounding checks. In those cases, the value of those variables is still considered to have the nullable reference type. However, the two types of warnings explained above (direct and indirect dereference) are suppressed on the value.
Discussion: This is a reversal from the original strawman, which stated that the value of those variables would change type to the underlying non-null type, when known to be non-null. The exact choice of mechanism here has an impact on type inference:
Tracked variables
The flow analysis tracks parameters and locals, and looks at tests and reassignments to determine the "null state" of a variable at a given point in the code. This is very similar to how definite assignment analysis works.
Discussion: This is a reversal from earlier, where we also intended to track "dotted chains" rooted in parameters, locals and
this
; e.g.p.MiddleName
above. Parameters and local variables are only likely to change as the result of direct manipulation in the source, and nullability can therefore be tracked with high confidence. Properties and fields, however, seem more likely to be modified through other means, e.g. as a result of a mutating method call, or the meddling of some other thread.For the purposes of the prototype we'll take the more restrictive approach. We realize that it is useful also to track the nullability of members directly, will be common in existing code and most likely it will still be right most of the time. However, we'll start without it, and see how badly we need it. If we do, we'll think of mitigations to the decrease in confidence.
Local variable declarations
When a local variable is declared with a type, it simply has that type, whether nullable or non-nullable. In addition, a local variable of nullable reference type, once it is definitely assigned, has a null-state determined by the flow analysis described above.
When a local variable is declared with
var
, its type is inferred from the type of the initializing expression as always. If the type is a nullable reference type, an initial null state is inferred from the initializer:Discussion: We've had a lot of debate about this. What we've landed on is simple and orthogonal, but it is likely to lead to a number of warnings on existing lines of code, where local variables are declared with explicit non-nullable types (when nullable wasn't an option), yet are appropriately checked for null in subsequent code.
We will need tooling to help fix these situations in bulk.
Array types and constructed types
There's an identity conversion between two types which are the same modulo nullability of reference types. This goes for array types and constructed types as well. However, there will be warnings on some of those conversions if the addition or removal of nullability is unsafe.
Essentially the rule is as follows:
T
toT?
T?
toT
Feature: The null-ignoring operator
The null-ignoring operator is a postfix
!
operator applied to expressions, also sometimes referred to as the "dammit operator". Its effect is to suppress nullability warnings arising from dereference or conversion of the expression.Discussion: There's no runtime null check here. The whole point is to avoid a check and tell the compiler "I know what I'm doing". One valid use of the operator in fact is to assign a potential null value to something that isn't marked as nullable.
Feature: Null warnings
The nullable reference types feature provides warnings to prevent the newly added nullable reference types from being unduly dereferenced. The null warnings feature helps ensure that references that are not explicitly nullable, are in fact not null. In other words, it provides warnings when null values of non-null reference type are created.
There are many code patterns that we can consider yielding this warning on, each trading safety against convenience. In the following we list out increasingly harsh warnings.
The warnings can be silenced with the
!
operator, by using nullable reference types instead, etc. The point of them is to make you think about how to mitigate the danger, and how to make the mitigation explicit in your code.Construction
There's a null warning if a field of non-nullable reference type is not assigned during construction (since that will leave it with its default null value).
Conversions
There's a warning if a
null
literal ordefault
expression is directly converted to a non-nullable reference type.If a null value of a non-null type was indeed intended (maybe to null out a no-longer-needed reference to make an object eligible for garbage collection), the
!
operator can be used:Default expressions
Default expressions with explicit non-nullable reference types
default(string)
themselves yield a warning, as they produce a null value of a not-nullable type.There is no way to suppress the warning, since the default expression itself causes it, not dereference or conversion of it. If a null value is desired, use
null!
ordefault!
instead:Array creation
When an array of a non-nullable reference type is created, all the elements will initially be null. The only reasonable place to warn about this is when the array - and the null values - are created.
There is no currently proposed way to silence the warning. There probably should be. A warning free way to have the same effect is to create a
string?[]
and then convert it to astring[]
using a!
to silence the warning:Structs with fields of non-nullable type
Structs can be created without going through a declared constructor, all fields being set to their default value. If those fields are of non-nullable reference type, their default value will still be null!
It seems we can chase this in three ways:
!
operator works here, since the whole point is you don't control initialization from user code.We haven't settled on an approach here, so for now we leave the hole open with option 1.
Type inference
Nullable reference types and null literals should contribute "nullness" to type inference, including "best common type" situations. If any contributing type or expression contributes nullness, then the inferred type should be nullable.
This changes type inference on existing code, in a way that can lead to new warnings on it:
Parallel to the type inference, expressions such as the conditional operator should propagate null-state of the branches:
Type parameters
Nullable reference types are allowed as type arguments to unconstrained type parameters, and where constraints allow it.
Constraints
The following constraints can be satisfied by nullable reference types without warning:
IDisposable?
,Person?
)class?
constraint, which, likeclass
, requires the type argument to be a reference type, but allows it to be nullablenew()
constraintFurthermore, a nullable reference type will satisfy the following constraints, but with a nullable warning:
IDisposable
,Person
)class
constraintThere is no constraint that requires a reference type argument to be nullable: Any constraint that is satisfied by a nullable reference type is also satisfied by its non-nullable counterpart. If a guaranteed nullable type is required, use a guaranteed non-nullable reference type parameter and apply
?
to it (see further below).An unconstrained type parameter is essentially equivalent to one constrained by
object?
. The type constraintobject
is now allowed, since it is no longer the "default" constraint.Type parameters
For a type parameter
T
that is unconstrained, or whose constraint can be satisfied by a nullable reference type without warning, the body of the generic type or method will have to assume that the type parameter can be either nullable or nonnullable.This means that it needs to yield warnings on behavior that would be unsafe to either.
In case
T
is a nullable reference type, we track the null state of variables of typeT
and yield warnings on unguarded dereference or assignment to non-nullable reference type:On the other hand, in case
T
is a non-nullable reference type, we prevent creating default values of it, or assigning null values to it.As usual,
!
can be used to silence the warnings:A type parameter
T
that is constrained a) with a non-nullable reference type constraint, and b) to be a reference type (with a class type constraint or aclass
constraint) is known to be a non-nullable reference type. It can be annotated with?
, and aT?
is free to be null:Defaultable types
A concept we are curious about in principle, but haven't worked out the details of in practice, is the idea of expressing a "defaultable" version of a type parameter. In our discussions we have been overloading the
?
notation for this, but it is not clear that this is the right syntax:The idea is that
T?
means the same asT
, except whenT
is a non-nullable type, where it would mean the nullable version of that.We want to keep this idea around, but aren't ready to act on it.
Opt-in
A number of the features described would lead to breaking of existing code in the form of new warnings. The simplest "solution" to this is to simply put all the warnings under a big switch. This formally dispenses with the breaking change problem, but is perhaps not very helpful or granular. Still: you could turn it on, fix some warnings, then turn it back off. Eventually you've fixed all the warnings.
This big switch is what we are initially going to do in the prototype, so we can start to understand which more granular options would be most needed and useful. Let's summarize the considerations we've had so far about more granular opt-in.
Warnings and breaks
The warnings we've introduced are:
Here is a set of breaking scenarios:
A - Preventing null in non-nullable reference types:
null
ordefault
being converted to non-nullable reference types (or type parameters that might be).default(T)
andnew T[e]
whereT
is a non-nullable reference type (or a type parameter that might be).These are all associated with warning number 1. This is the most unambiguously breaking type of warning, and we could consider giving it its own switch.
B - Inferring nullable reference types in existing code:
?:
expressions and invocations of generic methods with inferred type arguments may now implicitly yield nullable reference types.One granular option is to have a switch that triggers the inference of nullable reference types.
C - Discovering nullability in referenced libraries
?
annotations in referenced assemblies will have been ignored by older compilers. Upgrading to C# 8.0 will discover those annotations and trigger associated warnings.One way to avoid these would be to allow APIs to be referenced in "legacy mode" where warnings triggered by their annotations (or lack thereof) are suppressed.
Quasi-breaks
Warning behavior that isn't technically a breaking change can still be insufficiently granular, making it cumbersome to adopt the feature.
D - Unannotated types in libraries are treated as non-null:
The "legacy mode" mentioned above would give the client a way out of this situation. But another viewpoint is that the library itself should signal whether it considers unannotated reference types to be non-null. If not, then the client will suppress warnings from them.
Such library-side opt-in could even be with an attribute that can be applied to parts of the code. That would allow a gradual way of rolling out "non-null-ness" across a library surface area.
E - Library upgrade:
The "legacy mode" would provide a way for a client to postpone the new warnings until they are ready to deal with them.
F - warnings on harmless code:
This is likely to be common in well-tested code. You can imagine a transition mode where unannotated locals are allowed to contain nulls, and their null-state is tracked. This will reduce warnings to only where there's actually unchecked dereferencing and hence possible null reference exceptions. Such a mode could let you find bad bugs first, and clean up your code later.
G - Levels of harshness
Some warnings are harsher than others. This is particularly true of type 1 warnings, which could apply to creation of arrays with non-null element types, and structs with non-null fields. There may be one or more levers where you can opt in to them, depending on what your trade-off is.
Null-oblivious types
Reference types in C# today are essentially null-oblivious. They allow null values and dereferencing to be indiscriminately interspersed. This is the root of the problem that the feature sets out to solve.
Some of the opt-in mechanisms suggested above - specifically the "legacy mode" (client-side opt-out) and the "unannotated reference types are non-nullable" attributes (library-side opt-in) - imply that some warnings are ignored. This means essentially that some types continue to be treated as null-oblivious.
Thus, if we embrace those opt-in mechanisms, null-oblivious reference types become a third state "between" nullable and non-nullable, and we need to define the interactions between it and the others relative to conversions, type inference, etc.
Beta Was this translation helpful? Give feedback.
All reactions