Szybsze pisanie Debug.Assert() dzięki Expression<T>

11.09.2011

W projekcie nad którym aktualnie pracuję pojawia się bardzo dużo asercji w postaci:

internal class Foo
{
    public void Bar(int number)
    {
        Debug.Assert(number < 0, "number >= 0");

        //...
    }
}

W przypadku kiedy warunek nie jest spełniony na ekranie wyświetlany jest komunikat:

image_thumb11

Sprawa jasna. O ile samo wywołanie metody Debug.Assert() nie jest problematyczne. (przecież istnieje coś takiego jak Intellisense!), to problemem staje się wypełnianie argumentu message (“number >= 0”), zwłaszcza gdy robi się to kilkakrotnie dla każdej oprogramowywanej metody. Istnieje wersja metody Assert bez parametru message jednak budowanie asercji bez wiadomości powoduje, że ten sam komunikat jest mniej czytelny (nie ma tekstu number >= 0).

Rozwiązaniem mojego problemu okazał się mechanizm expression trees, czyli zamiana kodu na dane. Po lekturze MSDN doszedłem do wniosku, że moja, lepsza wersja Debug.Assert() będzie miała postać:

Check.For(() => number < 0);

Jeżeli warunek nie zostanie spełniony na ekranie wyświetli się komunikat:

image_thumb12

Dzięki takiemu rozwiązaniu możliwe jest korzystanie z dobrodziejstw Intellisense. Jednocześnie nie trzeba martwić się o nazwy parametrów po refaktoryzacji kodu/interfejsu metody. Minusem jest to, że komunikat nie jest identyczny z tym z pierwszego przykładu. Wymagałoby to dłuższej zabawy z expression trees , na którą w tym momencie nie miałem czasu i wiedzy. Daje on jednak wystarczającą ilość informacji (tekst!) do szybkiego zidentyfikowania, która linia kodu spowodowała wyświetlenie się błędu.

Kod klasy Check:

public static class Check
{
    [System.Diagnostics.Conditional("DEBUG")]
    public static void For(Expression<Func<bool>> exp)
    {
        var result = exp.Compile().Invoke();
        if (!result)
        {
            var stringExpr = ExtractStringExpression(exp.Body.ToString());
            System.Diagnostics.Debug.Assert(false, string.Format("{0} not satisfied.", stringExpr));
        }
    }

    [System.Diagnostics.Conditional("DEBUG")]
    public static void For(Expression<Func<bool>> exp, string message)
    {
        var result = exp.Compile().Invoke();
        if (!result)
        {
            System.Diagnostics.Debug.Assert(false, message);
        }
    }

    private static string ExtractStringExpression(string stringExpr)
    {
        var s = Regex.Replace(stringExpr, @"value\(.+\)\.", string.Empty, RegexOptions.Compiled);
        s = s.Trim('(', ')');
        return s;
    }
}

Jako dodatek dorzuciłem przeciążenie metody For przyjmującą parametr message jak w starym rozwiązaniu. Tak na wszelki wypadek Uśmiech