C# – Die Falle mit dem this[]-Indexer und dem Enum

Vor ein paar Tagen kam ein Kollege auf mich zu und meinte, er habe beim Unit-Test schreiben einen Fehler in meinem Code entdeckt. Um mir den Fehler zu zeigen, hat er mir eigens einen Unit-Test geschrieben, welcher in etwa so aussah:

// MyTypeList : List<string>
MyTypeList list = new MyTypeList {"a", "b", "c"};

if(list.ElementAt(0) == null)
{
    throw new NullReferenceException();
}

if(list[0] == null)
{
    // Hier knallt er!
    throw new NullReferenceException();
}

if(list[1] == null)
{
    throw new NullReferenceException();
}

Er knallte immer, wenn man auf list[0] zugreifen wollte. Im Debugger zeigte er mir alle Elemente an, list[1] usw. funktionierte auch. Ich stand erstmal auf dem Schlau. Da ich den Fehler nicht sofort identifizieren konnte, baute ich mir eine kleine Testapplikation, um mit einem Minimum an Code das Problem zu identifizieren.

Letzendlich lag es an einem Indexer, welchen ich selber erstellt habe. Und zwar erwartete dieser als Übergabetyp ein Enum. Dieser überschreibt zwar nicht den von List<T> geerbten Indexer this[int index], jedoch scheint der Compiler hier leichte Probleme zu machen.

Schauen wir uns mal den kompilierten Code in dotPeek an.

      MyTypeList myTypeList1 = new MyTypeList();
      myTypeList1.Add("a");
      myTypeList1.Add("b");
      myTypeList1.Add("c");
      MyTypeList myTypeList2 = myTypeList1;
      if (Enumerable.ElementAt<string>((IEnumerable<string>) myTypeList2, 0) == null)
        throw new NullReferenceException();
      if (myTypeList2[MyEnum.Value1] == null)
        throw new NullReferenceException();

Den ersten Zugriff auf den numerischen Indexer wurde auf den Enum-Indexer geändert. Eine Erklärung dazu konnte ich nicht wirklich finden, bzw. keine die ich nachvollziehen konnte. Letztendlich hat es damit zu tun, dass ein Enum im Bauch eigentlich nur ein int-Typ ist. Dies passiert immer, wenn ich den numerischen Indexer mit 0 zugreife, wohlgemerkt, nur mit 0. Alle anderen Werte funktionieren problemlos.

Wie kann ich das Problem lösen / umgehen?

Dafür gibt es mehrere Wege.

Wird der Index-Wert woher in eine Variable (z.B. innerhalb einer for-Schleife) gesetzt, kann er den korrekten Indexer auflösen.

int index = 0;
if(list[index] == null) {
...
}

Ein Cast direkt auf int hingegen funktioniert nicht (er nutzt wieder den Enum-Indexer):

if(list[(int) 0] == null) {
..
}

Lustiger Weise lässt sich die Solution nicht mehr kompilieren, sobald ich einen weiteren Enum-Indexer hinzufüge:

The call is ambiguous between the following methods or properties: 'DumpSolution.MyTypeList.this[DumpSolution.MyEnum]' and 'DumpSolution.MyTypeList.this[DumpSolution.MyEnum2]'

Die simpelste Lösung ist, einfach den Standard-Indexer zu überschreiben:

    public class MyTypeList : List<string>
    {
        public string this[MyEnum myEnum]
        {
            get { return this.FirstOrDefault(item => item == Enum.GetName(myEnum.GetType(), myEnum)); }
        }

        public string this[MyEnum2 myEnum]
        {
            get { return this.FirstOrDefault(item => item == Enum.GetName(myEnum.GetType(), myEnum)); }
        }

        public new string this[int index]
        {
            get { return base[index]; }
            set { base[index] = value; }
        }
    }

Danach funktioniert der Code oben problemlos und korrekt. Ach ja, ein anpassen des Enums wie folgt brachte auch keinen Erfolg.

enum MyEnum {
   Value1 = 1,
   Value2 = 2
}

Ich kann mir vorstellen, dass es das Problem auch bei anderen Listen gibt, ich selbst habe es aber aktuell nur bei List<T> festgestellt. Im Netz findet man einige Einträge dazu, dazu auch eine Erklärung (welche mich aber nur bedingt zufrieden stellt)

Ich habe ein kleines Beispiel, welches ihr hier herunterladen könnt.

Dämlicher Programmierfehler

Grad bestimmt 20 Minuten bei der Fehlersuche drauf gegangen. Folgender Code:


List<int> _list = new List<int>();
_list.Add(1);
_list.Add(2);
_list.Add(3);
_list.Add(4);

for(int i = 1; i <= 3; i++) {
   int x = _list[i];
   _list.Remove(x);
}

Mein Code war natürlich nicht mit Integerwerten. Ich musste noch ein paar andere Sachen machen, daher war ein RemoveRange() nicht möglich.
Nun wer findet den Fehler?

Ich liste es einfach mal auf:

Durchlauf: 1 - Lösche Element 1 - Rest 2
Durchlauf: 2 - Lösche Element 2 - Rest 1
Durchlauf: 3 - Lösche Element 3 - Rest 0

Beim letzten Durchlauf wird er immer Probleme kriegen. Das Element gibt es nicht mehr. Durch Remove() rücken die Elemente in der Indizierung nach.
Es müsste also heißen:

Durchlauf: 1 - Lösche Element 1 - Rest 2
Durchlauf: 2 - Lösche Element 1 - Rest 1
Durchlauf: 3 - Lösche Element 1 - Rest 0

oder in C#-Code:

for(int i = 1; i <= 3; i++) {
   int x = _list[1];
   _list.Remove(x);
}

Vor allem wenn man in den Debugger schaut, kamen da merkwürdige Werte raus.
Ich sehe, dass die Methode „DeleteEntries()“ mit dem Count-Parameter 2 aufgerufen wird, oben zeigt mir der Debugger aber 1 an. Liegt daran, dass in der Liste bereits ein Element entfernt wurde. Somit ist also, wenn man es genau nimmt, beides richtig.