4

I've read many of the SO questions on the null coalescing ?? operator but none of them seem to address the following specific issue, which concerns neither nullability (here), operator precedence (here and here) nor especially implicit conversion (here, here, here and here). I've also read the .NET docs (more here) and tried to read the offical spec, but sadly all to no avail.


So here goes. The only difference between the following two lines is the use of var for type inference in the second, versus explicit type Random in the first, yet the second line gives the error as shown, whereas the first is just fine.

Random x = new Random() ?? (x = new Random());        // ok

var    y = new Random() ?? (y = new Random());        // CS0841
                        //  ^-------- error here

CS0841: Cannot use local variable 'y' before it is declared

What is it precisely about the second line that makes the outcome uncertain?

From the hubub I cited above, I learned that the possibility of the left-side of the ?? operator being null introduces a dependency on the runtime determination of the actual instantiated type of its right-side. Hmm, ok, I guess,... whatever that means? Maybe the amount of alarm generally wafting about the ?? operator on this site should have been some kind of dire warning...

Zeroing-in now, I thought that the whole point of the var keyword (as very specifically opposed to dynamic) was that it was not suscceptible to runtime considerations like that, by definition.

In other words, even if we adopt the conservative but perfectly defensible rule of "never peering beyond any assignment = operator", such that we therefore get no helpful information whatsoever from the right-side of the ??, then based on the left-side alone, the overall result must be "compatible with" Random. That is, the result must be Random or a more specific (derived) type; it cannot be more general. By definition, therefore, shouldn't Random be the inferred type, for this compile-time use of var?

As far as I can understand, corrupting var with runtime considerations summarily defeats its purpose. Isn't that precisely what dynamic is for? So I guess the questions are:

  • Is the null-coalescing operator a lone and/or rare exception to my understanding of C# static (i.e., compile-time) typing philosophy?
  • If yes, then what are the benefits or tradeoffs between this design vs. what seems to be going on here, namely, deliberately introducing non-determinism to a system of static type inference, and which it did not previously exhibit? Couldn't dynamic have been implemented without corrupting the purity of static typing?
  • Isn't one of the main points of strong-typing to enable compile-time design rigor via actionable feedback to the developer? Why can't var just maintain a policy of strict conservatism--always inferring the most-specific type that can be statically inferred--at the same time as the null-coalescing operator is doing whatever it wants to do based on information from the future?
Glenn Slayden
  • 17,543
  • 3
  • 114
  • 108
  • 1
    "the result must be Random or a more specific (derived) type; it cannot be more general. " You have that backwards, replacing `var` by `object` would work perfectly fine. – Ben Voigt Jul 09 '17 at 02:22
  • `var` is not run-time, it's compile-time. `dynamic` is run-time. `dynamic` was created primarily for inter-oping with dynamic languages (e.g. Python) via the DLR. – Kenneth K. Jul 09 '17 at 02:25
  • @KennethK. that's exactly my point. Did you have a specific comment? – Glenn Slayden Jul 09 '17 at 02:27
  • @BenVoigt thanks so much; I fixed the error in the place you mentioned and an additional one as well. It's embarassing since information partial-ordering is supposed to be one of my main areas of specialization. – Glenn Slayden Jul 09 '17 at 02:31
  • Err, @BenVoigt reconsidering now… (this misunderstanding is actually a well-known problem in my field). I define "more derived" types to be "more specific", so `Object` is the "most general" type, and at the "top" of a conceptual tree with specificity going downwards. Some logicians interpret information partial-orders upside-down from that. Do you use that view? In my version, because the left-side of `??` is patently `Random`, the right side must be more derived, and can't be `Object` since an `Object` might not be `Random`. Oh, or did you think "right-side" was referring to the assignment? – Glenn Slayden Jul 09 '17 at 02:42
  • @GlennSlayden: Do you have a rule that says the left-hand operand of `??` wouldn't implicitly convert to the type of the right-hand one? Because clearly there is no problem with the expanded version `object z; var tmp = new Random(); if (tmp != null) z = tmp; else z = (z = new Random());` – Ben Voigt Jul 09 '17 at 02:46
  • The rule I found says that the right-hand type can be more general than the left-hand, in which case the left-hand gets upcast implicitly. See the fifth bullet at https://github.com/dotnet/csharplang/blob/master/spec/expressions.md#the-null-coalescing-operator – Ben Voigt Jul 09 '17 at 02:48
  • @BenVoigt Well, my point is that maximizing the compile-time utility of `var` entails that it adopt the rule of (1) always assuming most specific type that can be statically computed and (2) never punting, even if said best-effort is `Object`. Since you already explicitly declared `z` as `Object`, I think `var` has no work to do in your example. But your comment does further highlight that the current `??` spec/behavior masks whatever the design "intentions" of `var` might otherwise have been... we are blocked from having any way to find out! – Glenn Slayden Jul 09 '17 at 02:58
  • @Glenn: That comment was just addressing the more-general/more-specific side topic, not the `var` question. In any case, it still isn't clear to me what you expect to accomplish by using `y` in its own initializer in this way. Is this a simplification of a more complex expression? Or are you thinking in terms of `(var y = new Random()) ?? (y = new Random());` But that's not how the grammar works. – Ben Voigt Jul 09 '17 at 03:05
  • @BenVoigt I had seen that and yes, it seems to be the crux. The citation "...converted to type B, and this becomes the result" can only ***possibly*** have any meaning in relation to `dynamic`, obviously. There's no way to send info back in time to `var`. So the question remains, why does `var` suddenly decide to **entirely give up** (fatal error) based on potentialities? Most specifically so far, why couldn't `var` continue to predict the best static analysis *available to it*, instead of suddenly becoming a partial function just because an obscure new operator was introduced? – Glenn Slayden Jul 09 '17 at 03:08
  • @GlennSlayden: Are you thinking that `??` is causing this? But any operator whose type depends on the operand types will act exactly the same way. `var y = 1 + (y = 2)` will fail to infer a type. So will overloaded function calls. – Ben Voigt Jul 09 '17 at 03:11
  • @BenVoigt My examples here are minimal and considerably simplified from my actual use cases, which are mostly related to prevasive lock-free programming with `Interlocked` operations. But isn't it sufficient to note that, in the original contrast, line #1, without `var`, is perfectly inferrable? – Glenn Slayden Jul 09 '17 at 03:12
  • 1
    @GlennSlayden: Huh? No, line #1 is not inferrable. It compiles because there's no inference at all. What there is, is a useless extra assignment that takes place (if evaluated) before the initialization of the variable it assigns to. The initialization will always overwrite the assigned value. – Ben Voigt Jul 09 '17 at 03:14
  • @BenVoigt Ok, fair enough; I hope I didn't cut too much in over-zealous simplification. It's the nature of `var y = 1 + (y = 2)` that no information is available. In contrast, `var y = new Random() ?? …anything`, has actionable inference available that `var` does not maximally harvest. I am starting to conclude that this is indeed due to @CodingYoshi's "yelling" explanation, and thus does indeed represent a categorical shift in the previously presumed philosophy of `var`--towards one of *subtle steering* (towards `dynamic`), rather than *maximally assisting*, compile-time dev, as previously. – Glenn Slayden Jul 09 '17 at 03:31
  • @Glenn: I'm becoming more convinced that the problem here is your mental model of this initialized declaration. The declaration is NOT nested inside the left operand of the `??` operator. The `??` operator is nested inside the initializer, which in turn is part of the declaration. The `=` in `var y =` is NOT an operator, it's part of the syntax for a declaration... and therefore operator precedence doesn't apply to it. – Ben Voigt Jul 09 '17 at 03:32
  • So [you say that bullet point #5 would try to go back in time and change the type already inferred by `var`](https://stackoverflow.com/questions/44992303/c-sharp-type-inference-var-assignment-from-null-coalescing-operator?noredirect=1#comment76959988_44992303), but in fact all type analysis of the `??` operator takes place *before* `var` inference. There would not be any "going back in time". – Ben Voigt Jul 09 '17 at 03:33
  • @Glenn: Think in terms of recursive descent, for both parsing and type analysis, it'll help. Basically, the compiler is trying to do: `Locals.Add("y"); Locals["y"].StaticType = GetStaticType("new Random() ?? (y = new Random())");` and the latter evaluates to `Min(GetStaticType("new Random()"), GetStaticType("y = new Random"))`, which is `Min(typeof(Random), Locals["y"].StaticType)`. In the end you have `Locals["y"].StaticType = Min(typeof(Random), Locals["y"].StaticType);`, and as you see, that uses `y`'s type before it is known. – Ben Voigt Jul 09 '17 at 03:42
  • @BenVoigt, No that's not my mental model; I assume we've always been talking about `var y = (new Random() ?? (y = new Random()))`. And I think the issue I've been trying to get at is, regardless of the right side of `??` (which I've tried to make a compile-time lost-cause for the sake of discussion), strictly what can inferred about `y` from the **left-side only**? – Glenn Slayden Jul 09 '17 at 03:48
  • @Glenn: Nothing can be inferred from the left-side only. The result type of the `??` operator depends on the types of both operands. If you don't have both types, you cannot apply the rules for finding the result type (the ones in those bullet points we've been discussing). – Ben Voigt Jul 09 '17 at 03:52
  • Consider this: `int f(Random r) { return 1; } int f(object o) { return 2; };` What do you expect to get from `int z = f(new Random() ?? new object());` ? Is it "always 1", "always 2", or "sometimes 1, sometimes 2"? – Ben Voigt Jul 09 '17 at 03:54
  • @BenVoigt Yes, exactly, until runtime, according to the (possibly misguided) spec, which is why the "going back in time" is still at issue. That spec implicates the "type [of a] run-time... evaluation... and this becomes the result," a statement which clearly can have no meaning or utility in relation to `var`. I agree that the rules, as stated, are jarring, perhaps flying in the face of C# tradition. Consider: `var` accepts `new Random() ?? new Random()` as `Random` and both `new Object() ?? new Random()` and the swapped as `Object`, proving that `??` inference goes towards the *general*... – Glenn Slayden Jul 09 '17 at 04:05
  • I realized I made a mistake earlier. The pseudo-code for what the compiler is doing isn't `Locals.Add("y"); Locals["y"].StaticType = Min(typeof(Random), Locals["y"].StaticType);`, it's more like `Locals.Add("y", Min(typeof(Random), Locals["y"].StaticType))` which throws a `KeyNotFound` exception, translated to the error you get of using a variable that hasn't been declared yet, rather than the error I would have designed in, of `var` type inference failing. – Ben Voigt Jul 09 '17 at 04:05
  • @BenVoigt ...so given this discoverable "rule", as you suggested earlier, why isn't `var` satisfied to classify `new Object() ?? ...anything-under-the-sun` as `Object`, without even having to look at the right side? – Glenn Slayden Jul 09 '17 at 04:06
  • @BenVoigt What's jarring is that `var` escalates this all the way to fatal compilation, rather than just saying, "ok, then, `Object` it is." – Glenn Slayden Jul 09 '17 at 04:08
  • @Glenn: Because there's no bullet point for "if A is System.Object, the result is System.Object". Every bullet point needs to have B. Even if all possible values of B give the same result (which happens when A is System.Object). – Ben Voigt Jul 09 '17 at 04:08
  • Put into familiar terms, there's no short-circuiting in the type rules. – Ben Voigt Jul 09 '17 at 04:09
  • @Glenn: Actually, you partially have a point, because the rule "Otherwise, if `A` exists and an implicit conversion exists from `b` to `A`, the result type is `A`." doesn't need `B` to exist... that is, it doesn't need `b` to have any known type. Unfortunately, [there are types in C# that don't implicitly convert to `object`](https://blogs.msdn.microsoft.com/ericlippert/2009/08/06/not-everything-derives-from-object/). – Ben Voigt Jul 09 '17 at 04:14
  • @Glenn: In the final analysis, though, it's because there is no special type analysis for `var`. It reuses the type analysis that already exists in the compiler for e.g. determining the type of a function argument for purposes of overload resolution. – Ben Voigt Jul 09 '17 at 04:22
  • @BenVoigt Excellent catch, which might, I think, explain the bad smell I've vaguely noticed from working with `??` way too much (c.f. lock-free `Interlocked`...). But much insight here for me too and your first point about flipping the partial order may have helped the most. Against intuition, it is in the very nature of `??` that it aggregates its operand types **upwards** (towards `Object`) and now I see that it has to be this way. It follows that runtime conditions implicate static type inference more than usual and we should be thankful--instead of critical--that `var` does what it does do – Glenn Slayden Jul 09 '17 at 04:28
  • @Glenn: The result type of `??` (and every expression not involving `dynamic`) is determined statically. And the static determination must be done in such a way that it works for both branches, exactly because which branch is picked is not known until runtime, which is far too late to affect static analysis. It's the same reason that in C# (unlike some scripting languages), code in dead branches needs to be valid. Part of that is the ahead-of-time compilation model, and part is just C# strictness. For example, in `return; f(new Random());` the call is clearly unreached, but has to be valid. – Ben Voigt Jul 09 '17 at 04:35

2 Answers2

4

It's not a runtime consideration.

The compile time type of a variable declared using var is the static type of its initializer. The static type of a ?? expression is the common type of the static type of both operands. But the static type of the second operand is the static type of y, which isn't known. Therefore the static type of the whole initializer is unknown, and deduction fails.

It's true that there exist types for which the initialization would be consistent, but they can't be found using the C# inference rules.

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • In fact in this case both `object` and `Random` would be consistent types for `y`, so it's ambiguous. Actually, it's possible to get any number of consistent types by using two classes that each implement an arbitrary number of interfaces. In any case, this is a lot of effort going into analyzing a line of code that should never be used. – Kyle Jul 09 '17 at 02:47
0

When you use var, the type is figured out at compile time. Therefore, when you write this:

var    y = new Random() ?? (y = new Random()); 

the compiler cannot determine what y's type is at compile time and thus starts yelling-the decision whether the left side of ?? is null or not, will be determined at runtime.

A better example would be:

public interface IA { void Do(); }
public class A : IA { ... }
public class B : IA { ... }

A a = null;
var something = a ?? new B(); 

What should be the type of something: IA, A or B?

CodingYoshi
  • 25,467
  • 4
  • 62
  • 64
  • This is begging the question, WHY can't it determine that when it's just as obvious as the first. – Novaterata Jul 09 '17 at 02:24
  • @novaterata read my example and you will see why. In the question posted, it is obvious but in the example I have provided, it is not obvious at all. – CodingYoshi Jul 09 '17 at 04:46
  • @CodingYoshi: I doesn't matter what's `null` at runtime for your example. Error for `var something = new A() ?? new B();` shows that your example doesn't compile for a different reason, which is that it's impossible for any object that could possibly be stored in `a` to be a `B`, now or at runtime or ever..It's strongly ruled out because NET doesn't allow multiple inheritance of instances (classes can only have one base type). `??` does correctly allow complex cases where multiply-inherited *interfaces* might unify, usually requiring a cast on the left or right of `??` – Glenn Slayden Jul 09 '17 at 05:06
  • @CodingYoshi I may have over-claimed, your example can be made to work, as-is, by simply casting either side of the `??` to `(IA)`, since it's an interface. But that won't work for casting to `(A)` or `(B)`, since those are classes which are the ones that are "strongly ruled out" – Glenn Slayden Jul 09 '17 at 05:08
  • @GlennSlayden yes it will work so long as we tell the compiler we are expecting `IA` and that was my whole point. – CodingYoshi Jul 09 '17 at 06:21