I read the C# Language Specification on the Conditional logical operators ||
and &&
, also known as the short-circuiting logical operat
Is this correct behavior?
Yes, I'm pretty sure it is.
How can you infer that from the spec?
Section 7.12 of C# Specification Version 5.0, has information regarding the conditional operators &&
and ||
and how dynamic binding relates to them. The relevant section:
If an operand of a conditional logical operator has the compile-time type dynamic, then the expression is dynamically bound (§7.2.2). In this case the compile-time type of the expression is dynamic, and the resolution described below will take place at run-time using the run-time type of those operands that have the compile-time type dynamic.
This is the key point that answers your question, I think. What is the resolution that happens at run-time? Section 7.12.2, User-Defined conditional logical operators explains:
- The operation x && y is evaluated as T.false(x) ? x : T.&(x, y), where T.false(x) is an invocation of the operator false declared in T, and T.&(x, y) is an invocation of the selected operator &
- The operation x || y is evaluated as T.true(x) ? x : T.|(x, y), where T.true(x) is an invocation of the operator true declared in T, and T.|(x, y) is an invocation of the selected operator |.
In both cases, the first operand x will be converted to a bool using the false
or true
operators. Then the appropriate logical operator is called. With this in mind, we have enough information to answer the rest of your questions.
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?
For the ||
operator, we know it follows true(A) ? A : |(A, B)
. We short circuit, so we won't get a binding time exception. Even if A
was false
, we would still not get a runtime binding exception, because of the specified resolution steps. If A
is false
, we then do the |
operator, which can successfully handle null values, per Section 7.11.4.
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.
For similar reasons, this one also works. &&
is evaluated as false(x) ? x : &(x, y)
. A
can be successfully converted to a bool
, so there is no issue there. Because B
is null, the &
operator is lifted (Section 7.3.7) from the one that takes a bool
to one that takes the bool?
parameters, and thus there is no runtime exception.
For both conditional operators, if B
is anything other than a bool (or a null dynamic), runtime binding fails because it can't find an overload that takes a bool and a non-bool as parameters. However, this only happens if A
fails to satisfy the first conditional for the operator (true
for ||
, false
for &&
). The reason this happens is because dynamic binding is quite lazy. It won't try to bind the logical operator unless A
is false and it has to go down that path to evaluate the logical operator. Once A
fails to satisfy the first condition for the operator, it will fail with the binding exception.
If you try B as first operand, both B || A and B && A give runtime binder exception.
Hopefully, by now, you already know why this happens (or I did a bad job explaining). The first step in resolving this conditional operator is to take the first operand, B
, and use one of the bool conversion operators (false(B)
or true(B)
) before handling the logical operation. Of course, B
, being null
cannot be converted to either true
or false
, and so the runtime binding exception happens.