Spis treści:

Kategoria:C#


Dynamiczne dodawanie właściwości do obiektu w C#

Wzorujemy się na JavaScript

JavaScript ma kilka ciekawych właściwości. Jedną z nich jest możliwość szybkiego dodawania nowych pól i funkcji do już istniejących obiektów. Z jednej strony jest to niezwykła wygoda, z drugiej zaś ogromne źródło błędów. Nie zamierzam wszczynać dyskusji na temat wad i zalet języków silnie typowanych i języków słabo typowanychNie istnieje granica oddzielająca języki silnie typowane od języków słabo typowanych. Istnieją języki, w tym C#, który ma elementy silnie typowane (większość), ale umożliwia również dość luźne traktowanie danych. Wystarczy wspomnieć opisywany w artykule obiekt lub słowo kluczowe unsafe., bo każdy z nich ma swoje wady i zalety. Zamierzam pokazać, że język C#, od wersji .NET 4.0, również pozwala na zastosowanie w swoich projektach konstrukcji słabo typowanych. Nie chcę przekonywać do stosowania tego typu konstrukcji w swoim kodzie, bo sam mam wobec nich mieszane uczucia. Nie ważne co o tych rozwiązaniach myślimy - inni programiści, zafascynowani ich nowością i świeżością, będą ich używać. Będą ich używać, a my będziemy musieli to zrozumieć i zapewne niejednokrotnie poprawiać taki dynamiczny kod.

Większość zapewne zna typ danych dynamic, do którego można wpisać dowolną wartość. Jest jednak w .NET jeszcze ciekawszy dynamiczny typ - typ, do którego można w locie dodawać dowolne właściwości i metody. Ten typ to ExpandoObject. Jak on działa?

ExpandoObject w podstawowym wydaniu

Przyjrzyjmy się przykładowemu listingowi pokazującemu omawiany typ w najprostszej postaci:

dynamic extensible = new ExpandoObject();
extensible.Name = "Rozszerzalny";
extensible.Age = 5;

Console.WriteLine("Jestem {0} i mam {1} lat", extensible.Name, extensible.Age);

Właściwości można przypisywać w każdym miejscu i są one tworzone w momencie przypisania im wartości. Gdy odwołamy się do właściwości, do której nic nie zostało przypisane, otrzymamy wyjątek. Na pierwszy rzut oka takie magiczne obiekty wydają się bardzo elastyczne i miłe w obsłudze. Ładnie to wygląda w małym przykładzie. Jeżeli jednak kod zaczyna się rozrastać, różne właściwości dodawane są w różnych metodach, przez różnych użytkowników - wtedy można się nieco pogubić. Co zrobić, gdy chcemy pobrać właściwości, które zostały ustawione przez kogoś w innej metodzie, a ta inna osoba nie chce nam zdradzić szczegółów?

Pobieranie listy właściwości

Jednym z możliwych zastosowań obiektu ExpandoObject jest przekazanie go wywołującemu i pozwolenie mu na uzupełnienie dowolnych właściwości. Obiekt taki uzupełnia się stosunkowo łatwo - w każdym razie łatwiej niż słownik. Nadaje się on zatem do bardzo elastycznego API, w którym nie wiemy ile i jakiego typu będą konkretne właściwości.

Aby przebadać obiekt ExpandoObject można przyjrzeć się jego definicji. Dowiemy się, że implementuje on interfejs IEnumerable<KeyValuePair<string, object>>. Możemy wobec tego skorzystać z odpowiedniego rzutowania i wydobyć potrzebne nam informacje pisząc taką oto metodę:

private static IEnumerable<string> GetProperties(dynamic extensible)
{
    var enumerable = extensible as IEnumerable<KeyValuePair<stringobject>>;
    if (enumerable != null)
    {
        foreach (var item in enumerable)
        {
            yield return item.Key;
        }
    }
}

Otrzymamy w ten sposób listę wszystkich właściwości obiektu. Wykonując pokazany poniżej kod:

dynamic extensible = new ExpandoObject();
extensible.Name = "Rozszerzalny";
extensible.Age = 5;

Console.WriteLine("Właściwości: {0}"string.Join(", ", GetProperties(extensible)));

Otrzymamy następujący rezultat:

Właściwości: Name, Age

Samo rozpoznanie nazw właściwości niewiele nam jednak daje. Całe szczęście ExpandoObject ma coś w zanadrzu.

Rozpoznawanie typów poszczególnych właściwości

Mamy listę właściwości i wiemy jak się te właściwości nazywają. Odpowiednie wyświetlenie wymaga jednak czegoś więcej niż nazwy. Z pomocą przychodzi pewna właściwość .NET - każda właściwość, o ile nie jest równa null, obsłuży wywołanie metody ToString. Gdybyśmy jednak zechcieli pójść krok dalej i wygenerować na podstawie tego obiektu jakiś weselszy interfejs użytkownika - będziemy potrzebowali typu. Jak to zrobić? Wiemy, że mamy do dyspozycji interfejs IEnumerable<KeyValuePair<string, object>>, ale gdybyśmy jeszcze dokładniej przyjrzeli się deklaracji typu ExpandoObject zobaczylibyśmy, że implementuje on również interfejs IDictionary<string, object>. To z niego skorzystamDla niewielkiej liczby właściwości przeszukanie interfejsu IEnumerable może się okazać wydajniejsze niż skorzystanie ze słownika. Diabeł tkwi w szczegółach implementacji tablic z haszowaniem.. Popatrzmy na poniższy listing:

private static Type GetPropertyType(dynamic extensible, string propertyName)
{
    var dictionary = extensible as IDictionary<stringobject>;
    if (dictionary != null && dictionary.ContainsKey(propertyName))
    {
        return dictionary[propertyName].GetType();
    }
    else
    {
        return null;
    }
}

Przyjrzyjmy się jeszcze przykładowemu wywołaniu pokazanej metody:

dynamic extensible = new ExpandoObject();
extensible.Name = "Rozszerzalny";
extensible.Age = 5;

foreach (var typeName in GetProperties(extensible))
{
    Console.WriteLine("Właściwość {0} jest typu {1}", typeName, GetPropertyType(extensible, typeName));
}

Na konsoli powinien pojawić się następujący rezultat:

Właściwość Name jest typu System.String
Właściwość Age jest typu System.Int32

Jakie jeszcze interfejsy mamy do dyspozycji?

Powiadamianie o zmianach właściwości

ExpandoObject ma jeszcze jeden przydatny interfejs i jest nim INotifyPropertyChanged. Możemy zatem podpiąć metodę, która będzie wywoływana w momencie dodawania nowej, usuwania niepotrzebnej i modyfikowania istniejącej właściwości. Popatrzmy na przykład:

static void Program_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    Console.WriteLine("Właściwość {0} zmieniła się", e.PropertyName);
}

// Gdzieś w kodzie...
dynamic extensible = new ExpandoObject();
((INotifyPropertyChanged)extensible).PropertyChanged += Program_PropertyChanged;
extensible.NewProperty = 'G';
extensible.NewProperty = 'C';

Po uruchomieniu przykładu otrzymamy dwa komunikaty:

Właściwość NewProperty zmieniła się
Właściwość NewProperty zmieniła się

Pierwszy komunikat związany jest z utworzeniem właściwości i przypisaniem początkowej wartości, drugi komunikat związany jest już tylko ze zmianą wartości.

Usuwanie właściwości

Wiemy już, że dodawanie właściwości jest wyjątkowo proste i polega na przypisaniu do niej jakiejś wartości. Jak taką właściwość usunąć? Przypisać null? A jeżeli chcielibyśmy mieć null jako wartość właściwości? Rozwiązaniem problemu jest implementacja interfejsu IDictionary<string, object>:

private static void RemoveProperty(dynamic extensible, string propertyName)
{
    var dictionary = extensible as IDictionary<stringobject>;
    if (dictionary != null && dictionary.ContainsKey(propertyName))
    {
        dictionary.Remove(propertyName);
    }
}

Teraz wystarczy gdzieś w kodzie tę metodę wywołać i cieszyć się z rezultatu. Popatrzmy na poniższy przykład:

dynamic extensible = new ExpandoObject();
extensible.Name = "Rozszerzalny";
extensible.Age = 5;
extensible.NewProperty = 'G';
((INotifyPropertyChanged)extensible).PropertyChanged += Program_PropertyChanged;
RemoveProperty(extensible, "NewProperty");
Console.WriteLine("Właściwości: {0}"string.Join(", ", GetProperties(extensible)));

Wykonanie tych instrukcji sprawi, że w oknie konsoli pojawią się dwa komunikaty:

Właściwość NewProperty zmieniła się
Właściwości: Name, Age

Możemy się przy okazji przekonać, że usuwanie właściwości również wywołuje zdarzenie PropertyChanged.

Dynamiczne dodawanie dynamicznych właściwości

Wiemy już, że dodanie właściwości w kodzie jest bardzo proste i sprowadza się do napisania nazwy obiektu, kropki, nazwy właściwości i przypisania temu jakiejś wartości. Przetwarzanie obiektów dynamicznych czasem wymaga programowego dodania nowej właściwości. W takim przypadku znów trzeba sięgnąć do interfejsu słownika:

private static void AddProperty(dynamic extensible, string propertyName, object value)
{
    var dictionary = extensible as IDictionary<stringobject>;
    if (dictionary != null)
    {
        dictionary.Add(propertyName, value);
    }
    else
    {
        throw new NotImplementedException();
    }
}

Znów wszystko sprowadza się do rzutowania na interfejs słownika.

Nie będę już pokazywał przykładu wywołania tej metody w kodzie, bo nie wniesie ona nic nowego. Wykorzystam ją w nieco bardziej rozbudowanym przykładzie.

Dodawanie metod w locie

Obiekt ExpandoObject może tworzyć dynamicznie nie tylko właściwości - może też mieć metody. Metody w sensie właściwości reprezentujących metody. Mówiąc jeszcze dokładniej - delegaty. Popatrzmy na poniższy przykład i przekonajmy się:

dynamic extensible = new ExpandoObject();
extensible.AddProperty = (Action<stringobject>)((name, value) => AddProperty(extensible, name, value));
extensible.AddProperty("OneMore""Se, se, se...");

Pokazana w poprzednim przykładzie metoda do dodawania właściwości została nadpisana. Metoda była ogólna, bo potrafiła obsłużyć każdy obiekt ExpandoObject przekazany w postaci pierwszego parametru. Aby uprościć to wywołanie można ten pierwszy parametr zdefiniować jako... obiekt wywołujący metodę. Coś na kształt wskazania this. Metoda być może nie ma większego sensu, ale pokazuje pewną technikę. To, jak tę technikę wykorzystamy, i czy wogóle wykorzystamy, zależy od nas samych.

Warto zwrócić uwagę na rzutowanie na typ Action. Powód tego jest bardzo prosty: wyrażenie lambda nie jest typem delegate. Typ delegate reprezentuje konkretną metodę, a w zasadzie wskazanie na nią. Wyrażenie lambda reprezentuje sposób wykonywania pewnych operacji i nie może być wykonywane bezpośrednio. To, że wyrażenie lambda może być użyte w miejscu delegata, to tylko zasługa kompilatora. To dlatego tworząc wyrażenia należy je wcześniej skompilować (zobacz metodę LambdaExpression.Compile). Napiszę jeszcze inaczej - wyrażenie lambda nie jest nawet typem! Dopiero jawne wskazanie na konkretny typ delegata pozwoli nam zadowolić kompilator.

Dynamiczne nie zawsze znaczy dobre

Napisałem na początku, że nie jestem do końca przekonany do zbyt luźnego traktowania zmiennych i typów w kodzie. Można pisać wiele, ale jedna myśl będzie zawsze krążyła: dlaczego większość złożonych aplikacji pisana jest w językach silnie typowanych? Wystarczy wymienić CC i C++ posiadają wskaźniki, które reprezentują pewien obszar w pamięci procesu. Definiują one jedynie typ obiektów wskazywanych przez wskaźnik lub sam adres w przypadku wskaźnika void*., C++, Javę, C#. Czy te języki mają większe możliwości? Zaryzykowałbym stwierdzenie, że nie i że jest wręcz przeciwnie. Pozwalają one na znacznie więcej. Można wstawić wszystko, w każym momencie i wszędzie! Właśnie przez takie podejście analiza kodu jest znacznie trudniejsza, trudniejsze jest wyszukiwanie błędów, trudniejsze jest zlokalizowanie miejsca, w którym zmienne są ustawiane, zmieniane. Zdarzenia pojawiaja się z zaskoczenia. Drobne literówki w nazwaniu zmiennych mogą pozostać w ukryciu przez długi czas - dopóki ktoś nie wykona napisanego kodu. Ktoś, kto analizował nieczytelnie napisany kod JavaScript mający więcej niż kilka stron z pewnoscią rozumie problem. Ktoś może powiedzieć - wystarczy ładnie pisać kod! Praktyka jest jednak inna. Komunizm w założeniach też jest dobry - każdemu według potrzeb! W językach silnie typowanych mamy wielkiego, praktycznie nieomylnego pomocnika - jest nim kompilator. Trzeba docenić to, że już na etapie kompilacji potrafi wyłapać dużą liczbę błędów, potrafi ostrzec przed kontrowersyjnymi konstrukcjami języka programowania. Potrafi ostrzec przed niejawnymi konwersjami pomiędzy typami i zapobiec mnożeniu tekstu przez liczbę całkowitą. Większość opisanych w artykule technik można zrealizować przy pomocy zwykłego słownika - ośmielę się powiedzieć, że niektóre operacje będą nawet czytelniejsze. Powiem więcej - błąd braku klucza w słowniku jest powszechnie znany, a sama obecność słownika u większości zapala żółte światło oznaczające podwyższone ryzyko.

Musiałem o tym wszystkim napisać, żeby pobudzić do myślenia. Pamiętajmy, że być może kiedyś przyjdzie innym (lub, nie daj Boże nam!) poprawiać napisany przez nas kod. Superdynamiczne struktury mają to do siebie, że potrafią być niezrozumiałe.

Żeby to wszystko nie wzbudziło atmosfery grozy napiszę tak: ogniem można się poparzyć, nożem można się skaleczyć. Wszystko, co zawsze podkreślam, zależy od konkretnego przypadku. Mam jednak głęboką nadzieję, że choć trochę pobudziłem do myślenia. Takie bowiem, według mnie, zadanie programisty i projektanta - myśleć i wybierać odpowiednie narzędzia. Z tego nikt nas podczas programistycznych zmagań nie zwolni.

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 !