Poniższy artykuł pochodzi z serii Przygotowań do egzaminu 70-536.
Dzisiaj powiemy sobie o używaniu refleksji, tworzeniu instancji typów, wywoływanie metod do dynamicznego ładowania pakietów, ładowaniu pakietów itd.
Przez większość czasu mamy bezpośredni dostęp do typów w trakcie pisania kodu. Są jednak chwilę kiedy warto byłoby załadować assembly, typy i metody zawarte w nim dynamicznie. Na przykład aplikacja, która obsługuje wtyczki powinna być napisana tak aby uruchomić plug-in dynamicznie nawet gdy plug-in nie jest dostępny, podczas opracowywania takiego wniosku.
Jak załadować pakiet?
Możemy ładować pakiety dynamicznie (runtime). Po załadowaniu pakietu można zbadać jego atrybuty i w zależności od metody załadować go, utworzyć instancje typu lub uruchomić metodę. Oto metody których możemy użyć do załadowanie pakietu:
Assembly.Load- ładuje pakiet po nazwie zazwyczaj z Global Assembly Cache (GAC)
Assembly.LoadFile- ładuje pakiet poprzez podanie nazwy pliku
Assembly.LoadFrom- ładuje pakiet pobierając nazwę pliku lub ścieżkę
Assembly.ReflectionOnlyLoad- ładuje pakiet, zazwyczaj z GAC w reflection-only context (opisane poniżej)
Assembly.ReflectionOnlyLoadFrom- ładuje pakiet w reflection-only context poprzez podanie nazwy pliku
Załadowanie pakietu w reflection-only context pozwala na zbadanie pakietu ale nie tworzenie instancji typu lub uruchamianie metod. Dlatego warto używać tego kiedy mamy potrzebę tylko zbadania pakietu lub kody zawartego w nim. Często developerzy używają reflection-only context do zbadania czy pakiet nadaję się na inne platformy bądź inne wersje .NET Framework.
Jak utworzyć instancje i wywoływać metody?
Posłużę się tutaj kodem z training kit’a który jest dobrze po komentowany. Demonstruj on jak stworzyć instancje StringBuilder i wywołać metodę StringBuilder.Append przy użyciu refleksji.
1: // C#
2: // Create a Type instance.
3: // Typically, this would be done by loading an external assembly,
4: // and then calling Assembly.GetType()
5: Type t = typeof(StringBuilder);
6: // Create a ConstructorInfo instance that will allow us to create an
7: // instance of the Type we just loaded.
8: // GetConstructor requires a list of parameters in a Type array
9: // that match those required by the constructor.
10: // This example represents the StringBuilder constructor that
11: // requires a single parameter.
12: ConstructorInfo ci = t.GetConstructor(new Type[] { typeof(string) });
13: // Create an instance of the type by calling ConstructorInfo.Invoke.
14: // Provide the parameters required by the constructor: a single string.
15: // This creates a StringBuilder instance.
16: Object sb = ci.Invoke(new Object[] { "Hello, " });
17: // Create a MethodInfo instance representing the StringBuilder.Append method.
18: // GetMethod requires the first parameter to be the name of the method.
19: // The second parameter is a Type array representing the parameters required
20: // by the method. We're using the Append overload that requires a single string.
21: MethodInfo sbAppend = t.GetMethod("Append", new Type[] { typeof(string) });
22: // Call StringBuilder.Append and provide a single parameter: the string "world!".
23: Object result = sbAppend.Invoke(sb, new Object[] { "world!" });
24: // Write the StringBuilder instance to the console.
25: Console.WriteLine(result);
Po utworzeniu instancji MethodInfo można wywołać MethodInfo.GetMethodBase do pobrania instancji MethodBody. Następnie możemy wywołać MethodBody.GetILAsByteArray do pobrania tablicy bajtów zawierającą rzeczywisty kod pośredni (IL), kod do uruchomienia metody. MethodBody.LocalVariables jest zbiorem zmiennych lokalnych (choć nie można pobierać wartości lub nazwy zmiennych).
Dostęp do właściwości, pól, konstruktorów możemy uzyskać przy pomocy metod Type.GetProperty,Type.GetField, i Type.GetConstructors które zwracają odpowiednio PropertyInfo, FieldInfo ,i ConstructorInfo. Działają one w niemal identyczny sposób jak Type.GetMethod.
Poniższy przykład ilustruje wykorzystanie refleksji, aby uzyskać dostęp do wartości StringBuilder.Length (tylko do odczytu) i wyświetlanie tej wartości w konsoli:
1: // C#
2: Type t = typeof(StringBuilder);
3: ConstructorInfo ci = t.GetConstructor(new Type[] { typeof(string) });
4: Object sb = ci.Invoke(new Object[] { "Hello, world!" });
5: // Create a PropertyInfo instance representing the StringBuilder.Length property.
6: PropertyInfo lengthProperty = t.GetProperty("Length");
7: // Retrieve the Length property and cast it to the native type.
8: int length = (int)lengthProperty.GetValue(sb, null);
9: Console.WriteLine(length.ToString());
Aby przeglądać zdarzenia, pola, właściwości i metod na raz, musimy wywołać Type.GetMembers. Aby np. pobrać tylko prywatne składowe i statyczne (lub inne) musimy do tego celu wykorzystać typ wyliczeniowy BindingFlags. Spójrzmy na przykład:
1: // C#
2: Type t = typeof(Console);
3: MemberInfo[] mi = t.GetMembers(
4: BindingFlags.NonPublic | BindingFlags.Static);
5: foreach (MemberInfo m in mi)
6: {
7: Console.WriteLine("{0}: {1}", m.Name, m.MemberType);
8: }
BindingFlag oferuje następujące opcje które możemy połączyć logicznymi operatorami:
DeclaredOnly – ignoruje składowe dziedziczone
Default – nie określa w ogóle BindingFlag
FlattenHierarchy – zwraca składowe dziedziczone oraz chronione
Instance- składowe które są częścią typu i są dodawane
Oraz IgnoreCase, NonPublic, Public, Static – to raczej jasne.
Assembly Attributes
Assembly attributes opisują nasz pakiet np. nazwe, wersje, autora itd… Zazwyczaj atrybuty dodajemy w pliku AssemblyInfo w projekcie, który początkowo zawiera domyślne ustawienia dla wielu atrybutów. Plik AssemblyInfo zawiera (miedzy innymi) domyślnie:
[assembly: AssemblyCompany("Contoso, Inc.")]
[assembly: AssemblyCopyright("Copyright © 2008")]
[assembly: AssemblyVersion("1.0.0.0")]
Możemy dodać następujące atrybuty do naszego pakietu (ze względu na to, że nie widziałem w traning kit po tym rozdziale szczegółowych pytań do poszczególnych atrybutów wymienię je tylko a nadgorliwi w razie potrzeby znajdą co konkretny atrybut opisuje):
AssemblyAlgorithmId, AssemblyCompany, AssemblyConfiguration, AssemblyCopyright, AssemblyCulture, AssemblyDefaultAlias, AssemblyDelaySign, AssemblyDescription, AssemblyFileVersion, AssemblyFlags, AssemblyInformationalVersion, AssemblyKeyFile, AssemblyKeyName, AssemblyProduct, AssemblyTitle,
AssemblyTrademark, AssemblyVersion.
Możemy wczytać atrybuty wywołując metodę Assembly.GetCustomAttributes która zwróci tablice obiektów typu Attribute. Iterując po niej możemy wypisać interesujące nas informacje. Oto przykład:
1: // C#
2: Assembly asm = Assembly.GetExecutingAssembly();
3: foreach (Attribute attr in asm.GetCustomAttributes(false))
4: {
5: if (attr.GetType() == typeof(AssemblyCopyrightAttribute))
6: Console.WriteLine("Copyright: {0}",
7: ((AssemblyCopyrightAttribute)attr).Copyright);
8: if (attr.GetType() == typeof(AssemblyCompanyAttribute))
9: Console.WriteLine("Company: {0}",
10: ((AssemblyCompanyAttribute)attr).Company);
11: if (attr.GetType() == typeof(AssemblyDescriptionAttribute))
12: Console.WriteLine("Description: {0}",
13: ((AssemblyDescriptionAttribute)attr).Description);
14: }
Generating Types Dynamically
Możemy dynamicznie tworzyć klasy przy pomocy klas z przestrzeni nazw System.Reflection.Emit. Możemy stworzyć nowy typ wraz z konstruktorami, metodami, polami etc. Poniższy przykład pokazuje jak stworzyć pakiet, nowy typ, dodać do niego przykładowe metody a na koniec wywołać jedną z nich która wyświetli nam klasyczne “Hello World” ;)
1: // C#
2: // Create an AssemblyBuilder instance
3: AssemblyBuilder ab = AppDomain.CurrentDomain.DefineDynamicAssembly(
4: new AssemblyName("dynAssembly"), AssemblyBuilderAccess.RunAndSave);
5: // Create a ModuleBuilder
6: ModuleBuilder mb = ab.DefineDynamicModule("dynMod");
7: // Create a TypeBuilder public class
8: TypeBuilder tb = mb.DefineType("dynType",
9: TypeAttributes.Class | TypeAttributes.Public);
10: // Create a default constructor (this isn't necessary for this example)
11: ConstructorBuilder cb =
12: tb.DefineDefaultConstructor(MethodAttributes.Public);
13: // Create a public, static method named Greet that doesn't accept
14: // parameters or return a value
15: MethodBuilder method = tb.DefineMethod("Greet",
16: MethodAttributes.Public | MethodAttributes.Static);
17: // Create an ILGenerator for the method, which allows us to write code for it
18: ILGenerator dynCode = method.GetILGenerator();
19: // Add a line of code to the method, equivalent to Console.WriteLine
20: dynCode.EmitWriteLine("Hello, world!");
21: // Add a line of code to return from the method
22: dynCode.Emit(OpCodes.Ret);
23: // Create an instance of the dynamic type
24: Type myDynType = tb.CreateType();
25: // Call the static method we dynamically generated
26: myDynType.GetMethod("Greet").Invoke(null, null);
Kolejny artykuł z serii to 70-536: Creating an E-mail Message