views:

511

answers:

4

How come List.Find (and LINQ-queries on the list as well) always return the first enum element when the list does not contain the element I am searching for?

Scenario:

My enum:

public enum TestEnum
{
    EnumOne,
    EnumTwo,
    EnumThree
}

My test:

var TestEnum1 = TestEnum.EnumOne;
var TestEnum2 = TestEnum.EnumTwo;
var TestEnum3 = TestEnum.EnumThree;

List<TestEnum> testEnumList = new List<TestEnum>();//{ TestEnum1, TestEnum2 };
var selectedWithLinq = (from c in testEnumList where c.Equals(TestEnum3) select c).FirstOrDefault();
var selectedWithListFind = testEnumList.Find(myEnum => TestEnum3.Equals(myEnum)));

Both selectedWithLinq and selectedWithListFind in this case returns TestEnum.EnumOne. If I add TestEnum3 to the list, it will return correctly.

+2  A: 

Because the first member of an enum is, unless specified otherwise, zero, which is the default value for any enum.

OregonGhost
A: 

The default value of TestEnum will be its first item, so when you do .FirstOrDefault(), TestEnum.EnumOne will be returned as the default when a "first" matching your criteria can't be found.

asbjornu
I think this is not the reason, since I expect it to not work if there is no zero enum value.
OregonGhost
FirstOrDefault is designed to "work", even if it finds nothing. If you expectit to fail, use .First() and catch the exception.
Cylon Cat
With "not working", I meant it will not return EnumOne if the value of EnumOne is not zero. You can test this by setting EnumOne to 1. In this case, FirstOrDefault returns 0, which does not map to any value of TestEnum (but is still a valid enum value). It is coincidence that EnumOne is the (likely) desired value in the case the question author describes. In other words, FirstOrDefault does not "work" in this case as asbjornu described it, since it won't return EnumOne.
OregonGhost
Based on your example (that doesn't have explicit values set in the enum), my explanation is perfectly correct. Had you provided an example with explicitly set values where none of the values were `0`, I could have told you that the default base type of an enum is `int` (you can change the base type by writing `enum TestEnum : byte`) whose default value is in fact `0`. :)As Cylon Cat writes, you should use `First()` if you want it to fail when the value isn't found. Otherwise, `default(TestEnum)` will be returned.
asbjornu
+8  A: 

As TestEnum is a value type, when no element is found in the list it cannot simply return null (as it would do if you had an array of reference types) but it will return default(TestEnum) which equals to EnumOne in this case. But anyway what are you trying to achieve?

Darin Dimitrov
As an additional case, if you explicitly set the first element in the enumeration to 1, then you'll still get 0, but that will be an invalid value for the enumeration and won't be testable without casting, giving a good approximation of `null`. Don't do this though, add a `.None` option as the first option instead.
Matthew Scharley
@Darin: I was just curious about this somewhat odd behaviour of returning an element, when the element is not in the list. The default mechanism of value types seems like a logical explanation. Thanks :)@Matthew Good idea; will keep that in mind if that issue arrises!
John Korsnes
+2  A: 

As others have said, this is due to the default value of the enum coinciding with your first enum value. Options to work around this:

  1. Use a nullable type:

    var selectedWithLinq = testEnumList.Where(x => x == TestEnum3)
                                       .Select(x => (TestEnum?) x)
                                       .FirstOrDefault();
    if (selectedWithLinq != null)
    {
       var realValue = selectedWithLinq.Value;
        // etc
    }
    
  2. Write your own extension method based on the TryXXX pattern:

    public static bool TryFirst<T>(this IEnumerable<T> source,
                                   out T found)
    {
        using (IEnumerator<T> iterator = source.GetEnumerator())
        {
            if (iterator.MoveNext())
            {
                found = iterator.Current;
                return true;
            }
            else
            {
                found = default(T);
                return false;
            }
        }
    }
    
Jon Skeet