Reputation: 305
I put the code below, and also uploaded to a online c# compiler: jdoodle.com/a/1jww the code can compile and run online, however, it doesn't compile in my local visual studio.
I am using:
Visual Studio 2017 15.9.13,
Console app, .Net Framework 4.7.2
Language version c# 7.3
Microsoft (R) Visual C# Compiler version 2.10.0.0 (b9fb1610)
Below is the code:
class Program
{
static void Main()
{
Dictionary<string,int> myDict = new Dictionary<string,int>();
myDict.Add("hello", 1);
if (myDict?.TryGetValue("hello", out var value) == true)
{
Console.WriteLine("Hello" + value.ToString());
}
}
}
expecting to see Hello1 in Console.Output Because if the condition is true, the Null-Conditional check must have returned a non-null value, and the key is existing in the dictionary and the value must have been assigned when returning from the TryGetValue method.
Because according to the documentation:
the called method is required to assign a value before the method returns.
update:
This is an open issue in https://github.com/dotnet/roslyn/issues/32572
I think it's an true issue/bug if the requirement for compiler includes not to give false alarm.
My arguments:
Whenever CPU executing to the point inside the if bracket code block, the value must have returned from TryGetValue call and is NOT "unassigned local variable". An easier solution is to give something like "unable to determine assignment status" as warning instead of error, if compiler can't look forward for assignment status when interpreting null conditional operator.
Upvotes: 12
Views: 9829
Reputation: 7783
The null-conditional operator introduces a different execution path where it short-circuits TryGetValue
. However, the value
variable exists anyways since it is scoped outside of the if
and TryGetValue
statements, which would indicate this flavor of syntactic sugar must result in the good old declaration of the variable prior to its use. For instance:
if (myDict?.TryGetValue("hello", out var value) == true)
{
Console.WriteLine("Hello" + value.ToString());
}
//Current issue aside, this is legal as the variable exists at a higher scope,
//beyond the if and TryGetValue statements although the brackets give the illusion of scope.
value = 2;
Should be equivalent to:
int value;
if (myDict?.TryGetValue("hello", out value) == true)
{
Console.WriteLine("Hello" + value.ToString());
}
value = 2;
Which also results in the compilation error.
However, the same error would be issued for myDict
if it was null
and you didn't use the null-conditional operator:
Dictionary<string, int> myDict; //Not initialized
//You would get the warning for myDict here instead
if (myDict.TryGetValue("hello", out var value) == true)
{
Console.WriteLine("Hello" + value.ToString());
}
In other words, myDict
can never be null
and compile without the same error so there is no need to use a null-conditional operator. This is why removing it fixes the issue.
That being said, the documentation states:
No need to assign an initial value. By declaring the out variable where it's used in a method call, you can't accidentally use it before it is assigned.
So, in theory you are accidentally using the variable before it's assigned because the language constructs allow it. However, the compiler is identifying an issue at a lower level and not letting you go through with it.
In my mind, it's valid compiler verification obscured by syntactic sugar fog.
Upvotes: 0
Reputation: 17658
It's due to the compiler difference.
In this fiddle, https://dotnetfiddle.net/5GgGNS, you can see the error, which is omitted in the mono compiler.
I think the error is valid due to the fact that this line
if (myDict?.TryGetValue("hello", out var value) == true)
is not guaranteed to initialize the local variable value
.
If you would rewrite it to:
if (myDict?.TryGetValue("hello", out var value) == null)
it would try to access value
.
Now, the null
value, or true
in your case, could be a function's return value, which would only be known at run time.
But, since all variables are basically always initialized, it's just a compiler feature.
On the other hand, according to the C#5 specs:
A local variable introduced by a local-variable-declaration is not automatically initialized and thus has no default value. For the purpose of definite assignment checking, a local variable introduced by a local-variable-declaration is considered initially unassigned. A local-variable-declaration may include a local-variable-initializer, in which case the variable is considered definitely assigned only after the initializing expression (§5.3.3.4).
But your code is C# 6.
So my conclusion is that the compilers interpret it differently. The Microsoft compiler takes the ?.
operator into account. You should file it as a bug, or finding at least, maybe even at both parties.
Fun fact, if you use this code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
class Program
{
static void Main()
{
//Your code goes here
Dictionary<string,int> myDict = null;
if (myDict?.TryGetValue("hello", out var value) == null)
{
Console.WriteLine("Hello" + value.ToString());
}
}
}
[using https://www.jdoodle.com/compile-c-sharp-online , mono 5.10.1]
You'll see the actual initialization to default(T)
at work. The output is Hello0
. Nevertheless, it's remarkable because due to the ?
, and the fact that myDict
is null
, TryGetValue
shouldn't be called and leaving value
"uninitialized".
The null-conditional operators are short-circuiting. That is, if one operation in a chain of conditional member or element access operations returns null, the rest of the chain doesn't execute.
But..., since there are no uninitialized variables; if it compiles, the compiler will make sure it's behavior is not undefined.
So, since value
is initialized, on run-time, the question remains if it's a valid compiler error at build time. Regarding to the run-time intend of the code it is (and that's why the error was there in the first place), but I think it remains a grey area.
Do note that according to this default(T)
is not override-able, which would actually lead to no condition where it fails.
By running this little test:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
class Program
{
static void Main()
{
//Your code goes here
Dictionary<string,int> myDict = null;
if (myDict?.Bar(out var test) == null)
{
Console.WriteLine("does hit");
}
}
}
static class Foo
{
public static object Bar(this Dictionary<string,int> input, out int test)
{
test = 3;
Console.WriteLine("does not hit");
return 1;
}
}
[using https://www.jdoodle.com/compile-c-sharp-online , mono 5.10.1]
The output becomes:
does hit
And you can verify the correct run-time behavior of the ?.
operator.
Upvotes: 10
Reputation: 31403
The null conditional ?.
removes the guarantee that value
will be assigned to since TryGetValue
will only be called conditionally if myDict
is not null
.
You enforce the assignment of value
inside the if
statement afterwards with == true
since the left side will return null
if TryGetValue
is not called due to myDict
itself being null. The compiler, however, cannot make this inference leap in the general case so you have to help it out either by testing myDict
for null
beforehand (and skipping ?.
) or initializing value
beforehand.
Upvotes: 3
Reputation: 254
I would suggest initializing the value variable before your "TryGetValue" call.
Upvotes: 0