views:

107

answers:

4
class Program
{
    static void Main(string[] args)
    {
        string value = "12345";
        Type enumType = typeof(Fruits);
        Fruits fruit = Fruits.Apple;
        try
        {
            fruit = (Fruits) Enum.Parse(enumType, value);
        }
        catch (ArgumentException)
        {
            Console.WriteLine(String.Format("{0} is no healthy food.", value));
        }
        Console.WriteLine(String.Format("You should eat at least one {0} per day.", fruit));
        Console.ReadKey();
    }

    public enum Fruits
    {
        Apple,
        Banana,
        Orange
    }
}

If you execute the code above the result shows:

You should eat at least one 12345 per day.

I really expected an ArgumentException to be thrown if a unknown name (string) is passed. Taking a close look at the Enum.Parse definition reveals:

Summary:
Converts the string representation of the name or numeric value of one or more enumerated constants to an equivalent enumerated object.

Exceptions:
ArgumentException: enumType is not an Enum. -or- value is either an empty string or only contains white space. -or- value is a name, but not one of the named constants defined for the enumeration.

I.e. if a string representation of an integer is passed, a new enum value is created and now exception is thrown by design. Does this make sense?

At least I now know to call Enum.IsDefined(enumType, value) prior to Enum.Parse()

A: 

You need to use Enum.IsDefined:

http://msdn.microsoft.com/en-us/library/essfb559.aspx

using System;

    [Flags] enum Colors { None=0, Red = 1, Green = 2, Blue = 4 };

    public class Example
    {
       public static void Main()
       {
          string[] colorStrings = { "0", "2", "8", "blue", "Blue", "Yellow", "Red, Green" };
          foreach (string colorString in colorStrings)
          {
             try {
                Colors colorValue = (Colors) Enum.Parse(typeof(Colors), colorString);        
                if (Enum.IsDefined(typeof(Colors), colorValue) | colorValue.ToString().Contains(","))  
                   Console.WriteLine("Converted '{0}' to {1}.", colorString, colorValue.ToString());
                else
                   Console.WriteLine("{0} is not an underlying value of the Colors enumeration.", colorString);
             }
             catch (ArgumentException) {
                Console.WriteLine("'{0}' is not a member of the Colors enumeration.", colorString);
             }
          }
       }
    }
    // The example displays the following output:
    //       Converted '0' to None.
    //       Converted '2' to Green.
    //       8 is not an underlying value of the Colors enumeration.
    //       'blue' is not a member of the Colors enumeration.
    //       Converted 'Blue' to Blue.
    //       'Yellow' is not a member of the Colors enumeration.
    //       Converted 'Red, Green' to Red, Green.
Dave Swersky
He already said that in his question :)
Kent Boogaart
Yes I know, but I don't understand why Enum.Parse generates new values, it's like calling Int.Parse("big number not known to mankind") and getting a result...
Markus
+1  A: 

I personally think it's a pity that Enum.Parse accepts the string representation of a number. If you're looking for an alternative, you might want to look at my Unconstrained Melody project which has various parsing options, and is strongly typed too.

You certainly can use Enum.IsDefined in conjunction with parsing. Do you definitely want to accept string versions of numbers though? Or are you really only expecting names?

Jon Skeet
Is it really a pity that `Enum.Parse` accepts the string representation of a number? I would consider it broken if it didn't, because it would mean that an enum value might not round trip to and from a string, i.e. `Enum.Parse(enumValue.ToString())` could fail.
P Daddy
+1  A: 

The "named constant" is the textual representation of an Enum's value, not the number that you've assigned to it.

If you change:

string value = "12345";

To:

string value = "Cake";

You'll see the error you're expecting, because "value is a name, but not one of the named constants defined for the enumeration.". In this instance the value you're passing in is a name, "Cake", but not one in the enumeration.

Think of Enum.Parse(enumType, value); doing the following:

  1. If value is a null reference, throw an ArgumentNullException
  2. Is the value in value one of the named constants in the enumeration in enumType. If yes, return that value from the enumeration and stop.
  3. Is the value in value directly convertible to the underlying type (in this instance Int32), if yes, return that value and stop (even if there's no named constant for that value).
  4. Is the value in value directly convertible to the underlying type, but outside of the range of the underlying type? e.g. the value is a string containing a number one greater than MAXINT. If yes, throw an OverflowException.
  5. Is the value not castable to the underlying type? If yes, throw an ArgumentException.
Rob
@Rob: You are right, as Enum is a Int32 Enum.Parse("<integer representation>") is correct in returning a valid value. My problem was caused by seeing Enum as a constricted Int32 which it is in fact not.
Markus
This is not, strictly speaking, correct. A string is never "castable" to an int, it requires a conversion that's possible through a variety of methods (`Convert.ToInt32`, `int.Parse`, `int.TryParse`, etc.). A more accurate expression of 3.) would be "Is the value in `value` directly *convertible* to the underlying type (in this instance `Int32`)? If yes, return that value and stop (**even if there's no named constant for that value**).
Adam Robinson
@Adam, fair point, I've changed my answer accordingly =)
Rob
+1  A: 

An enum can be any value of its base integer type. It is not just restricted to named constants.

For instance, the following is perfectly valid:

enum Foo{
    A,
    B,
    C,
    D
}

Foo x = (Foo)5;

Even though 5 does not correspond to a named constant, it is still a valid value for Foo, since the underlying type for Foo is Int32.

If one were to call x.ToString(), the returned value would be simply "5", since no named constant corresponds with x's value.

Enum.Parse() is the converse function of Enum.ToString(). You should expect that whatever Enum.ToString() can return that Enum.Parse() can accept. This includes, for instance, comma-separated values for flags enums:

[Flags]
enum Foo{
    A = 1,
    B = 2,
    C = 4,
    D = 8
}

Foo x = Foo.A | Foo.B | Foo.C | Foo.D;
int i = (int)x;
string s = x.ToString();
Console.WriteLine(i);
Console.WriteLine(s);
Console.WriteLine((Foo)Enum.Parse(typeof(Foo), i.ToString()) == x);
Console.WriteLine((Foo)Enum.Parse(typeof(Foo), s) == x);

Output:

15
A, B, C, D
True
True

EDIT:

What you really seem to want is something like this:

static Enum GetEnumValue(Type enumType, string name){
    // null-checking omitted for brevity

    int index = Array.IndexOf(Enum.GetNames(enumType), name);
    if(index < 0)
        throw new ArgumentException("\"" + name + "\" is not a value in " + enumType, "name");

    return Enum.GetValues(enumType).GetValue(index);
}

or a case-insensitive version:

static Enum GetEnumValue(Type enumType, string name, bool ignoreCase){
    // null-checking omitted

    int index;
    if(ignoreCase)
        index = Array.FindIndex(Enum.GetNames(enumType),
            s => string.Compare(s, name, StringComparison.OrdinalIgnoreCase) == 0);
            // or StringComparison.CurrentCultureIgnoreCase or something if you
            // need to support fancy Unicode names
    else index = Array.IndexOf(Enum.GetNames(enumType), name);

    if(index < 0)
        throw new ArgumentException("\"" + name + "\" is not a value in " + enumType, "name");

    return Enum.GetValues(enumType).GetValue(index);
}
P Daddy
Thanks for the additional insight on `Enum.Parse()` and `ToString()`, I did not know about the feature with comma-separated values.
Markus