Опубликован: 25.07.2012 | Уровень: специалист | Доступ: платный
Лекция 3:

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

< Лекция 2 || Лекция 3: 12 || Лекция 4 >
Аннотация: В данной лекции проводится обзор базовых методик генерации кода, а также приводятся примеры. Рассматривается применение регулярных выражений и XML. Также рассматривается генерация кода с помощью текстовых шаблонов T4 в Visual Studio. Изучается обобщенный процесс генерации кода.

В первой лекции были изучены основные понятия, характеристики генераторов кода. Настало время рассмотреть примеры генераторов, изучить применяемые техники при их создании. Мы рассмотрим примеры генерации при помощи текстовых шаблонов и без них, путем вывода текста кода программой. Также будут приведены примеры способов введения метаданных: в коде программы, из файла 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);
            }
        }
    }
}
    
Пример 2.1.

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

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

Рассмотрим пример, где необходимо сгенерировать код объявления трех констант. В массиве 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 + ";");
}
    
Пример 2.2.

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

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

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);
    
Пример 2.3.

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

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);
    
Пример 2.4.

Для чтения текста программы используется потоковое чтение при помощи класса 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=3&id=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);
    
Пример 2.5.

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

string p_par = Request.QueryString["p_par"];
string url = "http://mysite.mycompany.com/action.aspx?a=3&id=1&p_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);
    }
}
    
Пример 2.6.

Резервная копия сохраняется в файлах с расширением ".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");
    
Пример 2.7.

В начало файла добавляются необходимые теги вроде 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">
using&nbsp;System;<br/>
<br/>
class&nbsp;MyClass<br/>
{<br/>
&nbsp;&nbsp;&nbsp;&nbsp;static&nbsp;void&nbsp;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>
    
< Лекция 2 || Лекция 3: 12 || Лекция 4 >
Дмитрий Клочков
Дмитрий Клочков
Россия, Рубцовск
Волков Олег
Волков Олег
Украина, Днепропетровск