Drugi raz w ciągu kilku dni przytrafiły mi się kłopoty podczas wykorzystania metody Convert.ChangeType(). Scenariusz jest bardzo prostu: mam wartość pobraną skądś-tam (baza danych, http request czy cokolwiek innego) reprezentującą znany mi typ, jednak przechowywaną w postaci stringa. Wszystko śmigało jak trzeba dopóki traktowałem w ten sposób zwykłe liczby i daty. Jakiś czas temu wpadł mi tam Guid, co skończyło się wyjątkiem InvalidCastException. Teraz z kolei to samo przytrafiło się dla TimeSpan. To samo zresztą tyczy się "typów nullowalnych".
Wystarczyło zajrzeć Reflectorem w trzewia .NETa, aby poznać tego przyczynę. Wspomniana metoda potrafi sobie poradzić tylko z kilkoma na sztywno zdefiniowanymi typami:
Kilka kliknięć dzieli nas od podejrzenia jakie to są typy; inicjalizacja tablicy je zawierającej odbywa się w statycznym konstruktorze klasy Convert:
Cóż nam pozostaje? Najpierw pomyślałem, że jedyne wyjście to rozszerzenie tej metody i stworzenie takiego oto monstera oraz rozwijanie go w razie wystąpienia kolejnych nieprzewidzianych typów:
1: public static object ConvertEx(object value, Type conversionType) 2: { 3: string str = value.ToString(); 4: 5: if (conversionType == typeof(Guid)) 6: return new Guid(str); 7: if (conversionType == typeof (TimeSpan)) 8: return TimeSpan.Parse(str); 9: 10: return Convert.ChangeType(value, conversionType); 11: }
Chwilę potem jednak dotarło do mnie, że przecież te typy JAKOŚ muszą być dynamicznie wewnątrz .NET tworzone z napisowej reprezentacji. Chociażby zwykłe ustawienia aplikacji w pliku .config mogą mieć takie typy i klasy odpowiedzialne za parsowanie konfiguracji radzą sobie z nimi doskonale. Strzał Reflectorem w klasę ConfigurationElement, która przecież ma tą funkcjonalność, okazał się dobrym początkiem poszukiwań. Oszczędzę omawiania całej drogi, w tym przypadku ważny jest sam rezultat. Rozwiązaniem okazała się klasa TypeDescriptor, z wykorzystaniem której działające rozwiązanie wygląda tak (działa dla TimeSpan, dla Guidów, dla typów nullowalnych…):
1: TimeSpan result = (TimeSpan)TypeDescriptor.GetConverter(typeof (TimeSpan)).ConvertFromInvariantString(str);
Jeden problem z głowy. Oczywiście od razu przychodzi na myśl napisanie fajnej metody generycznej, ale to już każdy może sobie sam zaimplementować.
Hmm, skoro znasz typ docelowy i podajesz do na poziomie kodu (a nie odgadujesz dynamicznie) to nie prościej, szybciej, czytelniej i bardziej elegancko byłoby napisać:
“TimeSpan res = TimeSpan.Parse(str);”
(większość wbudowanych typów “prostych” ma statyczną metodę Parse)
Jeżeli znam typ docelowy to oczywiście że użyję [Try]Parse(). W tym przypadku na przykładzie TimeSpan demonstrowałem jedynie, że prezentowane przeze mnie rozwiązanie działa również dla niego (w przeciwieństwie do Convert.ChangeType()). Docelowe wykorzystanie to tak jak napisałeś – dynamiczna konwersja na nieznany podczas kompilacji typ.
Jeżeli tak, to co miałeś na myśli przez “napisanie fajnej metody generycznej”? Takie metody nie działają dynamicznie, tz. nie można napisać:
string s = ConvertEx<obj.GetType()> (“s”);
Metoda generyczna przydałaby się na przykład w klasie odpowiedzialnej za dostarczanie aplkacji konfiguracji. Wówczas wygodniej byłoby korzystać z mechanizmu:
T Get<T>(string key);
var value = Get<TimeSpan>(“SomeInterval”);
niż każdorazowo pisać metody charakterystyczne dla typu który chcemu uzyskać:
object Get(string key);
var value = TimeSpan.Parse(Get(“SomeInterval”));
Moim zdaniem pierwszy przykład jest dużo czytelniejszy i, co dość istotne, wygląda tak samo dla każdego typu. Jednocześnie obsługę stostownych wyjątków umieszczamy tylko w jednym miejscu – metodzie Get<T> – zamiast zajmować się nią każdorazowo w zależności od tego jaki typ chcemy uzyskać.