Введение в генерацию программного кода

Техники генерации кода

Показывать лекцию целиком

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

Процедуры вывода кода в файл

В примерах будут использоваться процедуры вывода результатов генерации в файл. Давайте рассмотрим эти процедуры. Для промежуточного представления текста генерируемой программы в памяти будем использовать набор строк в виде переменной типа List<string>. Создадим класс Output для вывода этого набора строк в файл. В ней содержатся две перекрытые процедуры, одна для вывода строки, а вторая для вывода списка строк. Данный класс мы также будем использовать во многих лекциях курса.

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

public static class Output
{
    public static void PutResult(string text, string filepath)
    {
        if (File.Exists(filepath))
        {
            File.Delete(filepath);
        }
        using (StreamWriter sw = File.CreateText(filepath))
        {
            sw.WriteLine(text);
        }
    }

    public static void PutResult(List<string> strings, String filepath)
    {
        if (File.Exists(filepath))
        {
            File.Delete(filepath);
        }
        using (StreamWriter sw = File.CreateText(filepath))
        {
            foreach (string str in strings)
            {
                sw.WriteLine(str);
            }
        }
    }
}
    

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

Простой пример

Рассмотрим пример, где необходимо сгенерировать код объявления трех констант. В массиве var хранятся имена констант. В массиве val содержатся значения этих констант.

string[] var = new string[3]{"A","B","C"};
string[] val = new string[3]{"1","2","3"};
    

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

for (int i = 0; i < 3; i++)
{
    Console.WriteLine("const int " + var[i] + " = " + val[i] + ";");
}
    

Как видим, метаданные здесь заданы в массивах var и val, а шаблон - внутри вызова процедуры WriteLine. В результате работы программы на консоль будет выведен следующий код.

const int A = 1;
const int B = 2;
const int C = 3;
    

Метаданные можно хранить не только в массивах, но и в объектах других типов, например в словаре Dictionary. Следующий пример генерирует тот же самый код, но делает это с применением словаря:

Dictionary<string, string> constants = 
    new Dictionary<string, string> 
    {{ "A", "1" }, { "B", "2" }, { "C", "3" }};
foreach(KeyValuePair<string, string> cnst in constants)
{
    Console.WriteLine("const int " + cnst.Key + " = " + cnst.Value + ";");
}
    

Еще один пример

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

string[] var = new string[3] { "X", "Y", "Z" };
string print = "Console.WriteLine(";
for (int i = 0; i < 3; i++)
{
    Console.WriteLine("Console.Read(" + var[i] + ");");
    print += var[i];
    if (i < 2) print += " + ";
}
print += ");";
Console.WriteLine(print);
    

Сгенерированный код будет таким:

Console.Read(X);
Console.Read(Y);
Console.Read(Z);
Console.WriteLine(X + Y + Z);
    

В переменную print собирается код конкатенации переменных. В цикле добавляются имена тех переменных, которые были заданы в метаданных.

Замена комментариев кодом

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

Рассмотрим пример. Дан файл кода с комментариями. В комментариях содержатся присвоения выражений переменным. Эти переменные из левых частей выражений генератор должен объявить. А также записать присвоения выражений переменным, но уже без знаков комментариев.

a = 1;
b = 2;
//x=a+b;
//y=b-a;
    

Программа считывает файл построчно. Если в строке содержится комментарий (определяется наличием символов // в начале строки), то объявляется переменная, указанная в нем.

List<string> program = new List<string>();
string line;
using (StreamReader sr = File.OpenText(filepath))
{
    while (!sr.EndOfStream)
    {
        line= sr.ReadLine();
        if (line.Contains("//"))
        {
            program.Add("int " + line [2] + ";");
            program.Add(line.Substring(2));
        }
        else program.Add(line);
    }
}
Output.PutResult(program, filepath);
    

Для чтения текста программы используется потоковое чтение при помощи класса StreamReader. В переменной filepath содержится путь к изменяемому файлу. Потоковое чтение и запись используются часто при генерации кода и являются весьма удобными. Для записи нового файла также используется потоковая запись. Реализована она в уже рассмотренном выше методе PutResult класса Output.

В результате работы программы выходит следующий результат:

a = 1;
b = 2;
int x;
x=a+b;
int y;
y=b-a;
    

Мы видим, что комментарии были убраны, а для переменных x и y созданы объявления.

Модификация кода

Если необходимо вносить изменения разного характера в разных частях кода, то в комментариях перед строкой можно записать какие именно изменения требуются. Рассмотрим соответствующий пример. В странице ASP.Net надо добавить параметр в строку url. В комментариях указывается команда add parameter, которая считывает значение параметра и объявляет соответствующую ей переменную. В данном примере это единственная указываемая в комментариях команда, но их может быть и несколько. Команда же добавления значения параметра является неявной.

//add parameter p_par
string url = "http://mysite.mycompany.com/action?x=3id=1";
    

Для обработки этого кода напишем программу.

List<string> program = new List<string>();
string parameters = "";
string line;
using (StreamReader sr = File.OpenText(filepath))
{
    while (!sr.EndOfStream)
    {
        line= sr.ReadLine();
        if (line.Contains("//add parameter"))
        {
            string par = line.Substring(16);
            parameters += ""+par +"=\" + " + par;
            program.Add("string " + par + " = Request.QueryString[\"" + par + "\"];");
        }
        else if (line.Contains(".aspx?"))
        {
            program.Add(line.Substring(0, line.Length-2)+parameters + ";");
        }
    }
}
Output.PutResult(program, filepath);
    

Для каждой строки проверяется, содержит ли она команду на добавление параметра или строку с адресом ASP.Net страницы. Если это добавление параметра, то в переменную, содержащую список параметров добавляется еще один. Если же это строка, содержащая адрес страницы, то к ней добавляются все накопленные в памяти параметры. Результатом будет приведенный ниже код.

string p_par = Request.QueryString["p_par"];
string url = "http://mysite.mycompany.com/action.aspx?a=3id=1p_par=" + p_par;
    

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

Модификация группы файлов и резервное копирование

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

string parameters = "";
List<string> program = new List<string>();
if (Directory.Exists(filepath))
{
    foreach (string strFile in Directory.GetFiles(filepath, "*.cs"))
    {
        File.Copy(strFile, strFile + ".bkp");
        program.Clear();
        parameters = "";
        string[] lines = File.ReadAllLines(strFile);
        foreach (string line in lines)
        {
            if (line.Contains("//add parameter"))
            {
                string par = line.Substring(16);
                parameters += "" + par + "=\" + " + par;
                program.Add("string " + par + " = Request.QueryString[\"" + par + "\"];");
            }
            else if (line.Contains(".aspx?"))
            {
                program.Add(line.Substring(0, line.Length - 2) + parameters + ";");
            }
            else
            {
                program.Add(line);
            }
        }
        Output.PutResult(program, strFile);
    }
}
    

Резервная копия сохраняется в файлах с расширением ".bkp". В этом примере для чтения файлов используется класс File.

Преобразование кода программы в формат HTML

Еще один хороший способ применения генерации для преобразования кода - перевод текста кода в формат HTML для отображения на веб-сайте:

List<string> program = new List<string>();
string line;
program.Add("<html>");
program.Add("<head><title>MyClass.cs</title></head>");
program.Add("<body><font face=\"Courier\">");
using (StreamReader sr = File.OpenText(filepath))
{
    while (!sr.EndOfStream)
    {
        line = sr.ReadLine();
        line = Regex.Replace(line, "", "amp;");
        line = Regex.Replace(line, "<", "lt;");
        line = Regex.Replace(line, ">", "gt;");
        line = Regex.Replace(line, " ", "nbsp;");
        line = Regex.Replace(line, "\t", "nbsp;nbsp;nbsp;nbsp;");
        line = Regex.Replace(line, "\n", "<br>\n");
        line = Regex.Replace(line, "\"", "quot;");
        program.Add(line + "<br/>");
    }
}
program.Add("</font></body></html>");
Output.PutResult(program, filepath + ".html");
    

В начало файла добавляются необходимые теги вроде html, head, body, затем каждая строка кода подвергается обработке в цикле - производится замена каждого специального знака на соответствующий символьный объект. В конец файла добавляются закрывающие теги. Если текст кода файла MyClass.cs выглядит так:

using System;
class MyClass
{
    static void Main()
    {
        Console.WriteLine("Hello!");
        Console.ReadLine();
    }
}
    

Cгенерированный текст HTML будет следующим:

<html>
<head><title>MyClass.cs</title></head>
<body><font face="Courier">
usingnbsp;System;<br/>
<br/>
classnbsp;MyClass<br/>
{<br/>
nbsp;nbsp;nbsp;nbsp;staticnbsp;voidnbsp;Main()<br/>
nbsp;nbsp;nbsp;nbsp;{<br/>
nbsp;nbsp;nbsp;nbsp;nbsp;nbsp;nbsp;nbsp;Console.WriteLine(quot;Hello!quot;);<br/>
nbsp;nbsp;nbsp;nbsp;nbsp;nbsp;nbsp;nbsp;Console.ReadLine();<br/>
nbsp;nbsp;nbsp;nbsp;}<br/>
}<br/>
</font></body></html>
    

Шаблоны XSLT

Так как генерация кода в какой-то мере является преобразованием текста, то можно использовать технологии работы с текстом, например XML. Применяя технологию XSLT можно преобразовать метаданные в программный код.

Рассмотрим пример, в котором требуется создать несколько объявлений констант, опишем их названия и значения в следующем XML-файле:

<?xml-stylesheet type="text/xsl" href="variables.xsl"?>
<VARIABLES>
  <VARIABLE Active="true">
    <VAR>A</VAR>
    <VAL>1</VAL>
  </VARIABLE>
  <VARIABLE Active="true">
    <VAR>B</VAR>
    <VAL>2</VAL>
  </VARIABLE>
  <VARIABLE Active="true">
    <VAR>C</VAR>
    <VAL>3</VAL>
  </VARIABLE>
  <VARIABLE Active="false">
    <VAR>D</VAR>
    <VAL>4</VAL>
  </VARIABLE>
</VARIABLES>
    

Обратим внимание на константу D. Свойство Active установлено в значение false. У других же констант установлено значение true. Это означает, что для константы D код генерироваться не будет. Также обратим внимание, что в первой строке указан файл стиля variables.xsl. В этом файле будут указаны правила преобразования XML-файла.

<xsl:stylesheet xmlns:xsl="http://www.w3.org/TR/WD-xsl">
  <xsl:template match="/">
    <xsl:for-each select="VARIABLES/VARIABLE[@Active='true']">
      const int <xsl:value-of select="VAR"/> =
      <xsl:value-of select="VAL"/>;<BR/>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>
    

В файле стилей указано, что для каждого элемента VARIABLE, имеющего свойство Active со значением true, будут выполняться следующие действия. Выводится строка, содержащая в самом начале текст "const int", потом имя константы (содержимое элемента VAR), затем знак равенства, после него отображается значение константы (содержимое элемента VAL). В конце стоят точка с запятой и тэг перехода на следующую строку.

const int A = 1;
const int B = 2;
const int C = 3;
    

Более подробно технология XSLT будет рассмотрена в лекции 5.

Шаблоны T4

Кроме XSLT существуют и другие технологии для работы с шаблонами. Одна из них - это технология T4. В среде Visual Studio имеется возможность применять шаблоны T4. Для этого необходимо создать файл с расширением ".tt", после чего добавить код на одном из языков Visual Basic или C#, например, следующий:

<#@ template debug="false" language="C#" #>
<#@ output extension=".cs" #>
<# string[] vars = new string [] {"A", "B", "C"}; #>
class MyClass {
<# 
  foreach (string variable in vars)
  { #>
  private int <#= variable #> = 0;
<# } #>
}
    

Этот файл напоминает стили PHP или ASP.Net. Простым текстом написаны повторяющиеся статические участки кода, а между символами <# и #> расположен обрабатывающий программный код. Более подробно генерация кода при помощи шаблонов T4 будет рассмотрена в четвертой лекции. Результатом генерации по указанному выше шаблону будет следующий код:

class MyClass {
  private int A = 0; 
  private int B = 0;
  private int C = 0;
}
    

Применение регулярных выражений

Трудно переоценить роль регулярных выражений в генерации кода. Этот инструмент манипуляции текстом позволяет облегчить многие задачи, так как генераторы кода много работают с текстом. Регулярные выражения являются шаблонами для выполнения поиска подстроки в тексте или замены этой подстроки на другую подстроку. Значительно облегчается чтение из конфигурационных файлов, поиск и замена исходного кода в файлах.

Рассмотрим пример, когда согласно приведенным комментариям необходимо добавить объявление параметров.

List<string> program = new List<string>();
string line;
Regex pattern = new Regex(@"^//\s*(?<val>.*?)\s*=", RegexOptions.Singleline);
using (StreamReader sr = File.OpenText(filepath))
{
    while (!sr.EndOfStream)
    {
        line = sr.ReadLine();
        foreach (Match m in pattern.Matches(line))
        {
            program.Add("private int " + m.Groups["val"].Value + ";");
        }
        program.Add(Regex.Replace(line,@"^//\s*",""));
    }
}
Output.PutResult(program, filepath);
    

Здесь построчно считывается файл, для каждой строки проверяется соответствие шаблону "^//\s*(?<val>.*?)\s*=". В этом шаблоне ищутся подстроки, которые соответствуют следующему формату: начинаются знаками комментариев "//" в начале строки, затем идут ноль или больше пробелов, после чего могут встречаться любые символы (в скобках сохраняются в <val>), затем снова ноль или больше пробелов и знак равенства.

Для файла со следующими комментариями:

// a = 1;
// b = 2;
// c = a + b;
    

будет выведен в файл следующий код.

private int a;
a = 1;
private int b;
b = 2;
private int c;
c = a + b;
    

Генерация незавершенного кода

Не во всех случаях требуется или есть возможность сгенерировать завершенный код, который не нуждается в ручной модификации после генерации. Иногда более выгодно для экономии времени и ресурсов быстро создать генератор, выводящий большой объем неполного кода. Затем вручную изменить части кода, трудно поддающиеся генерации.

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

Рассмотрим случай генерации условия вида if-then-else. Условия внутри if могут задаваться в любом количестве. Последовательность их применения также может быть любой. Следовательно, скобки тоже могут быть любой сложности и любого уровня вложенности. Реализация такой структуры и ввод данных будет трудоемким занятием. Когда код требуется сгенерировать только один раз, то проще выполнить неполную генерацию. Что мы и сделаем. В XML-файле объявим секции ifsection, thensection, elsesection. В ifsection находятся логические условия, однако не будут указаны скобки и операции и\или, а в thensection и elsesection - операторы, которые надо выполнить в зависимости от истинности или ложности условия.

<?xml version="1.0" encoding="utf-8" ?>
<condition>
  <ifsection>
    <ifcase>y==1</ifcase>
    <ifcase>k!=0</ifcase>
    <ifcase>SomeFunction(z)</ifcase>
  </ifsection>
  <thensection>
    <action>t = y + k</action>
    <action>SomeProcedure(t,z)</action>
  </thensection>
  <elsesection>
    <action>z++</action>
  </elsesection>
</condition>
    

Рассмотрим программу, выполняющую чтение файла и генерацию кода.

string xmlpath = @"A:\input\ifcondition.xml";
string programpath = @"A:\input\ifcondition.cs";
List<string> program = new List<string>();
List<string> thensection = new List<string>();
List<string> elsesection = new List<string>();
string ifsection = "if (";
thensection.Add("{");
elsesection.Add("else {");
XmlDocument reader = new XmlDocument();
reader.Load(xmlpath);
XmlElement elem = reader.DocumentElement;
foreach(XmlElement child in elem.ChildNodes){
    if (child.Name == "ifsection")
    {
        foreach (XmlElement ifcase in child.ChildNodes)
        {
            ifsection += "(" + ifcase.InnerText + ")???";
        }
    }
    if (child.Name == "thensection")
    {
        foreach (XmlElement thencase in child.ChildNodes)
        {
            thensection.Add("\t" + thencase.InnerText + ";");
        }
    }
    if (child.Name == "elsesection")
    {
        foreach (XmlElement elsecase in child.ChildNodes)
        {
            elsesection.Add("\t" + elsecase.InnerText + ";");
        }
    }
}
ifsection += ")";
thensection.Add("}");
elsesection.Add("}");
program.Add(ifsection);
program.AddRange(thensection);
program.AddRange(elsesection);
Output.PutResult(program, programpath);
    

В переменной xmlpath содержится путь к XML-файлу, а в переменной programpath путь к файлу сгенерированного кода. Программа в цикле пробегает по каждой из секций ifsection, thensection, elsesection. В секциях просматривается каждое условие или команда. Условия программа заключает в скобки и добавляет три вопросительных знака. Команды завершает точкой с запятой. Заметим, что для отступов используется символ табуляции "\t". Посмотрим на сгенерированный код:

if ((y==1)???(k!=0)???(SomeFunction(z))???)
{
t = y + k;
	SomeProcedure(t,z);
}
else {
	z++;
}
    

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

Шаги построения генератора

Рассмотрим схематически основные шаги в разработке приложения без применения генератора.

(рис 2.1) Шаги построения кода без применения генерации

В самом начале выполняется проектирование архитектуры приложения, выявляются ее составные части и их взаимодействие. Когда архитектура становится ясной, выполняется разработка. При ее полном или частичном завершении выполняется тестирование разработанного кода. При необходимости по результатам тестирования выполняется отладка. Это только обобщенная схема, отражающая основные шаги. Такие этапы, как анализ предметной области, изначальный сбор требований, развертывание ПО и другие, не рассматриваются. Разумеется, реальный процесс разработки приложения будет содержать больше нюансов и деталей, которые могут варьироваться от проекта к проекту.

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

А теперь посмотрим на такой же обобщенный процесс, но с применением генератора:

(рис 2.2) Шаги построения кода с применением генерации

На этой схеме также рассматриваются основные шаги, имеющие отношение к разработке и сопровождению генератора кода. На каждый из шагов 2, 3, 4 добавляется похожий шаг, но связанный с применением генератора. Шаги, связанные с разработкой ручного кода остаются. На разных проектах доля ручного и сгенерированного кода может быть разной.

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

Формирование кода проекта по отношению к предметной области с учетом и генерации и ручной разработки будет выглядеть так:

(рис 2.3) Формирование кода при генерации и ручной разработке

В некоторых случаях генерацию нужно выполнять только для части метаданных. Для этого надо в метаданные добавить логический параметр, принимающий значения true и false и указывающий, нужно ли выполнять генерацию для этой части метаданных. Например, применять параметр to_generate или is_active. В примере 2.8 параметр назывался Active.

Вернуться к учебному плану