Parameterprüfungen zur Laufzeit

Programmierfehler sollen auffallen, und zwar so bald wie möglich! So banal, wie dieser Satz klingt, so leicht drückt man sich um das Kernthema herum. Ich kann mich noch gut daran erinnern, wie ich früher mit den Gedanken entwickelt habe: Auslösen von Exceptions vermeiden, Aufpoppen von Fehlermeldungen vermeiden, … Getrieben von diesen Vorsätzen programmiert man Methoden beispielsweise so, dass im Fehlerfall ein Default-Wert zurückgegeben wird, welcher sicherstellen soll, dass das Programm halt doch noch irgendwie weiterläuft und niemand etwas merkt. Im Extremfall tauchen leere Catch-Blöcke auf oder es wird (wenigstens) in eine Protokolldatei reingeschrieben, in die sowieso nie jemand reinschaut. Zunächst einmal hat es ja etwas Gutes, dass Programm stürzt nicht ab, wirkt sogar stabil. Aber… irgendwann wird man von der Zeit eingeholt und man hat ein echtes Problem, die tatsächlichen Fehler im Hintergrund zu finden und auszumerzen.

Ich glaube, mit den Sätzen oben schreibe ich vielen Lesern aus der Seele, die Frage ist halt immer bloß, wie geht man ordentlich damit um? Für mich gibt es hier relativ viele Maßnahmen, ein Kernpunkt für mich selbst ist mittlerweile folgender: Fehler sichtbar machen! Mittlerweile arbeite ich entgegen meinem Vorgehen von früher (bis vor ca. 4-5 Jahren) so, dass ich Fehlermeldungen/Exceptions wenn möglich bis zur Oberfläche durchreiche – entweder direkt als Fehler-Dialog oder zumindest als deutlichen Hinweis am Hauptfenster, über den mehrere Informationen zum Fehler abgerufen werden können.

Bei Seeing# habe ich seit gestern damit begonnen, aktiv an mehreren wichtigen Stellen Parameterprüfungen einzubauen, welche im Fehlerfall direkt Exceptions auslösen und so weit wie möglich nach oben bringen. Konkret handelt es sich um ganz einfache Sachen, wie “prüfe, ob variable xyz nicht null ist”, wie im folgenden Beispiel.

 /// <summary>
 /// Adds the given object to the scene.
 /// </summary>
 /// <param name="sceneObject">Object to add.</param>
 /// <param name="layer">Layer on wich the object should be added.</param>
 internal T Add<T>(T sceneObject, string layer)
     where T : SceneObject
 {
     sceneObject.EnsureNotNull("sceneObject");
     layer.EnsureNotNullOrEmpty("layer");

     // ...

Dann gibt es noch etwas “kompliziertere” Fälle wie beispielsweise “dieser Integer muss positiv sein” oder “dieser Vektor muss normalisiert sein”. Nachfolgend ein kleines Beispiel.

/// <summary>
/// Picks an object in 3D space.
/// </summary>
/// <param name="rayStart">Start of picking ray.</param>
/// <param name="rayDirection">Normal of picking ray.</param>
internal List<SceneObject> Pick(
    Vector3 rayStart, Vector3 rayDirection, 
    ViewInformation viewInformation, PickingOptions pickingOptions)
{
    rayDirection.EnsureNormalized("rayDirection");
    viewInformation.EnsureNotNull("viewInformation");
    pickingOptions.EnsureNotNull("pickingOptions");

    // ...

Letzten Endes ist auch das nicht wirklich komplex, aber enorm Hilfreich, da ohne diese Prüfungen unauffällige Fehler schnell unter den Tisch fallen. Wie geschrieben, aktuell bin ich dabei, solche Prüfungen an mehreren Stellen von Seeing# nachzuziehen und siehe da: Seit gestern bin ich auf diese Art schon auf 2 Probleme gestoßen. Eine gute Ausbeute, wenn man bedenkt, dass die Implementierung dieser Methoden vielleicht 10-20 Minuten gedauert hat.

Die Prüfmethoden selbst sind kein Hexenwerk. Es handelt sich lediglich um Erweiterungsmethoden, welche das entsprechende Kriterium prüfen und im Fehlerfall direkt eine Exceptions schmeißen. Einzige Besonderheit ist die, dass ich bei den Methoden das Conditional-Attribut verwende. Der Grund dafür ist, dass ich die Prüfmethoden an möglichst vielen Stellen der 3D-Engine verwenden möchte, ohne im Release-Build Performance-Einschnitte zu bekommen. Nachfolgend das Coding zweier dieser Prüfmethoden.

public static partial class Ensure
{
    [Conditional("DEBUG")]
    public static void EnsureNormalized(
        this Vector3 vectorValue, string checkedVariableName,
        [CallerMemberName]
        string callerMethod = "")
    {
        if (string.IsNullOrEmpty(callerMethod)) { callerMethod = "Unknown"; }

        if (!EngineMath.EqualsWithTolerance(vectorValue.Length(), 1f))
        {
            throw new SeeingSharpCheckException(string.Format(
                "Vector {0} within method {1} must be normalized!",
                checkedVariableName, callerMethod, vectorValue));
        }
    }

    [Conditional("DEBUG")]
    public static void EnsureNormalized(
        this Vector2 vectorValue, string checkedVariableName,
        [CallerMemberName]
        string callerMethod = "")
    {
        if (string.IsNullOrEmpty(callerMethod)) { callerMethod = "Unknown"; }

        if (!EngineMath.EqualsWithTolerance(vectorValue.Length(), 1f))
        {
            throw new SeeingSharpCheckException(string.Format(
                "Vector {0} within method {1} must be normalized!",
                checkedVariableName, callerMethod, vectorValue));
        }
    }
}

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.