1. Wprowadzenie
C# — przedstawiciel języków z silnym typowaniem. To znaczy, że na etapie kompilacji zmienna powinna mieć określony typ i do członków klasy można odwoływać się tylko zgodnie z tym typem. To jest bezpieczne, wygodne, czytelne dla IDE i kompilatora.
Ale czasami trzeba operować na obiektach, których struktura może być nieznana na etapie kompilacji — na przykład, gdy:
- IronPython lub IronRuby), gdzie typowanie jest dynamiczne.
- Przy pracy z obiektami COM (np. z Microsoft Office przez Interop).
- Przy współpracy z dynamicznymi strukturami danych: na przykład z mało sformalizowanymi obiektami JSON.
- Nie chcemy pisać sterty szablonowego kodu, łatwiej „ufając” obiektowi na słowo i próbując wywołać potrzebną metodę po nazwie.
Z pojawieniem się w C# 4.0 słowa kluczowego dynamic i platformy DLR pojawiła się możliwość pisania kodu, który zachowuje się „jak w Pythonie, tylko w C#”: weryfikacja odbywa się w czasie wykonania, a nie podczas kompilacji. Dzięki temu magicznemu słowu można sprawić, żeby kompilator był mniej rygorystyczny i ręcznie sprawdzić, czy jest dana metoda/właściwość dopiero gdy program naprawdę do tego miejsca dotrze.
Co to jest DLR?
Dynamic Language Runtime (DLR) — to infrastrukturalne rozszerzenie CLR (Common Language Runtime), które pozwala na implementację wsparcia dla języków dynamicznych (i dynamicznych zachowań w językach statycznych). DLR zajmuje się wykonywaniem operacji, których kompilator nie może sprawdzić: na przykład wywołanie metody po nazwie, jeśli typ będzie znany dopiero w czasie działania programu. Dzięki DLR C# dostaje „tryb dynamiczny”.
2. Słowo kluczowe dynamic: jak to działa
Różnica względem object
Niektórzy początkujący myślą, że dynamic to po prostu modniejszy sposób napisania object i ciągłego rzutowania. To nieprawda.
- Zmienne typu object zawsze wymagają jawnego rzutowania do konkretnego typu, żeby wywołać metody (i kompilator to sprawdza).
- Zmienne typu dynamic dają kompilatorowi polecenie: „uwierz mi, sam wiem, jaki tam będzie typ — sprawdzimy to w runtime!”. Jeśli metoda nie istnieje — będzie wyjątek w czasie wykonania.
Przykład:
// Przykład z object
object obj = "Hello, C#!";
// obj.ToUpper(); // Błąd kompilacji! Kompilator nie wie, że obj to string
string s = ((string)obj).ToUpper(); // Tylko tak
// Przykład z dynamic
dynamic dyn = "Hello, C#!";
string upper = dyn.ToUpper(); // Kompilator nie protestuje, weryfikacja będzie w runtime
Z dynamic można odwoływać się do nieistniejących właściwości i metod. Jeśli ich nie ma — zostanie rzucony wyjątek typu RuntimeBinderException.
Użycie w przykładzie aplikacji
Załóżmy, że w naszym mini-aplikacji jest moduł pracy z danymi JSON, których struktura może się zmieniać w locie. Z dynamic można odwoływać się do właściwości, jakby one zawsze istniały:
dynamic user = new System.Dynamic.ExpandoObject();
user.Name = "Wiktor";
user.Age = 35;
Console.WriteLine($"Imię: {user.Name}, wiek: {user.Age}");
3. DLR w akcji: co się dzieje „pod maską”
Gdy zmienna zadeklarowana jest jako dynamic, kompilator C# zapamiętuje, że wszelkie odwołania do metod, właściwości, indeksatorów, operatorów itp. dla tego obiektu nie da się sprawdzić z wyprzedzeniem.
Zamiast tego w miejscu każdego dynamicznego wywołania wstawiany jest kod odwołania do systemowego „wolnego dispatcher'a”. DLR obserwuje: czy faktycznie istnieje ta metoda lub właściwość, do której zwracamy się dość nonszalancko, z optymizmem? Jeśli tak — wykonuje wywołanie, jeśli nie — rzuca wyjątek.
Schemat wizualny
+------------------------+
| Kod z dynamic |
+----------+-------------+
|
v
+----------+-------------+
| Kompilator C# wstawia |
| "zapytanie do DLR" |
+----------+-------------+
|
v
+----------+-------------+
| W runtime DLR szuka |
| metody/właściwości |
+----------+-------------+
|
/---------\
| |
znaleziono nie znaleziono
| |
v v
wywołanie wyjątek
metody (RuntimeBinderException)
DLR dba o wyszukiwanie metod i właściwości w obiekcie — i nawet cache'uje często używane dynamiczne wywołania!
Trochę historii
DLR został stworzony dla wsparcia języków pierwotnie dynamicznych: IronPython, IronRuby, oraz dla ułatwienia integracji z dynamicznymi obiektami w C#. Dziś jest aktywnie używany przy pracy z obiektami typu ExpandoObject, DynamicObject, a także z COM i dynamicznie serializowanymi danymi.
4. Porównanie: static vs dynamic
Porównajmy, jak wyglądają wywołania właściwości i metod w paradygmacie statycznym i dynamicznym:
| Sposób | Deklaracja | Sprawdzenie podczas kompilacji | Zachowanie przy błędzie |
|---|---|---|---|
|
|
Wymaga castu do odpowiedniego typu | Błąd kompilacji lub InvalidCastException |
|
|
Nie, wszystko w runtime | RuntimeBinderException, jeśli metoda nie istnieje |
| typ statyczny | |
Sprawdzane przez kompilator | Błąd kompilacji |
Przykład dla przejrzystości:
// Statyczne typowanie
MyUser u = new MyUser();
u.PrintInfo(); // Sprawdzane przez kompilator
// dynamic
dynamic du = new MyUser();
du.PrintInfo(); // Kompilator nie sprawdza, znajdzie metodę dopiero w runtime
// object
object ou = new MyUser();
// ou.PrintInfo(); // Błąd kompilacji
((MyUser)ou).PrintInfo(); // Trzeba jawnie rzutować do typu
Metody statyczne i dynamic
dynamic d = "Hello";
Console.WriteLine(d.Length); // 5 — wszystko OK
Console.WriteLine(d.FakeMethod()); // RuntimeBinderException w czasie wykonania
Ale trzeba być ostrożnym! Jeśli się przegapisz albo nazwa metody nie istnieje — ujawni się to dopiero podczas działania aplikacji. Klasyczny przykład „błędu na produkcji”.
5. Praktyczne przykłady: dynamika w rzeczywistych zadaniach
Praca z ExpandoObject
using System.Dynamic;
dynamic person = new ExpandoObject();
person.Name = "Stephen";
person.Greet = (Action)(() => Console.WriteLine($"Cześć, {person.Name}!"));
person.Greet(); // Wypisze: Cześć, Stephen!
Tutaj w locie dodaliśmy właściwość Name i nawet delegat Greet. Bardzo podobne do pracy z obiektami w JavaScript.
Mapowanie do struktur JSON
using System.Dynamic;
using Newtonsoft.Json;
string json = @"{""name"": ""Andy"", ""age"": 30}";
dynamic obj = JsonConvert.DeserializeObject
(json); Console.WriteLine(obj.name); // Andy Console.WriteLine(obj.age); // 30
Zwróć uwagę: jeśli napiszesz obj.surname — będzie błąd już podczas wykonania.
COM-Interop (automatyzacja Office)
dynamic excel = Activator.CreateInstance(Type.GetTypeFromProgID("Excel.Application"));
excel.Visible = true; // „Widzimy” właściwość, chociaż kompilator nie zna jej typu
6. Przydatne niuanse
DLR, dynamic i użycie z innymi językami .NET
- IronPython/IronRuby: można instancjonować obiekty tych języków i wywoływać ich metody z C# przez dynamic.
- Obiekty COM: automatyczne opakowanie metod i właściwości przez dynamic.
- Własne dynamiczne obiekty: implementując interfejs IDynamicMetaObjectProvider, możesz zrobić własną klasę z dynamicznymi wywołaniami.
Przykład integracji z IronPython
using IronPython.Hosting;
using Microsoft.Scripting.Hosting;
ScriptEngine engine = Python.CreateEngine();
dynamic py = engine.Execute("class Test: pass\nTest()");
py.x = 42;
Console.WriteLine(py.x); // 42
Jak tworzy się własne typy dynamiczne
Zwykle łatwiej jest dziedziczyć po DynamicObject.
using System.Dynamic;
class MyDynamic : DynamicObject
{
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
if (binder.Name == "Ping")
{
result = "Pong!";
return true;
}
result = null;
return false;
}
}
dynamic d = new MyDynamic();
Console.WriteLine(d.Ping); // Pong!
Console.WriteLine(d.Miss); // null (nie rzuca wyjątku)
Kiedy dynamic jest szkodliwy
- Zmniejsza wydajność (wywołania dynamiczne są wolniejsze od statycznych).
- Utrudnia debugowanie i utrzymanie kodu.
- Odbiera większość „fajnych rzeczy” z IDE: autouzupełnianie, refaktoryzację, wyszukiwanie użyć.
- Może prowadzić do błędów wykrywanych dopiero u użytkownika lub na serwerze.
Kiedy warto naprawdę używać dynamic
- Integracja z zewnętrznymi bibliotekami, gdzie niemożliwe lub nieopłacalne jest pisanie wrapperów (COM, Python, JavaScript).
- Operacje na słabo typowanych strukturach (JSON/XML/ExpandoObject).
- Do pisania frameworków, metaprogramowania i elastycznych API, które mają być z założenia dynamiczne.
We wszystkich innych przypadkach lepiej używać typów statycznych i cieszyć się pomocą kompilatora :-)
7. Typowe błędy przy pracy z dynamic i DLR
Błąd nr 1: Odwołanie do nieistniejących członków.
Wywołanie nieistniejącej metody lub właściwości dla dynamic prowadzi do RuntimeBinderException w runtime, co jest trudne do wykrycia podczas rozwoju.
Błąd nr 2: Ignorowanie sprawdzeń typów.
Bez sprawdzenia typu przed wywołaniem (np. if (obj is IDictionary<string, object>)) możesz dostać niespodziewane błędy w runtime.
Błąd nr 3: Używanie dynamic w publicznych API.
Typy dynamiczne komplikują utrzymanie kodu, bo IDE i analizatory nie mogą zasugerować poprawnych członków, co obniża czytelność.
Błąd nr 4: Nadużywanie dynamic w wydajnym kodzie.
Wywołania dynamiczne są wolniejsze od statycznych i zwiększają narzut. Używaj dynamic tylko gdy to konieczne.
GO TO FULL VERSION