I read the C# Language Specification on the Conditional logical operators ||
and &&
, also known as the short-circuiting logical operators. To me it seemed unclear if these existed for nullable booleans, i.e. the operand type Nullable<bool>
(also written bool?
), so I tried it with non-dynamic typing:
bool a = true;
bool? b = null;
bool? xxxx = b || a; // compile-time error, || can't be applied to these types
That seemed to settle the question (I could not understand the specification clearly, but assuming the implementation of the Visual C# compiler was correct, now I knew).
However, I wanted to try with dynamic
binding as well. So I tried this instead:
static class Program
{
static dynamic A
{
get
{
Console.WriteLine("'A' evaluated");
return true;
}
}
static dynamic B
{
get
{
Console.WriteLine("'B' evaluated");
return null;
}
}
static void Main()
{
dynamic x = A | B;
Console.WriteLine((object)x);
dynamic y = A & B;
Console.WriteLine((object)y);
dynamic xx = A || B;
Console.WriteLine((object)xx);
dynamic yy = A && B;
Console.WriteLine((object)yy);
}
}
The surprising result is that this runs without exception.
Well, x
and y
are not surprising, their declarations lead to both properties being retrieved, and the resulting values are as expected, x
is true
and y
is null
.
But the evaluation for xx
of A || B
lead to no binding-time exception, and only the property A
was read, not B
. Why does this happen? As you can tell, we could change the B
getter to return a crazy object, like "Hello world"
, and xx
would still evaluate to true
without binding-problems...
Evaluating A && B
(for yy
) also leads to no binding-time error. And here both properties are retrieved, of course. Why is this allowed by the run-time binder? If the returned object from B
is changed to a "bad" object (like a string
), a binding exception does occur.
Is this correct behavior? (How can you infer that from the spec?)
If you try B
as first operand, both B || A
and B && A
give runtime binder exception (B | A
and B & A
work fine as everything is normal with non-short-circuiting operators |
and &
).
(Tried with C# compiler of Visual Studio 2013, and runtime version .NET 4.5.2.)
First of all, thanks for pointing out that the spec isn't clear on the non-dynamic nullable-bool case. I will fix that in a future version. The compiler's behavior is the intended behavior;
&&
and||
are not supposed to work on nullable bools.The dynamic binder does not seem to implement this restriction, though. Instead, it binds the component operations separately: the
&
/|
and the?:
. Thus it's able to muddle through if the first operand happens to betrue
orfalse
(which are boolean values and thus allowed as the first operand of?:
), but if you givenull
as the first operand (e.g. if you tryB && A
in the example above), you do get a runtime binding exception.If you think about it, you can see why we implemented dynamic
&&
and||
this way instead of as one big dynamic operation: dynamic operations are bound at runtime after their operands are evaluated, so that the binding can be based on the runtime types of the results of those evaluations. But such eager evaluation defeats the purpose of short-circuiting operators! So instead, the generated code for dynamic&&
and||
breaks the evaluation up into pieces and will proceed as follows:x
)bool
via implicit conversion, or thetrue
orfalse
operators (fail if unable)x
as the condition in a?:
operationx
as a resulty
)&
or|
operator based on the runtime type ofx
andy
(fail if unable)This is the behavior that lets through certain "illegal" combinations of operands: the
?:
operator successfully treats the first operand as a non-nullable boolean, the&
or|
operator successfully treats it as a nullable boolean, and the two never coordinate to check that they agree.So it's not that dynamic && and || work on nullables. It's just that they happen to be implemented in a way that is a little bit too lenient, compared with the static case. This should probably be considered a bug, but we will never fix it, since that would be a breaking change. Also it would hardly help anyone to tighten the behavior.
Hopefully this explains what happens and why! This is an intriguing area, and I often find myself baffled by the consequences of the decisions we made when we implemented dynamic. This question was delicious - thanks for bringing it up!
Mads