-
ProblemConsider this code:
With In order to have all variables nullable, I'd either have to change the return type of ProposalIf ConsiderationsThis syntax can theoretically be allowed for reference types and value types. For both cases, For value types, the behavior will be:
which is equivalent to
For reference types, the question mark behaves the same like the one in
|
Beta Was this translation helpful? Give feedback.
Replies: 78 comments 83 replies
-
This will no longer produce a warning in C# 9.0 as the language team has decided to always treat |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
If the selected branch is "master" then it should be using the C# 9.0 bits. I do not see the warnings there, but I do see the warning if I change the branch back to "Default".
The flow analysis of NRTs is pretty good at determining when it's safe to dereference the variable and it's already a core aspect of the experience. The behavior of the variable will remain the same. If the method returns a non-nullable, the compiler will consider the variable to not be null and will not warn on dereferencing. If you assign string SomeMethod() => "";
var s = SomeMethod();
var length = s.Length; // does not warn
s = null; // does not warn
length = s.Length; // warns
if (s != null) {
length = s.Length; // does not warn
} The nullability annotations aren't that important on locals, the compiler will infer from usage. It's at the boundary where the annotations are much more important because the compiler can't know the expected state of the value crossing that boundary.
That is what the language team thought, too. But the experience and feedback from that decision is that the behavior is suboptimal, especially around existing code, so the team decided to reverse that decision. The warning on assignment isn't particularly helpful if that variable isn't dereferenced again without checking for nullability. Anyway, here's the meeting notes from when the language team already considered the addition of https://github.com/dotnet/csharplang/blob/master/meetings/2019/LDM-2019-12-18.md#var |
Beta Was this translation helpful? Give feedback.
-
@HaloFour the behavior you link to is enabled for C# 8 as well, provided the compiler version is new enough. |
Beta Was this translation helpful? Give feedback.
-
I'd rather opt into being pedantic about forcing you to express what you wanted by using Thanks a lot for the link though. |
Beta Was this translation helpful? Give feedback.
-
You can always write an analyzer yourself to find and report thsi if you want that extra restriction :)
We looked into this, and it was effectively awful. So much code that was null safe and was written in idiomatic C# was then flagged as needing additional work. This violates my own goal for the feature which is that good, existing, coding practices should not feel like something that is now punishing you. |
Beta Was this translation helpful? Give feedback.
-
Not every programmer has the skill or time to write custom analyzers to mitigate shortcomings of the native compiler. ;)
I understand your point and see where you're coming from. But quite honestly - with this behavior for If If it bothers me that I must write And maybe, after killing |
Beta Was this translation helpful? Give feedback.
-
The goal of NRTs is to avoid NREs, not nulls. A null assigned to a local that is never dereferenced is innocuous. You can argue that this makes annotating locals with their nullability is pointless if flow analysis can make this determination, and I agree with you. The annotation makes sense at the boundary where flow analysis can't track what happens. But I disagree that this decision somehow weakens NRTs or makes them irrelevant. NRTs were never intended to be a change to the type system, they're meant to be guardrails, and they need to be a very pragmatic solution so that can be practically applied to tons and tons of existing code. |
Beta Was this translation helpful? Give feedback.
-
No, that is not how the design works. You must, at minimum, turn on the flow analysis with |
Beta Was this translation helpful? Give feedback.
-
Question 1: What is the point of having a wrong warning "Dereference of a possibly null reference." in line 2 instead of a correct warning "Expression is always false." in line 1 with In other words: if NRT are meant to be "guardrails", then why are those rails installed in a way that it is easy to make them ineffective (by adding a useless null-check), and if we deemed them sensible nevertheless, why would I have to explicitly ask for the guardrails to be installed? |
Beta Was this translation helpful? Give feedback.
-
The guardrails exist to steer you away from NREs. That may warn spuriously, but it won't NRE. More interesting would be non-pathological cases where the compiler doesn't warn and can result in an NRE. |
Beta Was this translation helpful? Give feedback.
-
Consider instead a public API: It's annotated as not accepting null, but of course someone could be calling you from a nullable disabled context or they could put a |
Beta Was this translation helpful? Give feedback.
-
I disagree. The cost to doing something like this is extremely small. So either this is a significant issue for you, in which case writing hte analyzer is a cheap and easy way to solve the problem. Or it isn't an significant issue, in which case it doesn't really matter if it solved :)
This was never the point. The point was to diminish the quantity of null reference exceptions, and to introduce a system that had a low amount of false positives and false negatives.
It would be fine to introduce an analyzer to give such a warning. No one is stopping that. |
Beta Was this translation helpful? Give feedback.
-
If that is so, this is the root cause of my problems with that behavior, and apparently some others. Because everything that has been named, said, documented or marketed about this feature promises exactly what you say "was never the point". Just read the introduction of the official documentation:
What's even worse, the documentation continues to compare it to the "earlier" behavior:
Comparing this to the current behavior:
Statements about when a reference "can be null":
Statement about when a refereces "is assumed to be not null" (in fact, the feature was marketed to get rid of "assume" and introduce "declare", which is stronger):
I hope you can see what a gigantic let down this feature appears to have become, compared to what was promised. I want you to take a step back and not look at why a decision was made, but look at the bigger picture of what the feature now brings to the table in comparison of what is said it brings to the table. Optimizing the behavior under the premise of "show as few warnings as possible" and "signal 'everything is fine, you don't have to change a thing' as long as possible" is creating a meaningless tool. NRT/NNRT promised and is marketed to allow me to get rid of vague assumptions and replace those with rigid declarations. But everything that has been done ever since were softening those "declarations" back to "assumptions". These new assumptions may be smarter than before, which is a fine thing in itself, but why even introduce "better algorithms" as a new feature, not as improvement of the already existing feature. I'm genuinely disappointed. It was announced as "Declare if a reference can be |
Beta Was this translation helpful? Give feedback.
-
In short:
|
Beta Was this translation helpful? Give feedback.
-
Or you can make those warnings into errors. |
Beta Was this translation helpful? Give feedback.
-
The compiler enforces this by telling you about the issue. You can choose to have that be blocking or not. But we leave that choice to you, we do not want to enforce that on an astronomically large ecosystem with thousands and thousands of groups and projects all with varying needs in terms of how they can adopt this concept. |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
I think you're confusing "blocking" with "enforcing". Our goal of NRT was to be non-blocking for users. Any reasonable sized project will take a fair amount of effort to move to NRT. In roslyn, we've been continuing to NRTify our whole codebase, and it's been taking months and months and months. It's imperative that people be able to make this sort of progress in an enforced, but non-blocking, fashion. That's what we provided and it was very intentional and necessary to be viable. OUr defaults seem to not be what you want. However, you can change the defaults here to better suit you if you want. |
Beta Was this translation helpful? Give feedback.
-
I am satisfied with this result, there are some quirks and it still sucks that However I really honestly do recommend to change the documentation to not use the phrase "enforce a rule". I'm convinced the majority of all readers will completey misunderstand that. Ask 100 C# programmers who aren't involved in compiler development:
I bet at least 90 will answer "A". 🙂 |
Beta Was this translation helpful? Give feedback.
-
I have to agree with @LWChris completely on this one. I didn't have a particularly strong opinion on the introduction of non-nullable types, but if you're going to do it, why make something as fundamental as the |
Beta Was this translation helpful? Give feedback.
-
NRT is opt-in specifically because it involves a migration for any existing codebase. The goal is to eventually get to 100% adoption where NRT is the default behavior, but that will take time. Given the vast impact NRTs has on existing codebases compromises had to be made so that it would be easier to adopt. When someone is evaluating opting-into NRTs for their codebase if they see thousands of warnings they will be less likely to consider the effort necessary to carry out the migration, so it's desirable to ensure that those warnings only appear in cases where NREs are more likely to occur. The feedback from the first release is that inferring the nullability based on the RHS resulted in way too many extraneous warnings. While warning on assignment may help you identify where a The perfect is the enemy of the good.
Flow analysis only really works well within the body of a method. It becomes infinitely more complicated across the boundaries between methods. Sure, the compiler could have inferred the metadata on the boundary based on use, but at that point the nullability becomes a part of the contract of that method, and the team is pretty consistent on the belief that said contract should be explicitly defined. As for locals, I agree, having to explicitly notate which are nullable has a lot less value and could have been driven entirely by flow analysis. |
Beta Was this translation helpful? Give feedback.
-
I hope one day we get a v2 of non-nullable reference types with with real non-nullable types at runtime level. That would solve a host of confusions and complexities that arise because flow-analysis nullability is different from underlying nullability. It would also remove the need for run-time null checks of non-nullables and mean |
Beta Was this translation helpful? Give feedback.
-
@HaloFour Of course turning on nullable reference types for existing codebases is likely to create a bunch of warnings. But it's opt-in. I guess I misunderstood the purpose of this, because I assumed it would be for new code. Why not turn non-nullables on for new code, and use flow analysis on existing code? |
Beta Was this translation helpful? Give feedback.
-
The goal is to make it work across all codebases and to make it relatively easy to adopt for maintainers of existing projects. Avoiding spurious warnings is intentional, especially if they arise in common cases. |
Beta Was this translation helpful? Give feedback.
-
Well I just spent a good hour or so of my time debugging and then googling why a set of circumstances resulted in a runtime null deref that should never have happened. I would definitely prefer it if This is not what you might call "easily guessable" EDIT: If changing |
Beta Was this translation helpful? Give feedback.
-
Somewhere in the chain of custody, somebody is giving a guarantee they shouldn't give, and somebody else is believing that guarantee. In extremis, if the analyzer cannot guarantee that the object handed back by the deserializer actually conforms to the class' invariants (Here: All reference types have non-null values) it should not believe the deserializer's claim to have created such an object. Alternatively: I, as the user, should not believe the compiler/analyzer's assurance that those non-nullable refrences don't contain null - but if those assurances aren't credible, I'd rather not receive them in the first place. The actual solution I ended up with was marking all the references in the object as nullable - but I'd certainly have appreciated a warning or compile error instead. |
Beta Was this translation helpful? Give feedback.
-
I got here because (like many others) I was surprised by the seemingly changed behavior of Now I am starting to read that people are abandoning the use of I totally understand that you cannot have a RHS handling of @CyrusNajmabadi Any thoughts on adding an additional flag for getting back the old behavior on variable assignments declared with |
Beta Was this translation helpful? Give feedback.
-
I would like to chime in again after all this time to report what I think about it now: The biggest trouble back when I wrote this came from the stark difference between what the documentation claimed NRTs would do, and how they actually worked. You know, statements like "The variable can never be assigned the value null", but in reality you absolutely can do that and it even compiles just fine with merely a warning. These statements are long-gone since the docs have been changed. I was (and would still be) hyped for a way to get what was promised back then, i. e. "can never" means "can never successfully compile, like As for the current state of NRTs - I think it works "well enough" for about 80% of my code. The pain points are
|
Beta Was this translation helpful? Give feedback.
-
Migrating this discussion. The conversation has revealed that this is not a request for a language change, but rather suggestions around how the information should be presented in IDEs like VS and VSCode. As such, this should be continued over at roslyn. As per the above, if anyone here is interested in submitting PRs to improve the experience, they would be welcome. Thanks! |
Beta Was this translation helpful? Give feedback.
This will no longer produce a warning in C# 9.0 as the language team has decided to always treat
var
as nullable and to rely on flow analysis to determine when to warn. Assigningnull
will no longer warn, but trying to deference after that assignment will.Example