1. Вступ
C# — представник строго типізованих мов. Це означає, що на етапі компіляції кожна змінна має визначений тип, а звертатися до членів класу можна лише згідно з цим типом. Це безпечно, зручно та зрозуміло для IDE й компілятора.
Але інколи доводиться звертатися до об’єктів, структура яких на етапі компіляції може бути невідомою — наприклад, коли:
- виконується код, написаний іншою .NET‑мовою (наприклад, IronPython або IronRuby), де типізація динамічна;
- потрібна робота з COM‑об’єктами (наприклад, з Microsoft Office через Interop);
- йдеться про взаємодію з динамічними структурами даних, зокрема зі слабоформалізованими JSON‑об’єктами;
- не хочеться писати купу шаблонного коду, простіше «повірити» об’єктові на слово й спробувати викликати у нього потрібний метод за ім’ям.
Із виходом C# 4.0 та появою ключового слова dynamic і платформи DLR з’явилася можливість писати код, який «працює наче в Python, але в C#»: перевірки відбуваються під час виконання, а не під час компіляції. Це «чарівне» слово змушує компілятор бути менш суворим: чи існує потрібний метод або властивість, з’ясовується лише тоді, коли програма реально дійде до цього місця.
Що таке DLR?
Dynamic Language Runtime (DLR) — це інфраструктурне розширення до CLR (Common Language Runtime), яке дає змогу реалізувати підтримку динамічних мов (і динамічної поведінки у статичних мовах). DLR бере на себе виконання операцій, які компілятор не може перевірити наперед: наприклад, виклик методу за ім’ям, якщо тип стане відомий лише під час роботи програми. Завдяки DLR C# отримує «динамічний режим».
2. Ключове слово dynamic: як воно працює
Відмінність від object
Дехто з новачків думає, що dynamic — це просто «сучасніший» запис object з подальшим приведенням під кожен виклик. Це не так.
- Змінна типу object завжди потребує явного перетворення до конкретного типу для виклику методів (і компілятор це перевіряє).
- Змінна типу dynamic дає компіляторові сигнал: «повір мені, я сам знаю, який там буде тип — розберемося на місці!». Якщо методу не існує — отримаєте виняток під час виконання.
Приклад:
// Приклад з object
object obj = "Привіт, C#!";
// obj.ToUpper(); // Помилка компіляції! Компілятор не знає, що obj — рядок
string s = ((string)obj).ToUpper(); // Тільки так
// Приклад з dynamic
dynamic dyn = "Привіт, C#!";
string upper = dyn.ToUpper(); // Компілятор не заперечує, перевірка буде в рантаймі
Із dynamic можна звертатися до неіснуючих властивостей і методів. Якщо їх немає — буде виняток типу RuntimeBinderException.
Використання в прикладі застосунку
Припустімо, у нашому міні‑застосунку є блок роботи з JSON‑даними, структура яких може змінюватися на льоту. Із dynamic можна звертатися до властивостей так, ніби вони завжди існують:
dynamic user = new System.Dynamic.ExpandoObject();
user.Name = "Вася";
user.Age = 35;
Console.WriteLine($"Ім'я: {user.Name}, вік: {user.Age}");
3. DLR у дії: що відбувається під капотом
Коли змінну оголошують як dynamic, компілятор C# запам’ятовує, що будь‑які звернення до методів, властивостей, індексаторів, операторів тощо над цим об’єктом не можна перевірити наперед.
Натомість у місці кожного динамічного виклику вставляється код звернення до системного «універсального диспетчера». DLR перевіряє, чи існує насправді той метод або властивість, до яких ми так легко й оптимістично звертаємося. Якщо так — виконує виклик, якщо ні — кидає виняток.
Візуальна схема
+------------------------+
| Код із dynamic |
+----------+-------------+
|
v
+----------+-------------+
| Компілятор C# вставляє |
| "запит до DLR" |
+----------+-------------+
|
v
+----------+-------------+
| У рантаймі DLR шукає |
| метод/властивість |
+----------+-------------+
|
/---------\
| |
знайдено не знайдено
| |
v v
виклик виняток
методу (RuntimeBinderException)
DLR дбає про пошук методів і властивостей в об’єкті — і навіть кешує часто вживані динамічні виклики!
Трішки історії
DLR створювали для підтримки мов, які від початку динамічні: IronPython, IronRuby, а також для полегшення інтеграції з динамічними об’єктами в C#. Сьогодні це активно використовують під час роботи з об’єктами типу ExpandoObject, DynamicObject, а також із COM і динамічно серіалізованими даними.
4. Порівняння: static vs dynamic
Порівняймо, як виглядають виклики властивостей і методів у статичній та динамічній парадигмах:
| Спосіб | Оголошення | Перевірка під час компіляції | Поведінка при помилці |
|---|---|---|---|
|
|
Потребує явного перетворення до потрібного типу | Помилка компіляції або InvalidCastException |
|
|
Ні, усе під час виконання | RuntimeBinderException, якщо методу немає |
| статичний тип | |
Перевіряється компілятором | Помилка компіляції |
Приклад для наочності:
// Статична типізація
MyUser u = new MyUser();
u.PrintInfo(); // Перевіряється компілятором
// dynamic
dynamic du = new MyUser();
du.PrintInfo(); // Компілятор не перевіряє, знайде метод лише в рантаймі
// object
object ou = new MyUser();
// ou.PrintInfo(); // Помилка компіляції
((MyUser)ou).PrintInfo(); // Потрібно явно привести до типу
Статичні методи і dynamic
dynamic d = "Hello";
Console.WriteLine(d.Length); // 5 — усе добре
Console.WriteLine(d.FakeMethod()); // RuntimeBinderException під час виконання
Але варто бути обережними! Якщо ви помилитеся в імені або методу не існує — це проявиться лише під час роботи застосунку. Класичний приклад «помилки у продакшні».
5. Практичні приклади: динаміка в реальних задачах
Робота з ExpandoObject
using System.Dynamic;
dynamic person = new ExpandoObject();
person.Name = "Сергій";
person.Greet = (Action)(() => Console.WriteLine($"Привіт, {person.Name}!"));
person.Greet(); // Виведе: Привіт, Сергій!
Тут ми на льоту додали властивість Name і навіть делегат Greet. Дуже схоже на роботу з об’єктами в JavaScript.
Прив’язка до структур JSON
using System.Dynamic;
using Newtonsoft.Json;
string json = @"{""name"": ""Андрій"", ""age"": 30}";
dynamic obj = JsonConvert.DeserializeObject
(json); Console.WriteLine(obj.name); // Андрій Console.WriteLine(obj.age); // 30
Зверніть увагу: якщо написати obj.surname, помилку отримаєте вже під час виконання.
COM-Interop (Office Automation)
dynamic excel = Activator.CreateInstance(Type.GetTypeFromProgID("Excel.Application"));
excel.Visible = true; // “Бачимо” властивість, хоча компілятор не знає її типу
6. Корисні нюанси
DLR, dynamic і використання з іншими .NET‑мовами
- IronPython/IronRuby: можна створювати об’єкти цих мов і викликати їхні методи з C# через dynamic.
- COM‑об’єкти: автоматичне обгортання методів і властивостей через dynamic.
- Власні динамічні об’єкти: реалізувавши інтерфейс IDynamicMetaObjectProvider, можна зробити свій клас із динамічними викликами.
Приклад інтеграції з 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
Як реалізувати свої динамічні типи
Зазвичай простіше успадкуватися від 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 (не викидає виняток)
Коли dynamic — шкідливо
- Знижує продуктивність (динамічні виклики повільніші за статичні).
- Ускладнює налагодження та підтримку.
- Позбавляє більшості можливостей IDE: автодоповнення, рефакторингу, пошуку за використаннями.
- Може призвести до помилок, які виявляються лише в користувача або на сервері.
Коли справді варто використовувати dynamic
- Інтеграція зі сторонніми бібліотеками, де неможливо або недоцільно писати обгортки (COM, Python, JavaScript).
- Маніпулювання слабо типізованими структурами (JSON/XML/ExpandoObject).
- Для написання фреймворків, метапрограмування та гнучких API, які за задумом мають бути динамічними.
В усіх інших випадках краще використовувати статичні типи й радіти допомозі компілятора :-)
7. Типові помилки під час роботи з dynamic і DLR
Помилка № 1: Звернення до неіснуючих членів.
Виклик неіснуючого методу або властивості для dynamic призводить до RuntimeBinderException під час виконання, що складно спіймати на етапі розробки.
Помилка № 2: Ігнорування перевірки типів.
Без перевірки типів перед викликом (наприклад, if (obj is IDictionary<string, object>)) можна отримати неочікувані помилки під час виконання.
Помилка № 3: Використання dynamic у публічних API.
Динамічні типи ускладнюють підтримку коду, адже IDE й аналізатори не можуть підказати доступні члени, що знижує читабельність.
Помилка № 4: Зловживання dynamic у продуктивному коді.
Динамічні виклики повільніші за статичні та збільшують накладні витрати. Використовуйте dynamic лише за потреби.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ