Zagadnienia egzaminu 70-483 opisane w tej notatce:

  • Refleksja - znajdywanie, wykonywanie i tworzenie typów w czasie wykonania.
  • Atrybuty - tworzenie, stosowanie i odczyt atrybutów które mogą być używane do zmiany zachowania klas.
  • CodeDOM - tworzenie generatorów kodu.
  • Wyrażenia Lambda - skrócona składnia tworzenia metod bez ich deklaracji.

Użycie przestrzeni nazw System.Reflection

Refleksja odnosi się do zdolności do zbadania kodu i dynamicznego odczytu, modyfikacji lub wywołania zachowania dla assembly, modułu lub typu. Typ jest tutaj rozumiany jako klasa, interfejs, tablica, typ wartości, enumeracja, parametr lub typ generyczny. Można użyć klas w przestrzeni nazw System.Reflection lub klasy System.Type aby dowiedzieć się jaka jest nazwa assembly, przestrzeń nazw, właściwości, metody, klasa bazowa i wiele innych metadanych o klasie lub zmiennej.

Przestrzeń nazw System.Reflection zawiera wiele klas służących do odczytu metadanych lub dynamicznego wywołania zachowania dla typu. Najczęściej wykorzystywane klasy to:

  • Assembly - reprezentuje plik DLL lub EXE i zawiera właściwości dla nazwy assembly, klas, modułów i innych metadanych czasu wykonania.
  • EventInfo - reprezentuje zdarzenia zdefiniowane w klasie i zawiera właściwości takie jak nazwa zdarzenia.
  • FieldInfo - reprezentuje pole zdefiniowane w klasie i zawiera właściwości takie jak to czy pole jest publiczne czy prywatne.
  • MemberInfo - abstrahuje metadane o klasie i może reprezentować zdarzenie, pole itd.
  • MethodInfo - reprezentuje metodę zdefiniowaną w klasie i może być użyta do wywołania metody.
  • Module - moduł jest plikiem który tworzy assembly, zwykle plik DLL lub EXE.
  • ParameterInfo - reprezentuje deklarację parametru dla metody lub konstruktora. Pozwala na określenie typu parametru, nazwy itp.
  • PropertyInfo - reprezentuje właściwość zdefiniowaną w klasie i zawiera właściwości takie jak nazwa i typ.

Refleksja jest bardzo potężnym narzędziem i może być użyta z niektórymi wzorcami projektowymi takimi jak Factory lub Inversion of Control.

Klasa Assembly

Assembly jest zasadniczo skompilowanym kawałkiem kodu, który jest zazwyczaj plikiem DLL lub EXE. Można użyć klasy Assembly aby załadować assembly, odczytać metadane o assembly a nawet utworzyć instancje typów które sa zawarte w assembly. 
Lista najważniejszych właściwości klasy Assembly:

  • CodeBase - zwraca ścieżkę do assembly.
  • DefinedTypes - zwraca kolekcję typów zdefiniowanych w assembly.
  • ExportedTypes - zwraca kolekcję publicznych typów zdefiniowanych w assembly.
  • FullName - zwraca nazwę assembly.
  • GlobalAssemblyCache - zwraca true jeżeli assembly została załadowana z global assembly cache.
  • ImageRuntimeVersion - zwraca wersję CLR dla assembly.
  • Location - zwraca ścieżkę lub lokalizację UNC dla assembly.
  • Modules - zwraca kolekcję zawierającą moduły w assembly.
  • SecurityRuleSet - zwraca wartość która wskazuje, który zestaw reguł bezpieczeństwa jest zastosowany dla assembly przez CLR.

Przykład kodu ładującego assembly System.Data i wypisującego niektóre właściwości assembly:

Assembly myAssembly = Assembly.Load("System.Data, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089");

Debug.WriteLine("CodeBase: {0}", myAssembly.CodeBase);
Debug.WriteLine("FullName: {0}", myAssembly.FullName);
Debug.WriteLine("GlobalAssemblyCache: {0}", myAssembly.GlobalAssemblyCache);
Debug.WriteLine("ImageRuntimeVersion: {0}", myAssembly.ImageRuntimeVersion);
Debug.WriteLine("Location: {0}", myAssembly.Location);

Wynik działania kodu:

CodeBase: file:///C:/Windows/Microsoft.Net/assembly/GAC_32/System.Data/ 
v4.0_4.0.0.0__b77a5c561934e089/System.Data.dll 
FullName: System.Data, Version=4.0.0.0, Culture=neutral, 
PublicKeyToken=b77a5c561934e089 
GlobalAssemblyCache: True 
ImageRuntimeVersion: v4.0.30319 
Location: C:\Windows\Microsoft.Net\assembly\GAC_32\System.Data\ 
v4.0_4.0.0.0__b77a5c561934e089\System.Data.dll

Lista najczęściej używanych metod klasy Assembly:

  • CreateInstance(String) - tworzy instancję klasy poprzez przeszukanie assembly dla nazwy klasy.
  • GetCustomAttributes(Boolean) - zwraca tablicę obiektów reprezentujących niestandardowe atrybuty dla tego assembly.
  • GetExecutingAssembly - zwraca obiekt klasy Assembly dla aktualnie wykonywanego programu.
  • GetExportedTypes - zwraca klasy publiczne zawarte w assembly.
  • GetModule - zwraca wyszczególniony moduł zawarty w assembly.
  • GetModules() - zwraca wszystkie moduły z assembly.
  • GetName() - zwraca AssemblyName.
  • GetReferencedAssemblies - zwraca tablicę obiektówAssemblyName reprezentującą wszystkie assembly do których odwołuje się to assembly.
  • GetTypes - zwraca tablicę obiektów Type zdefiniowanych w tym assembly.
  • Load(String) - ładuje assembly wg długiej nazwy.
  • LoadFile(String) - ładuje zawartość pliku assembly wg ścieżki pliku.
  • LoadFrom(String) - ładuje assembly wg nazwy lub ścieżki.
  • ReflectionOnlyLoad(String) - ładuje assembly, ale można wykonać na jej typach tylko refleksję.
  • UnsafeLoadFrom - ładuje assembly pomijając pewne kroki bezpieczeństwa.

Metoda statyczna GetExecutingAssembly pozwalająca na pobranie referencji do aktualnie wykonywanego kodu. Metody GetExportedTypes i GetTypes są używane do pobierania referencji do typów zdefiniowanych w assembly. 
Poniższy fragment kodu wyświetla wszystkie typy zdefiniowane w aktualnie wykonywanym assembly:

Assembly myAssembly = Assembly.GetExecutingAssembly();

Type[] myAssemblysTypes = myAssembly.GetTypes();

foreach (Type myType in myAssemblysTypes)
{
Debug.WriteLine(string.Format("myType Name: {0}", myType.Name));
}

Właściwość Modules lub metody GetLoadedModulesGetModules i GetModule zwracają listę modułów ()lub wybrany moduł) zdefiniowanych w assembly. Moduł jest plikiem który wchodzi w skład assembly. Jest to zwykle jeden plik DLL lub EXE
Poniższy fragment kodu wyświetla wszystkie moduły zdefiniowane w assembly System.Data:

Assembly myAssembly = Assembly.Load("System.Data, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
);

Module[] myAssemblysModules = myAssembly.GetModules();

foreach (Module myModule in myAssemblysModules)
{
Debug.WriteLine(string.Format("Module Name: {0}", myModule.Name));
}

Wynik działania fragmentu kodu:

Module Name: System.Data.dll

Poprzedni fragment kodu użył metody Load aby załadować assembly System.Data do pamięci. Ponieważ assembly została załadowana używając metody Load to można wykonywać kod należący do assembly. Jeżeli nie potrzebujemy wykonywać kodu to możemy użyć metodyReflectionOnlyLoad
Assembly możemy także załadować za pomocą metod LoadFrom lub LoadFile które pobierają ścieżkę jako parametr. Różnica pomiędzy tymi metodami polega na różnych typach kontekstów w jakich refleksja wyszukuje assembly. Assembly może być w jednym z trzech kontekstów lub w żadnym z nich:

  • load context - zawiera assembly odnalezione poprzez wykrywanie w GAChost assembly store, folderze assembly lub w folderze prywatnym bin.
  • load-from context - zawiera assembly zlokalizowane w ścieżce podanej jako parametr.
  • reflection-only context - zawiera assembly załadowane przez metody ReflectionOnlyLoad lub ReflectionOnlyLoadFrom.

Rekomendowaną metodą jest użycie metody Load. Użycie metody LoadFrom wymaga praw dostępu oraz mogą w nim wystąpić konflikty nazw i identyfikatorów. Metody LoadFile możemy użyć jeżeli istnieją dwie assembly z tym samym identyfikatorem w różnych folderach na komputerze.

Kiedy mamy już załadowane assembly to możemy utworzyć instancje klas zdefiniowanych w tym assembly. Aby utworzyć instancję klasy używamy metody CreateInstance
Poniższy fragment kodu tworzy instancję klasy DataTable i wypisuje liczbę wierszy w tabeli:

Assembly myAssembly = Assembly.Load("System.Data, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089");

DataTable dt = (DataTable)myAssembly.CreateInstance("System.Data.DataTable");

Debug.Print("Number of rows: {0}", dt.Rows.Count);

Jeżeli jako parametr metody CreateInstance podamy nieistniejącą nazwę klasy to zwrócona zostanie wartość null i nie zostanie zgłoszony żaden wyjątek.

Metoda GetReferencesAssemblies jest używana do odkrywania referencji w assembly. Może to być przydatne podczas rozwiązywania problemów wdrażania. 
Poniższy kod wypisuje wszystkie referencje dla assembly System.Data:

Assembly myAssembly = Assembly.Load("System.Data, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089");

AssemblyName[] referencedAssemblyNames = myAssembly.GetReferencedAssemblies();

foreach (AssemblyName assemblyName in referencedAssemblyNames)
{
Debug.WriteLine("Assembly Name: {0}", assemblyName.Name);
Debug.WriteLine("Assembly Version: {0}", assemblyName.Version);
}

Wynik działania kodu:

Assembly Name: mscorlib 
Assembly Version: 4.0.0.0 
Assembly Name: System 
Assembly Version: 4.0.0.0 
Assembly Name: System.Xml 
Assembly Version: 4.0.0.0 
Assembly Name: System.Configuration 
Assembly Version: 4.0.0.0 
Assembly Name: System.Transactions 
Assembly Version: 4.0.0.0 
Assembly Name: System.Numerics 
Assembly Version: 4.0.0.0 
Assembly Name: System.EnterpriseServices 
Assembly Version: 4.0.0.0 
Assembly Name: System.Core 
Assembly Version: 4.0.0.0

Klasa System.Type

Klasa System.Type reprezentuje klasę, interfejs, tablicę, typ wartości, enumerację, parametr i typ generyczny. Najczęściej jest używana do pobrania informacji o klasie zawartej w assembly. Możesz uzyskać odwołanie do typu na dwa sposoby. 
Można użyć słowa kluczowego typeof(), np:

System.Type myType = typeof(int);

Można również użyć metody GetType() na instancji typu, np:

int myIntVariable = 0;
System.Type myType = myIntVariable.GetType();

Kiedy posiadamy referencję do typu to możemy zbadać jego właściwości. Lista najczęściej używanych właściwości dla klasy System.Type:

  • Assembly - zwraca Assembly w którym typ jest zadeklarowany.
  • AssemblyQualifiedName - zwraca ciąg znaków, który jest kompletną nazwą tego typu, i który zawiera nazwę assembly, z którego typ został załadowany.
  • BaseType - zwraca typ z którego dany typ dziedziczy.
  • FullName - zwraca ciąg znaków, który jest kompletną nazwą tego typu z przestrzenią nazw.
  • IsAbstract - zwraca true jeżeli typ jest abstrakcyjny.
  • IsArry - zwraca true jeżeli typ jest tablicą.
  • IsClass - zwraca true jeżeli typ jest klasą a nie interfejsem ani typem wartości.
  • IsEnum - zwraca true jeżeli typ jest enumeracją.
  • IsInterface - zwraca true jeżeli typ jest interfejsem.
  • IsNotPublic - zwraca true jeżeli typ nie jest zadeklarowany jako publiczny.
  • IsPublic - zwraca true jeżeli typ jest zadeklarowany jako publiczny.
  • IsSerializable - zwraca true jeżeli typ jest serializowany.
  • IsValueType - zwraca true jeżeli typ jest typem wartości.
  • Name - zwraca nazwę typu.
  • Namespace - zwraca przestrzeń nazw typu.

Przykład wyświetlający niektóre właściwości typu int:

int myIntVariable = 0;
System.Type myType = myIntVariable.GetType();

Debug.WriteLine("AssmeblyQualifiedName: {0}", myType.AssemblyQualifiedName);
Debug.WriteLine("FullName: {0}", myType.FullName);
Debug.WriteLine("IsValueType: {0}", myType.IsValueType);
Debug.WriteLine("Name: {0}", myType.Name);
Debug.WriteLine("Namespace: {0}", myType.Namespace);

Wynik działania kodu:

AssmeblyQualifiedName: System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, 
PublicKeyToken=b77a5c561934e089 
FullName: System.Int32 
IsValueType: True 
Name: Int32 
Namespace: System

Klasa System.Type posiada również metody których można użyć do pobrania metadanych typu. 
Lista najczęściej wykorzystywanych metod klasy System.Type:

  • GetArrayRank - jeżeli typ jest tablicą, to metoda zwraca liczbę wymiarów w tablicy.
  • GetConstructor(Type[]) - szuka publicznych wystąpień konstruktora którego parametry pasują do typów określonych w tablicy i zwraca obiekt klasy ConstructorInfo.
  • GetConstructors() - zwraca tablicę obiektów ConstructorInfo dla wszystkich publicznych konstruktorów zdefiniowanych dla danego typu.
  • GetEnumName - jeżeli typ jest enumeracją to metoda zwraca nazwę elementu który posiada określoną wartość.
  • GetEnumNames - jeżeli typ jest enumeracją to metoda zwraca wszystkie nazwy członków enumeracji.
  • GetEnumValues - jeżeli typ jest enumeracją to metoda zwraca tablicę wartości enumeracji.
  • GetField(String) - szuka publicznego pola o określonej nazwie.
  • GetFields() - zwraca wszystkie publiczne pola.
  • GetInterface(String) - zwraca interfejs o określonej nazwie.
  • GetMember(String) - zwraca publiczny element o określonej nazwie.
  • GetMembers - zwraca wszystkie publiczne element typu.
  • GetMethod(String) - zwraca publiczną metodę o określonej nazwie.
  • GetMethods() - zwraca wszystkie metody publiczne typu.
  • GetProperty(String) - zwraca publiczną właściwość o określonej nazwie.
  • GetProperties() - zwraca wszystkie publiczne właściwości typu.
  • GetTypeArray - zwraca wszystkie typy obiektów w wybranej tablicy.
  • InvokeMember(String, BindingFlags, Binder, Object, Object[]) - wykonuje metodę używając określonych powiązan i dopasowując listę argumentów.

GetArrayRank

Jeżeli typ jest tablicą, to metoda zwraca liczbę wymiarów w tablicy. Poniższy kod tworzy trzy-wymiarową tablicę i wyświetla liczbę wymiarów tablicy:

int[,,] myIntArray = new int[5,6,7];
Type myIntArrayType = myIntArray.GetType();
Debug.Print("Array Rank: {0}", myIntArrayType.GetArrayRank());

Wynik:

3

GetConstructors

Metoda zwraca tablicę obiektów ConstructorInfo dla wszystkich publicznych konstruktorów zdefiniowanych dla danego typu. Poniższy kod wyświetla konstruktory publiczne dla klasy System.DataTable:

DataTable myDataTable = new DataTable();
Type myDataTableType = myDataTable.GetType();
ConstructorInfo[] myDataTableConstructors = myDataTableType.GetConstructors();

for(int i = 0; i <= myDataTableConstructors.Length - 1; i++)
{
ConstructorInfo constructorInfo = myDataTableConstructors[i];
Debug.Print("\nConstructor #{0}", i + 1);

ParameterInfo[] parameters = constructorInfo.GetParameters();
Debug.Print("Number Of Parameters: {0}", parameters.Length);

foreach (ParameterInfo parameterin parameters)
{
Debug.Print("Parameter Name: {0}", parameter.Name);
Debug.Print("Parameter Type: {0}",
parameter.ParameterType.Name);
}
}

Wynik:

Constructor #1 
Number Of Parameters: 0 
Constructor #2 
Number Of Parameters: 1 
Parameter Name: tableName 
Parameter Type: String 
Constructor #3 
Number Of Parameters: 2 
Parameter Name: tableName 
Parameter Type: String 
Parameter Name: tableNamespace 
Parameter Type: String

GetEnumName, GetEnumNames i GetEnumValues

Jeżeli typ jest enumeracją to metody GetEnum… pozwalają na pobranie wszystkich nazw i wartości enumeracji. 
Przykładowa enumeracja:

privateenumMyCustomEnum
{
Red = 1,
White = 2,
Blue = 3
}

Kod wyświetlający wszystkie nazwy enumeracji:

Type myCustomEnumType = typeof(MyCustomEnum);
string[] enumNames = myCustomEnumType.GetEnumNames();

foreach (string enumName in enumNames)
{
Debug.Print(string.Format("Name: {0}", enumName));
}

Wynik:

Name: Red 
Name: White 
Name: Blue

Wartości enumeracji można pobrać za pomocą metody GetEnumValues. Poniższa metoda wyświetla wszystkie wartości enumeracji:

Type myCustomEnumType = typeof(MyCustomEnum);
Array enumValues = myCustomEnumType.GetEnumValues();

foreach (object enumValue in enumValues)
{
Debug.Print(string.Format("Enum Value: {0}", enumValue.ToString()));
}

Wynik:

Enum Value: Red 
Enum Value: White 
Enum Value: Blue

Zauważmy, że wartości są takie same jak nazwy.

Można również użyć metody GetEnumName aby pobrać nazwę. Poniższy kod wyświetla wszystkich członków używając wartości enumeracji:

Type myCustomEnumType = typeof(MyCustomEnum);

for (int i = 1; i <= 3; i++)
{
string enumName = myCustomEnumType.GetEnumName(i);
Debug.Print(string.Format("{0}: {1}", enumName, i));
}

Wynik:

Red: 1 
White: 2 
Blue: 3

GetField i GetFields

Pole jest zmienną zdefiniowaną w klasie lub strukturze. Metoda GetField jest używana do pobrania obiektu klasy FieldInfo dla pola. Metoda GetFields pobiera tablicę obiektów FieldInfo. Metoda GetFields może również zwrócić pola z odziedziczonych klas. Kiedy ją wywołujemy to wstawiamy enumerację BindingFlags aby wybrać zakres pól które chcemy pobrać. 
Poniższa klasa zawiera pięć pól z różnych zakresów:

classReflectionExample
{
privatestring _privateField = "Hello";
publicstring _publicField = "Goodbye";
internalstring _internalfield = "Hola";
protectedstring _protectedField = "Adios";
staticstring _staticField = "Bonjour";
}

Możemy użyć metody GetFields aby pobrać wartości tych zmiennych w zależności od zakresu, np:

ReflectionExample reflectionExample = new ReflectionExample();
Type reflectionExampleType = typeof(ReflectionExample);

FieldInfo[] fields = reflectionExampleType.GetFields(BindingFlags.Public |
BindingFlags.Instance |
BindingFlags.Static |
BindingFlags.NonPublic |
BindingFlags.FlattenHierarchy);

foreach (FieldInfo field in fields)
{
object fieldValue = field.GetValue(reflectionExample);

Debug.WriteLine(string.Format("Field Name: {0}, Value: {1}", field.Name,
fieldValue.ToString()));
}

Wynik:

Field Name: _privateField, Value: Hello 
Field Name: _publicField, Value: Goodbye 
Field Name: _internalfield, Value: Hola 
Field Name: _protectedField, Value: Adios 
Field Name: _staticField, Value: Bonjour

Wywołując metodę GetFieldsmożemy użyć operatora bitowego aby wybrać więcej niż jedną wartość enumeracji BindingFlags.

Dzięki refleksji można odczytać także zmienne prywatne więc trzeba mieć na uwadze co zapisujemy w tych zmiennych.

Obiekt FieldInfo posiada także metodę SetValue która pozwala zmienić wartość zmiennej, nawet jeżeli jest prywatna lub chroniona. 
Przykładowo dodajmy do klasy ReflectionExample właściwość:

publicstring PrivateField
{
get { return privateField; }
}

Poniższy kod zmienia wartość zmiennej privateField i ją wyświetla:

ReflectionExample reflectionExample = new ReflectionExample();
Type reflectionExampleType = typeof(ReflectionExample);

reflectionExampleType.GetField("privateField", BindingFlags.NonPublic |
BindingFlags.Instance).SetValue(reflectionExample, "My New Value");

Debug.Print("Private Field Value: {0}",
reflectionExample.PrivateField);

Wynik:

Private Field Value: My New Value

GetProperty i GetProperties

Metody GetProperty i GetProperties są podobne do GetField i GetFields ponieważ pozwalają na pobranie właściwości, pobranie ich wartości i ustawienie ich wartości. Metody zwracają obiekty klasy PropertyInfo zamiast FieldInfo.

GetMethod i GetMethods

Metody GetMethod i GetMethods pozwalają na pobranie informacji o metodach danego typu. Po pobraniu referencji do metody możemy ją wykonać za pomocą metody Invoke obiektu klasy MethodInfo. Wybraną metodę można również wykonać za pomocą metody InvokeMember klasySystem.Type
Dodajmy poniższą metodę do klasy ReflectionExample:

publicdoubleMultiply(double x, double y)
{
return x * y;
}

Poniższy kod wykonuje metodę Multiply i wyświetla jej wynik:

ReflectionExample reflectionExample = new ReflectionExample();
Type reflectionExampleType = typeof(ReflectionExample);

MethodInfo methodInfo = reflectionExampleType.GetMethod("Multiply");
double returnValue = (double)methodInfo.Invoke(reflectionExample,
new object[] { 4, 5 });

Debug.Print("Return Value: {0}", returnValue);

Parametry metody wstawiamy używając tablicy obiektów jako drugiego parametru metody Invoke
Wynik:

Return Value: 20

Używając metody InvokeMember składnia jest następująca:

ReflectionExample reflectionExample = new ReflectionExample();
Type reflectionExampleType = typeof(ReflectionExample);

double returnValue = (double)reflectionExampleType.InvokeMember("Multiply",
BindingFlags.InvokeMethod,
null,
reflectionExample,
newobject[] { 4, 5 });

Debug.Print(string.Format("Return Value: {0}", returnValue));

Drugim parametrem jest BindingFlags.InvokeMethod, która wywołuje metodę InvokeMember.

Odczyt i tworzenie atrybutów niestandardowych

Atrybuty pozwalają definiować metadane dla klas, właściwości lub metod. Klasa, właściwość lub metoda są określane jako cel atrybutu. Refleksja może być użyta do odczytu tych atrybutów dynamicznie i zmiany zachowania celu. Atrybuty są zawarte w nawiasy kwadratowe “[ ]” powyżej celu i mogą być układane jeden po drugim gdy potrzebnych jest wiele atrybutów do definiowania celu. Przykładowo jeżeli chcemy aby klasa była serializowalna to musimy dodać atrybut [Serializable()] do definicji klasy, np:

[Serializable()]
publicclassMyClass
{
//…
}

Atrybut Serializable jest klasą zdefiniowaną w .NET Framework’u która dziedziczy z klasy System.Attribute. Klasa System.Attribute jest klasą abstrakcyjną która jest klasą bazową dla wszystkich atrybutów niestandardowych.

Odczyt atrybutów

Przestrzen nazw System.Reflection posiada wiele klas pozwalających na odczyt metadanych klas wewnątrz assembly. Assembly posiada metodę GetCustomAttributes która pozwala na odczyt wszystkich klas atrybutów niestandardowych zdefiniowanych w assembly lub odfiltrowanie konkretnego typu atrybutu. 
Poniższy kod iteruje przez wszystkie assembly do których odnosi się aktualna assembly i wyświetla nazwy klas niestandardowych atrybutów:

Assembly assembly = Assembly.GetExecutingAssembly();
AssemblyName[] assemblyNames = assembly.GetReferencedAssemblies();

foreach (AssemblyName assemblyName in assemblyNames)
{
Debug.WriteLine("\nAssembly Name: {0}", assemblyName.FullName);

Assembly referencedAssembly = Assembly.Load(assemblyName.FullName);

object[] attributes = referencedAssembly.GetCustomAttributes(false);

foreach (object attribute in attributes)
{
Debug.WriteLine(\nAttribute Name: {0}",
attribute.GetType().Name);

//Get the properties of this attribute
PropertyInfo[] properties = attribute.GetType().GetProperties();
foreach (PropertyInfo property in properties)
{
Debug.WriteLine("{0} : {1}", property.Name,
property.GetValue(attribute));
}
}
}

Aktualnie wykonywane assembly posiada referencje do: mscorlibSystem.Data i System. Wynik działania kodu:

Assembly Name: mscorlib, Version=4.0.0.0, Culture=neutral, 
PublicKeyToken=b77a5c561934e089 
Attribute Name: StringFreezingAttribute 
TypeId : System.Runtime.CompilerServices.StringFreezingAttribute 
… 
Assembly Name: System, Version=4.0.0.0, Culture=neutral, 
PublicKeyToken=b77a5c561934e089 
Attribute Name: ComVisibleAttribute 
Value : False 
TypeId : System.Runtime.InteropServices.ComVisibleAttribute

Tworzenie atrybutów

Aby utworzyć własny atrybut niestandardowy należy utworzyć klasę dziedziczącą z klasy abstrakcyjnej System.Attribute, np:

classMyCustomAttribute : System.Attribute
{
}

Nazywając klasę atrybutu należy dodać do nazwy sufiks Attribute. Kiedy korzystamy z atrybutów możemy odwoływać się do nich pomijając sufiks.

Po zadeklarowaniu klasy można dodać do niej właściwości i enumeracje tak samo jak do każdej innej klasy. 
Przykładowo dodajmy enumerację i trzy właściwości:

classMyCustomAttribute : System.Attribute
{
publicenum MyCustomAttributeEnum
{
Red,
White,
Blue
}

publicbool Property1 { get; set; }
publicstring Property2 { get; set; }
public MyCustomAttributeEnum Property3 { get; set; }
}

Kolejnym krokiem jest zdefiniowanie zakresu atrybutu. Przykładowo ograniczymy zakres atrybutu do klas i struktur. Aby tego dokonać używamy atrybutu System.AttributeUsage, np:

[System.AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
classMyCustomAttribute : System.Attribute
{
//…
}

Teraz możemy użyć tego atrybutu definiując klasę, np:

[MyCustom(Property1 = true, Property2 = "Hello World", Property3 =
MyCustomAttribute.MyCustomAttributeEnum.Red)
]
class MyTestClass()
{
}

Zauważmy, że użyliśmy nazwy atrybutu MyCustom a nie MyCustomAttribute
Aby ustawić wartości właściwości wstawiliśmy ich wartości jako parametry nazwane. Można również dodać konstruktor do klasy atrybutu i w nim ustawić wartości.

Teraz gdy mamy klasę z atrybutami to możemy odczytać wartośi właściwości atrybutów za pomocą refleksji:

Type myTestClassType = typeof(MyTestClass);
MyCustomAttribute attribute =
(MyCustomAttribute)myTestClassType.GetCustomAttribute
(
typeof(MyCustomAttribute),
false
);

Debug.WriteLine("Property1: {0}", attribute.Property1);
Debug.WriteLine("Property2: {0}", attribute.Property2);
Debug.WriteLine("Property3: {0}", attribute.Property3);

Wynik:

Property1: True 
Property2: Hello World 
Property3: Red

Generowanie kodu za pomocą CodeDOM

The Code Document Object ModelCodeDOM jest zestawem klas w .NET Framework który umożliwia tworzenie generatorów kodu. CodeDOM zawiera klasy reprezentujące właściwości, metody, klasy, logikę, parametry i inne typy kodu.

Można użyć klas CodeDOM do wygenerowania kodu w C#VB.NET lub JScript. Kod wygenerowany przez CodeDOM jest bardzo dobry do automatyzacji powtarzalnych zadań lub egzekwowania wzorców w projekcie.

W tej sekcji zademonstrujemy jak utworzyć klasę która posiada pola, właściwości i metody, używając CodeDOM
Typowa struktura klasy w C# posiada następujące elementy:

  • Plik tekstowy w którym znajduje się kod klasy.
  • Zestaw wyrażeń using.
  • Deklarację przestrzeni nazw.
  • Deklarację nazwy klasy.
  • Zestaw pól, właściwości i metod. Metody mogą posiadać logikę z pętlami, wyrażeniami itp.

Przestrzeń nazw CodeDOM posiada klasy które umożliwiają utworzenie struktóry nazywanej grafem CodeDOM Graph, który modeluje te elementy.

Lista najczęściej używanych klas przestrzeni CodeDOM:

  • CodeArgumentReferenceExpression - reprezentuje referencję do wartości argumentu wstawianego do metody.
  • CodeAssignStatement - instrukcja przypisania.
  • CodeBinaryOperatorExpression - wyrażenie zawierające operację binarną.
  • CodeCastExpression - wyrażenie rzutowania typów.
  • CodeComment - komentarz.
  • CodeCompileUnit - kontener dla CodeDOM, zawierający deklaracje, przestrzeń nazw, klasy i komponenty klasy.
  • CodeConditionStatement - instrukcja warunkowa np if.
  • CodeConstructor - konstruktor.
  • CodeFieldReferenceExpression - referencja do pola.
  • CodeIterationStatement - pętla np for.
  • CodeMemberEvent - deklaracja zdarzenia.
  • CodeMemberField - deklaracja pola.
  • CodeMemberMethod - deklaracja metody.
  • CodeMemberProperty - deklaracja waściwości.
  • CodeMethodInvokeExpression - wyrażenie wywołujące metodę.
  • CodeMethodReturnStatement - instrukcja return.
  • CodeNamespace - deklaracja przestrzeni nazw.
  • CodeNamespaceImport - dyrektywa importu przestrzeni nazw.
  • CodeObjectCreateExpression - wyrażenie tworzące nową instancję typu.
  • CodeParameterDeclarationExpression - deklaracja parametru dla metody, właściwości lub konstruktora.
  • CodePropertyReferenceExpression - referencja do wartości właściwości.
  • CodePropertySetValueReferenceExpression - argument wartości dla metody set właściwości.
  • CodeRegionDirective - nazwa i tryb regionu kodu.
  • CodeSnippetCompileUnit - reprezentuje tekstowy fragment kodu który może być skompilowany.
  • CodeSnippetStatement - reprezentuje tekstowy fragment kodu.
  • CodeThisReferenceExpression - referencja do instancji klasy lokalnej.
  • CodeThrowExceptionStatement - wyrażenie wyrzucania wyjątku.
  • CodeTryCatchFinallyStatement - blok try-catch-finally.
  • CodeTypeConstructor - statyczny konstruktor klasy.
  • CodeTypeDeclaration - deklaracja klasy, struktury, interfejsu lub enumeracji.
  • CodeTypeDelegate - deklaracja delegaty.
  • CodeVariableDeclarationStatement - deklaracja zmiennej.
  • CodeVariableReferenceExpression - referencja do zmiennej lokalnej.

Przestrzeń nazw CodeDOM posiada klasę dla każdego typu wyrażenia które można utworzyć w .NET Framework
Kod klasy który wygenerujemy za pomocą CodeDOM:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespaceReflection
{
publicclassCalculator
{
publicdouble X { get; set; }

publicdouble Y { get; set; }

publicdoubleDivide()
{
if (this.Y == 0)
{
return0;
}
else
{
returnthis.X / this.Y;
}
}

publicdoubleExponent(double power)
{
return Math.Pow(this.X, power);
}
}
}

Jest to prosta klasa nazwana Calculator znajdująca się w przestrzeni nazw Reflection, zawierająca dwa pola, dwie właściwości i dwie metody. poniższe sekcje zademonstrują jak wygenerować taką klasę dynamicznie za pomocą CodeDOM.

CodeCompileUnit

Klasa CodeCompileUnit jest kontenerem dla wszystkich obiektów które chcemy wygenerować. Mozna myśleć o niej jak o pliku który ma zawierać cały wygenerowany kod. 
Poniższy kod służy do utworzenia instancji tej klasy:

CodeCompileUnit codeCompileUnit = new CodeCompileUnit();

CodeNamespace i CodeNamespaceImport

Klasa CodeNamespace jest używana do deklarowania przestrzenii nazw. Konstruktor tej klasy pobiera nazwę przestrzeni nazw jako parametr:

CodeNamespace codeNamespace = new CodeNamespace("Reflection");

Klasa CodeNamespaceImport dodaje instrukcje using:

codeNamespace.Imports.Add(newCodeNamespaceImport("System"));
codeNamespace.Imports.Add(newCodeNamespaceImport("System.Collections.Generic"));
codeNamespace.Imports.Add(newCodeNamespaceImport("System.Linq"));
codeNamespace.Imports.Add(newCodeNamespaceImport("System.Text"));
codeNamespace.Imports.Add(newCodeNamespaceImport("System.Threading.Tasks"));

CodeTypeDeclaration

Klasa CodeTypeDeclaration służy do deklaracji klasy:

CodeTypeDeclaration targetClass = new CodeTypeDeclaration("Calculator");
targetClass.IsClass = true;
targetClass.TypeAttributes = TypeAttributes.Public;
//Add the class to the namespace.
codeNamespace.Types.Add(targetClass);

Ustawienie wartości właściwości IsClass na true mówi .NET Framework’owi, że ma wygenerować deklarację klasy. Właściwość TypeAttributes pozwala deklarować atrybuty publicprivate i static. Można je kombinować za pomocą operatora bitowego (|). Po zadeklarowaniu klasy należy ją dodać do kolekcji Types przestrzeni nazw.

CodeMemberField

Za pomocą klasy CodeMemberField deklarujemy pola klasy:

CodeMemberField xField = new CodeMemberField();
xField.Name = "x";
xField.Type = new CodeTypeReference(typeof(double));
targetClass.Members.Add(xField);

CodeMemberField yField = new CodeMemberField();
yField.Name = "y";
yField.Type = new CodeTypeReference(typeof(double));
targetClass.Members.Add(yField);

CodeMemberProperty

Za pomocą klasy CodeMemberProperty deklarujemy właściwości klasy:

//X Property
CodeMemberProperty xProperty = new CodeMemberProperty();
xProperty.Attributes = MemberAttributes.Public | MemberAttributes.Final;
xProperty.Name = "X";
xProperty.HasGet = true;
xProperty.HasSet = true;
xProperty.Type = new CodeTypeReference(typeof(System.Double));

xProperty.GetStatements.Add(new CodeMethodReturnStatement(
new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "x")));

xProperty.SetStatements.Add(new CodeAssignStatement(
new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "x"),
new CodePropertySetValueReferenceExpression()));

targetClass.Members.Add(xProperty);

//Y Property
CodeMemberProperty yProperty = new CodeMemberProperty();
yProperty.Attributes = MemberAttributes.Public | MemberAttributes.Final;
yProperty.Name = "Y";
yProperty.HasGet = true;
yProperty.HasSet = true;
yProperty.Type = new CodeTypeReference(typeof(System.Double));

yProperty.GetStatements.Add(new CodeMethodReturnStatement(
new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "y")));

yProperty.SetStatements.Add(new CodeAssignStatement(
new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "y"),
new CodePropertySetValueReferenceExpression()));

targetClass.Members.Add(yProperty);

Klasa CodeMemberProperty posiada dwie właściwości HasGet i HasSet które trzeba ustawić aby wygenerować akcesory Get i Set
Kolekcja GetStatements jest używana aby dodać kod do akcesora Get. W tym przypadku metoda Get zwraca pole this.x. Aby wygenerować referencję this używamy klasy CodeThisReferenceExpression
Kolekcja SetStatements zawiera kod ustawiający pole this.x. Aby ustawić wartość trzeba użyć klasy CodeAssignStatement.

CodeMemberMethod

Klasa CodeMemberMethod służy do tworzenia metod. poniższy kod tworzy metodę o nazwie Divide która zwraca double i ustawia metodę jako final i public:

CodeMemberMethod divideMethod = new CodeMemberMethod();
divideMethod.Name = "Divide";
divideMethod.ReturnType = new CodeTypeReference(typeof(double));
divideMethod.Attributes = MemberAttributes.Public | MemberAttributes.Final;

Kiedy mamy już sygnaturę metody to należy utworzyć jej ciało. Metoda Divide sprawdza czy właściwość Y jest równa 0, jeżeli tak to zwraca 0, w przeciwnym wypadku iloraz. Logika If jest utworzona za pomocą klasy CodeConditonStatement:

CodeConditionStatement ifLogic = new CodeConditionStatement();

ifLogic.Condition = new CodeBinaryOperatorExpression(
new CodeFieldReferenceExpression(
new CodeThisReferenceExpression(), yProperty.Name),
CodeBinaryOperatorType.ValueEquality,
new CodePrimitiveExpression(0));

ifLogic.TrueStatements.Add(new CodeMethodReturnStatement(
new CodePrimitiveExpression(0)));

ifLogic.FalseStatements.Add(new CodeMethodReturnStatement(
new CodeBinaryOperatorExpression(
new CodeFieldReferenceExpression(
new CodeThisReferenceExpression(), xProperty.Name),
CodeBinaryOperatorType.Divide,
new CodeFieldReferenceExpression(
new CodeThisReferenceExpression(), yProperty.Name))));

divideMethod.Statements.Add(ifLogic);

CodeParameterDeclarationExpression i CodeMethodInvokeExpression

Następnym krokiem jest utworzenie metody Exponent. Metoda ta pobiera parametr o nazwie power i zwraca this.Y podniesione do potęgi o wartości power.

CodeMemberMethod exponentMethod = new CodeMemberMethod();

exponentMethod.Name = "Exponent";
exponentMethod.ReturnType = new CodeTypeReference(typeof(double));
exponentMethod.Attributes = MemberAttributes.Public | MemberAttributes.Final;

CodeParameterDeclarationExpression powerParameter =
new CodeParameterDeclarationExpression(typeof(double), "power");
exponentMethod.Parameters.Add(powerParameter);

CodeMethodInvokeExpression callToMath = new CodeMethodInvokeExpression(
new CodeTypeReferenceExpression("System.Math"),
"Pow",
new CodeFieldReferenceExpression(new CodeThisReferenceExpression(),
xProperty.Name), new CodeArgumentReferenceExpression("power"));

exponentMethod.Statements.Add(new CodeMethodReturnStatement(callToMath));

targetClass.Members.Add(exponentMethod);

Parametr power tworzymy za pomocą klasy CodeParameterDeclarationExpression
Wywołanie metody i wstawienie do niej parametrów jest realizowane za pomocą klasy CodeMethodInvokeExpression.

CodeDOMProvider

Ostatnim krokiem jest wygenerowanie pliku klasy. Do utworzenia pliku klasy używamy klasy CodeDOMProvider. Klasa ta posiada metodę GenerateCodeFromCompileUnit która pobiera jako parametry obiekty klas CodeCompileUnitTextWriter i CodeGeneratorOptions. KlasaCodeGeneratorOptions posiada właściwości pozwalające na kontrole formatowania kodu. Poniższy przykład mówi kompilatorowi aby użył interlinii pomiędzy deklaracjami. Ustawia właściwość BracingStyle na wartość “C”, która mówi, że nawiasy mają być w osobnych liniach.

CodeDOMProvider provider = CodeDOMProvider.CreateProvider("CSharp");

CodeGeneratorOptions options = new CodeGeneratorOptions();
options.BlankLinesBetweenMembers = false;
options.BracingStyle = "C";

using (StreamWriter sourceWriter = new StreamWriter(@"c:\CodeDOM\Calculator." +
provider.FileExtension))
{
provider.GenerateCodeFromCompileUnit(codeCompileUnit, sourceWriter, options);
}

Lampda Expressions

Wyrażenia Lambda są skróconą składnią pisania metod anonimowych. Wyrażenia Lambda są bardzo często używane w LINQ więc ich zrozumienie jest bardzo ważne.

Delegaty

Delegata jest typem który przechowuje referencje do metod. Kiedy deklarujemy delegatę, określamy sygnaturę metod jakie maja być przechowywane przez delegatę. 
Przykładowo utwórzmy metodę WriteToConsoleForward:

staticvoidWriteToConsoleForward(string stringToWrite)
{
Console.WriteLine("This is my string: {0}", stringToWrite);
}

Jeżeli chcielibyśmy zapisać referencję do tej metody to musielibyśmy utworzyć delegatę o takiej samej sygnaturze, np:

delegatevoidMyFirstDelegate(string s);

Teraz możemy powiązać zmienną utworzonego typu z metodą:

MyFirstDelegate myFirstDelegate = new
MyFirstDelegate(LambdaExpressions.WriteToConsoleForward);

Teraz zmienna myFirstDelegate zasadniczo posiada odniesienie do metody. 
Teraz możemy wywołać metodę używając zmiennej myFirstDelegate i wstawiając do niej parametr:

myFirstDelegate("Hello World");

Utwórzmy teraz kolejną metodę o takiej samej sygnaturze:

staticvoidWriteToConsoleBackwards(string stringToWrite)
{
char[] charArray = stringToWrite.ToCharArray();
Array.Reverse(charArray);

Console.WriteLine("This is my string backwards: {0}",
newstring(charArray));
}

Ponieważ metoda posiada taką samą sygnaturę to referencja do niej może być przechowywane przez tą samą delegatę.

Stwórzmy metodę która przyjmuje delegatę jako parametr i wywołuje metodę:

staticvoidWriteToConsole(MyFirstDelegate myDelegate, string stringToWrite)
{
myDelegate(stringToWrite);
}

Teraz możemy wywołać metodę WriteToConsole i wstawić metodę jako parametr:

WriteToConsole(LambdaExpressionExample.WriteToConsoleForward, "Hello World");

WriteToConsole(LambdaExpressionExample.WriteToConsoleBackwards, "Hello World");

Wynik działania kodu:

This is my string: Hello World 
This is my string backwards: dlroW olleH

Ważną rzeczą dotyczącą delegat jest koncept kowariancji i kontrawariancji.
Kowariancja pozwala na posiadanie metod z typem zwracanym który jest odziedziczony z typu który jest zadeklarowany w delegacie. Więc typ delegaty może być klasą bazową a typ zwracany przez metody może dziedziczyć z tego typu bazowego. 
Kontrawariancja pozwala na posiadanie parametrów typu który jest klasą bazową dla typu zdefiniowanego w delegacie. Więc typ parametru metody może być klasa bazową pod warunkiem, że parametr delegaty jest typu dziedziczącego z tej klasy bazowej.

Metody anonimowe

Metody anonimowe pozwalają na deklarację delegaty i bezpośrednie przypisanie do niej metody w tej samej linii kodu, np:

MyFirstDelegate forward = delegate(string s2)
{
Console.WriteLine("This is my string: {0}", s2);
};
forward("Hello World");

Jedną z różnic między metodą anonimową a delegata jest to, że można w niej odnosić się do zmiennych lokalnych które nie zostały przekazane jako parametr. 
Poniższy kod demonstruje utworzenie delegaty bez parametrów i użycie zmiennej lokalnej w metodzie anonimowej:

delegate voidMyAnonymousMethod();

staticvoidMain(string[] args)
{
string myLocalString = "Hello World";

//Create an anonymous methodusing the local variable.
MyAnonymousMethod forward = delegate()
{
Console.WriteLine(string.Format("This is my string: {0}", myLocalString));
};

forward();
}

Lambda Expressions

Wyrażenia lambda pozwalają na tworzenie metod anonimowych używając skróconej notacji, np:

delegate doublesquare(double x);

staticvoidMain(string[] args)
{
square myLambdaExpression = x => x * x;
Console.WriteLine("X squared is {0}", myLambdaExpression(5));
}

Wyrażeniem lambda z przykładu jest x => x x*. Operator => nazywany jest operatorem goes to. Lewa strona operatora zawiera parametry wejściowe metody. Ciało metody zapisywane jest po prawej stronie operatora. Jeżeli używamy kilku parametrów to zapisujemy je w nawiasie oddzielając je przecinkami, np:

delegate boolGreaterThan(double x, double y);
staticvoidMain(string[] args)
{
GreaterThan gt = (x, y) => x > y;
Console.WriteLine("Is 6 greater than 5. {0}", gt(6, 5));
}

Jeżeli metoda zawiera tylko jedno wyrażenie to nazywamy takie wyrażenie wyrażeniem lambda (ang. expression lambda). 
Jeżeli metoda wymaga użycia wielu wyrażeń w swoim ciele to takie nazywamy ją deklaracją lambda (ang. statement lambdas) i otaczamy nawiasem klamrowym { }, np:

s =>
{
char[] charArray = s.ToCharArray();
Array.Reverse(charArray);
Console.WriteLine("This is my string to write backwards: {0}",
new string(charArray));
};

Można użyć wyrażenia lambda aby wstawić funkcję do metody. poniższy przykład używa wyrażenia lambda do wywołania metody WriteToConsole:

WriteToConsole(x => Console.WriteLine("This is my string {0}", x), "Hello World");

Podsumowanie

Refleksja(ang. Reflection)

  • Istnieja dwa sposoby aby pobrać referencję do typu Type obiektu. Używając na obiekcie metod typeof() lub .GetType().
  • Można uzyć klasy System.Reflection.Assembly aby zbadać typy w pliku EXE lub DLL.
  • Metoda Assembly.Load pobiera assembly do pamięci i pozwala na wykonanie jej kodu.
  • Metoda Assembly.ReflectionOnlyLoad pobiera assembly do pamięci ale nie pozwala na wykonanie jej kodu.
  • Metoda Assembly.CreateInstance tworzy instancję typu.
  • Klasa System.Type reprezentuje klasę, interfejs, tablicę, typ wartości, enumerację, parametr i typ generyczny.
  • Metoda Type.GetProperty zwraca obiekt PropertyInfo i pozwala na pobranie i ustawianie wartości właściwości.

Atrybuty

  • Atrybuty służą do tworzenia metadanych dla klas, właściwości i metod.
  • Atrybuty definiuje się w nawiasach kwadratowych [].
  • Atrybuty niestandardowe muszą dziedziczyć z klasy System.Attribute.

Code Document Object Model (CodeDOM)

  • Przestrzeń nazw CodeDOM zawiera klasy które pozwalają tworzyć generatory kodu.
  • Klasa System.CodeDom.CodeCompileUnit jest kontenerem dla kodu który chcemy wygenerować.
  • Klasa System.CodeDom.CodeDOMProvider generuje plik klasy w wybranym z języków: C#VB lub JScript.

Lambda expressions

  • Wyrażenie lambda są skróconą notacją dla metod anonimowych.
  • Delegata jest typem który przechowuje referencje do metod.
  • Kowariancja pozwala na posiadanie metod z typem zwracanym który jest odziedziczony z typu który jest zadeklarowany w delegacie.
  • Kontrawariancja pozwala na posiadanie parametrów typu który jest klasą bazową dla typu zdefiniowanego w delegacie.
  • Operator => jest odczytywany jako “goes to”.