The Case of the Missing Generic (Parse Method)
I’ve noticed that quite a few people in the community have been wondering why on earth the System.Enum type in version 2 of the .NET Framework lacks a generic Parse method. If you haven’t used the Parse method lately, an example will help to clarify the frustration many developers have experienced.
I will use C# for this illustration since that is the language that causes much of the frustration.
:)
Here goes:
FileShare share1 = FileShare.Read | FileShare.Delete;
string text = share1.ToString();
Debug.Assert("Read, Delete" == text);
FileShare share2 = (FileShare) Enum.Parse(typeof(FileShare),
text);
Debug.Assert(share1 == share2);
FileShare is an enum declared in the System.IO namespace. It defines the constraints placed on how a particular file can be shared. Since the CLR defines a common type system, FileShare ultimately derives from System.Object and inherits its virtual ToString method. In the example above, the share1 value is implicitly boxed and the ToString method is then dispatched against the boxed value. Since the FileShare type includes the Flags attribute, the ToString implementation provided by the abstract System.Enum value type knows to comma-separate the flags. Had the Flags attribute been omitted, the ToString override would simply have returned the string representation of the enum’s underlying type, typically a 32-bit integer. So far so good. It’s when you attempt to convert the string representation back to an enum value that the frustration begins.
To convert strings into enum values, the System.Enum type provides a static Parse method with the following signature, expressed in C#:
object Parse(Type enumType,
string value,
bool ignoreCase);
Since enums can have any underlying integral type, the Parse method cannot return a value type and thus returns an object. But what about returning System.Enum? There are two challenges with that. Firstly Enum is abstract so it cannot be returned by value and it’s abstract because Enum itself doesn’t contain the enum’s value field and returning it or passing it by value would slice away the value field. Secondly, C# doesn’t support strongly-typed boxed values – all boxed values are expressed as System.Object types in C#. C++/CLI allows you to express the Parse method more accurately:
Enum^ Parse(Type^ enumType,
String^ value,
bool ignoreCase);
If you’re not familiar with C++/CLI, the ^ declarator indicates that the type declaration is a handle to a CLR reference type. Enum^ is thus a handle to an Enum-derived value type. Unfortunately C# doesn’t support strongly-typed boxed values, but it would not help much anyway since an explicit conversion would still be required to convert the abstract Enum type to a concrete enum value. So let’s give up trying to improve on the respectable Parse method provided by the BCL and focus on how we might improve the situation from within the language.
Here is the Parse method usage again from the C# example:
FileShare share2 = (FileShare) Enum.Parse(typeof(FileShare),
text);
There are fundamentally two problems with this code. The first is the need to repeatedly indicate the exact type of the enum we wish to convert to. A veteran C++ programmer might instinctively write the following (that is if the veteran C++ programmer were familiar with C++/CLI):
template <typename T>
T Parse(String^ value,
bool ignoreCase = false)
{
return safe_cast<T>(Enum::Parse(T::typeid,
value,
ignoreCase));
}
This solves the first problem nicely. The template function generates the necessary code to pass the type to the Parse method as well as to convert the result to the appropriate type. The typeid keyword returns the Type object for the type T. The safe_cast keyword unboxes the Object^ returned by the Parse method, converting it to the type indicated by the template type parameter.
With this template function available, the original C# example can be written more cleanly in C++ as follows:
FileShare share1 = FileShare::Read | FileShare::Delete;
String^ text = share1.ToString();
Debug::Assert("Read, Delete" == text);
FileShare share2 = Parse<FileShare>("Read, Delete");
Debug::Assert(share1 == share2);
This is certainly more elegant but there is still one more problem that plagues both the C# version as well as the C++ template version, although it’s less of a problem in C++. The issue is that we’re not using the compiler to perform any kind of validation to ensure that the types used with the Parse method are in fact of the correct type. In the C# example you can use two completely different types without as much as a warning from the compiler. The C++ example at least doesn’t allow that, but you can still use a non-enum type as the type parameter and the code will compile without any errors. This is less of a problem in practice since you will typically be assigning the result of the Parse method to a strongly-typed variable and the compiler will complain if the Parse template function’s calculated return type does not match the target of the assignment. Notice in this C# example that the wrong type is passed to the Parse method.
FileShare share2 = (FileShare) Enum.Parse(typeof(FileMode),
text);
The template solution is adequate for C++, but can we do better? Why yes! There is even some hope for the C# programmer.
Templates use structural constraints when they are instantiated by the compiler. This is ideal for many classes of problems. In this case however we would like something a little different. What we need is a technique that provides subtype constraints. A CLR generic will do nicely. Consider the following generic C++ functions:
generic <typename T> where T : Enum
static T Parse(String^ value)
{
return Parse<T>(value,
false);
}
generic <typename T> where T : Enum
static T Parse(String^ value,
bool ignoreCase)
{
return safe_cast<T>(Enum::Parse(T::typeid,
value,
ignoreCase));
}
By using a generic function, we can declare a subtype constraint for T such that the compiler will ensure that all uses of the Parse generic function are used with a type parameter that derives from Enum, thus only allowing CLR enumerated types. A default argument cannot be used as with the previous template function because default arguments are not allowed on generic functions or members of managed types. Default arguments are not supported by the CLR.
Finally, the question that may have popped into your head is why doesn’t the .NET Framework’s Enum class provide a generic Parse function as I have just described? The trouble is that C# doesn’t allow Enum to be used as a subtype constraint and the BCL is undoubtedly written in C#. Had the Visual C++ team been ready with C++/CLI when .NET was first being hatched, I suspect things might have turned out differently. Fortunately C++/CLI is coming soon in Visual C++ 2005 and brings all the power and flexibility of templates, generics and multi-paradigm programming to the .NET Framework.
If you’re a C# programmer and aren’t ready to switch to C++ then not to work. Although C# doesn’t allow you to declare the generic function equivalent to the C++ example above, the good news is that Visual C++ generates perfectly legal CLI metadata and IL. Simply expose the generic Parse method on a public class in a C++ library assembly and reference it from your C# project. Using it is simple and natural in C#:
FileShare share2 = Utility.Parse<FileShare>(text);
Don’t you just love .NET! Happy coding, whatever your language(s) of choice.
© 2005 Kenny Kerr