FYI, C# 2.0 Has A Breaking Change in Enum Subtraction
A customer brought to my attention the other day that the C# 2.0 beta release has a breaking change from the previous release. Namely, this code
enum E : byte {
A = 1,
B = 2
};
// . . .
E a = E.A;
E b = E.B;
int j = a - b;
sets
j to -1 in the previous release but to 255 in the upcoming release.
First off, let me say that we regret the breaking change. We agonize over all breaking changes because we know the pain that they cause customers. We also regret introducing the bug in the first place, thereby forcing us to choose between continuing to be in violation of the specification and breaking existing code. Sorry about all that.
Second, I should describe why exactly the original behaviour is in violation of the C# specification. It's pretty straightforward. Start with section 7.7.5:
Every enumeration type implicitly provides the following predefined operator, where E is the enum type, and U is the underlying type of E: U operator –(E x, E y); This operator is evaluated exactly as (U)((U)x – (U)y)
That clearly means that the assignment above should have the same semantics as
int j = (byte)((byte)a-(byte)b));
C# defines only four built-in subtraction operators:
int operator –(int x, int y);
uint operator –(uint x, uint y);
long operator –(long x, long y);
ulong operator –(ulong x, ulong y);
There is an implicit conversion from
byte to all four types, so we must select the best one. According to section 7.4.2.3 the int version is the best one (because signed is preferable to unsigned and int goes to long but long does not go to int.) So what we generate here is the equivalent of:
int j = (byte)((int)(byte)a-(int)(byte)b));
The conversions from E to byte to int will go off without a hitch, and the subtraction will result in an int set to -1. That then gets cast to byte. What happens when we try to cast a computed-at-runtime integer to a byte? Section 7.5.12 says
For non-constant expressions (expressions that are evaluated at run-time) that are not enclosed by any
checked or unchecked operators or statements, the default overflow checking context is unchecked unless external factors (such as compiler switches and execution environment configuration) call for checked evaluation.
Therefore this is an unchecked cast, and -1 goes to 255 as a
byte. That then gets converted back to an int during the assignment.
Third, I should talk a bit about the process we go through when making breaking changes like this. The change was made to the C# 2.0 compiler on the 14th of January 2004, six months before beta 1, and one of the reasons we try to push betas out really early is to get feedback on whether breaking changes like this affect millions, thousands, or dozens of people. Since to my knowledge the first customer to run into a break contacted us this week, and we're only taking "the product electrocutes millions of users" bug fixes right now, unfortunately this one does not make the bar for choosing backwards compatibility over correctness. I feel bad about that, but I hope you understand our reasoning here. We've got to ship this thing! We'll also make sure that a Knowledge Base article describing the problem gets written.
Comments
Anonymous
October 19, 2005
Eric, was this particular customer advocating that the previous behavior be preserved, or just pointing out the change in behavior? If you explained the situation as well as you did here, I'd be inclined to say "Okay, we'll go back and change/check our code." Unless I was a big customer with 300,000 lines of C# that abso-frickin-lutely depended on this behavior, in which case I'd talk to our Microsoft sales guy. :)Anonymous
October 19, 2005
Unfortunately the customer is in fact using this idiom in numerous places in their code, and was hoping that we would change it back for the final release. I hate to disappoint people, but the Whidbey train is about to leave the station and we can't risk destabilizing the release for anything short of a major security or geopolitical issue.Anonymous
October 19, 2005
Eric,
Will there be a FXCop rule to warn users on potential problems?Anonymous
October 19, 2005
Good idea. I'll pass on that suggestion to the FXCOP team.Anonymous
October 19, 2005
Of course a subtracting operation on bytes must return a byte. If one wants 1 - 2 to be equal to -1, one explicitly casts at least one of the arguments to int. That’s not a breaking change, it’s a bugfix.Anonymous
October 19, 2005
"Breaking change" and "bug fix" are orthogonal. In this case we have a bug fix that is also a breaking change -- we've taken a legal program and changed its semantics. That we did so to fix a bug is not hugely relevant to the unfortunate customers who must now track down all the places in their code where they rely on the old, buggy behaviour.Anonymous
October 19, 2005
The comment has been removedAnonymous
October 20, 2005
Personally I am all for this. I think especially in C# to continue to be the great language it is and you look back at some of the things that have gone wrong with the languages before one of the things is they do not adhere to the specs. While you might have a breaking change, if you can not expect the language to behave correctly then your shooting yourself in the foot in the long run.Anonymous
October 20, 2005
Jonathan: I call any change that could in any way break a customer a breaking change. In fact this is a breaking change in your sense too though. int j = (E.A-E.B); used to compile but since the RHS expression involves only constants, this is a checked context, and therefore will give a compiler error when the -1 tries to go to byte.Anonymous
October 20, 2005
Jeff: I agree, you have to look at the long run.
One way to look at it is like this: The number of people affected by this change is already small. In the long run the number of affected people will get smaller and smaller. Most new developers will never even use C# 1.0 once 2.0 finally ships, so 1->2 forwards compatibility isn't important to them.
On the other hand, as the installed base of programs gets larger and larger, more and more of the space of legal language strings is consumed and therefore the likelihood of more problems like this when going from 2->3 goes up, so forwards compat becomes MORE important as time goes on.
I guess what I'm saying is that in the long run, forwards compatibility for a particular version becomes less important, but forwards compatibility for the current version becomes more important.
It's a hard problem no matter how you slice it.Anonymous
October 22, 2005
Well, if people will rely on unspecified behaviour... :)
(I know, backwards compatibility is a serious issue. That said, I'm more surprised that you're trying to pass off the reason for not regressing to the non-standard implementation on the late stage of development, rather than just appologising for having the bug in the first place)Anonymous
October 22, 2005
Clearly these kinds of things are judgment calls and opinions vary. Everyone on the C# team believes that spec compliance and backwards compatibility are important goals -- but when those goals are in conflict, different people have different opinions on which one should win in a particular situation.
When we were working on scripting, there is absolutely no doubt in my mind that we would never have made such a breaking change in the engine semantics without a much better reason than "it's in the spec". The thing with JScript is that there is only one JScript engine on your machine, and when its upgraded, its upgraded. There's no going back to the old behaviour, you just start getting broken pages. Backwards compat was WAY more important than spec compliance in JScript.
But with C# we have side-by-side compilers, and end users are not compiling and running code, pro devs are. In this scenario, my personal opinion is that spec compliance begins to edge out backwards compatibility.
Others on the C# team are very much for backwards compat over spec compliance when the two goals conflict. Ultimately these things have to be judgment calls with all constituencies represented in the argument -- C# programmers, end users, tools vendors (who rely on the specification being accurate), and the internal needs of the C# dev/test/PM/user ed teams.Anonymous
May 03, 2006
PingBack from http://jinfeng10.theblog.net/a/2006/05/03/about_enumAnonymous
July 24, 2006
The comment has been removedAnonymous
September 06, 2007
We on the C# team hate making breaking changes. As my colleague Neal called out in his article on theAnonymous
February 11, 2008
The comment has been removed