In the C#—Java shootout, Java edges it in my opinion when it comes to support for enums. C# enums are pretty vanilla: a set of named values, with support for flags via the Flags attribute. I’m often frustrated that I can’t add functionality and extra data to my enums. For example, here’s the canonical Java 5 example:
public enum Planet {
MERCURY (3.303e+23, 2.4397e6),
VENUS (4.869e+24, 6.0518e6),
EARTH (5.976e+24, 6.37814e6),
MARS (6.421e+23, 3.3972e6),
JUPITER (1.9e+27, 7.1492e7),
SATURN (5.688e+26, 6.0268e7),
URANUS (8.686e+25, 2.5559e7),
NEPTUNE (1.024e+26, 2.4746e7),
PLUTO (1.27e+22, 1.137e6);
private final double mass; // in kilograms
private final double radius; // in meters
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
}
public double mass() { return mass; }
public double radius() { return radius; }
// universal gravitational constant (m3 kg-1 s-2)
public static final double G = 6.67300E-11;
public double surfaceGravity() {
return G * mass / (radius * radius);
}
public double surfaceWeight(double otherMass) {
return otherMass * surfaceGravity();
}
}
Each planet has a mass and radius associated with it directly in the enum definition. For example, Mercury’s mass is 3.303e+23 kg and its radius is 2.4397e6 m. The enum has a constant, G, and public methods to calculate the surface gravity of the planet and surface weight of a mass on the planet.
Emulating this in .NET 2.0
Let’s try to emulate this in .NET 2.0. We want a Planet enum with 9 entries, each with a mass and radius. From these, we want to be able to calculate the surface gravity of the planet and surface weight of a mass.
How should we express this in C#? Our Planet can’t be an enum since we can’t poke around with them (System.Enum is a very special type), so it’ll have to be a struct or a class. Enums are value types so let’s go with a struct. We still want the compiler conveniences afforded to enums so let’s keep the planets as a nested enum:
public struct Planet
{
public enum _
{
Mercury,
Venus,
Earth,
Mars,
Jupiter,
Saturn,
Uranus,
Neptune,
Pluto
}
private Planet._ enumValue;
public Planet(Planet._ enumValue)
{
this.Value = enumValue;
}
public Planet._ Value
{
get { return this.enumValue; }
private set { this.enumValue= value; }
}
public double Mass
{
get
{
/* TODO */
}
}
public double Radius
{
get
{
/* TODO */
}
}
public const double G = 6.67300E-11;
public double SurfaceGravity
{
get { return G * this.Mass / (this.Radius * this.Radius); }
}
public double SurfaceWeight(double otherMass)
{
return otherMass * this.SurfaceGravity;
}
}
So this is a good starting point. We can construct a Planet like so:
Planet myplanet = new Planet(Planet._.Mercury);
We can switch on its value:
switch (myplanet.Value)
{
case Planet._.Mercury:
/* ... */
break;
case Planet._.Venus:
/* ... */
}
Adding the data
However, we’ve yet to add the data, so let’s do that. We’ll change the Mass and Radius properties like so:
public double Mass
{
get
{
switch (this.Value)
{
case _.Mercury: return 3.303e+23;
case _.Venus: return 4.869e+24;
case _.Earth: return 5.976e+24;
case _.Mars: return 6.421e+23;
case _.Jupiter: return 1.9e+27;
case _.Saturn: return 5.688e+26;
case _.Uranus: return 8.686e+25;
case _.Neptune: return 1.024e+26;
case _.Pluto: return 1.27e+22;
default: throw new Exception("Illegal state");
}
}
}
public double Radius
{
get
{
switch (this.Value)
{
case _.Mercury: return 2.4397e6;
case _.Venus: return 6.0518e6;
case _.Earth: return 6.37814e6;
case _.Mars: return 3.3972e6;
case _.Jupiter: return 7.1492e7;
case _.Saturn: return 6.0268e7;
case _.Uranus: return 2.5559e7;
case _.Neptune: return 2.4746e7;
case _.Pluto: return 1.137e6;
default: throw new Exception("Illegal state");
}
}
}
Finally we have a complete implementation of the Java 5 Planet enum that we can use. Two things bug me at this point:
- It’s a lot less terse than the Java 5 definition. Surely we can move some of this out into a base class? Also, we’ve not overridden Equals and operator == which we really should do for a struct, or implemented IComparable, IFormattable or IConvertible like System.Enum. The full enum would be quite a bit longer
- To get at the actual enum value, we have to type myplanet.Value which is less intuitive and clean than just typing myplanet.
Adding Syntactic Sugar
Let’s create a base class, ExtendedEnum<T>, to be the base class for all our data enums. We’ll have to change our Planet struct into a sealed class that extends our base class.
public abstract class ExtendedEnum
: IComparable, IFormattable, IConvertible
where T : struct, IComparable, IFormattable, IConvertible
{
private T enumValue;
public ExtendedEnum(T enumValue)
{
this.Value = enumValue;
}
public ExtendedEnum(string enumName)
{
this.Value = (T)Enum.Parse(typeof(T), enumName);
}
public T Value
{
get { return this.enumValue; }
private set { this.enumValue = value; }
}
// Overloads the unary + operator to convert the ExtendedEnum to
// its enum value. This is a shorthand for extendedEnum.Value
public static T operator +(ExtendedEnum extendedEnum)
{
return extendedEnum.Value;
}
// Implicit conversion from ExtendedEnum to the enum value.
public static implicit operator T(ExtendedEnum extendedEnum)
{
return extendedEnum.Value;
}
public override bool Equals(object obj)
{
if (obj == null)
return false;
if (object.ReferenceEquals(this, obj))
return true;
if (obj is T)
return this.Equals((T)obj);
else if (obj is ExtendedEnum)
return this.Equals((ExtendedEnum)obj);
else
return false;
}
public static bool operator ==(ExtendedEnum left,
ExtendedEnum right)
{
return left.Equals(right);
}
// public static bool operator != ...
public bool Equals(T otherValue)
{
return this.Value.Equals(otherValue);
}
public bool Equals(ExtendedEnum other)
{
return this.Value.Equals(other.Value);
}
// public override int GetHashCode() ...
// public override string ToString() ...
// IComparable, IFormattable, IConvertible Members ...
}
Note that the type parameter, T, is constrained to a value type, and not to System.Enum, since certain special types cannot be used in type constraints.
Now our enum looks like this:
public sealed class Planet
: ExtendedEnum
{
public enum _
{
Mercury,
Venus,
Earth,
Mars,
Jupiter,
Saturn,
Uranus,
Neptune,
Pluto
}
public Planet(Planet._ enumValue)
: base(enumValue)
{
}
public Planet(string enumValue)
: base(enumValue)
{
}
public double Mass
{
get
{
switch (+this)
{
case _.Mercury: return 3.303e+23;
case _.Venus: return 4.869e+24;
case _.Earth: return 5.976e+24;
case _.Mars: return 6.421e+23;
case _.Jupiter: return 1.9e+27;
case _.Saturn: return 5.688e+26;
case _.Uranus: return 8.686e+25;
case _.Neptune: return 1.024e+26;
case _.Pluto: return 1.27e+22;
default: throw new Exception("Illegal state");
}
}
}
public double Radius
{
get
{
switch (+this)
{
case _.Mercury: return 2.4397e6;
case _.Venus: return 6.0518e6;
case _.Earth: return 6.37814e6;
case _.Mars: return 3.3972e6;
case _.Jupiter: return 7.1492e7;
case _.Saturn: return 6.0268e7;
case _.Uranus: return 2.5559e7;
case _.Neptune: return 2.4746e7;
case _.Pluto: return 1.137e6;
default: throw new Exception("Illegal state");
}
}
}
public const double G = 6.67300E-11;
public double SurfaceGravity
{
get { return G * this.Mass / (this.Radius * this.Radius);
}
public double SurfaceWeight(double otherMass)
{
return otherMass * this.SurfaceGravity;
}
}
That looks pretty good; our “enum” class now only contains code relevant to the enumeration (plus two constructors), but it’s still not that concise. This is as far as I went with the .NET 2 implementation.
Let’s take a look at what we managed to do:
- Enumerations with data (properties) and “behaviour” (methods)
- Syntactic sugar for accessing the enum value and casting, though the + operator is not very intuitive
- Can still use the enum in a switch statement
- Reasonably terse code
All in all, not bad. I’ll be looking at a .NET 3.5 solution in the next post, and possibly some more outlandish solutions after that. Until then, how would you improve the .NET 2.0 solution?
The source code for this experiment is available at Google Code as a Visual Studio 2008 project, licensed under a MIT license.