Refleksja i serializacja Mateusz Dybciak inż. Paweł Karwowski Łukasz Koć
Metadane Metadane, dane o danych – ustrukturalizowane informacje stosowane do opisu zasobów informacji lub obiektów informacji, dostarczające szczegółowych danych, dotyczących atrybutów zasobów lub obiektów informacji, w celu ułatwienia ich znalezienia, identyfikacji, a także zarządzania tymi zasobami.
Metadane w bazach danych- tu metadanymi są definicje tabel, widoków, kluczy itp Metadanymi są też informacje na temat plików i katalogów zapisanych za pomocą systemu plików na zewnętrznym nośniku
Znaczenie Metadanych w .Net Zdolność kompletnego opisywania typów (klas, interfejsów, struktur, enumeracji i delegatów) jest jednym z kluczowych elementów platformy .NET. Wiele technologii .NET, takich jak: Serializacja obiektów, Zdalne wywołania, XML-owe usługi sieciowe,
Czym są metadane Metadane to zbiór tabel, zawierających ściśle sformatowane opisy wszystkich definicji typów i elementów powiązanych.
Metadane przechowywane są w formie binarnej pozwalającej na duże upakowanie informacji. Każdy typ zdefiniowany w aktualnym pakiecie jest udokumentowany za pomocą elementu TypeDef #n (ang. type definition). Typ, do którego się odnosimy jest udokumentowany za pomocą elementu TypeRef #n (ang. type reference).
REFLEKSJA Refleksja jest procesem odkrywania i wykorzystywania typów w czasie wykonania programu. Korzystając z usług refleksji, możliwe jest programowe zdobycie tej samej wiedzy o metadanych, którą uzyskujemy stosując zewnętrzne programy. Informacje te są prezentowane w postaci odpowiednio zaprojektowanych klas. Typy reprezentujące te informacje, zostały zdefiniowane w przestrzeni naw System.Reflection.
Zastosowanie Za pomocą refleksji możemy (np.): Pozyskać listę wszystkich typów zawartych w danym pakiecie, włącznie z: Metodami, Polami danych, Właściwościami. Zdarzeniami zdefiniowanymi w ramach danego typu. Dynamicznie odkryć zbiór interfejsów wspieranych przez dany typ, parametry metod, I inne powiązane z tym szczegóły (klasy bazowe, informacje o przestrzeniach nazw, dane z manifestu itd.).
Pobieranie obiektu klasy Type Instancję klasy Type możemy uzyskać na kilka sposobów. Czego nie możemy zrobić ,to ją zdefiniować poprzez słowo kluczowe new, ponieważ klasa Type jest klasą abstrakcyjną. Klasa System.Object definiuje metodę GetType() zwracającą obiekt odpowiedniej klasy dziedziczącej po Type. Obiekt ten reprezentuje metadane typu obiektu, na rzecz którego ta metoda została wywołana.
Aby zdobyć informacje o typie w bardziej elastyczny sposób, możemy skorzystać ze statycznej metody GetType() zdefiniowanej w klasie System.Type. Parametrem tej metody jest łańcuch znaków reprezentujący pełną kwalifikowaną nazwę danego typu. Używając tego podejścia, nie musimy znać definicji danego typu w czasie kompilacji. Sama nazwa typu może być, na przykład, dostarczona poprzez plik konfiguracyjny.
Ostatnim sposobem uzyskania informacji o typie, jest wykorzystanie operatora typeof.
Dynamiczne ładowanie pakietów? Jest to wczytywanie zewnętrznego pliku w trakcie działania programu. Przestrzenie nazw wykorzystywane w tym celu to System.Relection która definiuje klasę Assembly, klasa ta definiuje informacje i metody dotyczące pakietów. Między innymi metody: Load() i LoadFrom() które służą do programowego wykorzystania najczęściej definiowanych informacji w pliku konfiguracyjnym po stronie klienta.
Późne wiązanie To technika pozwala na stworzenie instancji danego typu i wywołanie jego składowych w czasie wykonania programu bez posiadania wiedzy o tym typie w czasie kompilacji. późne wiązanie odgrywa krytyczną role podczas tworzenia rozszerzalnych aplikacji. Trochę bardziej po ludzku: Polega to na tworzeniu obiektu klasy, używanie jej składowych , podczas wykonywania programu. Nie trzeba posiadać wiedzy o typie utworzonego obiektu w czasie kompilacji programu. Późne wiązanie jest bardzo dobrą praktyką jeśli programiście zależy na rozszerzalności programu.
Atrybuty Atrybuty to po prostu adnotacje kodu, które mogą być zastosowane do: typu (klasy, interfejsu, struktury, itd.), składowej (właściwości, metody, itd.), pakietu, modułu.
Atrybuty(cd.) specjalne konstrukcje do przekazywania dodatkowych informacji w programie. mogą wpływać na program Np. określa jak widziana jest klasa podczas debugowania(debuggerdisplay) atrybuty opisuja metadane
Atrybuty(cd.2) Atrybuty są klasami dziedziczącymi po klasie System.Attribute. Są praktycznie bezużyteczne do momentu, gdy inny program odwoła się do nich poprzez refleksję. W C#, atrybuty definiujemy za pomocą nawiasów kwadratowych.
Przykład
Serializacja Pozwala ona na utrwalenie i odczytanie stanu danego obiektu do/z dowolnego strumienia (typu dziedziczącego po System.IO.Stream). określa proces utrwalania (i ewentualnie transformacji) stanu obiektu do strumienia (strumienia do pliku, strumienia do pamięci, itd.). Sekwencja utrwalonych danych zawiera wszystkie informacje niezbędne do rekonstrukcji (deserializacji) danego stanu obiektu do późniejszego użytku. Przy pomocy tej technologii , bardzo prostym staje się zapisanie bardzo dużej ilości danych (w różnych formatach) przy minimalnym zaangażowaniu programisty (mniejszym niż w przypadku używania typów w przestrzeni System.IO).
Oznaczanie obiektów do serializacji Aby dany obiekt był dostępny dla usług serializacji platformy .NET, wystarczy oznaczyć każdą z powiązanych klas (lub struktur) atrybutem [Serializable]. Jeżeli zdecydujemy, że dany typ posiada dane składowe, które nie powinny (lub nie mogą) brać udziału w serializacji, oznaczamy takie pola atrybutem [NonSerialized].
Formatery serializacji Każda z możliwości jest reprezentowana przez jedną z poniższych klas: BinaryFormater, SoapFormater, XmlSerializer.
Klasa BinaryFormater Typ BinaryFormater serializuje stan obiektu do wskazanego strumienia używając spakowanego formatu binarnego. aby skorzystać z tego formatera wystarczy wpisać odpowiednią dyrektywę using
Klasa SoapFormater Typ SoapFormater utrwala stan obiektu jako wiadomość SOAP. aby skorzystać z tego typu należy dodać referencję do pakietu System.Runtime.Serialization.Formatters.Soap.d ll, a następnie dodać do programu odpowiednią dyrektywę using.
Klasa XmlSerializer XmlSerializer serializuje wyłącznie pola publiczne i pola prywatne widoczne poprzez publiczne właściwości. Zwykłe pola prywatne są pomijane. utrwala stan obiektu w postaci dokumentu XML.
Wspólne interfejsy Wszystkie wymienione formatery dziedziczą bezpośrednio po klasie System.Object. Nie dzielą zatem wspólnego zbioru składowych związanych z serializacją. Jednakże, BinaryFormater i SoapFormater wspierają wspólne składowe poprzez implementację interfejsów IFormater i IRemotingFormater XmlSerializer nie implementuje żadnego z nich.
Różnice… Najbardziej oczywistą różnicą między trzema poznanymi formaterami jest sposób, w jaki graf obiektów jest utrwalany w strumieniu (binarny, SOAP lub XML). Podczas korzystania z typu BinaryFormater, utrwalone zostaną nie tylko pola danych, ale również pełna kwalifikowana nazwa każdego z typów i pełna nazwa definiującego je pakietu (nazwa, wersja, klucz publiczny, i lokalizacja). Utrwalanie tych dodatkowych danych czynią typ BinaryFormater idealnym wyborem, jeżeli chcemy przenosić stan obiektu (pełną jego kopię) między maszynami pracującymi w oparciu o platformę .NET.
Różnice cd… Typ SoapFormater utrwala informację o pakiecie w postaci XML-owych przestrzeni nazw. Na przykład, w przypadku serializacji klasy Person jako wiadomości SOAP, otrzymalibyśmy następujący wynik:
Rożnica cd…. Typ XmlSerializer nie próbuje zachowywać całkowitej zgodności typów i dlatego nie utrwala pełnej kwalifikowanej nazwy typu ani nazwy pakietu. Powodem tego jest otwarta natura XML-owej reprezentacji danych. Serializacja ta różni się tym od wcześniejszych, że możemy podać informacje o serializowanych typach. XmlSerializer wymaga zdefiniowania dla każdej z klas biorącej udział w serializacji domyślnego konstruktora.
Kolejne fazy procesu serializacji (1) W momencie serializacji obiektu, typ BinaryFormater, przesyła następujące informacje do danego strumienia: pełną kwalifikowaną nazwę obiektów w grafie obiektów, nazwę pakietu definiującego graf obiektów, instancję klasy SerializationInfo zawierającą informację o stanach każdego z obiektów w utrwalanym grafie obiektów. Podczas procesu deserializacji, typ BinaryFormater wykorzystuje te informacje do odtworzenia identycznej kopi obiektu (jak przed zapisem). Typ SoapFormater zachowuje się podobnie
Kolejne fazy procesu serializacji (2) Poza przenoszeniem danych do i ze strumienia, formatery analizują składowe obiektów w grafie szukając następujących elementów infrastruktury: Sprawdzane jest, czy typ danego obiektu oznaczony jest atrybutem [Serializable]. Jeżeli jest oznaczony tym atrybutem, sprawdza się, czy typ danego obiektu implementuje interfejs ISerializable. Jeżeli tak, wywoływana jest metoda GetObjectData() danego obietku. Jeżeli dany obiekt nie implementuje tego interfejsu, uruchomiany jest domyślny proces serializujący wszystkie pola nieoznaczone atrybutem [NonSerialized]. Dodatkowo, oprócz sprawdzania implementowania interfejsu ISerializable, sprawdzane jest czy dany typ obiektu posiada metody oznaczone atrybutami [OnSerializing], [OnSerialized], [OnDeserializing], [OnDeserialized].
Konfiguracja serializacji za pomocą interfejsu ISerializable Obiekty oznaczone atrybutem [Serializable] mogą implementować interfejs ISerializable. Pozwala to na zaangażowanie się w proces serializacji i wykonywanie formatowania danych w wybranym momencie (przed lub po). Interfejs ISerializable definiuje jedną metodę.
Klasa SerializationInfo Metoda GetObjectData() jest wywoływana automatycznie przez formater w czasie serializacji. Implementacja tej metody wypełnia parametr (SerializationInfo) zbiorem par nazwa/wartość reprezentujących pola danych obiektu. Typ SerializationInfo definiuje wiele wariantów przeładowanej metody Addvalue() oraz kilka właściwości pozwalających na pobranie i ustawienie nazwy typu, pakietu i liczby składowych.
Specjalny konstruktor (1) Klasa implementująca interfejs ISerializable musi również definiować specjalny konstruktor o następujących parametrach: Konstruktor jest deklarowany jako chroniony po to, aby nie mogli go wywoływać „zwykli” użytkownicy. Pierwszym parametrem konstruktora jest instancja typu SerializationInfo, zawierająca zbiór parametrów odczytanych ze strumienia, które następnie zostają przypisane odpowiednim polom danych odtwarzanego obiektu.
Specjalny konstruktor (2) Drugim parametrem tego specjalnego konstruktora jest typ StreamingContext, który zawiera informacje odnoście źródła lub miejsca przeznaczenia danych.
Konfiguracja za pomocą atrybutów (1) Drugim sposobem konfiguracji procesu serializacji (preferowanym sposobem) jest definiowanie metod oznaczanych odpowiednimi atrybutami mówiącymi o momencie w procesie serializcji, w którym mają być wywołane. Atrybuty te to [OnSerializing], [OnSerialized], [OnDeserializing] i [OnDeserialized]. Użycie atrybutów (w porównaniu z implementacją interfejsu ISerializable) jest wygodniejsze, ponieważ nie wymaga bezpośredniej interakcji z typem SerializationInfo.
Konfiguracja za pomocą atrybutów (2) Metody oznaczone tymi atrybutami muszą przyjmować jako parametr typ StreamingContext i nie mogą nic zwracać. Nie jest wymagane uwzględnianie wszystkich atrybutów związanych z serializacją. Możemy zastosować tylko te, które nas w danej chwili interesują.