.NET так же как и Java поддерживает возможность выполнения программного кода из обычной текстовой строки непосредственно во время работы программы (на лету).
Однако если для Java лучше всего использовать сторонние библиотеки (ту же BeanShell), то в .NET весь необходимый инструментарий присутствует уже изначально.
Рассмотрим его использование на примере C#.
Краткое знакомство с пространством имён System.CodeDom.Compiler и компиляция временной сборки
Средства для работы с компилятором расположены в пространстве имён System.CodeDom.Compiler. Основным из них является семейство классов CodeDomProvider, которое обеспечивает доступ к компиляции для поддерживаемых языков программирования ( в настоящее время поддерживаются C#, VB.NET, JScript).
Для получения доступа к компиляции C# необходимо вызвать метод CreateProvider указав соответствующий строковый параметр.
1 |
var compiler = CodeDomProvider.CreateProvider("CSharp"); |
Далее необходимо задать параметры компиляции с помощью класса CompilerParameters.
1 2 3 4 5 6 |
var parameters = new CompilerParameters { CompilerOptions = "/t:library", GenerateInMemory = true, IncludeDebugInformation = false }; |
Приведённые в коде свойства имеют следующий смысл:
- CompilerOptions – параметры командной строки компилятора;
- GenerateInMemory – создавать ли файл сборки в «памяти» (true) или в виде «обычного файла» (false);
- IncludeDebugInformation – включать ли в сборку отладочную информацию.
Приведённая комбинация значений этих параметров означает следующее.
В результате компиляции будут создана сборка в виде dll библиотеки, которая будет расположена в «памяти» и при этом не будет включать отладочной информации.
Слова «память» и «обычный файл» взяты в кавычки не случайно.
Дело в том, что при компиляции в обоих случаях формируется физический файл сборки. Только когда сборка создаётся в «памяти» её файл не сохраняется на постоянной основе, а удаляется, когда в нём отпадает надобность. Поэтому значение true наиболее оптимально при выполнении кода на лету (автоматическое освобождение места на диске).
Доступ к скомпилированной сборке осуществляется при помощи класса CompileResults. Этот класс предоставляет разработчику достаточно широкий арсенал средств.
В частности, при помощи его свойства PathToAssembly можно получить или задать путь к откомпилированной сборке. По умолчанию, сборка компилируется во временную папку учётной записи пользователя. Но, изменение этого пути имеет смысл лишь, если сборка компилируется в «обычный файл».
Гораздо больший интерес представляет сама компиляция, а также работа с классами и методами готовой сборки.
Процесс компиляции осуществляется достаточно просто при помощи метода CompileAssemblyFromSource соответствующего класса семейства CodeDomProvider.
1 |
CompilerResults results=compiler.CompileAssemblyFromSource(parameters,_sourceCode); |
Первый параметр метода – параметры компиляции. Второй параметр – строка с исходным кодом сборки.
Данная строка должна представлять собой синтаксически правильный текст программы (в данном случае на C#).
1 2 3 4 5 6 7 8 9 10 11 12 |
private static string _sourceCode = @" using System; namespace Test { public class TestClass { public void TestMethod() { Console.Write("Hello, World!"); } } }"; |
В ней должны обязательно присутствовать:
- Пространство имён;
- Классы со своими членами;
- Подключения других пространств имён (директивы using) при необходимости.
Здесь проявляется одно из ключевых отличий от того как работает выполнение кода на лету в Java.
В C# строка исходного кода включает только статичное описание программы. Запуск на выполнение кода непосредственно в данной строке не допускается. Для этого в .NET предусмотрен иной подход, который будет подробно описан ниже.
Если строка исходного кода содержит синтаксические ошибки, компиляция также завершится с ошибками.
Сообщения об ошибках компиляции доступны при помощи свойства Errors класса CompileResults. При необходимости их можно вывести, например, на консоль или в текстовый файл.
1 2 3 4 5 6 7 |
if (results.Errors.Capacity>0) { foreach (var r in results.Errors) { Console.WriteLine(r.ToString()); } } |
Выполнение кода из скомпилированной сборки
Механизм выполнения кода из скомпилированной сборки абсолютно идентичен для обоих типов сборок (в «памяти» и «обычных файлов»).
Для того чтобы вызвать метод класса из сборки необходимо вначале создать его экземпляр.
1 |
var instance = results.CompiledAssembly.CreateInstance("Test.TestClass"); |
Затем при помощи метода Invoke вызвать сам метод. При этом экземпляр класса передаётся в качестве первого параметра этого метода.
1 |
results.CompiledAssembly.GetType("Test.TestClass").GetMethod("TestMethod").Invoke(instancel, null); |
Если вызываемый метод статический, то создавать экземпляр класса не требуется. В этом случае, в качестве первого парметра метода Invoke можно передать null.
1 |
results.CompiledAssembly.GetType("Test.TestClass").GetMethod("StaticTestMethod").Invoke(null, null); |
Модифицируем метод TestMethod, так чтобы он принимал два параметра и выводил их на экран.
1 2 3 4 |
public void TestMethod(string msg, string msg2) { Console.Write("Hello, World!";+msg+msg2); } |
Массив параметров пусть будет иметь вид.
1 |
string[] arrParams = {"Test message.","Second test message."}; |
Соответственно вызов метода TestMethod и передача параметров.
1 |
results.CompiledAssembly.GetType("Test.TestClass").GetMethod("TestMethod").Invoke(instance,arrParams); |
В результате выполнения программы на экран консоли буду последовательно выведены текст «Hello, World!» и оба переданных параметра.
Если необходимо получить результаты вычислений, которые выполнялись в сборке, то для этого достаточно изменить методы класса в строке исходного кода таким образом, чтобы они возвращали значение.
Однако метод Invoke всегда возвращает эти значения в формате object. Поэтому приведение типов обязательно.
Переделаем класс TestClass. Пусть у него теперь будут два метода. Один возвращает строковое значение другой целочисленное.
1 2 3 4 5 6 7 8 9 10 11 |
public class TestClass { public string StringTestMethod(string msg, string msg2) { return ";Hello, World!"+msg+msg2; } public int IntTestMethod() { return 1+2; } } |
Теперь получим значения этих методов и выведем результат на консоль.
1 2 3 4 5 |
string[] arrParams = {"Test message.","Second test message.";}; string resultMsg=results.CompiledAssembly.GetType("Test.TestClass").GetMethod("StringTestMethod").Invoke(instance,arrParams).ToString(); int a = (int)results.CompiledAssembly.GetType("Test.TestClass").GetMethod("IntTestMethod").Invoke(instance, null); Console.WriteLine(resultMsg); Console.WriteLine(a); |
Вывод показан на скриншоте ниже:
Метод, возвращающий значение не единственный способ получения данных. Также поддерживаются выходные параметры.
Значения выходные параметров выводятся в массив, который служит вторым аргументом метода Invoke. Интерпретация элементов массива в данном случае аналогична передаче входных параметров (первый элемент массива – первый выходной параметр и т.д.)
В качестве примера рассмотрим следующий метод:
1 2 3 4 5 |
public void OutParamTestMethod(out string s1,out string s2) { s1="Строка для выходного параметра 1"; s2="Строка для выходного параметра 2"; } |
Выполним его и выведем на консоль значения параметров.
1 2 3 4 |
string[] outParams = new string[2]; results.CompiledAssembly.GetType("Test.TestClass").GetMethod("OutParamTestMethod").Invoke(instance, outParams); Console.WriteLine(outParams[0]); Console.WriteLine(outParams[1]); |
В окне консоли отображаются значения обоих выходных параметров.
Резюме
.NET предоставляет разработчику весьма широкий спектр возможностей в плане работы с компилятором непосредственно из программы. В данной статье рассмотрена лишь малая их часть. При этом все необходимые средства доступны уже изначально в составе непосредственно .NET Framework.
Также стоит отметить, что, несмотря на громоздкий синтаксис, код, выполняемый в скомпилированной сборке, достаточно легко и тесно интегрируется с основной программой. В частности, при получении результатов вычислений можно уйти от работы с параметрами (или переменными, как в случае BeanShell) и получить нужное значение напрямую из соответствующего метода.
В тоже время у рассмотренных в статье инструментов есть и слабые места.
- Компиляция сборки весьма ресурсоёмкая операция. При больших объёмах обрабатываемого кода программа серьёзно потеряет в производительности;
- Так как всегда создаётся файл сборки, необходимо наличие прав на запись в выходную папку;
- Параметры передаются в методы в виде массива без явного указания их назначения, что, несмотря на однозначную их интерпретацию, всё же чревато возникновением ошибок.
Однако при внимательном использовании данные недостатки в значительной степени нивелируются, и средства .NET для работы с компилятором могут стать надёжными помощниками в расширении возможностей разрабатываемых программ.
Добавить комментарий