Полуавтоматическая конвертация лени в код

в 7:25, , рубрики: .net, метапрограммирование, Программирование, метки: ,

Всем доброго дня. Сегодня я хочу поговорить об автоматической генерации кода C#. Например, свойства в классах, описывающих сущности предметной области, обычно описываются по совершенно одинаковой схеме. И мне элементарно лениво писать для каждого примитивного свойства одинаковые конструкции. Немного спасает применение сниппетов и активных шаблонов, но когда приходит нужда что-то поменять в этой схеме, то приходится перелопачивать кучу кода. Так почему бы это однообразие не генерировать автоматически в процессе сборки?
В какой-то момент кинетическая энергия творчества ненадолго пересилила потенциальную энергию лени, и результатом этого стала маленькая библиотечка для автоматической генерации некоторых исходных файлов программы на основе внешних данных. Приглашаю под кат всех ленивых (в хорошем смысле этого слова) разработчиков на C#.

Пролог

Когда я программировал на С++, то использовал для этих целей директиву #define с параметрами. Великолепный механизм, который позволяет в виде простой строки со вставками продекларировать кусок кода, который потом повторно использовать много раз. Это совсем не то же самое, что предлагают шаблоны или generic классы.
К сожалению, разработчики C# не включили поддержку директивы #define, аналогичную С++. Для этого, наверняка, есть причины, кроющиеся, в первую очередь, в читаемости и прозрачности кода. Но я все равно хотел иметь возможность избежать необходимости дублирования кода при объявлении однообразных конструкций.
Надо сказать, что Microsoft довольно активно использует генерацию кода. Взять хотя бы классы Linq2Sql – все объявления структуры классов находятся в xml файле, на основе которого генерируется код. Более того, .NET Framework включает целое пространство имен System.CodeDom, посвященное генерации кода. Многочисленные методы и классы позволяют генерировать код в терминах CLR и сохранять его в виде любого языка, поддерживаемого .NET – C#, VB.Net. Выглядит это примерно вот так:

        /// <summary>
        /// конструирует простое свойство-значение
        /// </summary>
        /// <param name="newC"></param>
        /// <param name="e"></param>
        private void CreateProperty(CodeTypeDeclaration newC, XmlElement e)
        {
            string propName = e.GetAttribute("name");
            var propType = ResolveType(e.GetAttribute("datatype"));

            var rel_field_prop_name = createPropertyConstName(newC, e, propName);

            var new_prop = new CodeMemberProperty
              {
                  Attributes = MemberAttributes.Public | MemberAttributes.Final,
                  Name = "p_" + propName,
                  HasGet = true,
                  HasSet = true,
                  Type = propType
              };

            var comment = e.GetAttribute("displayname");
            if (!string.IsNullOrEmpty(comment))
                new_prop.Comments.Add(new CodeCommentStatement("<summary>n " + comment + "n </summary>", true));

            new_prop.GetStatements.Add(new CodeMethodReturnStatement(
                new CodeCastExpression(propType,
                    new CodeMethodInvokeExpression(
                        new CodeMethodReferenceExpression(new CodeBaseReferenceExpression(), "GetDataProperty"),
                        new CodeFieldReferenceExpression(null, rel_field_prop_name)))));

            new_prop.SetStatements.Add(
                    new CodeMethodInvokeExpression(
                        new CodeMethodReferenceExpression(new CodeBaseReferenceExpression(), "SetDataProperty"),
                        new CodeFieldReferenceExpression(null, rel_field_prop_name), new CodePropertySetValueReferenceExpression()));

            newC.Members.Add(new_prop);
        }

Я несколько раз использовал этот механизм для решения частных задач, и пришел к выводу, что для моей задачи он не подходит. Ведь мне требуется иметь возможность быстро подсунуть кусок кода для подстановки, а упомянутые методы генерируют код на основе его структуры. Т.е. чтобы воспользоваться встроенными средствами, нужно сначала написать транслятор шаблонов, который разберет код C#, чтобы на его основе сгенерировать его же. Полный бред.

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

В 2010 студии Microsoft придумала T4 Text Templates. Судя по документации, это почти то, что нужно. Но, во-первых, у меня все еще есть проекты на 2008-й студии, а во-вторых, мне не удалось заставить их работать :-(.

Еще бывает такая штука как Nemerle. Здесь вообще есть возможность придумать свой язык поверх C#. Классно, но опять не то, что нужно.
Ведь я всего лишь хочу иметь возможность повторно использовать куски кода на C#.

Хочется чего-нибудь простого

Итак, я дошел до состояния готовности – уже чешутся руки что-нибудь запрограммировать. Формулируются основные хотелки:

  1. «Кодогенерирующий» код должен быть простым и легко читаемым.
  2. Классы для кодогенерации должны реализовывать цепные интерфейсы в стиле Linq.
  3. Шаблон, который используется для генерации, должен объявляться простой строкой.
  4. Должна быть возможность комбинировать в одном классе генерируемый и написанный вручную код

И на основе них вырабатываются технические решения:

  1. Делается библиотечка, содержащая классы и методы для простой генерации кода
  2. В Solution включается проект исполняемого приложения, которое собирается одним из первых, сразу запускается (в PostBuildStep) и генерирует нужные части для остальных проектов, используя упомянутую библиотеку.
  3. Чтобы иметь возможность комбинировать генерируемый и написанный код используем partial классы. (Наверное, эту возможность добавили в C#, чтобы было не так обидно из-за отсутствия #define  ).
  4. Исходные данные для генерации кода оформляем, например, в виде перечислений. Почему? Да просто это удобно.

Итог – код для генерации класса с набором однотипных свойств выглядит примерно так:

//объявляем шаблон свойства
const string CommandPropertyTemplate =
@"public static <type> <name>
{{
    get
    {{
        return PVCommand.<name>.GetCommand();
    }} 
}}

";

//конструируем исходный файл со статическим классом
 var commandsClass = CodeWriter
     .BeginSource("Commands.cs")
     .Using("MyApp.Display")
     .BeginNamespace("MyAppCommands")
     .AddClass("Commands").Static();

//для каждого элемента перечисления добавляем одноименное свойство в класс
//используя одинаковый шаблон
var allcmds = System.Enum.GetValues(typeof(PVCommand)).Cast<PVCommand>();
foreach (var cmd in allcmds)
{
    commandsClass.AddBlock<RoutedUICommand>(cmd.ToString(), CommandPropertyTemplate);
}

Несколько комментариев по поводу оформления шаблона. Двойные фигурные скобки используются чтобы просто отдавать эту строку в string.Format. Ключевые слова и заменяются на имя и тип свойства, которые передаются в качестве параметров в метод AddBlock().

Так зачем все это нужно?

Ну а теперь давайте немного пофантазируем, как все это можно применить.

Работа с локализуемыми строками

В одном из проектов у меня возникала задача локализации WPF приложения, которая была решена вот таким образом. Однако, добавление каждой новой строки требовало вписывания большого однообразного куска в XAML. Когда я начинал следующий проект, то решил усовершенствовать решение с использованием данной библиотеки. Итак, на вход подается перечисление, которое содержит ключи строковых ресурсов, а значения для нейтральной локали помещаются в атрибут Description:

    public enum Strings
    {
        [Description("MyCoolApp - trial version")]
        AppTitle,
    }

На основе этого перечисления генерируются два артефакта:

  1. XAML с ресурсами. (Для его генерации используются классы из Linq2XML)
  2. Статический класс для удобного доступа к ресурсам из кода.

Генерация всего кода, необходимого для работы с локализуемыми строками в WPF, выглядит так:

//шаблон для свойства класса
const string ResourceEntry =
@"
public const <type> <name>Key = ""<name>"";

public static <type> <name> {{
    get{{
        return App.RString(<name>Key);
    }}
}}
";
 
//объявляем статический класс для ресурсов
var stringTable = CodeWriter
    .BeginSource("Strings.cs")
    .BeginNamespace("MyApp")
    .AddClass("StringTable")
    .Static();

//добавляем свойства для доступа к ресурсам
foreach (var rstring in GetResourceStrings())
{
    var resourseKey = rstring.ToString();
    stringTable.AddBlock<string>(resourseKey, ResourceEntry);
}




//функция для генерации XAML
void StringTableXamlTo(string dir)
{
    var nsDefault = "http://schemas.microsoft.com/winfx/2006/xaml/presentation";
    var nsX = "http://schemas.microsoft.com/winfx/2006/xaml";
    var nsCore = "clr-namespace:MyApp.Core;assembly=MyApp.Core";
    
    var resourcedict = new XElement(XName.Get("ResourceDictionary", nsDefault),
        new XAttribute(XName.Get("Uid", nsX), "StringTable"), 
        new XAttribute(XNamespace.Xmlns + "core", nsCore),
        new XAttribute(XNamespace.Xmlns + "x", nsX));

    foreach (var rstring in GetResourceStrings())
    {
        var resourseKey = rstring.ToString();
        var resourceValue = EnumHelper.GetDescription(rstring);

        var resourceDecl = new XElement(XName.Get("StringObject", nsCore),
            new XAttribute(XName.Get("Uid", nsX), "UID_" + resourseKey),
            new XAttribute(XName.Get("Key", nsX), resourseKey),
            new XAttribute("Localization.Attributes", 
                        "Value (Readable Modifiable Text)"),
                 new XAttribute("Value", resourceValue)
                 );

             resourcedict.Add(resourceDecl);
         }
         var xaml = new XDocument(resourcedict);
         xaml.Save(Path.Combine(dir, "stringtable.xaml"));
     }
}

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

Модель предметной области

Другой пример. Вот у меня есть классы, моделирующие сущности предметной области. Каждый класс реализует интерфейс INotifyPropertyChanged. У каждого из них есть свойства. Эти свойства все устроены по одной и той же схеме – скрытое поле, в котором хранится значение свойства, getter просто возвращает значение свойства, а setter изменяет значение поля и генерирует уведомление об изменении. Раньше я каждое такое свойство писал руками. Потом научился вставлять код по шаблону. А теперь я хочу в одном месте описать шаблон, и перечислить свойства с их типами. Перечень свойств класса объявим, опять же, в виде перечисления. Каждому элементу перечисления добавим атрибут, описывающий тип свойства. Генерация кода класса почти ничем не будет отличаться от примеров, приведенных выше, нужно будет лишь прочитать тип свойства из соответствующего атрибута. Отличие будет только в шаблоне свойства:

const string ModelPropertyTemplate = 
@"
<type> _<name>;

public <type> <name>
{{
    get
    {{
        return _<name>;
    }}
    set
    {{
        _<name> = value;
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(""<name>""));
    }}
}
"; 

Заключение

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

Спасибо за внимание!

Автор: ganouver

Поделиться

* - обязательные к заполнению поля