Wywołanie metody ogólnej z parametrem typu znanym tylko w czasie wykonywania można znacznie uprościć, stosując dynamic
typ zamiast interfejsu API odbicia.
Aby użyć tej techniki, typ musi być znany z rzeczywistego obiektu (nie tylko instancji Type
klasy). W przeciwnym razie musisz utworzyć obiekt tego typu lub użyć standardowego rozwiązania interfejsu API odbicia . Możesz utworzyć obiekt za pomocą metody Activator.CreateInstance .
Jeśli chcesz wywołać metodę ogólną, która w „normalnym” użyciu miałaby wywnioskowany typ, po prostu przychodzi do rzutowania obiektu nieznanego typu na dynamic
. Oto przykład:
class Alpha { }
class Beta { }
class Service
{
public void Process<T>(T item)
{
Console.WriteLine("item.GetType(): " + item.GetType()
+ "\ttypeof(T): " + typeof(T));
}
}
class Program
{
static void Main(string[] args)
{
var a = new Alpha();
var b = new Beta();
var service = new Service();
service.Process(a); // Same as "service.Process<Alpha>(a)"
service.Process(b); // Same as "service.Process<Beta>(b)"
var objects = new object[] { a, b };
foreach (var o in objects)
{
service.Process(o); // Same as "service.Process<object>(o)"
}
foreach (var o in objects)
{
dynamic dynObj = o;
service.Process(dynObj); // Or write "service.Process((dynamic)o)"
}
}
}
A oto wynik tego programu:
item.GetType(): Alpha typeof(T): Alpha
item.GetType(): Beta typeof(T): Beta
item.GetType(): Alpha typeof(T): System.Object
item.GetType(): Beta typeof(T): System.Object
item.GetType(): Alpha typeof(T): Alpha
item.GetType(): Beta typeof(T): Beta
Process
jest ogólną metodą instancji, która zapisuje prawdziwy typ przekazywanego argumentu (za pomocą GetType()
metody) i typ parametru ogólnego (za pomocą typeof
operatora).
Rzucając argument dynamic
typu na typ, odroczyliśmy podawanie parametru type do czasu działania. Gdy Process
metoda jest wywoływana z dynamic
argumentem, kompilator nie dba o typ tego argumentu. Kompilator generuje kod, który w czasie wykonywania sprawdza rzeczywiste typy przekazywanych argumentów (za pomocą odbicia) i wybiera najlepszą metodę do wywołania. Tutaj jest tylko jedna ogólna metoda, więc jest wywoływana z odpowiednim parametrem typu.
W tym przykładzie dane wyjściowe są takie same, jakbyś napisał:
foreach (var o in objects)
{
MethodInfo method = typeof(Service).GetMethod("Process");
MethodInfo generic = method.MakeGenericMethod(o.GetType());
generic.Invoke(service, new object[] { o });
}
Wersja z typem dynamicznym jest zdecydowanie krótsza i łatwiejsza do napisania. Nie należy również martwić się wydajnością wywołania tej funkcji wiele razy. Następne wywołanie z argumentami tego samego typu powinno być szybsze dzięki mechanizmowi buforowania w DLR. Oczywiście możesz napisać kod, który buforuje wywoływanych delegatów, ale używając dynamic
typu otrzymujesz to zachowanie za darmo.
Jeśli ogólna metoda, którą chcesz wywołać, nie ma argumentu sparametryzowanego (więc nie można wywnioskować jej parametru typu), możesz zawrzeć wywołanie metody ogólnej w metodzie pomocniczej, jak w poniższym przykładzie:
class Program
{
static void Main(string[] args)
{
object obj = new Alpha();
Helper((dynamic)obj);
}
public static void Helper<T>(T obj)
{
GenericMethod<T>();
}
public static void GenericMethod<T>()
{
Console.WriteLine("GenericMethod<" + typeof(T) + ">");
}
}
Zwiększone bezpieczeństwo typu
To, co jest naprawdę świetne w używaniu dynamic
obiektu jako zamiennika do używania refleksyjnego interfejsu API, polega na tym, że tracisz tylko sprawdzanie czasu kompilacji tego konkretnego typu, którego nie znasz przed uruchomieniem. Inne argumenty i nazwa metody są jak zwykle analizowane statycznie przez kompilator. Jeśli usuniesz lub dodasz więcej argumentów, zmienisz ich typy lub zmienisz nazwę metody, pojawi się błąd kompilacji. Nie stanie się tak, jeśli podasz nazwę metody jako ciąg znaków, Type.GetMethod
a argumenty jako tablicę obiektów MethodInfo.Invoke
.
Poniżej znajduje się prosty przykład ilustrujący, w jaki sposób niektóre błędy mogą zostać wykryte w czasie kompilacji (kod komentarza), a inne w czasie wykonywania. Pokazuje także, w jaki sposób DLR próbuje rozwiązać metodę, którą należy wywołać.
interface IItem { }
class FooItem : IItem { }
class BarItem : IItem { }
class Alpha { }
class Program
{
static void Main(string[] args)
{
var objects = new object[] { new FooItem(), new BarItem(), new Alpha() };
for (int i = 0; i < objects.Length; i++)
{
ProcessItem((dynamic)objects[i], "test" + i, i);
//ProcesItm((dynamic)objects[i], "test" + i, i);
//compiler error: The name 'ProcesItm' does not
//exist in the current context
//ProcessItem((dynamic)objects[i], "test" + i);
//error: No overload for method 'ProcessItem' takes 2 arguments
}
}
static string ProcessItem<T>(T item, string text, int number)
where T : IItem
{
Console.WriteLine("Generic ProcessItem<{0}>, text {1}, number:{2}",
typeof(T), text, number);
return "OK";
}
static void ProcessItem(BarItem item, string text, int number)
{
Console.WriteLine("ProcessItem with Bar, " + text + ", " + number);
}
}
Tutaj ponownie wykonujemy jakąś metodę, rzutując argument na dynamic
typ. Tylko weryfikacja typu pierwszego argumentu jest odraczana do środowiska wykonawczego. Otrzymasz błąd kompilatora, jeśli nazwa metody, którą wywołujesz, nie istnieje lub jeśli inne argumenty są niepoprawne (zła liczba argumentów lub niewłaściwe typy).
Gdy przekazujesz dynamic
argument do metody, to wywołanie jest ostatnio powiązane . Metoda rozwiązywania przeciążenia ma miejsce w czasie wykonywania i próbuje wybrać najlepsze przeciążenie. Jeśli więc wywołujesz ProcessItem
metodę z obiektem BarItem
typu, wówczas faktycznie wywołasz metodę inną niż ogólna, ponieważ lepiej pasuje do tego typu. Jednak podczas przekazywania argumentu Alpha
typu pojawi się błąd czasu wykonania, ponieważ nie ma metody, która mogłaby obsłużyć ten obiekt (metoda ogólna ma ograniczenie, where T : IItem
a Alpha
klasa nie implementuje tego interfejsu). Ale o to chodzi. Kompilator nie ma informacji, że to wywołanie jest prawidłowe. Jako programista wiesz o tym i powinieneś upewnić się, że ten kod działa bez błędów.
Typ zwrotu gotcha
Kiedy dzwoni non-void metody z parametrem dynamicznym typu, jego typ zwracany będzie prawdopodobnie być dynamic
zbyt . Więc jeśli zmienisz poprzedni przykład na ten kod:
var result = ProcessItem((dynamic)testObjects[i], "test" + i, i);
wówczas typ obiektu wynikowego to dynamic
. Wynika to z faktu, że kompilator nie zawsze wie, która metoda zostanie wywołana. Jeśli znasz typ zwracanego wywołania funkcji, powinieneś niejawnie przekonwertować go na wymagany typ, aby reszta kodu została wpisana statycznie:
string result = ProcessItem((dynamic)testObjects[i], "test" + i, i);
Otrzymasz błąd czasu wykonania, jeśli typ nie pasuje.
W rzeczywistości, jeśli spróbujesz uzyskać wartość wyniku w poprzednim przykładzie, otrzymasz błąd czasu wykonania w drugiej iteracji pętli. Jest tak, ponieważ próbowałeś zapisać wartość zwracaną funkcji void.