Replies: 147 comments
-
Given that whatever we get will be opt-in, I'd be happier with a two-state system and errors rather than warnings. And ten times happier with CLR enforcement of a type system guarantee of non-defaultness. |
Beta Was this translation helpful? Give feedback.
-
So if the diagnostics related to this feature are reported at assignment and use time, we have the following cases:
Each informational case is a diagnostic at "hidden" severity, which could be examined by tools or have the severity increased to gain knowledge about the amount of protection being offered by the use of null analysis. ¹ This case preserves safety, but addressing it could reduce the complexity of code. For flow analysis, we also update the type upon assignment:
For flow analysis, we may also have to merge two values:
Inputs to APIs which have not yet opted-in will be oblivious. Outputs from APIs which have not yet opted-in will be oblivious. |
Beta Was this translation helpful? Give feedback.
-
💭 Breaking changes over time are likely (caused by introduction of new warnings as the flow analysis strength is increased). This could be mitigated by making the compiler only responsible for the preservation of type information in signatures. The flow analysis for warnings could then occur as a set of analyzers which are versioned independently. |
Beta Was this translation helpful? Give feedback.
-
Since you labeled option 0 "Mads", let me address how it would handle your six scenarios. It is in keeping with design decisions last week, (and I will get the notes out soon), but goes beyond what has yet been discussed in LDM. The core of option 0 is that there's a "nullable reference types" feature that is on by default in C# 8.0, and does not cause breaking changes (except insofar as a referenced library uses Then there's another feature, which we can call "Only nullable reference types should be null", which does its best to ensure that you don't put null into reference types unless they are marked nullable. This feature is opt-in, because it will generate warnings on existing legal code. It is also somewhat unreliable, in that it doesn't come close to full protection: it has many known holes. Below, "opted-in" means you have that feature on by default. Here's how "The Mads option" handles each scenario: [scenario 1] var s = ""; // infer 'string'
s = null; // warning only if opted-in
string q = s; No problems here. When you opt-in, you start getting warnings about putting non into things that aren't nullable. Great! [scenario 2] var s = c ? "" : null; // infer 'string' if not opted-in
Print(s.Length); // therefore no warning if not opted-in If you haven't opted in to null in not-nullable reference types being a bad thing, you shouldn't be concerned about this behavior. We infer string, as always, and we put null into it, as always. The question is what should happen when you opt-in. I think this is an open design question, and we have two options:
While 1 is simpler, in that it avoids changing type inference under a compiler switch, it also leads to quite unintuitive and unhelpful behavior. I am more and more convinced that we should do 2. [scenario 3] var s = "";
s = null; // Print is external API, declared with Print(string!)
Print(s.F()); This example is a little cryptic: What's the problem exactly? In the Mads option, there is no notion of
[scenario 4] var s = Foo();
if (s == null)
FailFast(); // throws, but the compiler doesn't know that
Print(s.Length); // unfortunate warning, breaks compat Agreed that none of the proposals address this on its own, though I don't see that it breaks compat: Somebody added a It's still a problem though. We'd need to think of adjoint features to help us when logic relevant to the flow analysis is factored out into helper methods. Here it's the fact that [scenario 5] string s = null; // warning if opted-in This one's the textbook case for why we need an opt-in to warnings about assignment of [scenario 6] string s = c ? "" : null; // warning when opted-in Depending on the design decision in [scenario 2], this yields a warning for either one reason or the other, but only when opted-in:
I hope that helps clarify things, and how we can live with just two states: Nullable reference types and not nullable reference types. |
Beta Was this translation helpful? Give feedback.
-
By the way, we haven't discussed URTANN in the design meeting yet, but I am a staunch opponent of it. It adds another thick layer of complication to the whole feature, and I don't think it adds much value. For most APIs that haven't been annotated yet, an opted-in client should be ok with not passing null to them. Rarely does passing null to an API obtain something uniquely valuable, and when it does, there'll be an easy workaround (pass In the rare cases where this really does go against the API's pattern of usage, or where you simply can't deal with fixing your code up (even though you opted in), use the reference-level opt-out to silence warning from that library wholesale, until:
|
Beta Was this translation helpful? Give feedback.
-
@MadsTorgersen In Scenario 3, you mention the following:
However, if |
Beta Was this translation helpful? Give feedback.
-
Re TL;DR,
var s = ReturnsNonNullable();
var l1 = s.Length; // no warning
s = ReturnsNullable(); // no warning
var l2 = s.Length; // warning
s = null; // no warning
var l3 = s.Length; // warning or maybe even error Honestly, I might go so far to argue that the To that end I think I lean towards Option 2. And sorry for being thick but did I miss where "URTANN" was defined? Or is that just a placeholder for the boundary metadata attribute? |
Beta Was this translation helpful? Give feedback.
-
@sharwell, we aren't keen on doing the flow analysis as separate analyzers, because we think that their implementation and performance will benefit greatly from being integrated in the compilers. Also, probably most improvements of the flow analysis will remove warnings based on recognizing more details, or more specially annotated methods, or whatnot. However, as a middle ground, if we find the need to add additional warnings, maybe they can be added behind a "warning waves" flag, along with other "breaking" warnings that we may choose to add to a given version of C#. |
Beta Was this translation helpful? Give feedback.
-
This was my first thought, but then I realized we have a potential problem related to flow analysis. If a change in flow analysis rules results in code that was previously "oblivious" being treated as either Example: var value = LegacyType.GetValue();
if (value != null) // This is a bug that non-null reference types feature could catch someday
Environment.FailFast();
NewType.SendNonNullValue(value); In the above code, the result of Moving the analysis to analyzers decouples the maintenance of this complicated algorithm from the compiler's compat concerns, allowing much more rapid development of its capabilities. The time saved in maintenance issues will likely cover the investment cost of performance parity and then some. |
Beta Was this translation helpful? Give feedback.
-
@MadsTorgersen Thanks for the clarifications on the "Mads option" :-) It seems we still end up with three cases for symbols coming from a library:
We can say that we only have two cases: If we're going to compute/track those bits of information for symbols from libraries (regardless of how we organize them into two or three states), then we should consider allowing source symbols to take advantage of such opt-out as well. From prototype, Chuck found that migrating just one field in Roslyn (BoundExpression.Type) is hard enough that we'd likely want to stage the migration of our APIs. Whether this is done with some granular mechanism for opt-in like URTANN or some granular mechanism for opt-out instead, it seems likely useful for practical migrations. For scenario 2 ( |
Beta Was this translation helpful? Give feedback.
-
If the analysis which reports diagnostics lives as an analyzer, then the compiler type system can use the inferred type |
Beta Was this translation helpful? Give feedback.
-
@sharwell Your proposal seems orthogonal to that problem. We could also shift the two warnings ("warnings when you dereference a nullable reference type, or convert it to a type that is not nullable, except if flow analysis determines that it won't be null in that particular place in the code") to be tied to opt-in switch. Either way (shifting more warnings to be opt-in or shifting them to an analyzer), it means that you don't get some useful warnings by default even when you write code explicitly with new syntax |
Beta Was this translation helpful? Give feedback.
-
True, or at least possibly true. However, I have serious doubts regarding "improvements within the bounds of back compat" being a strong motivator for adopting the new feature. I believe people will care substantially more about 1) overall coverage¹ and 2) the migration story for large and established, but still actively developed code bases. If we disable warnings until opting in, I doubt anyone would bat an eye. ¹ Think code coverage tooling, except looking for instances of "oblivious" types in the CFG |
Beta Was this translation helpful? Give feedback.
-
I think there would be a break either in Roslyn or in APIs. And if the former is not an option, latter is the way to go. My opinion is based on option 4 but taking things a bit further-
|
Beta Was this translation helpful? Give feedback.
-
To copy @MadsTorgersen's phrase, I am a staunch opponent of this. To my mind, Mads' ideas on all six scenarios make total sense to me: they are exactly as I'd imagine the (non)nullable ref types feature would work. Everything else is just plain weird and confusing to me. For scenario 2, option 2, "Start inferring |
Beta Was this translation helpful? Give feedback.
-
One of the main purposes of the "damnit" operation It is a core goal of ours that you be able to adopt this feature in a gradual manner. |
Beta Was this translation helpful? Give feedback.
-
The |
Beta Was this translation helpful? Give feedback.
-
@DavidArno What if the flow analyzer is never smart enough to allow me to remove all |
Beta Was this translation helpful? Give feedback.
-
That would be another advantage to Nullable reference types: Mads's current thoughts, the analyzers could have configuration to support a variety of APIs that could be considered null-checks, both BCL and potentially our own. |
Beta Was this translation helpful? Give feedback.
-
There are
I'd really like option 1, but it may well be hard to implement in practice. |
Beta Was this translation helpful? Give feedback.
-
The bang! operator should be named as the "2B$ mistake" operator. I think R# approach with NotNull, CanBeNull, ItemNotNull attributes, accompanied with ContractAnnotation and some flow analysis should be enough. Thankfully you can't put attributes on local variables, in contrast with the no-nullable ref types approach. I believe that nullability is a contractual attribute and enforcing it on the type-system is wrong |
Beta Was this translation helpful? Give feedback.
-
If something is a contract, isn't the type system the ideal place to express it? |
Beta Was this translation helpful? Give feedback.
-
@jnm2 Like exceptions that might be thrown from a method, for example ? |
Beta Was this translation helpful? Give feedback.
-
That's exactly what these proposals suggest. The |
Beta Was this translation helpful? Give feedback.
-
That's probably the weakest possible example you could come up with, but funny you mention that. Indeed. I don't want to derail the conversation though. |
Beta Was this translation helpful? Give feedback.
-
@HaloFour You mean that for example Func<string?,string> is interchangeable with Func<string,string?>. How about ValueTuple<string?,string> with ValueTuple<string,string?>. C# can't put attributes to those. It may not affect the underlying type system at CLR level, but still you have to deal with all those exceptions using that bang! operator. Another problem to solve and that is a problem you have to solve for each statement and each assignment. Is not limited with what arguments the method accepts and what is the return value as is with R# solution or any other solution that uses contracts. @jnm2 Java has this and Java programmers, don't find it funny at all. It is an example of a contractual attribute implemented in a type-system, with disastrous (in my opinion) results |
Beta Was this translation helpful? Give feedback.
-
The attributes would apply to the delegate parameter itself, not the generic type arguments. This is identical to how As for checked exceptions, sure they exist in Java. Watching how annoying they are and how infrequently they're used appropriately is exactly why the CLR and C# don't support them. They omitted them on purpose. |
Beta Was this translation helpful? Give feedback.
-
To be honest, I don't understand how dynamic relates to nullable type definitions and metadata. My understanding was that the compiler will perform just a Type erasure of everything related to nullable ref types and leaving only some attributes to methods and method's attributes and without any "exotic" code generation. |
Beta Was this translation helpful? Give feedback.
-
I was referring to the application of that metadata. There is no For example, given the following method: public void Foo(Dictionary<string, Func<string?, int>> dict) { } The compiler will emit the following: public void Foo(
[Nullable(new bool[] {
false, // Dictionary<,>
false, // string
false, // Func<,>
true, // string
false // int
})] Dictionary<string, Func<string, int>> dict
) { } So yes, from the CLR's point of view, |
Beta Was this translation helpful? Give feedback.
-
Sure. I'm sure there will be an analyzer somewhere (or maybe something built in)that says that these should be warnings (or errors). You could then opt in whenever it suited you. I see no reason why any of this would be aggressively forced on people. The point is to make it so you can gently and inexpensively adopt this at whatever pace makes sense for you. If you don't do that, then people will not be able to opt in at all here. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Discussion started off from Chuck's PR following recent LDM discussion, and continued with Neal.
The way we initially tried to formalize this was with two or maybe three states (
?
meaning nullable,!
meaning non-null, oblivious), two warning scenarios (dotting and assignment), and three contexts (C# 7.0, C# 8.0, C# 8.0 + non-null flag for additional warnings).We started with two warnings:
?
type to a!
type is a warning?
type is a warningI was not clear what warnings should be produced for C# 8.0, and what additional warnings should be produced when the non-null compilation flag is set.
As we tried to clarify, two scenarios jumped out:
[scenario 1]
If
s
is a?
andq
is a!
, then this code compiles in 7.0, but breaks in 8.0.[scenario 2]
Problems:
?
type, then this code compiles in 7.0, but breaks in 8.0.Those two scenarios, plus some others below, make us think that all new nullability warnings must be disabled for 8.0, and only enabled when you set the non-null warnings on.
Some other scenarios we discussed:
[scenario 3]
[scenario 4]
I think all the designs we discussed produce a false alert on scenario 4 (FailFast method throws, but the compiler doesn't know it).
We fell back onto having three states (?, !, oblivious) instead of just two (?, !).
[scenario 5]
We'll want a warning here, so we need a third warning: 'conversion of null to !'.
[scenario 6]
We'll want a warning here, so we extend the better type rule for conditional expression. Just like
c ? 1 : null
should be inferred to have type nullable type (int?
),c ? "" : null
should get the nullable annotation. The""
is a!
, there is a 'null conversion to ?' on thenull
.This leaves us with three warnings (only when non-null flag is set):
The information from symbol tells you "what you can put into it". The information from flow analysis tells you "what you get out" (for local variables). For fields there is an open question.
Options
Option 0 (Mads)
This option strongly relies on the import setting per library (see details below). Somehow the warnings are not produced for types from opted-out libraries.
The upsides are that:
The downsides are that:
Option 1
For both fields and locals:
The downsides are that:
Option 2 (Neal)
Same as option 1, but with a difference for locals ([URTANN] does not apply to method bodies):
The upsides are that:
The downsides are that
Option 3
Same as option 2, but URTANN applies to method bodies.
URTANN dictates how "string" is interpreted in a method.
The upsides are that you get helpful warnings in method bodies, and "string" means the same in APIs and in methods.
The downside is that the migration becomes more difficult (you need to add ? on locals).
Option 4 (Julien)
Another way to keep "string" to have the same meaning between APIs and locals, is for APIs to align with the rules for locals.
The upside of this option is that "string" has the same meaning everywhere (in field and in local).
The downside is that we introduce a syntax for ! and APIs will be littered with it (but not method bodies).
Importing libraries
There is also an orthogonal discussion on what to do with types from libraries. It can be bolted onto the various proposals.
There is a proposal to import a library with some opt-out, which means that every type is treated as oblivious.
Open issues
Open issues we mentioned but didn't discuss in details:
Beta Was this translation helpful? Give feedback.
All reactions