Speak.Me Учить иностранные слова

C#: динамическое связывание (Dynamic Binding)

Связывание — это процесс разрешения, или соотнесения идентификаторов типов (членов типа, операций)  с логическими частями программы, представляющими соответствующие типы. Как правило это процесс происходит во время компиляции, когда компилятор находит в исходном коде идентификаторы типов и соотносит их с соответствующей логикой.

Динамическое связывание позволяет отложить этот процесс с момента компиляции до момента выполнения. Динамическое связывание может быть полезно, когда на момент компиляции компилятор не может знать о существовании какого-либо типа (члена типа, функции), но мы можем быть точно уверены, что на момент выполнения программы соответствующий тип будет существовать. Такая ситуация может возникнуть при взаимодействии, например, с динамическими языками, COM или в сценариях использующих отражение (reflection).

Динамический тип объявляется с использованием ключевого слова dynamic:

Динамический тип дает указание компилятору не выполнять связывания. Мы предполагаем, что динамический тип d будет иметь метод Quack, но на момент компиляции мы не можем этого гарантировать. Так как d — динамический тип, компилятор отложит связывание для него и его метода Quack до момента выполнения.

Статическое связывание (Static Binding) и динамическое связывание (Dynamic Binding)

Канонический пример связывания — сопоставление имени с конкретной функцией при компиляции выражения. Чтобы скомпилировать следующее выражение, компиляторы необходимо найти реализацию метода с именем Quack:

Предположим, что d имеет статический тип Duck:

В простейшем случае компилятор выполнит связывание найдя не принимающий параметров метод с именем Quack в классе Duck. Если такой метод найти не удастся, компилятор расширит поиск до методов принимающих необязательные параметры, методов базового класса для класса Duck и методов расширений, принимающих Duck в качестве первого параметра. Если совпадений не будет найдено, произойдет ошибка компиляции. Независимо от того, какой метод будет привязан, суть состоит в том, что связывание будет завершено компилятором. При этом связывание полностью зависит от того, известны ли на момент компиляции все необходимые типы. Такое связывание называется статическим.

Теперь заменим статический тип d на object:

Вызов метода Quack вызовет ошибку при компиляции, т.к. хотя объект сохраненный в d может и содержать метод Quack, компилятор не может об этом знать, поскольку он располагает только информацией, основанной на типе переменной, а в данном случае это object.

Теперь заменим статический тип d на dynamic:

Тип dynamic схож с типом object — он также не несет никакой информации о реальном типе объекта. Разница состоит в том, что его можно использовать теми способами, о которых во время компиляции еще ничего не известно. Динамический объект связывается во время выполнения и основывается он на типе, который приобретает во время выполнения, а не на том, который ему задан при компиляции. Когда компилятор встречает динамически связанное выражение (любое выражение, содержащее значение с типом dynamic), он просто упаковывает выражение таким образом, чтобы связывание можно было осуществить позже, во время выполнения.

Во время выполнения если динамический объект реализует интерфейс IDynamicMetaObjectProvider, этот интерфейс используется для осуществления связывания. В противном случае связывание осуществляется так, как если бы реальный тип динамического объекта уже был бы известен компилятору. Эти два альтернативных способа называются пользовательское связывание (custom binding) и языковое связывание (language
binding).

Пользовательское связывание (Custom Binding)

Пользовательское связывание происходит, когда динамически объект реализует интерфейс IDynamicMetaObjectProvider (IDMOP). Типы, написанные на C#, могут реализовывать интерфейс IDMOP и это может быть полезно, но как правило объекты реализующие данный интерфейс импортируются из динамических языков, реализованных в .NET на Dynamic Language Runtime (DLR), например IronPython или IronRuby. Объекты из этих языков автоматически реализуют интерфейс IDMOP и поэтому автоматически контролируют все операции выполняемые с ними.

Класс Duck не содержит метода Quack, вместо этого он использует пользовательское связывание, чтобы перехватывать и интерпретировать вызов всех методов.

Языковое связывание (Language Binding)

Языковое связывание происходит, когда динамический объект не реализует интерфейс IDynamicMetaObjectProvider. Языковое связывание может быть полезно при работе с не полностью разработанными типами или неотъемлемыми ограничениями системы типов .NET. Например, при работе с числовыми типами всегда возникает проблема с тем, что они не имеют общего интерфейса. Поэтому методы и операторы, взаимодействующие с числовыми типами могут использовать динамическое связывание, чтобы обойти эту проблему:

Выгода очевидна — нет необходимости дублировать код для каждого числового типа. Однако такой подход менее безопасен, чем статическое связывание: ошибки связывания будут возникать не во время компиляции, а при выполнении программы. Однако динамическое связывание более безопасно чем отражение (reflection), т.к. не отменяет правила доступности членов.

Динамическое связывание отрицательно влияет на производительность. Правда DLR имеет встроенный механизм кэширования повторных вызовов, позволяя оптимизировать использование динамических выражений в циклах.

RuntimeBinderException

Если динамическое связывание выполнить не удается, выбрасывается исключение RuntimeBinderException:

dynamic и object

Типы dynamic и object практически идентичны. Например, следующее выражение при выполнении вернет true:

Этот принцип распространяется на составные типы и массивы:

Как и ссылка типа object, ссылка типа dynamic может ссылаться на объекты любого типа:

Структурно нет никаких различий между ссылкой типа object и ссылкой типа dynamic. Динамическая ссылка просто позволяет выполнять динамические операции над объектом, на который она ссылается. Можно преобразовывать object в dynamic для выполнения любых динамических операций над object:

Преобразование dynamic

Тип dynamic может быть автоматически преобразован в и из любого другого типа. Чтобы преобразование увенчалось успехом, необходимо чтобы тип, который будет привязан динамическому объекту при выполнении, мог автоматически преобразовывать в/из целевого типа (тип, в/из которого осуществляется преобразование):

var и dynamic

var и тип dynamic на первый взгляд очень схожи, но на самом деле они несут разный смысл: var указывает компилятору на необходимость вычислить тип самостоятельно (при компиляции), а dynamic предписывает вычислить тип во время выполнения.

Динамические выражения

Поля, свойства, методы, события, конструкторы, индексаторы, операторы и преобразования — все могут быть вызваны динамически. Динамические выражения также как статические должны возвращать результат. Если попытать вместо результата вернуть void, будет зафиксирована ошибка, но не при компиляции как для статических выражений, а во время выполнения.

Выражения, принимающие динамические операнды, тоже являются динамическими:

Но из этого правила есть два очевидных исключения: во-первых, приведение динамического выражения к статическому типу вернет статическое выражение; во-вторых, вызов конструктора всегда вернет статическое выражение, даже если он вызван со статическим аргументом. Кроме того, есть еще несколько случаев, когда выражение, содержащее динамический аргумент, будет статическим (например, передача индекса в массив, создание делегата).

Идентификация перегрузки динамических членов

Канонически случай использования типа dynamic — применение динамического получателя (receiver): когда динамический объект используется как получатель для вызова динамической функции:

Однако это не единственный вариант применения динамического связывания: аргументы методов также могут связываться динамически. Эффект от вызова функции с динамическим аргументом — возможность перенести выбор варианта перегруженного метода с момента компиляции до выполнения программы:

Выбор варианта перегруженного метода во время выполнения программы также называется множественной отсылкой (multiple dispatch) и используется в реализации паттерна гость (visitor).

Если не используется динамический получатель, компилятор может статически проверить, увенчается ли динамический вызов успехом или нет: он проверяет что функция с указанным названием и числом аргументов существует. Если подходящей функции найдено не будет, возникнет ошибка компиляции.

Если функция вызывается  с несколькими аргументами, одни из которых статические, а другие динамические, то будет использовано смешанное связывание: для статических методов — статическое (во время компиляции), для динамических — динамическое (во время выполнения).

Невызываемые функции

Некоторые функции не могут быть вызваны динамически:

  • методы расширения (можно вызвать как статические методы)
  • члены интерфейса
  • базовые члены, скрытые производным классом

Причина этого ограничения в том, что для динамического связывания необходима информация, во-первых, об имени вызываемой функции, и, во-вторых, об объекте, через который эта функция вызывается. Однако во всех трех невызываемых сценариях задействован дополнительный тип, известный только во время компиляции. Определить этот тип динамически возможности нет.

Для методов расширений этим дополнительным типом является класс в котором определены методы расширения. Этот класс указывается в директиве using в исходном коде, автоматически определяется компилятором и после компиляции исчезает). При вызове членов через интерфейс, дополнительный тип связан с приведение типов. Схожая ситуация с вызовом скрытых членов базового класса: дополнительный тип возникает в результате приведения типов или использования ключевого слова base, а во время выполнения программы этот тип недоступен.