Spis treści:

Kategoria:C#


Wywoływanie metod prywatnych w C#

Prywatne obiekcje dotyczące metod prywatnych

Zanim przejdę do konkretów przypomnę to, co powinno być powszechnie znane. Zgodnie z zasadami programowania obiektowego nie powinno się używać prywatnych pól, metod i właściwości. Przeczy to zasadzie hermetyzacji, czy jak kto woli ukrywania implementacji. Przyjęło się, że metody publiczne to coś, co jest niezmienne. Podobnego założenia nie można przyjąć w odniesieniu do metod prywatnych. Zakłada się też, że wywołanie publicznych metod nie powinno zniszczyć wewnętrznego stanu obiektów. I znów - podobnego założenia nie można przyjąć w odniesieniu do metod prywatnych. Nieprawidłowa kolejność wywołań metod prywatnych może powodować niepożądane efekty uboczne, zmiany wewnętrznych wartości, flag, wskaźników a czasem nawet wycieki zasobów. Piszę o tym nie dlatego, żeby zniechęcić do stosowania interesujących technik, które wymagają wywołania metod prywatnych. Piszę po to, aby robić to świadomie i zachować ostrożność, zanim się zagalopujemy i cały nasz kod będzie obłożony dynamicznymi wywołaniami metod prywatnych. Jeżeli jest to tylko możliwe warto rozważyć inne metody, takie, które nie stoją w sprzeczności z ogólnie przyjętymi zasadami programowania obiektowego. Pisząc o dynamicznym wywoływaniu metod prywatnych zakładam, że świadomość zagrożeń jest na odpowiednim poziomie.

Przestrzeń nazw System.Reflection

Większość klas, struktur i typów wyliczeniowych biorących udział w dynamicznym wywoływaniu metod znajduje się w przestrzeni nazw System.Reflection. Nie zamierzam powielać dokumentacji. To co pokażę, to praktyczny przykład. Przyjrzyjmy się poniższemu listingowi.

public class TestClass
{
    private string Test()
    {
        return "Test";
    }
}

//Gdzieś w programie
TestClass t = new TestClass();
var method = t.GetType().GetMethod("Test"BindingFlags.Instance | BindingFlags.NonPublic);
string returnValue = (string)method.Invoke(t, null);

Każdy obiekt w .NET posiada metodę GetType(), która służy do pobrania obiektu typu reprezentującego rozważany obiekt. Wspomniany obiekt typu posiada z kolei metodę GetMethod(), która służy do pobrania obiektu typu MethodInfo, reprezentującą dowolną metodę dowolnego obiektu. Pierwszy parametr określa nazwę wyszukiwanej metody, natomiast drugi typ metody. Metody mogą być statyczne i niestatyczne, mogą być opatrzone atrybutem private i public. Daje to różne kombinacje, które definiuje się poprzez łączenie flag z typu wyliczeniowego BindingFlags. Obiekt MethodInfo przypisany jest do konkretnego typu i można sobie go wyobrazić jako klasę przechowującą adresy poszczególnych pól oraz wskaźników na konkretne metody. Ten obiekt MethodInfo może być zatem użyty wielokrotnie, dla różnych wystąpień obiektów, z których typu został pobrany. Pokazany na powyższym listingu obiekt typu MethodInfo zapisany w zmiennej method może być zatem użyty na dowolnym obiekcie klasy TestClass.

Pisząc o użyciu obiektu typu MethodInfo mam na myśli wywołanie metody, którą ten obiekt reprezentuje. Jest to realizowane przez metodę Invoke(). Dość często powtarza się słowo metoda, ale nic na to nie poradzę. MethodInfo reprezentuje metodę i ma metody. Mam nadzieję, że to rozróżnienie reprezentowanej metody i metody jest wystarczająco czytelne. Pierwszym parametrem metody Invoke() jest obiekt, którego reprezentowaną metodę chcemy wywołać. Drugi parametr to tablica wartości, które zostaną kolejno podstawione w postaci parametrów reprezentowanej metody. W pokazanym przykładzie jest null, bo metoda nie ma parametrów.

To były podstawy. Teraz coś trudniejszego, ale też bardziej praktycznego.

Klasa testowa i problem z przeciążeniami

Każda metoda może mieć wiele wersji, byle tylko różniły się zestawem parametrów. Jak takie metody rozpoznać? Wspomniana wcześniej metoda GetMethod() także występuje w kilku wersjach. Zanim jednak pokażę przykład, zmodyfikuję klasę TestClass.

public class TestClass
{
    private string Test()
    {
        return "Test";
    }
    private string GetString()
    {
        return "GetString";
    }
    private string GetString(string a)
    {
        return String.Format("GetString-{0}", a);
    }
    private string GetString(string a, int b)
    {
        return String.Format("GetString-{0}-{1}", a, b);
    }
}

Co się stanie, gdy teraz spróbujemy dynamicznie wywołać prywatną metodę GetString techniką pokazaną na początku? Dostaniemy wyjątek typu System.Reflection.AmbiguousMatchException z komunikatem "Ambiguous match found.". Rozwiązaniem jest inna wersja metody GetMethod(), która został pokazana poniżej:

TestClass t = new TestClass();
var method = t.GetType().GetMethod("GetString",
        BindingFlags.Instance | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
string returnValue = (string)method.Invoke(t, null);

Tak jak kompilator rozpoznaje przeciążoną metodę na podstawie zestawu parametrów, tak i metoda GetMethod() potrzebuje informacji o typach kolejnych argumentów wyszukiwanej metody. Te typy przekazywane są w czwartym parametrze. W pokazanym przykładzie jest to pusta tablica typów, więc zwrócona zostanie metoda bezparametrowa.

Klasa do dynamicznego wywoływania metod prywatnych

Jeżeli zamierzamy stworzyć jakiś interesujący mechanizm oparty o wywołania metod prywatnych, najprawdopodobniej przyda nam się jakaś przyjemna klasa. Popatrzmy na jedno z rozwiązań:

public class Dynamic
{
    public static object CallPrivate(object obj, string name, params object[] parameters)
    {
        //pobierz listę typów
        Type[] types = new Type[parameters.Length];
        for (int i=0; i<parameters.Length; i++)
            types[i] = parameters[i].GetType();

        return obj.GetType().GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic,
            null, types, null).Invoke(obj, parameters);
    }

    public static Y CallPrivate<Y>(object obj, string name, params object[] parameters)
    {
        return (Y)CallPrivate(obj, name, parameters);
    }
}

Pierwsza z metod rozpoznaje, jakie typy mają poszczególne parametry i przekazuje je do metody GetMethod(). Tylko tyle i aż tyle. Druga metoda może się przydać wtedy, gdy potrzebujemy konkretnego typu, a nie chcemy wykonywać jawnego rzutowania. Zwykle wygodniej jest przenieść logikę rzutowania do środka metody zamiast rozrzucać rzutowania w różnych miejscach w kodzie. Przed rzutowaniem w metodzie można oczywiście sprawdzić, czy typ się zgadza, a w razie niezgodności zwrócić domyślną wartość dla typu lub wyrzucić stosowny wyjątek.

Metoda rozszerzająca do wywoływania metod prywatnych

Można pójść o krok dalej i stworzyć metody rozszerzające do konkretnych typów. Ja pozwoliłem sobie stworzyć metodę rozszerzającą do wszystkich obiektów .NET. Zwykle nie jest potrzebna aż tak głęboka ingerencja. Zwykle lepiej sprawdza się metoda rozszerzająca konkretny zestaw klas lub klasy implementujące pewien określony interfejs. Myślę, że taka zmiana nie będzie stanowiła problemu. Przyjrzyjmy się zatem przykładowej implementacji metod rozszerzających:

public static class DynamicExtension
{
    public static T CallPrivate<T>(this object obj, string name, params object[] parameters)
    {
        return Dynamic.CallPrivate<T>(obj, name, parameters);
    }
    public static object CallPrivate(this object obj, string name, params object[] parameters)
    {
        return Dynamic.CallPrivate(obj, name, parameters);
    }
}

Trzeba pamiętać, że korzystanie z metod rozszerzających wymaga dodania przestrzeni nazw, która zawiera klasę z metodami rozszerzającymi, za pomocą słowa kluczowego using. Same metody rozszerzające muszą być statyczne i zawierać przy deklaracji rozszerzanego typu słowo kluczowe this.

Przykład wywołania metod prywatnych

Odpowiednie obudowanie metod z przestrzeni nazw System.Reflection sprawia, że wywoływanie metod prywatnych jest już stosunkowo proste. Popatrzmy na jeszcze jeden listing:

var obj = new TestClass();
//Wywołanie bez rzutowania
Console.WriteLine(Dynamic.CallPrivate(obj, "GetString"));

//Wywołania z rzutowaniem
Console.WriteLine(Dynamic.CallPrivate<string>(obj, "GetString"));
Console.WriteLine(Dynamic.CallPrivate<string>(obj, "GetString""XXX"));
Console.WriteLine(Dynamic.CallPrivate<string>(obj, "GetString""XXX", 5));

//Metoda rozszerzająca bez rzutowania
Console.WriteLine(obj.CallPrivate("GetString""ZZZ"));
Console.WriteLine(obj.CallPrivate("GetString""ZZZ", 6));

//Metoda rozszerzająca z rzutowaniem
Console.WriteLine(obj.CallPrivate<string>("GetString"));
Console.WriteLine(obj.CallPrivate<string>("GetString""YYY", 7));

Możliwości jest dużo, bo podobno od przybytku głowa nie boli. Spośród zaprezentowanych można sobie wybrać taką, która będzie najwygodniejsza. O to przecież chodziło.

Zachęcam do zgłaszania uwag i dzielenia się ciekawymi rozwiązaniami wykorzystującymi wywołania metod prywatnych. Może się bowiem okazać, że istnieją lepsze techniki, niewiele trudniejsze, nie łamiące jednej z podstawowych zasad programowania obiektowego.

Kategoria:C#

, 2013-12-20

Brak komentarzy - bądź pierwszy

Dodaj komentarz
Wyślij
Ostatnie komentarze
bardo ciekawe , można dzięki takim wpisom dostrzec wędkę..
Bardzo dziękuję za jasne tłumaczenie z dobrze dobranym przykładem!
Dzieki za te informacje. były kluczowe
Dobrze wyjaśnione, dzięki !