InBetween
InBetween

Reputation: 32740

Compile time constants and reference types

Ok, consider the following code:

    const bool trueOrFalse = false;
    const object constObject = null;

    void Foo()
    {
        if (trueOrFalse)
        {
            int hash = constObject.GetHashCode();
        }

        if (constObject != null)
        {
            int hash = constObject.GetHashCode();
        }
    }

trueOrFalse is a compile time constant and as such, the compiler warns correctly that int hash = o.GetHashCode(); is not reachable.

Also, constObject is a compile time constant and as such the compiler warns again correctly that int hash = o.GetHashCode(); is not reachable as o != null will never be true.

So why doesn't the compiler figure out that:

    if (true)
    {
        int hash = constObject.GetHashCode();
    }

is 100% sure to be a runtime exception and thus issue out a compile time error? I know this is probably a stupid corner case, but the compiler seems pretty smart reasoning about compile time constant value types, and as such, I was expecting it could also figure out this small corner case with reference types.

Upvotes: 6

Views: 710

Answers (4)

Eric Lippert
Eric Lippert

Reputation: 659994

UPDATE: This question was the subject of my blog on July 17th 2012. Thanks for the great question!

Why doesn't the compiler figure out that my code is 100% sure to be a runtime exception and thus issue out a compile time error?

Why should the compiler make code that is guaranteed to throw into a compile-time error? Wouldn't that make:

int M()
{
    throw new NotImplementedException();
}

into a compile-time error? But that's exactly the opposite of what you want it to be; you want this to be a runtime error so that the incomplete code compiles.

Now, you might say, well, dereferencing null is clearly undesirable always, whereas a "not implemented" exception is clearly desirable. So could the compiler detect just this specific situation of there being a null ref exception guaranteed to happen, and give an error?

Sure, it could. We'd just have to spend the budget on implementing a data flow analyzer that tracks when a given expression is known to be always null, and then make it a compile time error (or warning) to dereference that expression.

The questions to answer then are:

  • How much does that feature cost?
  • How much benefit does the user accrue?
  • Is there any other possible feature that has a better cost-to-benefit ratio, and provides more value to the user?

The answer to the first question is "rather a lot" -- code flow analyzers are expensive to design and build. The answer to the second question is "not very much" -- the number of situations in which you can prove that null is going to be dereferenced are very small. The answer to the third question has, over the last twelve years, always been "yes".

Therefore, no such feature.

Now, you might say, well, C# does have some limited ability to detect when an expression is always/never null; the nullable arithmetic analyzer uses this analysis to generate more optimal nullable arithmetic code (*), and clearly the flow analyzer uses it to determine reachability. So why not just use the already existing nullability and flow analyzer to detect when you've always dereferenced a null constant?

That would be cheap to implement, sure. But the corresponding user benefit is now tiny. How many times in real code do you initialize a constant to null, and then dereference it? It seems unlikely that anyone would actually do that.

Moreover: yes, it is always better to detect a bug at compile time instead of run time, because it is cheaper. But the bug here -- a guaranteed dereference of null -- will be caught the first time the code is tested, and subsequently fixed.

So basically the feature request here is to detect at compile time a very unlikely and obvioulsy wrong situation that will always be immediately caught and fixed the first time the code is run anyways. It is therefore not a very good candidate for spending budget on to implement it; we have lots of higher priorities.


(*) See the long series of articles on how the Roslyn compiler does so which begins at http://ericlippert.com/2012/12/20/nullable-micro-optimizations-part-one/

Upvotes: 11

jason
jason

Reputation: 241601

Why would you even want that to be a compile-time error? Why would you want code that is guaranteed to throw an exception to be invalid at compile time? What if I, the programmer, want the semantics of my program to be:

static void Main(string[] args) {
    throw new NullReferenceException();
}

It's my program. The compiler has no business telling me this isn't valid C# code.

Upvotes: 2

Jon Hanna
Jon Hanna

Reputation: 113242

It also won't warn about the following code that has the same effect:

throw new NullReferenceException();

There's a balance with warnings. Most compiler errors happen when the compiler can't produce anything meaningful from it.

Some happen with things that affect verifiability, or which cross a threshold of how likely they are to be a bug. For example, the following code:

private void DoNothing(out string str)
{
  return;
}

private void UseNothing()
{
  string s;
  DoNothing(s);
}

Won't compile, though if it did it would do no harm (the only place DoNothing is called doesn't use the string passed, so the fact that it is never assigned isn't a problem). There's just too high a risk that I'm doing something stupid here to let it go.

Warnings are for things that are almost certainly foolish or at least not what you wanted to happen. Dead code is likely enough to be a bug to make a warning worthwhile, but likely enough to be sensible (e.g. trueOrFalse may change as the application is developed) to make an error inappropriate.

Warnings are meant to be useful, rather than nuisances, so the bar for them is put quite high. There's no exact science, but it was deemed that unreachable code made the cut, and trying to deduce when throwing exceptions wasn't the desired behaviour didn't.

It no doubt helps that the compiler already detects unreachable code (and doesn't compile it) but sees one deliberate throw much like another, no matter how convoluted on the one hand or direct on the other.

Upvotes: 2

Daniel Moses
Daniel Moses

Reputation: 5858

While unreachable code is useless and does not affect your execution, code that throws an error is executed. So

if (true) { int hash = constObject.GetHashCode();}

is more or less the same as

throw new NullReferenceException();

You might very well want to throw that null reference. Whereas the unreachable code is just taking up space if it were to be compiled.

Upvotes: 4

Related Questions