W pracy dla testowania jednej aplikacji “X” musiałem szybko napisać kod, który tworzyłby plik CSV na bazie danej kolekcji. Miałem ułatwione zadanie, ponieważ znałem kolekcje i znałem jej elementy, znałem także właściwości elementów. Jednym słowem napisanie takiego programu nie zajęło mi dużo czasu.
W trakcie tego zadania napisałem prostą metodę rozszerzeniową LINQ , która na bazie kolekcji , która dziedziczy po interfejsie IEnumerable<T> pisze jedną linijkę pliku CSV. Metoda ta była bardzo pomysłowa, ponieważ jest uniwersalna i działa nie tylko dla tablicy elementów . Może to być lista, bądź inna kolekcja musi ona jedynie dziedziczyć po interfejsie IEnumerable<T> .
Metoda ta też jest metodą rozszerzeniową, co znaczy ,że może być wywołana w sposób pokazany powyżej.
public static string ToLineCsv<T>(this IEnumerable<T> source)
{
if (source == null) throw new ArgumentException("Source is null");
return string.Join(",", source.Select(s => s.ToString()));
}
Sama metoda nie jest zbyt skomplikowana w działaniu.
Pomyślałem sobie czemu więc nie napisać metody rozszerzeniowej, która pisałaby cały plik CSV bazując na kolekcji X i jej elementach Y oraz ich właściwościach Z.
Algorytm pisania pliku CSV jest bardzo prosty. Jeden element w kolekcji reprezentuje jedną linijkę pliku CSV. W każdej linijce po przecinkach znajdują się dane symbolizujące wartości właściwości elementów kolekcji.
Oczywiście metoda ToCSV() musi działać uniwersalnie. To oznacza , że musi zadziałać, nawet jeśli nie znamy:
- Typu kolekcji Typu bądź typów elementów kolekcji
- (chociaż wiele “typów” elementów kolekcji nie ma sensu dla pliku CSV.Kolekcja musi mieć stały typ elementów. W końcu jak później ten plik zostanie odczytany, jeśli linijki nie będą się zgadzać)
- Liczby właściwości elementów
- Wartości typów właściwości elementów
Wydaje się ,że w takiej metodzie nie da się napisać zbyt dużo niewiadomych. W sumie typ kolekcji nas nie interesuje, tak długo, jeśli dziedziczy po interfejsie IEnumerable , ale co z pozostałymi niewiadomymi?
Na pomoc przychodzi refleksja.
Jak do tej pory nie miałem okazji użyć refleksji w c# i osobiście nie wiedziałem do końca jak ona działa ,a z drugiej strony kojarzyła mi się z czymś skomplikowanym i niekoniecznie efektownym.
Ostatnio w wolnych chwilach miałem okazję poczytać o refleksji i muszę się przyznać , że dużo zapamiętałem. Zatem czas na KOD.
public static string ToCsv<T>(this IEnumerable<T> source)
{
if (source == null) throw new ArgumentException("Source is null");
StringBuilder sb = new StringBuilder();
foreach (var item in source)
{
PropertyInfo[] pi = item.GetType().GetProperties();
List<object> lista = new List<object>();
foreach (var property in pi)
{
lista.Add(property.GetValue(item, null));
}
sb.AppendLine(lista.ToLineCsv());
}
return sb.ToString();
}
Refleksje to mechanizm pozwalający na podglądanie w trakcie działania programu innych klas bądź samego programu. Stąd nazwa “refleksja”, którą można skojarzyć z odbiciem, ponieważ aplikacja patrzy na samą siebie, bądź na inną bibliotekę.
W tym wypadku Refleksja pozwoli nam określić w trakcie działania programu:
- Typ elementów kolekcji
- Liczbę właściwość elementów w kolekcji
- Wartości właściwości elementów kolekcji.
Za pomocą metody GetProperties() możemy pobrać dynamicznie tablicę właściwości elementu generycznego“T”. Wcześniej jednak musimy wywołać metodę GetType(), ponieważ program przed wykonaniem metody GetProperties() musi znać dany typ elementu generycznego. W trakcie wywoływania metody ten typ jest znany.
Później mając już tablicę właściwości uruchamiam pętleforeach, która będzie spacerować po właściwościach danego elementu. Jak widać nie muszę znać liczby właściwości danego elementu.
W pętli foreach do listy pomocniczej dodaję wartości tych właściwości. Za pomocą metody GetValue() mogę pobrać dynamicznie wartości właściwości. Oczywiście wszystkie wartości są reprezentowane przez typ Object. Nie jest to żadna przeszkoda dla metody ToCSV(), ponieważ typ Object ma metodęToString() ,a resztę zadania dla każdego typu właściwości zrobi za nas mechanizm polimorfizmu w C#. Jednym słowem interesuje mnie wyświetlenie wartości jako typ string i jest to możliwe dla typu Object.
Po przejściu po właściwościach elementów dodaję linijkę do CSV za pomocą metody rozszerzeniowej ToLineCSV() , którą omówiłem już wcześniej.
Ten proces trwa aż przejdę po wszystkich elementach w kolekcji.
Kod powyżej pokazuje metodę ToCSV() , która zwraca całą treść pliku CSV jako string , ale być może chcesz od razu zapisać wartość tekstową do pliku CSV. Oto dwie metody przeciążone ToCSV() Jedna z nich tworzy plik w określonym miejscu na dysku inna zaś zapisuje plik do określonego strumienia.
public static void ToCsv<T>(this IEnumerable<T> source, Stream stream)
{
if (source == null) throw new ArgumentException("Source is null");
using (StreamWriter sw = new StreamWriter(stream))
{
foreach (var item in source)
{
PropertyInfo[] pi = item.GetType().GetProperties();
List<object> lista = new List<object>();
foreach (var property in pi)
{
lista.Add(property.GetValue(item, null));
}
sw.WriteLine(lista.ToLineCsv());
}
}
}
public static void ToCsv<T>(this IEnumerable<T> source, string path)
{
if (source == null) throw new ArgumentException("Source is null");
using (StreamWriter sw = new StreamWriter(path))
{
foreach (var item in source)
{
PropertyInfo[] pi = item.GetType().GetProperties();
List<object> lista = new List<object>();
foreach (var property in pi)
{
lista.Add(property.GetValue(item, null));
}
sw.WriteLine(lista.ToLineCsv());
}
}
}
Czy ten kod jest wydajny? Zapewne nie ale napisanie takiego kodu sprawiło mi wiele frajdy. Zwłaszcza że jak się okazało znam i umiem korzystać z refleksji na dzień dobry.
Następnym razem spróbujemy odczytać plik CSV i być może zmierzymy czas jego odczytu ,aby sprawdzić jak wydajnie działa kod.
To do zobaczenia.