W notatce zmienne języka C# pojawiło się stwierdzenie, że język C# jest językiem z mocnym nadawaniem typów. Oznacza to, że wszystkie używane w nim zmienne i obiekty muszą mieć jawnie zadeklarowany typ danych. Istnieje jednak wyjątek w postaci typu dynamicznego reprezentowanego przez słowo kluczowe dynamic
. Typowanie dynamiczne jest traktowane w sposób uprzywilejowany przez kompilator, dzięki czemu wykonywane jest na poziomie wykonywania kodu, a nie jego kompilacji. Zmienne typu dynamic
, podobnie jak object
, mogą przechowywać referencje do wszystkiego za wyjątkiem wskaźników. Różnica polega na tym, że w przypadku dynamic
możemy zrobić znacznie więcej.
public static object DoSomething(object x, object y)
{
return x + y;
}
Próba kompilacji powyższej metody zakończy się błędem. Kompilator nie może sprawdzić, czy przekazane typy udostępniają operator dodawania. Jednak użycie słowa kluczowego dynamic
rozwiązuje ten problem, a właściwie odsuwa go w czasie. Sprawdzenie, czy przekazane typy udostępniają operator dodawania, nie jest wykonywane na poziomie kompilacji kodu, a podczas jego wykonywania. W sytuacji, gdy operator dodawania nie zostałby udostępniony przez przekazany typ, zgłoszony zostanie wyjątek RuntimeBinderException
. O potencjalnym problemie dowiemy się dopiero podczas działania aplikacji, co gorsza, problem może występować tylko dla części obiektów.
public static object DoSomething(dynamic x, dynamic y)
{
return x + y;
}
Dodatkowo, ponieważ kompilator nie jest w stanie przeanalizować typów na poziomie kompilacji, usługa IntelliSense również nie wspomoże nas podczas tworzenia kodu.
Głównym powodem, dla którego powstał typ dynamiczny wraz ze słowem kluczowym dynamic
, było współdziałanie z technologią COM. Technologia COM, powstała zanim opracowano .NET Framework, a jej celem było tworzenie kodu, który w systemie Windows mógł być wykorzystywany przez wiele języków programowania.
using Microsoft.Office.Interop.Excel;
using System.Reflection;
namespace HelloExcel
{
class Program
{
static void Main(string[] args)
{
var app = new Microsoft.Office.Interop.Excel.Application();
var workBook = app.Workbooks.Add(Missing.Value);
var worksheet = (Worksheet)workBook.Worksheets[1];
var cell = (Range)worksheet.Cells[1, "A"];
cell.Value = "123";
}
}
}
Przykład powyżej korzysta z API udostępnionego przez pakiet Office. Microsoft Office zapewnia sporo możliwości, niestety ma też kilka wad. Jedną z nich jest fakt, że właściwość może zwrócić różne typy, np. właściwość Worksheets może zwrócić obiekt Worksheet lub Chart. W związku z tym konieczne jest rzutowanie na oczekiwany typ. Sprawa wygląda podobnie w przypadku właściwości Cells. Innym problemem jest konieczność zapewnienia parametrów. Przykład poniżej korzysta z klasy Missing
, w celu zapewnienia informacji o braku opcjonalnego argumentu. Spróbujmy wywołać metodę Add właściwości Worksheets, w jej przypadku będziemy zmuszeni przekazać aż cztery argumenty typu Missing
.
Kod poniżej korzysta z typów dynamicznych, dzięki czemu możemy zrezygnować z rzutowania. Dodatkowo pozbyliśmy się użycia klasy Missing
. Możliwe jest również niejawne użycie typu dynamic
, poprzez przypisanie zmiennej udostępnionej np. przez Worksheets do zmiennej typu Worksheet
lub skorzystanie ze słowa kluczowego var
.
namespace HelloExcel
{
class Program
{
static void Main(string[] args)
{
var app = new Microsoft.Office.Interop.Excel.Application();
var workBook = app.Workbooks.Add();
dynamic worksheet = workBook.Worksheets[1];
dynamic cell = worksheet.Cells[1, "A"] = "123";
}
}
}
Klasa ExpandoObject
została zaprojektowana do współpracy z typem dynamic
. Jej cechą jest dynamiczne dodawanie właściwości. W sytuacji, gdy chcemy stworzyć zawartość dynamicznego obiektu, zastosowanie ExpandoObject
będzie znacząco prostszym rozwiązaniem, niżeli samodzielne tworzenie obiektu dynamicznego. W przykładzie poniżej, do obiektu person dodane zostaną 4 właściwości: ID, FirstName, Surname oraz DateOfBirth.
dynamic person = new ExpandoObject();
person.ID = "ATA880167";
person.FirstName = "Jan";
person.Surname = "Kowalski";
person.DateOfBirth = new DateTime(1988, 01, 10);
Console.WriteLine("{0} {1} ({2}), date of birth: {3} ",
person.Surname, person.FirstName, person.ID, person.DateOfBirth);
Projektanci, tworząc klasę ExpandoObject
wykorzystali interfejs słownika IDictionary<string, object>
, dzięki czemu możliwy jest dostęp do wartości poprzez indeksator. Prezentuje to kod poniżej.
IDictionary<string, object> dicPerson = person;
Console.WriteLine("{0} {1} ({2}), date of birth: {3} ",
dicPerson["Surname"], dicPerson["FirstName"], dicPerson["ID"], dicPerson["DateOfBirth"]);
W celu określenia aspektów dynamicznego zachowania typu możemy skorzystać z interfejsu IDynamicMetaObjectProvider
, implementującego metodę GetMetaObject, zwracającą typ DynamicMetaObject
. Implementacja klasy pochodnej od typu DynamicMetaObject
nie należy do łatwych. Znacznie łatwiejszym rozwiązaniem będzie stworzenie klasy dziedziczącej z DynamicObject
. Więcej informacji wraz z ciekawym przykładem znajdziesz tutaj.
Korzystając z typu dynamicznego, należy znać jego ograniczenia. Obsługa delegatów przez typ dynamiczny jest raczej znikoma i ogranicza się do prostych zastosowań. Spójrzmy na metodę PrintInt z przykładu poniżej. O ile możliwe jest przypisanie metody do zmiennej typu delegatu Action<int>
, a następnie do zmiennej typu dynamic
, to bezpośrednie przypisanie metody do zmiennej dynamicznej nie jest już możliwe. Istnieje kilka typów delegatów, których kompilator mógłby użyć, przez co bez dokładnego określenia nie ma zastosowania.
public static void PrintInt(int i)
{
Console.WriteLine(i);
}
static void Main(string[] args)
{
Action<int> a = PrintInt;
dynamic da = a;
da(123);
// Cannot convert method group 'PrintInt' to non-delegate type 'dynamic'.
dynamic dPrintInt = PrintInt;
}
Kolejnym ograniczeniem jest brak wsparcia podczas konwersji pomiędzy zgodnymi typami delegatów. Kod poniżej definiuje typ delegatu IntHandler, który jest zgodny z Action<int>
. Teoretycznie można by go użyć do stworzenia delegatu dla metody PrintInt. O ile kompilator nie protestuje podczas kompilacji, o tyle próba wykonania konwersji Action<int>
do IntHandler zakończy się błędem.
public delegate void IntHandler(int i);
public static void PrintInt(int i)
{
Console.WriteLine(i);
}
static void Main(string[] args)
{
Action<int> a = PrintInt;
dynamic da = a;
da(123);
IntHandler ih = da;
ih(321);
}
Należy również wiedzieć, że metody posiadające argument typu dynamic
są zgodne z dużo mniejszą liczbą delegatów, niż można by się spodziewać. Próba zapisania metody Print do zmiennej typu Action<int>
zakończy się błędem.
public static void Print(dynamic d)
{
Console.WriteLine(d);
}
static void Main(string[] args)
{
Action<int> a = Print;
}
Kolejnym problemem jest brak wsparcia dla metod rozszerzeń. Wyrażenia lambda bazują na wnioskowaniu typów, a to z kolei na typowaniu statycznym.
Podsumowując, język C# udostępnia typ dynamic
, który z punktu widzenia CLR jest niczym Object
. Kompilator jednak rozpoznaje ten typ, przez co generuje wyrażenia w nieco inny sposób, dzięki czemu część operacji zostaje odroczona do czasu wykonania. W rzeczywistości kompilator nie sprawdza, czy operacje można wykonać. Podstawowym zadaniem typu dynamic
jest zapewnienie wygodnego sposobu korzystania z technologii COM.