Ключевое слово dynamic

55

Ключевое слово var позволяет объявлять локальную переменную таким образом, что ее действительный тип данных определяется начальным присваиванием (это называется неявной типизацией). Как только начальное присваивание выполнено, вы получаете строго типизированную переменную, и любая попытка присвоить ей несовместимое значение приведет к ошибке компиляции.

Чтобы приступить к исследованию ключевого слова C# dynamic, создадим консольное приложение. После этого поместим в класс Program показанный ниже метод и удостоверимся, что финальный оператор кода действительно инициирует ошибку во время компиляции, если убрать с него символы комментария:

using System;
using System.Collections.Generic;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main()
        {
            var v = new List<int>();
            v.Add(90);

            // Следующий код недопустим!
            // v = "String";
        }
    }
}

Использование неявной типизации просто потому, что она возможна, считается плохим стилем (если известно, что нужен тип List<int>, то его и следует указывать). Однако, неявная типизация очень полезна в сочетании с LINQ, поскольку многие запросы LINQ возвращают перечисления анонимных классов (через проекции), которые объявить явно в коде C# не получится. Тем не менее, даже в этих случаях неявно типизированная переменная на самом деле является строго типизированной.

Как известно System.Object находится на вершине иерархии классов в .NET Framework и может представлять все, что угодно. После объявления переменной типа object получается строго типизированный элемент данных, однако то, на что он указывает в памяти, может отличаться в зависимости от присваивания ссылки. Для того чтобы получить доступ к членам объекта, на который установлена ссылка в памяти, необходимо выполнять явное приведение.

В версии .NET 4.0 язык C# стал поддерживать ключевое слово dynamic. На самом высоком уровне dynamic можно рассматривать как специализированную форму System.Object, в том смысле, что типу данных dynamic может быть присвоено любое значение. На первый взгляд, это порождает ужасную путаницу, поскольку теперь получается, что доступны три способа определения данных, внутренний тип которых явно не указан в коде:

using System;
using System.Collections.Generic;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main()
        {
            var a = "String";
            object b = "SomeString";
            dynamic c = "AllString";
            Console.WriteLine("a: "+a.GetType()+"\nb: "+b.GetType()+"\nc: "+c.GetType());

            Console.ReadLine();
        }
    }
}
Разное объявление неявно типизированных переменных

Динамическую переменную от переменной, объявленной неявно или через ссылку System.Object, значительно отличает то, что она не является строго типизированной. Другими словами, динамические данные не типизированы статически. Для компилятора C# это выглядит так, что элемент данных, объявленный с ключевым словом dynamic, может получить какое угодно начальное значение, и на протяжении времени его существования это значение может быть заменено новым (и возможно, не связанным с первоначальным).

Вызов членов на динамически объявленных данных

Теперь, учитывая, что тип данных dynamic может на лету принимать идентичность любого типа (как переменная типа System.Object), следующий вопрос, который наверняка возник, связан с вызовом членов на динамической переменной (свойств, методов, индексаторов, регистрации событий и т.п.). В отношении синтаксиса никаких отличий нет. Нужно просто применить операцию точки к динамической переменной, указать общедоступный член и передать ему необходимые аргументы.

Однако (и это очень важно) корректность указываемых членов компилятором не проверяется! Помните, что в отличие от переменной, объявленной как System.Object, динамические данные не являются статически типизированными. Вплоть до времени выполнения не известно, поддерживают ли вызываемые динамические данные указанный член, переданы ли корректные параметры, правильно ли указан член, и т.д. Поэтому, как бы странно это не выглядело, следующий метод cкомпилируется без ошибок:

static void DynamicData()
{
   dynamic textData = "Hello";
   Console.WriteLine(textData.ToUpper());
   // Здесь следовало ожидать ошибку компилятора, но все компилируется нормально.
   Console.WriteLine(textData.toupper());
   Console.WriteLine(textData.Foo(10, "ее", DateTime.Now));
}

Обратите внимание, что во втором вызове WriteLine() производится обращение к методу по имени toupper() на динамической переменной. Как видите, textData имеет тип string, и потому известно, что у этого типа нет метода с таким именем в нижнем регистре. Более того, тип string определенно не имеет метода по имени Foo(), который принимает int, string и DateTime!

Тем не менее, компилятор C# ни о каких ошибках не сообщает. Однако если вызвать этот метод в Main(), возникнет ошибка времени выполнения.

Другое значительное отличие между вызовом членов на динамических и строго типизированных данных состоит в том, что после применения операции точки к элементу динамических данных средство IntelliSense в Visual Studio 2010 не активизируется. Вместо этого отображается следующее общее сообщение:

Динамические данные не активизируют IntelliSence

То, что средство IntelliSense недоступно с динамическими данными, имеет смысл. Однако это означает, что при наборе кода C# с такими переменными следует соблюдать исключительную осторожность. Любая опечатка или некорректный регистр символов в имени члена приведет к ошибке времени выполнения, а именно — к генерации экземпляра класса RuntimeBinderException.

Сборка Microsoft.CSharp.dll

Сразу же после создания нового проекта C# в Visual Studio 2010 автоматически получается комплект ссылок на новую сборку .NET 4.0 по имени Microsoft.CSharp.dll (в этом легко убедиться, заглянув в папку References (Ссылки) в проводнике решений). Эта очень маленькая библиотека определяет единственное пространство имен (Microsoft.CSharp.RuntimeBinder) с двумя классами:

Сборка Microsoft.CSharp.dll

Как можно догадаться по их именам, оба класса представляют собой строго типизированные исключения. Более общий класс — RuntimeBinderException — представляет ошибку, которая будет сгенерирована при попытке вызова несуществующего члена на динамическом типе данных (как в случае методов toupper() и Foo()). Та же ошибка будет инициирована, если будут указаны неверные данные параметров для существующего члена.

Поскольку динамические данные столь изменчивы, каждый вызов члена на переменной, объявленной с ключевым словом dynamic, должен быть помещен в правильный блок try/catch, и предусмотрена соответствующая обработка ошибок.

Разумеется, процесс помещения всех динамических вызовов методов в блоки try/catch довольно утомителен. Если вы тщательно следите за написанием кода и передачей параметров, то это делать не обязательно. Однако перехват исключений удобен, когда заранее не известно, будет ли член представлен в целевом типе.

Лучший чат для C# программистов