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