-4

I find it surprising that nobody has asked this question before, but why does the compiler limit itself to just a warning in such a flagrant case?

object obj = null;
string str = obj.ToString(); //must be a compile-time error?

I especially fail to understand it after decompiling the code and seeing this:

((object) null).ToString();

I've checked similar (yet different) SO questions (1 and 2), and the answers boil down to "it's not possible to check the value of a variable at compile-time".
But why isn't it possible? What am I missing here? Can the compiler see the result it produced?

Razor23 Donetsk
  • 466
  • 1
  • 5
  • 13
  • 3
    do you expect your compiler to de-compile the assembly after compiling in order to check that? – MakePeaceGreatAgain Feb 23 '23 at 05:19
  • 2
    Did you activate [nullable reference types](https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references)? If so the compiler will warn you. And if you treat all warnings as errors it won't compile. [DEMO](https://dotnetfiddle.net/23nP5l) - but there is only a warning because I can't change any compile time settings for that online compiler. We're using that feature since it's available and it seams to work fine even in .Net Framework projects. – Sebastian Schumann Feb 23 '23 at 05:28
  • @MakePeaceGreatAgain, I don't understand your question. Can't the compiler check it during the compilation?? I would expect to see an error even after looking at the "normal" code, and I'm just saying that the decompiled version made me wonder even more. – Razor23 Donetsk Feb 23 '23 at 20:25
  • @TylerH, really, it does? I think I'm asking a very specific and detailed part about the matter that hasn't been addressed in those questions. Prove me wrong. I got the answer here from Chris Schaller, but not there. – Razor23 Donetsk Feb 24 '23 at 18:20
  • @Razor23Donetsk Not sure what "prove me wrong" means; you just need to read the answers to be proven wrong. – TylerH Feb 24 '23 at 22:20
  • @TylerH I have read and couldn't find the answer. You closed my question, so I suppose you managed to find there more than I did. So please, kindly share the information or a particular link with me. Otherwise, be so kind as to explain the reason for closing the question. – Razor23 Donetsk Feb 25 '23 at 01:52

1 Answers1

0

The short answer to why it is a warning and not an error is that null reference analysis is not infallible and cannot easily take into account parallel or indirect references. The code itself satisfies all the compiler rules for compilation and execution. The fact that the execution fails at runtime is not necessarily an error, it is only an exception that your application may or may not handle.

  • The developer might specifically want to induce a runtime error.

The compiler can't assume that the developer doesn't know that this will result in a runtime error. Once we start doing that what other assumptions might we get wrong?

Errors are for genuine code violations. The fact that the value of a variable is null requires specific knowledge about the expected state of that variable at runtime. Only at the time of executing obj.ToString() can we know with certainty that the value of obj is null, everything else up to that point is an assumption at best. Everything else that the compiler might have opinions about like conventions and code styles become warnings or notes. It is up to each individual developer or team to decide if they want to promote specific warnings to error status or if you treat all warnings as errors.


It might help to think about this from another perspective. Your example represents 2 lines of code that happen to exist next to each other in your script today:

object obj = null;
string str = obj.ToString();

During design time it is conceivable that more code might come to exist between these two statements, the net effect of this is that the additional logic might set a non-null value to obj:

object obj = null;
...
obj = "hello world!";
...
string str = obj.ToString();

But what about parallel processing, what if obj were set from a different logic block running in parallel, such that between instantiation of obj and the call to obj.ToString() the value was set outside of the code we see here?

... What if the method being called was an extension method? Extension methods after all can be called from null references.

.ToString() is only obvious that it will raise a runtime error because it is a member declared method and if we can be sure that the object has not yet been initialized. There are a lot of possibilities to check for and in a large code base this effort needs to be expended for every variable and every invocation. This type of functionality was not in the original versions of the C# compiler.

20 years ago, even if we could, the effort to do this level analysis on code on standard hardware would have significantly reduced developer productivity. Good SDLC practices like testing your code before release would pick these types of issues up anyway, to get this level of checking from the compiler was simply not a priority.

Fast forward to today, the compiler has evolved and the devs have squashed all the bugs and have been able to focus on runtime and productivity improvements. One such improvement is Nullable Reference Type analysis. This pretty much covers checking for your scenario and really complex ones by establishing a new set of conventions that developers need to abide by.

If you are interested in migrating old code to NRT then refer to Update a codebase with nullable reference types to improve null diagnostic warnings but beware, many existing code bases will have thousands of warnings the first time you turn this on. If you're OK with ignoring warnings, then I'm not sure why you would bother, for the rest of us who treat warnings as errors you will have a lot of code to review and fix or annotate.

I'm going to call out a specific comment to why I need to enable NUllable element in the csporj to use nullable reference types?, this comment by Jeroen Mostert is the best summary of Why this isn't an error even if NRT means we can reasonably assume that it is an error: (emphasis is mine)

As to it not making any difference, that's by design. NRTs were specifically and carefully crafted to have minimal impact on existing code except where you explicitly opt-in, so as to make sure the feature can actually add value and be gradually integrated with existing codebases, rather than an all-or-nothing approach that wouldn't see adoption because nobody wants to fix 200 warnings up front in code that was previously working just fine. This is also why, e.g., writing string? where NRTs are not enabled is not flat-out considered an error.

If you missed it back in 2019, you should read Embracing nullable reference types. There are many of us who are yet to fully embrace this personally and many more legacy code bases that are stable enough or have sufficient robust testing such that there is less or no value in rewriting the codebase to take advantage of NRT, in fact to do so might pose significant risk to the products because we should retest everything that has changed before releasing with the updated code.

Chris Schaller
  • 13,704
  • 3
  • 43
  • 81
  • All the points are valid, and thanks for the great answer! I just want to clarify one more thing. If the code looks exactly the way I showed it: local variables without a value-assigning operation in between them. Is there smth that stops the compiler from inferring that it's gonna be 100% NRE? If I'm not mistaken, there can't be parallel processing or elsewhere assignment happening in this particular case. – Razor23 Donetsk Feb 24 '23 at 01:08
  • Also, I'm sure, the compiler development team has discussed this feature many times. I'd love to read some, sort of 'proof-of-concept', document if it were open to the general public =) – Razor23 Donetsk Feb 24 '23 at 01:12
  • 1
    I can't find a specific source to cite, but a common response when discussing this is that _"developers are not that stupid, if they are then the NullReferenceException raised at runtime will be more than adequate, especially given that preceeding line will explain the error."_ **NOTE:** with with NRTs enabled, the warning will make it obvious to the dev. The effort to do a special case for every detected NRE because the code has touching proximity in the code file, is simply not worth it. Check the NRT warnings and it won't be a problem. – Chris Schaller Feb 24 '23 at 06:45
  • 1
    Interesting side read from Mads Torgersen in 2017: https://devblogs.microsoft.com/dotnet/nullable-reference-types-in-csharp/ – Chris Schaller Feb 24 '23 at 07:35
  • 1
    This query is as close as i can get: https://github.com/dotnet/csharplang/discussions/categories/language-ideas?discussions_q=nrt+error+on+obvious+nulls+sort%3Adate_created+category%3A%22Language+Ideas%22 If you want to post your idea in the discussion forum for language ideas, then this is the place, but be prepared to explain in detail the logical rules that govern your expected scenario. – Chris Schaller Feb 24 '23 at 15:22
  • I think the "developers are not that stupid" point is rather misleading. Because if I want to cause NRE purposefully, I will just throw the exception myself. Besides, the compiler won't let you do "int i = 10 / 0", which, to me, looks equally stupid. I think they didn't make it an error because you can't make such a check consistent. The assumption about null value can be confidently made only if these are local variables and there is no other usage of them somewhere in between. So, ultimately, the example I provided is the only use case. Still, would be nice to have, IMO =) – Razor23 Donetsk Feb 25 '23 at 17:34
  • 1
    `int i = 10 / 0` is different, because literals are used the expression is immutable and can be evaluated at compile time. NRTs are interpreted and often open to interpretation, as such there is a degree of uncertainty, this I tried to explain in my post is the most important reason that they cannot be errors by default. If I had to choose your feature over all the others I want to see implemented in C#, identifying this one as an error goes to the bottom of the list for me, it is already a warning, so the compiler is already doing this check for you, can't ask for much else. – Chris Schaller Feb 26 '23 at 12:58
  • Sorry for not letting it go. Sure, my example with division by zero wasn't correct cuz the following will compile "int i = 0; int j = 10 / i;". But could you just clarify one more thing for me? The compiler does see the result of the compilations, doesn't it? I mean, potentially, it can analyze the outcome like the one "((object) null).ToString();" and take an action based on it (warning or error, etc). Or do I completely misunderstand how the compiler works? – Razor23 Donetsk Feb 26 '23 at 15:45
  • 1
    So short answer, that is **not** what the compiler sees. The compiler still see's a call to the instance method `[System.Runtime]System.Object::ToString()` on _a_ variable. When you decompile the IL back to C#, the decompiler further transposes the variable declaration to be inline with the method call to avoid creating an interim variable. You need to look at the _IL_ to view the actual output. Could the compiler do this, sure, but only by parsing the output which would be a significant overhead to detect a trivial case that is already flagged in the general CS8600 nullable warning. – Chris Schaller Feb 27 '23 at 04:22