Dynamic C# 4.0 przedstawił nowy typ “dynamic”. Istnieje on już od pewnego czasu, ale pytanie, dlaczego nie jest tak często używany? Odpowiedź jest oczywiście prosta, ponieważ słowo kluczowe dynamic przydaje się w wybiórczych przykładach.
Przykładowo typ dynamic jest używany do wiązania danych na samym dnie C#. Kiedyś te operacje wykonywały się na napisach bądź obiekcie System.Object, teraz to wszystko wędruje do dynamicznej zmiennej, która może określać wszystko.
Dynamic też przyda się bardzo przy operacjach z bibliotekami COM. COM w końcu to system, który jest w połowie dynamiczny. Wiele metod COM zwraca więc dynamiczny obiekt. Przed C# 4.0 zostawał zwracany obiekt, co trochę komplikowało sprawę. Trzeba było wtedy rzutować i zgadywać czym ten obiekt jest.
W sumie tak samo jest z typem dynamic, ale jeśli mam pewność, że wszystkie obiekty umieszczone w tym typie zawsze będą miały tę jedną wspólną właściwość, to wszystko jest w porządku. Właściwie typ mnie nie interesuje bardziej zakładam, że dana właściwość lub pole pojawią się bez względu na wszystko.
Taki kod jest całkowicie poprawny.
//Umieść cokolwiek chcesz
dynamic d1 = "OK";
d1 = 111;
d1 = "Also Ok";
//Wszystko jest okej dopóki dynamiczny obiekt ma
//metodę Remove i toArray()
dynamic d2 = new List<int>() { 4, 5, 5 };
dynamic operationStatus = d2.Remove(4);
dynamic array = d2.ToArray();
d2 = new List<double>() { 1.5, 2.5, 7.5, };
dynamic operationStatus2 = d2.Remove(1.5);
dynamic array2 = d2.ToArray();
dynamic sum = d2.Sum();
Oczywiście są jednak pewne ograniczenia. Po pierwsze nie można w metodach, w obiekcie dynamicznym stosować wyrażeń lambda.
Dynamiczny obiekt też nie uruchomi metod rozszerzeniowych. Dynamiczny obiekt jest listą liczb ułamkowych, a ona ma metodę rozszerzeniową SUM. Niestety w trakcie działania programu nie jest to uwzględnione.
Nie można więc uruchamiać metod rozszerzeniowych na dynamicznych obiektach.
Mimo to dynamiczne obiekty przydają się na przykład przy pracy z danymi, które są dynamiczne jak JSON I XML. Nie muszę tworzyć klasy dedykowanej dla serializowanego obiektu JSON. Przy użyciu JSON.NET wyglądałoby to tak:
dynamic stuff = JsonConvert.DeserializeObject
("{ 'Name': 'Jon Smith',"+
" 'Address':" +
"{ 'City': 'New York', 'State': 'NY' }," +
" 'Age': 42 }");
string name = stuff.Name;
string address = stuff.Address.City;
Jest bardziej przydatne niż się wydaje ponieważ powyższy przykład reprezentuje odczyt danych. Tymczasem do klienta często wysyłamy dynamicznego JSONA, który po stronie serwera nie jest reprezentowany przez żadna klasę.
bool someting = true;
dynamic person = new System.Dynamic.ExpandoObject();
person.Name = "Cezary";
person.Age = 12;
if (someting)
{
person.StatusBlocked = true;
}
string json = Newtonsoft.Json.JsonConvert.
SerializeObject(person);
Używając ExpandoObject mogę utworzyć obiekt z dynamicznymi właściwościami i potem skonwertować go do formatu JSON.
Zwrócony przez klienta JSON jest więc w swojej składni dynamiczny.
Poza tym dynamiczna zmienna przydaje się, gdy chcemy ogólnie uruchomić metodę, o której wiemy, że zawsze będzie istnieć dla tego obiektu. Wcześniej, żeby coś takiego zrobić trzeba było bawić się refleksją i sztywnym napisem nazwy metody.
A co z tym DuckTyping? Jest sens z tego mechanizmu korzystać? Jeśli tak, to po co?
Zobaczmy czy, jeśli coś chodzi jak kaczka może być także helikopterem, który ma kacze nogi.
DuckTyping w C#
Mam w projekcie dwa foldery, w których mam kolekcję identycznych klas. To co ich wyróżnia to, to że te klasy znajdują się w innej przestrzeni nazw. Oznacza to, że w C# reprezentują one dwa różne typy danych.
W przestrzeni Old mam takie klasy.
namespace ConsoleApplication3.Old
{
public abstract class Employee {
public string Name { get; set; }
}
public class HR : Employee { }
public class Programmer : Employee { }
}
W przestrzeni NewTransformed ma takie klasy
namespace ConsoleApplication3.NewTransformed
{
public abstract class Employee
{
public string Name { get; set; }
}
public class HR : Employee { }
public class Programmer : Employee { }
}
Obie przestrzenie nazw mają klasę abstrakcyjną Employee oraz swoje klasy reprezentujące osoby pracujące w HR i programowaniu.
Pomimo tych różnych typów danych nic nie stoi na przeszkodzie, aby pobrać imię pracownika bez względu na to, z jakiej przestrzeni nazw on pochodzi i czy klasy dziedziczą po sobie.
Oczywiście dużym minusem używania typów dynamicznych w C# jest fakt, że kompilator nam nie pomoże. Kto wie może kiedyś Visual Studio będzie umiało podpowiadać jakie właściwości i metody ta zmienna może mieć na podstawie wszystkich przyrównań i wywołań metod.
C# to jednak nie język Python. Mam więc metodę, która zwróci mi nazwę pracownika.
public static string GetNameOfEmployee(dynamic employee)
{
return employee.Name;
}
Oczywiście działa ona bezbłędnie bez względu na to, jakiego pracownika mam na myśli.
var s1 = GetNameOfEmployee
(new Old.HR() { Name = "CW" });
var s2 = GetNameOfEmployee
(new NewTransformed.Programmer { Name = "MS" });
var s3 = GetNameOfEmployee
(new Old.Programmer { Name = "KL" });
var s4 = GetNameOfEmployee
(new NewTransformed.HR { Name = "PL" });
Jeśli więc posiadasz kolekcję klas, które nie dziedziczą po sobie, ale mają wspólne właściwości , to oczywiście możesz je uruchomić.
Kto wie może w prawdziwym świecie będziesz miał kolekcję klas, które reprezentują osoby fizyczne. Co znaczy, że będą miały imiona. Oczywiście jest to sprzeczne z filozofią C# i świata obiektowego, ponieważ najlepiej by było stworzyć interfejs, który nam daje gwarancję, że dany obiekt na 100% ma np. imię.
public interface IPhisicalPerson
{
string Name { get; set; }
}
Aplikacja w końcu zawsze może się wywalić w tym miejscu. Co przypomina o niebezpieczeństwie typów dynamicznych.
Interfejsy i klasy abstrakcyjne górą. Nie zawsze ich użycie jest jednak możliwe. Przykładowo jak pracujesz z zamkniętym na zmiany starym kodem.
Na myśl przychodzi mi także inny przykład, który może wystąpić w prawdziwym życiu.
Użyteczność dynamic
Mam więc pracowników. Stare i nowe ich reprezentacje. W pewnym miejscu w aplikacji następuje konwersja typów ze starych pracowników na nowe.
Zakładając, że nie korzystasz z klas mapujących jak np. (Automapper,FastMapper) to taka konwersja jest uciążliwa. Musiałbyś po pierwsze sprawdzić co jest czym….
public static NewTransformed.Employee ConvertOld
(Old.Employee employee)
{
if (employee == null)
return null;
if (employee is Old.HR)
return ConvertInternal((Old.HR)employee);
if (employee is Old.Programmer)
return ConvertInternal((Old.Programmer)employee);
throw new ArgumentException("Unknown Employee", nameof(employee));
}
…a potem po rzutowaniu uruchomić określoną metodę.
public static NewTransformed.HR ConvertInternal(Old.HR a)
{
//Konwersja starego na nowy
return new NewTransformed.HR();
}
public static NewTransformed.Programmer ConvertInternal(Old.Programmer a)
{
//Konwersja starego na nowy
return new NewTransformed.Programmer();
}
Całą tę logikę if-else is this można uprościć do tego stopnia, przy użyciu słowa dynamic.
public static NewTransformed.Employee Convert(Old.Employee employee)
{
return employee != null ?
ConvertInternal((dynamic)employee) : null;
}
W trakcie wykonywania kodu kompilator uruchomi mi określoną metodę i nie muszę się o nic martwić. Kto by pomyślał, że ducktyping może być wykorzystany w taki sposób.
Napisałem ten wpis, aby przypomnieć sobie po co mamy słowo dynamic w C# i jak widać po małym odświeżeniu informacji okazuje się, że słowo dynamic ratuje nas w wielu przypadkach.