Cast operators do not obey the distributive law

Cast operators do not obey the distributive law

Rate This
  • Comments 23

Another interesting question from StackOverflow. Consider the following unfortunate situation:

object result;
bool isDecimal = GetAmount(out result);
decimal amount = (decimal)(isDecimal ? result : 0);

The developer who wrote this code was quite surprised to discover that it compiles and then throws “invalid cast exception” if the alternative branch is taken.

Anyone see why?

In regular algebra, multiplication is “distributive” over addition. That is q * (r + s) is the same as q * r + q * s. The developer here was probably expecting that casting was distributive over the conditional operator. It is not. This is not the same as

decimal amount = isDecimal ? (decimal)result : (decimal)0;

which is in fact the correct code here. Or, better still:

decimal amount = isDecimal ? (decimal)result : 0.0m;

The problem faced by the compiler is that the type of the conditional expression must be consistent for both branches; the language rules do not allow you to return object on one branch and int on the other.

We choose the best type based on the types we have in the expression itself, not on the basis of types that are outside the expression, like the cast. Therefore the choices are object and int. Every int is convertible to object but not every object is convertible to int, so the compiler chooses object. Therefore this is the same as

decimal amount = (decimal)(isDecimal ? result : (object)0);

And therefore the zero returned is a boxed int. The cast then unboxes the boxed int to decimal. As we’ve already discussed at length, it is illegal to unbox a boxed int to decimal. That throws an invalid cast exception, and there you go.

  • Given the (I hope) unlikeliness that somebody is depending on that invalid exception being thrown, and the fact it should be fairly deterministic when this happens, this seems like a good candidate for a compiler warning. When you see that a boxed value is going to be unboxed as an incompatible type, there should be a warning.

    Pavel: The problem with F#'s approach is that when you have an operator for everything, you end up with 80 operators. And the problem with having 80 operators is that every developer has to know all of them to be fluent in the language because most symbols aren't very intuitive (knowing the -> operator doesn't help you know the <- operator; knowing :> and :?> doesn't tell you what :? does).

    jsrfc58: There are plenty of places where you need an expression, and a statement is not valid, like in LINQ expressions or expression lambdas. And who's to say that GetAmount is "broken" in the first place? Eric has presented it as an analog to the Decimal.TryParse function, which would arguably be much more broken if it returned 0 when it couldn't parse its input.

  • I prefer C# to have the syntax like M(isDecimal?result:0 as int) ...  or M((Decimal) ()=>isDecimal?result:0) to resolve the Ambiguous, instead of the compiler generates an exception that may blow up in the customer site.

    I guess I will make sure I using suffixes - 0f, 0m for now.

  • >> And the problem with having 80 operators is that every developer has to know all of them to be fluent in the language because most symbols aren't very intuitive

    Arguably, it's better to see/use an unfamiliar operator and go look it up to see what it _actually_ does, than see/use a (seemingly) familiar operator in a new context, think that you know what it does, and guess wrongly because you hit some corner case where the semantical similarity which was the original reasoning for using identical syntax suddenly vanishes - such as in Eric's example.

    In any case, operators don't have to be symbolic, as demonstrated by VB's "DirectCast", C++'s "static_cast", or C#'s own "is" and "as". We could just as well have "convert", "upcast", "downcast" etc.

  • I like your term "regular algebra", though some would have preferred "elementary" but your explanation is a case of a cure worse than its disease.

    An algebra is not "a multiplication operator closed over a field", because a field is already an algebraic structure that includes addition and multiplication operators. The real numbers are a set; combining them with + and * using the "regular" rules defines a field because they satisfy all the field axioms, including not only closure but also inverse elements, identity elements, associativity, commutativity and distributivity.

    You are quite right that there are many algebras; a field is only one example. The reals plus only * would constitute a group, which is also an algebraic structure. In your casting example, a group would be a more parsimonious analogy than a field.

    Indeed. You are, I'm sure, correct. Keep in mind that it has been 18 years since I last took linear algebra, and even then I had a hard time remembering what the differences amongst a ring, group, field and algebra are. - Eric

  • Oh dear! I have created a false impression, for I am not a "real" mathematician. And I am older than you, so my school days are correspondingly more distant.

    All the same, that academic grounding was useful when the time came for me to learn sigma algebra (another algebra!), martingale probability, stochastic calculus. The theories we held as students - that the stuff we were learning could never possibly be useful for anything - proved to be wrong.

  • Nice factoid!

    The source example is horrible in more ways than one though. I'd definitely write that code as such:

    decimal? value = GetAmount(result);

    decimal amount = value.Value;


    decimal amount = GetAmount(result) ?? 0.0m;

  • I had to make a blog post about it:

  • I think some of you guys are reading these blogs for the wrong reason.

    I personally had no profound revelation learning that "cast operators do not obey the distributive law" but still I find it to be a materialization of a belief I could not have enumerated if asked: tell me what you know about C#.

    I really appreciate Eric's mathematical approach. I'm much younger that he is and have already forgotten much of the little vector-algebra I once knew. I still consider this post as being a *Fabulous* one.

    And Bent, *An-Anti-Pattern-Denied* sounds a bit like a double negation :). Make sure your readers don't do the exact opposite.

Page 2 of 2 (23 items) 12