Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Null-coalescing out parameter gives unexpected warning

Using this construct:

var dict = new Dictionary<int, string>();
var result = (dict?.TryGetValue(1, out var value) ?? false) ? value : "Default";

I get an error saying CS0165 use of unassigned local variable 'value' which is not what I expect. How could value possibly be undefined? If the dictionary is null the inner statement will return false which will make the outer statement evaluate to false, returning Default.

What am I missing here? Is it just the compiler being unable to evaluate the statement fully? Or Have I messed it up somehow?

like image 340
Jakob Möllås Avatar asked Mar 17 '19 12:03

Jakob Möllås


People also ask

What is the use of null-coalescing operator?

The null-coalescing operator ?? returns the value of its left-hand operand if it isn't null ; otherwise, it evaluates the right-hand operand and returns its result.

Can I use null-coalescing?

In cases where a statement could return null, the null-coalescing operator can be used to ensure a reasonable value gets returned. This code returns the name of an item or the default name if the item is null.

What is difference between || and in typescript?

As seen above, the difference between the operators ?? and || is that one is checking for nullish values and one is checking for falsey values.

Is null C# operator?

operator is known as Null-coalescing operator. It will return the value of its left-hand operand if it is not null. If it is null, then it will evaluate the right-hand operand and returns its result. Or if the left-hand operand evaluates to non-null, then it does not evaluate its right-hand operand.


2 Answers

Your analysis is correct. It is not the analysis the compiler makes, because the compiler makes the analysis that is required by the C# specification. That analysis is as follows:

  • If the condition of a condition?consequence:alternative expression is a compile-time constant true then the alternative branch is not reachable; if false, then the consequence branch is not reachable; otherwise, both branches are reachable.

  • The condition in this case is not a constant, therefore the consequence and alternative are both reachable.

  • local variable value is only definitely assigned if dict is not null, and therefore value is not definitely assigned when the consequence is reached.

  • But the consequence requires that value be definitely assigned

  • So that's an error.

The compiler is not as smart as you, but it is an accurate implementation of the C# specification. (Note that I have not sketched out here the additional special rules for this situation, which include predicates like "definitely assigned after a true expression" and so on. See the C# spec for details.)

Incidentally, the C# 2.0 compiler was too smart. For example, if you had a condition like 0 * x == 0 for some int local x it would deduce "that condition is always true no matter what the value of x is" and mark the alternative branch as unreachable. That analysis was correct in the sense that it matched the real world, but it was incorrect in the sense that the C# specification clearly says that the deduction is only to be made for compile-time constants, and equally clearly says that expressions involving variables are not constant.

Remember, the purpose of this thing is to find bugs, and what is more likely? Someone wrote 0 * x == 0 ? foo : bar intending that it have the meaning "always foo", or that they've written a bug by accident? I fixed the bug in the compiler and since then it has strictly matched the specification.

In your case there is no bug, but the code is too complicated for the compiler to analyze, so it is probably also too complicated to expect humans to analyze. See if you can simplify it. What I might do is:

public static V GetValueOrDefault<K, V>(
  this Dictionary<K, V> d,
  K key,
  V defaultValue)
{
    if (d != null && d.TryGetValue(key, out var value))
      return value;
    return defaultValue;
}
…
var result = dict.GetValueOrDefault(1, "Default");

The goal should be to make the call site readable; I think my call site is considerably more readable than yours.

like image 76
Eric Lippert Avatar answered Sep 17 '22 12:09

Eric Lippert


Is it just the compiler being unable to evaluate the statement fully?

Yes, more or less.

The compiler does not track unassigned, it tracks the opposite 'defintely assigned'. It has to stop somewhere, in this case it would need to incorporate knowledge about the library method TryGetValue(). It doesn't.

like image 25
Henk Holterman Avatar answered Sep 17 '22 12:09

Henk Holterman