Korzystamy z funkcji rozszerzających, więc pierwszą czynnością jest przygotowanie statycznej klasy, w której zamieścimy implementację - nie ma tu nic odkrywczego:
public static class ArgumentValidator
{
...
}Nazwa klasy statycznej nie ma tutaj żadnego znaczenia z punktu widzenia korzystania z kodu - jedynie pomaga się zorientować w strukturze projektu.
Następnie deklarujemy funkcję CheckArg - również musi być statyczna, a jej pierwszym argumentem jest obiekt, na którym będą wykonywane operacje (czyli obiekt, który rozszerzymy o funkcję CheckArg). W naszym przypadku chcemy rozszerzyć każdy obiekt, więc korzystamy z funkcji z typami generycznymi:
public static void CheckArg<T>(this T me, Action<T> action)
{
...
}Action<T> oznacza, że oczekujemy tam wyrażenia lambda, któremu jako argument będzie przekazany obiekt typu T (w tym wypadku dowolny).
W tej chwili możemy już wywołać nasze funkcje:
this.CheckArg(x => x.Sth());
string.CheckArg(x => x.IsNullOrEmpty("tekst"));Wpisujemy powyższe w IDE i co widzimy? Błąd kompilacji :). Dzieje się tak dlatego, że funkcja string.IsNullOrEmpty zwraca jakieś dane - w wyrażeniach lambda takie funkcje są reprezentowane przez Func<T,TR> (gdzie TR to typ zwracany), a nie Action<T>. Dodajmy zatem analogiczną funkcję do ArgumentValidator:
public static void CheckArg<T,TR>(this T me, Func<T,TR> action)
{
...
}Od strony składniowej mamy już spełnione wszystkie warunki. Działa jak chcieliśmy.
Przejdźmy zatem do analizy przekazanego wyrażenia. Analizujemy zmienną action i co widzimy? A no nic. Jedyne co można zrobić to wykonać action(me) oraz sprawdzić co za funkcję otrzymaliśmy - nie ma natomiast możliwości sprawdzenia przekazanych w argumentach wartości. Trochę googlania i już wiemy, że dostęp do drzewa wywołania zapewnia klasa Expression. Zmieniamy więc nasze funkcje na następujące:
public static void CheckArg<T>(this T me, Expression<Action<T>> expression)
{
...
}
public static void CheckArg<T,TR>(this T me, Expression<Func<T,TR>> expression)
{
...
}Teraz jeśli zajrzymy do expression to zobaczymy Body, a to jest to co nas interesuje. Jest to pełne drzewo wywołania przekazane jako lambda expression, zawierającego w sobie podwyrażenia, które zostały użyte jako argumenty. Powyższa deklaracja funkcji jest potrzebna tylko w momencie pisania (IntelliSense pięknie pomaga wykorzystać powyższy kod) oraz kompilacji - tylko tyle już wystarczy do zapewnienia pełnej kontroli typów. Analiza wyrażenia będzie następowała w runtime, więc nie potrzebujemy już takiej typowalności - spokojnie możemy zrzutować expression na LambdaExpression w obu funkcjach, co sprawi, że ciała obu funkcji będą praktycznie identyczne, więc zgodnie ze sztuką wydzielamy oddzielną funkcję przyjmującą jako argument wyłącznie LambdaExpression, dzięki czemu uzyskujemy następujący kod:
public static void CheckArg<T>(this T me, Expression<Action<T>> expression)
{
CheckArgument(expression);
}
public static void CheckArg<T,TR>(this T me, Expression<Func<T,TR>> expression)
{
CheckArgument(expression);
}
public static void CheckArgument(LambdaExpression expression)
{
...
}
W tym momencie polecam zabawę z debuggerem oraz Reflectorem, aby zapoznać się z tym, jak zbudowane jest to drzewo i jak się po nim poruszać. Namespace System.Linq.Expressions zawiera m.in. wszystkie klasy, których możemy się spodziewać w Body oraz w poszczególnych gałęziach drzewa.
Brak komentarzy:
Prześlij komentarz