Das „Try-“ Pattern als Antwort auf „return null“

Eine Sache die mich seit meinen (wenig umfangreichen) C Tagen nervt, sind Methoden die null Werte im Fehlerfall zurück geben. Mag sein, dass dies in einer Parallelwelt ohne Exceptionhandling seine Berechtigung hat. In Zeiten von Java und .NET ist so etwas aber antiquiert.

Ich weiß, dass sich schon eine ganze Reihe Blogs über das Thema ausgelassen haben, aber ich möchte an der Stelle einfach mal meinen Beitrag leisten, denn in meinem Arbeitsalltag stoße ich immer wieder darauf.

Folgende Methode sei gegeben:

public DataContainer GetDataContainer(Guid id)
{
     DataContainer container = null;

     // suche nach dem passenden Container
     // ...

     return container;
}

Wie man sieht wird unter Umständen ein Nullwert zurück gegeben. Alle Aufrufer müssen demnach prüfen ob der erwartete Wert zurück geliefert wurde oder nicht. Wenn sie dies nicht tun gibt es eine NullReferenceException und diese ist häufig verwirrend.

Warum verwirrend? Weil nicht jeder Rückgabewert einer Methode auf seine Validität geprüft werden kann und auch nicht geprüft werden sollte. Zumal der Rückgabewert im gegebenen Fall nicht selten einfach weiter gereicht wird. Die Exception fliegt dann wahrscheinlich an einer Stelle die mit ihrer Ursache nichts zu tun hat.

Um so etwas also zu verhindern schmeißen wir gleich selbst eine Exception falls der Erwartungswert (passender Wert zum Parameter) nicht erfüllt wird. Dies hat nicht nur den Vorteil, dass wir den tatsächlichen Verursacher gleich anhand des Stacktraces ausfindig machen können, nein wir können sogar unsere eigene Exception-Klasse mit entsprechenden Informationen verwenden und damit die Problemlösung unter Umständen vereinfachen.

public DataContainer GetDataContainer(Guid id)
{
   DataContainer container = null;

   // suche nach dem passenden Container
   // ...

   if (container == null)
       throw new DataNotFoundException(string.Format(
	"Data with id {0} was not found", id));

   return container;
}

Code-Duplikate zum Prüfen auf null gehören damit der Vergangenheit an. Darüber hinaus, muss der Aufrufer sich keine Gedanken über den internen Zustand des DataManagements zu machen. Wenn etwas nicht stimmt, wird er es schon merken…

Nun haben wir aber ein Problem. Denn um Fehler zu vermeiden muss sich der Aufrufer wirklich sicher sein, dass die Erwartungswerte auch erfüllt werden. Es kann aber sein, dass Daten noch nicht verfügbar sind und dann evtl. angelegt werden sollen.

Was also tun? Hier kommt „Try“ ins Spiel. Man sagt damit aus, dass man nicht unter allen Umständen die Ausführung einer Aktion verlangt, sondern auch damit rechnet, dass diese „schief“ gehen könnte. Auf diese Weise vermeidet man, dass manch Unbedarfter einfach jede Exception fängt die im vorangegangen erläuterten Beispiel geschmissen würde und daraus seine Schlüsse zieht (oder eben nicht…).

public bool TryGetDataContainer(Guid id, out DataContainer result)
{
   // suche nach dem passenden Container
   var resultCollection = from x in containers where x.Id == id select x;

   if (resultCollection.Count() == 0)
   {
       result = null;
       return false;
   }
    // möglicher fehler hier wird erstmal ignoriert...
    result = resultCollection.First();
    return true;
}

Wenn man jetzt noch etwas optimiert kann man auch die letzten Code-Duplikate beseitigen:

public class DataManagement
{
  List<DataContainer> containers = new List<DataContainer>();

  public DataContainer GetDataContainer(Guid id)
  {
     DataContainer container;

     if (!TryGetDataContainer(id, out container))
     {
       throw new DataNotFoundException(
         string.Format"Data with Guid {0} was not found", id));
     }

     return container;
 }

 public bool TryGetDataContainer(Guid id, out DataContainer result)
 {
    // suche nach dem passenden Container
    var resultCollection = from x in containers where x.Id == id select x;

    var collectionCount = resultCollection.Count()
    if (collectionCount == 0)
    {
       result = false;
       return false;
    }
    else if(collectionCount != 1)
    {
       throw new Exception(
           string.Format("to many results for id {0}", id));
    }

    result = resultCollection.First();
    return true;
 }
}

Das ist alles nichts Neues und wird spätestens seit .Net 2.0 beispielsweise beim Dictionary oder den Methoden zum Parsen von typischen Wertetypen verwendet (int.TryParse). Nun muss es nur noch überall eingesetzt werden.