W ostatnim poście opisałem i porównałem wydajność różnych sposobów wykorzystania regexów. Zdecydowanie najlepiej sprawowało się wyrażenie, które przygotowałem za pomocą metody Regex.CompileToAssembly(). Dziś omówię ją nieco dokładniej i podpowiem, jak zautomatyzować cały proces aktualizowania zebranych w oddzielnym assembly regexów.
By odnaleźć się w kontekście, zachęcam do przeczytania poprzedniego posta, bo w dużej mierze kontynuuję dziś wątki, które zacząłem wcześniej.
Zacznijmy więc od omówienia wspomnianej metody Regex.CompileToAssembly(). Cały kod odpowiedzialny za prekompilowanie wybranych, często używanych wyrażeń zebrałem w oddzielnym projekcie (w tej samej solucji). Cały projekt jest aplikacją konsolową i jeśli ktoś nie dba o jakość kodu w tego typu małych aplikacjach, to można go zamknąć wewnątrz metody Main (czego jednak nie polecam).
Co właściwie robi tytułowa metoda? Przyjmuje informacje o potrzebnych nam regexach i prekompiluje je, tworząc kilka klas dziedziczących po klasie Regex i umieszcza je w oddzielnym, zdefiniowanym przez nas pliku .dll. Po dodaniu referencji utworzonego .dll do głównego projektu, możemy korzystać z zestawu prekompilowanych klas zawierających nasze regexy. Korzysta się z nich niemalże tak samo jak z utworzonych instancji klasy Regex z kilkoma małymi różnicami.
Po pierwsze nie możemy zmieniać wzoru naszego regexa, po drugie jego inicjalizacja trwa odrobinę dłużej niż zwykły new Regex(). Trzecią wyraźną różnicą jest prędkość z jaką działa, gdy już utworzymy jego instancję, o czym można poczytać we wspomnianym poprzednim poście.
Kod, który używany jest do wygenerowania nowego Assembly wygląda mniej więcej w ten sposób:
1 2 3 4 5 6 7 8 9 |
var pattern = @"(([\w-]+\.)+[\w-]+|([a-zA-Z]{1}|[\w-]{2,}))@((([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\.([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\.([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\.([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])){1}|([a-zA-Z]+[\w-]+\.)+[a-zA-Z]{2,4})"; var info = new RegexCompilationInfo(pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant, "ExtractEmail", "My.Regex.Namespace", true); var infoArray = new RegexCompilationInfo[] { info }; Regex.CompileToAssembly(infoArray, new System.Reflection.AssemblyName("RegexLib, Version = 1.0.0.1001, Culture = neutral, PublicKeyToken = null")); |
Kluczowe tutaj są tak naprawdę dwie rzeczy. Pierwszą z nich jest instancja klasy RegexCompilationInfo, której w konstruktorze przekazujemy, jak sama nazwa wskazuje, informacje dotyczące tworzonego regexa. Przyjmowane parametry to w kolejności:
- Pattern
- Enumy RegexOption oddzielone znakiem ‘|’, które definiują szczegółowe ustawienia naszego wyrażenia
- Nazwa klasy, która zostanie utworzona w assembly (w tym wypadku do utworzenia zbudowanego regexa użyjemy ‘new ExtractEmail()‘
- Namespace w jakim będzie znajdowała się klasa z regexem i który najprawdopodobniej umieścimy w jakimś usingu
- Bool, który określa czy klasa będzie dostępna publicznie
- (nie użyty powyżej) TimeSpan, który określa domyślny timeout dla utworzonej klasy
Tak utworzone obiekty RegexCompilationInfo zberamy w tablicę i przekazujemy metodzie statycznej Regex.CompileToAssembly() wraz z nową instancją AssemblyName, która określa nazwę pod jaką utworzony zostanie plik .dll, samo Assembly i kilka innych właściwości, którymi tak naprawdę nie musimy się przejmować.
W powyższym przykładzie utworzony zostanie plik RegexLib.dll. Aby do tego doprowadzić musimy tylko zbudować i odpalić nasz projekt z aplikacją konsolową. I tutaj uwaga. Bardzo nietaktownie wali on dllkę pod siebie i gdy będziemy dodawać referencję do niej, musimy jej szukać w tym samym miejscu, w którym odpalaliśmy utworzony pliczek wykonywalny z aplikacją konsolową.
Jest to jednak najmniejszy problem, bo o wiele bardziej kłopotliwe jest dokładanie nowych regexów do Assembly. Bo jak to? Za każdym razem mam budować projekt, odpalać aplikację i dopiero budować główny projekt? Byłoby to trochę bez sensu.
I tutaj przychodzi na ratunek bardzo fajna funkcja, z której możemy skorzystać w Visual Studio – pre-build events. Wystarczy kliknąć prawym przyciskiem myszy na nasz główny projekt, odpalić jego właściwości i w menu odnaleźć Build Events, a naszym oczom ukażą się dwa okna. Jeden w którym możemy napisać mały skrypt, który będzie odpalany w konsoli przed buildem i drugi, który zostanie odpalony już po nim. Nas będzie interesował ten pierwszy.
W tym miejscu możemy umieścić kawałek skryptu identycznego jak w zwykłej, Windowsowej konsoli i zostanie od wykonany. Mamy nawet dostęp do małej listy zmiennych, które możemy umieścić w tekście, by ułatwić nam pracę. Co ja w nim umieściłem? Jedną linijkę, która odpala stworzony projekt konsolowy i dzięki której mam święty spokój.
1 |
$(SolutionDir)ConsoleApplication2\bin\Debug\ConsoleApplication2.exe |
Dzięki temu przy każdym rebuildzie mam tworzone świeżutkie Assembly z regexami. Mało tego. Aplikacja uruchamiana jest w kontekście głównego projektu, co oznacza, że plik .dll umieszczany jest w lokalizacji głównego projektu, razem z innymi dllkami. Jest to bardzo przyjemne, ale jeśli dodawaliśmy wcześniej referencję do assembly wygenerowanego po ręcznym uruchomieniu projektu, to musimy pamiętać o zmianie ścieżki do załączonej referencji, by móc swobodnie korzystać z dodawanych na bieżąco, nowych regexów.