James Michael Hare

...hare-brained ideas from the realm of software development...
posts - 136 , comments - 1089 , trackbacks - 0

My Links

News

Welcome to my blog! I'm a Sr. Software Development Engineer in Seattle, WA. I've been doing C++/C#/Java development for over 18 years, but have definitely learned that there is always more to learn!

All thoughts and opinions expressed in my blog and my comments are my own and do not represent the thoughts of my employer.

Blogs I Read

MCC Logo MVP Logo

Follow BlkRabbitCoder on Twitter

Tag Cloud

Archives

Post Categories

C#/.NET Little Pitfalls: Implicit Zero To Enum Conversion

C# is a wonderful language for modern programming.  While everything in C# has a reason and a place, occasionally there are things that can be confusing for a developer who isn’t aware of what is happening behind the scenes. This is another post in the Little Pitfalls series where I explore these issues; an index of the Little Wonders and Little Pitfalls posts is here.

Many times, we create overloaded methods or constructors to allow them to accept different kinds of data.  Further, there are times that we may accept object when any value will do.  This works well (aside from boxing/unboxing concerns for value types), but if you have an overload that accepts object and one that takes an enum, and you pass a constant expression of 0, where does it go?

The Pitfall: Constant zero-value expressions are special

Let’s use a contrived example of a messaging framework where you can either construct messages with a specific payload, or construct a “basic” message which will have one of an enumerated set of “standard” payloads.  For example, let’s say we allow these basic messages:

   1: public enum BasicMessage
   2: {
   3:     Ack,
   4:     Heartbeat,
   5: }

And we allow for a Message to be constructed either using an object, or from a BasicMessage:

   1: public class Message
   2: {
   3:     public string Detail { get; private set; }
   4:  
   5:     public Message(object messageDetail)
   6:     {
   7:         Detail = messageDetail.ToString();
   8:     }
   9:  
  10:     public Message(BasicMessage messageType)
  11:     {
  12:         switch (messageType)
  13:         {
  14:             case BasicMessage.Ack:
  15:                 Detail = "Acknowledgement";
  16:                 break;
  17:  
  18:             case BasicMessage.Heartbeat:
  19:                 Detail = "Heartbeat";
  20:                 break;
  21:  
  22:             default:
  23:                 Detail = "Unknown";
  24:                 break;
  25:         }
  26:     }
  27: }

Seems fine, right?  So if we had something like:

   1: // Detail will be "This is a test message."
   2: var m1 = new Message("This is a test message.");
   3:  
   4: // Detail will be "3.1415927"
   5: var m2 = new Message(3.1415927);
   6:  
   7: // Detail will be "Acknowledgement"
   8: var m3 = new Message(BasicMessage.Ack);
   9:  
  10: Console.WriteLine(m1.Detail);
  11: Console.WriteLine(m2.Detail);
  12: Console.WriteLine(m3.Detail);

Everything looks fine and dandy.  We see the output:

   1: This is a test message
   2: 3.1415927
   3: Acknowledgement

So it seems that our overloads work and we are able to handle both our special BasicMessage enumeration, and any other object, which will use the ToString() result on that object for the message details.  It all sounds straightforward, but what if we had this:

   1: // We'd expect Detail to be "0"
   2: var m4 = new Message(0);
   3:  
   4: Console.WriteLine(m4.Detail);

We’d expect to get a “0”, but we don’t, we get “Acknowledgement” instead.  What happened?

Well, it happens that any (well, nearly any – depending on .NET version) constant zero expression has an implicit conversion to any enum.  In short, this means that .NET prefers to implicitly convert the 0 above into BasicMessage instead of an object.  This is all according to the language specs which state:

An implicit enumeration conversion permits the decimal-integer-literal 0 to be converted to any enum-type and to any nullable-type whose underlying type is an enum-type. In the latter case the conversion is evaluated by converting to the underlying enum-type and wrapping the result.

For this to occur, it must be a constant zero expression.  That is, it must involve literals or compile-time constants and no variables.  In truth, depending on your .NET version, there are some const zero expressions that don’t qualify, but we won’t get too far in the weeds on which do or don’t (See Eric Lippert’s blog posts referenced in the summary for more details).  The main point is to be prepared for the possibility of it happening.  So, consider the following:

   1: // 0 is a constant literal, converts to the enum
   2: var m4 = new Message(0);
   3:  
   4: // still a const zero expression, converts to the enum
   5: var m5 = new Message(0 * 13);
   6:  
   7: const double IHaveNothing = 0.0;
   8:  
   9: // STILL a const zero value (double at that...), converts to the enum
  10: var m6 = new Message(IHaveNothing * 13);
  11:  
  12: // not a const zero expression, because involves a non-const variable, goes to object
  13: int x = 0;
  14: var m7 = new Message(x);

Curious, eh?  Nearly any constant zero value will prefer to implicitly convert to an enum rather than an object if no other choice exists.  This is true even if you attempt to force a cast to int, however casting to object would force it to the “correct” side:

   1: // The cast to int does nothing here, still const zero expression
   2: var m4 = new Message((int)0);
   3:  
   4: // This works, because it will box and then prefer object
   5: var m5 = new Message((object)0);

This also means that you can assign an enum a const zero expression as well:

   1: // complies, implicit conversion from const zero expression to enum
   2: BasicMessage b1 = 0;
   3:  
   4: // does not compile, must cast if non-zero or non-const
   5: BasicMessage b2 = 13;

In truth, the situations in which this happens seem to be contrived, yet you’d be surprised how many times this question comes up on sites like Stack Overflow.  So how do we keep us from biting us?

The Solution: Make explicit overloads

First of all, this behavior is as designed, this is not a bug, so if this is ever not our desired behavior we should provide overloads for int and the other appropriate numeric value types (double, short, long, etc.) to prevent the issue. 

In this way it won’t need to choose between object and an enum, but can directly match the appropriate numeric parameter.   Just making an int overload isn’t always enough, this would not catch larger-than-int types like double, long, etc., and it would create an ambiguous overload error for smaller-than-int types like short, byte, etc. since now it can’t decide between the enum or the widening of the value type, as both are simple implicit conversions.

Thus, to really bullet-proof, we either need to have overloads for all the valid numeric value types:

   1: public Message(object messageDetail) { ... }
   2:  
   3: public Message(int messageDetail) { ... }
   4:  
   5: public Message(short messageDetail) { ... }
   6:  
   7: public Message(long messageDetail) { ... }
   8:  
   9: public Message(float messageDetail) { ... }
  10:  
  11: public Message(double messageDetail) { ... }
  12:  
  13: // etc for sbyte, byte, char, ushort, uint, ulong, and decimal

Or instead have a method with a generic type parameter.  Note that this won’t work on overloading a constructor where the generic type parameter is not a generic type parameter of the class itself (can’t add generic type parameters to a constructor), but it will work on other methods where you may have a similar issue with overloads.

Summary

In short, when you are supplying overloads to handle many different values types, be aware that a constant zero expression can silently and implicitly convert to an enum instead of going to an object

As such, if this is not the behavior you desire, make sure you have overloads for the primitive numeric types (or use a generic if possible), which also has the added benefit of avoiding boxing costs.

See Eric Lippert’s excellent blog entries “The Root of All Evil, Part One” and  “The Root Of All Evil, Part Two” for more details on when and why this happens.

 

Technorati Tags: , , , , ,

Print | posted on Thursday, January 26, 2012 6:31 PM | Filed Under [ My Blog C# Software .NET Fundamentals Little Pitfalls ]

Feedback

Gravatar

# re: C#/.NET Little Pitfalls: Implicit Zero To Enum Conversion

It could also cause headaches when an enum is used as default indexer on a collection : http://stackoverflow.com/questions/7474609/overloaded-indexer-with-enum-impossible-to-use-default-indexer
1/27/2012 7:19 AM | styx31
Gravatar

# re: C#/.NET Little Pitfalls: Implicit Zero To Enum Conversion

Gah, I hate this. I wish enums started at 1 instead of 0.
1/27/2012 9:19 PM | Ryan
Gravatar

# re: C#/.NET Little Pitfalls: Implicit Zero To Enum Conversion

Side note. If you create an enum called AdvancedMessage and try to create a constructor for it too you will get a compiler error. You cannot have two constructors that only take in an enum. "The call is ambiguous... "
1/30/2012 8:47 AM | Joe
Gravatar

# re: C#/.NET Little Pitfalls: Implicit Zero To Enum Conversion

Correction to my post. It will compile of there is no code calling it with a constant. Once you add code like 'Message m = new Message(0);' that line will bring up the compiler error.
1/30/2012 8:53 AM | Joe
Gravatar

# re: C#/.NET Little Pitfalls: Implicit Zero To Enum Conversion

@Joe: Yes, because at that point it's ambigous, the literal 0 can implicitly convert to either enum, and the compiler won't assume the responsibility of two equal choices.
1/30/2012 9:16 AM | James Michael Hare
Gravatar

# re: C#/.NET Little Pitfalls: Implicit Zero To Enum Conversion

@Ryan: hah, well, historically C# and it's ancestors (Java, C++, C) have always started enum and array indexes, etc, at 0. Just the nature of the beast. You CAN start your enums at 1 if you choose, though:

public enum MyEnum { A = 1, B, C, D, E, F }

Though, of course that means default(MyEnum) still == 0 which is undefined in your values. Typically, especially if I'm getting data from a file or a web service, etc, I'll have an Unknown (0) value for my enum so that any enum value I don't understand (or if it's not assigned) will be "flagged" showing it as bad:

public enum SomeEnum { Unknown, FirstGood, SecondGood, etc }
1/30/2012 9:20 AM | James Michael Hare
Gravatar

# re: C#/.NET Little Pitfalls: Implicit Zero To Enum Conversion

I have a hell of alot simpler way for handling this:

public enum BasicMessage

2: {
Null = 0,
3: Ack,

4: Heartbeat,

5: }

(fyi it sucks copying your blog code samples)

Now all problems solved. You can directly test for the presence of Null and error when appropriate.
1/31/2012 1:22 PM | Chris Marisic
Gravatar

# re: C#/.NET Little Pitfalls: Implicit Zero To Enum Conversion

@Chris: That's not quite a null check, per se, since the null is qualified by the enum. In fact I'd be very hesitant to make an enum value be a keyword even if the casing is different.

Instead of doing that, you could just use Nullable<T>:

BasicMessage? x = null;
1/31/2012 7:27 PM | James Michael Hare
Post A Comment
Title:
Name:
Email:
Comment:
Verification:
 
 

Powered by: