Основы компиляции и выполнения кода на лету в C#

.NET так же как и Java поддерживает возможность выполнения программного кода из обычной текстовой строки непосредственно во время работы программы (на лету).

Однако если для Java лучше всего использовать сторонние библиотеки (ту же BeanShell), то в .NET весь необходимый инструментарий присутствует уже изначально.

Рассмотрим его использование на примере C#.

Краткое знакомство с пространством имён System.CodeDom.Compiler и компиляция временной сборки

Средства для работы с компилятором расположены в пространстве имён System.CodeDom.Compiler. Основным из них является семейство классов CodeDomProvider, которое обеспечивает доступ к компиляции для поддерживаемых языков программирования ( в настоящее время поддерживаются C#, VB.NET, JScript).

Для получения доступа к компиляции C# необходимо вызвать метод CreateProvider указав соответствующий строковый параметр.

Далее необходимо задать параметры компиляции с помощью класса CompilerParameters.

Приведённые в коде свойства имеют следующий смысл:

  • CompilerOptions – параметры командной строки компилятора;
  • GenerateInMemory – создавать ли файл сборки в «памяти» (true) или в виде «обычного файла» (false);
  • IncludeDebugInformation – включать ли в сборку отладочную информацию.

Приведённая комбинация значений этих параметров означает следующее.

В результате компиляции будут создана сборка в виде dll библиотеки, которая будет расположена в «памяти» и при этом не будет включать отладочной информации.

Слова «память» и «обычный файл» взяты в кавычки не случайно.

Дело в том, что при компиляции в обоих случаях формируется физический файл сборки. Только когда сборка создаётся в «памяти» её файл не сохраняется на постоянной основе, а удаляется, когда в нём отпадает надобность. Поэтому значение true наиболее оптимально при выполнении кода на лету (автоматическое освобождение места на диске).

Доступ к скомпилированной сборке осуществляется при помощи класса CompileResults. Этот класс предоставляет разработчику достаточно широкий арсенал средств.

В частности, при помощи его свойства PathToAssembly можно получить или задать путь к откомпилированной сборке. По умолчанию, сборка компилируется во временную папку учётной записи пользователя. Но, изменение этого пути имеет смысл лишь, если сборка компилируется в «обычный файл».

Гораздо больший интерес представляет сама компиляция, а также работа с классами и методами готовой сборки.

Процесс компиляции осуществляется достаточно просто при помощи метода CompileAssemblyFromSource соответствующего класса семейства CodeDomProvider.

Первый параметр метода – параметры компиляции. Второй параметр – строка с исходным кодом сборки.

Данная строка должна представлять собой синтаксически правильный текст программы (в данном случае на C#).

В ней должны обязательно присутствовать:

  • Пространство имён;
  • Классы со своими членами;
  • Подключения других пространств имён (директивы using) при необходимости.

Здесь проявляется одно из ключевых отличий от того как работает выполнение кода на лету в Java.

В C# строка исходного кода включает только статичное описание программы. Запуск на выполнение кода непосредственно в данной строке не допускается. Для этого в .NET предусмотрен иной подход, который будет подробно описан ниже.

Если строка исходного кода содержит синтаксические ошибки, компиляция также завершится  с ошибками.

Сообщения об ошибках компиляции доступны при помощи свойства Errors класса CompileResults. При необходимости их можно вывести, например, на консоль или в текстовый файл.

Выполнение кода из скомпилированной сборки

Механизм выполнения кода из скомпилированной сборки абсолютно идентичен для обоих типов сборок (в «памяти» и «обычных файлов»).

Для того чтобы вызвать метод класса из сборки необходимо вначале создать его экземпляр.

Затем при помощи метода Invoke вызвать сам метод. При этом экземпляр класса передаётся в качестве первого параметра этого метода.

Если вызываемый метод статический, то создавать экземпляр класса не требуется. В этом случае, в качестве первого парметра метода Invoke можно передать null.

Второй параметр метода Invoke служит для передачи значений параметров в метод, вызываемый из сборки. При этом параметры передаются в виде массива object[] и интерпретируются последовательно (первый элемент массива – первый параметр метода и т.д.)

Модифицируем метод TestMethod, так  чтобы он принимал два параметра и выводил их на экран.

Массив параметров пусть будет иметь вид.

Соответственно вызов метода TestMethod и передача параметров.

В результате выполнения программы на экран консоли буду последовательно выведены текст «Hello, World!» и оба переданных параметра.

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

Однако метод Invoke всегда возвращает эти значения в формате object. Поэтому приведение типов обязательно.

Переделаем класс TestClass. Пусть у него теперь будут два метода. Один возвращает строковое значение другой целочисленное.

Теперь получим значения этих методов и выведем результат на консоль.

Вывод показан на скриншоте ниже:

Метод, возвращающий значение не единственный способ получения данных. Также поддерживаются выходные параметры.

Значения выходные параметров выводятся в массив, который служит вторым аргументом метода Invoke. Интерпретация элементов массива в данном случае аналогична передаче входных параметров (первый элемент массива – первый выходной параметр и т.д.)

В качестве примера рассмотрим следующий метод:

Выполним его и выведем на консоль значения параметров.

В окне консоли отображаются значения обоих выходных параметров.

Резюме

.NET предоставляет разработчику весьма широкий спектр возможностей в плане работы с компилятором непосредственно из программы. В данной статье рассмотрена лишь малая их часть. При этом все необходимые средства доступны уже изначально в составе непосредственно .NET Framework.

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

В тоже время у рассмотренных в статье инструментов есть и слабые места.

  • Компиляция сборки весьма ресурсоёмкая операция. При больших объёмах обрабатываемого кода программа серьёзно потеряет в производительности;
  • Так как всегда создаётся файл сборки, необходимо наличие прав на запись в выходную папку;
  • Параметры передаются в методы в виде массива без явного указания их назначения, что, несмотря на однозначную их интерпретацию, всё же чревато возникновением ошибок.

Однако при внимательном использовании данные недостатки в значительной степени нивелируются, и средства .NET для работы с компилятором могут стать надёжными помощниками в расширении возможностей разрабатываемых программ.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *