- PVSM.RU - https://www.pvsm.ru -
Недавно возникла задача массовой рассылки писем, текст которых формируется на основе шаблона, в котором помимо статического содержимого есть информация о получателе и фрагменты текста. В моем случае это шаблон автоматического оповещения подписчиков о публикации новых статей, соответственно в нем есть обращение к адресату и красиво оформленная ссылка на публикацию.
Сразу возник вопрос — как это реализовать? На ум приходили различные решения, начиная от задания в шаблоне неких константных значений, которые бы заменялись на данные модели, и заканчивая полноценными вьюхами Razor (сайт построен на MVC 5).
После непродолжительной битвы с самим собой, я пришел к выводу, что эту достаточно распространенную задачу пора решить раз и навсегда, и что ее решение должно быть не очень сложным (т.е. не должно зависеть от библиотек, не входящих в состав .NET Framework 4), но при этом достаточно функциональным, чтобы решать поставленную задачу и иметь запас по расширяемости.
В данной статье я расскажу о решении на основе генератора байт-кода, которое удовлетворяет этим требованиям, а также прокомментирую наиболее интересные фрагменты кода.
Если вас интересует только шаблонизатор, ссылочки ниже:
Исходные коды шаблонизатора (Genesis.Patternizer) и тестовой консоли в проекте на SourceForge: https://sourceforge.net/projects/open-genesis/?source=navbar [1]
Или в архиве одним файлом: Patternizer.zip [2]
Для начала определимся с синтаксисом. Лично мне по душе функция string.Format, широко используемая для форматирования простых значений. Воспользуемся ее синтаксисом для обозначения мест вставок значений в шаблон:
'{' <выражение> [ ':' <строка формата> ] [ '|' <строка по умолчанию> ] '}'
Примеры: {User.GetFIO()}, {User.Name|юзверь}, {User.Birthdate:dd.MM.yyyy}, {User.Score:0.00|ничегошеньки}.
Значение по умолчанию будет подставляться, если искомое значение нулевое (null) или отсутствует вовсе, т.е. если указанное свойство/поле/метод не найдено в модели. Для экранирования фигурных скобочек будем использовать двойные фигурные скобочки (как в функции string.Format), для экранирования символов в строке формата и значении по умолчанию — слеш.
А вот пример готового шаблона, который будет использоваться в тестовом примере:
Здравствуйте, {User.GetFIO()|юзверь}!
Вот фрагмент кода, который вы заказывали:
function PrintMyName()
{{
Console.WriteLine("My name is {{0}}. I'm {{1}}.", "{UserName|юзверь}", {User.Age:0});
}}
Данное сообщение сформировано автоматически {Now:dd MMMM yyyy} в {Now:HH:mm:ss}
Изначально я предполагал, что шаблон будет поддерживать только публичные свойства модели, но в процессе разработки была добавлена поддержка полей, а также методов (с возможностью передачи аргументов типа строка, число, булевый тип и null) и обращений к массиву любой размерности. Т.е. следующее выражение также будет корректным шаблоном:
Неведомое нечто: {User.Account[0].GetSomeArrayMethod("a", true, 8.5, null)[5,8].Length:0000|NULL}
Для начала надо понять, что делать с текстовым шаблоном. Конечно, можно при каждом вызове подстановки модели анализировать данные шаблона, искать и подставлять значения. Но это очень медленный способ. Гораздо эффективнее будет один раз разобрать шаблон на отдельные логические фрагменты (элементы шаблона) и в дальнейшем оперировать уже этими элементами. Существует три очевидных типа элемента: строковая константа (та часть шаблона, которая непосредственно идет в результат в неизменном виде), подстановка (то, что внутри фигурных скобок) и комментарий (данный элемент не реализован, но, полагаю, вы понимаете, о чем речь).
На основе этих рассуждений опишем базовый класс для элемента шаблона:
/// <summary>
/// элемент шаблона
/// </summary>
public abstract class PatternElement
{
/// <summary>
/// оценочная длина элемента шаблона
/// </summary>
public virtual int EstimatedLength { get { return 0; } }
/// <summary>
/// значение при пустом значении модели
/// </summary>
public abstract string GetNullValue();
}
Смысл свойства EstimatedLength и метода GetNullValue() будет раскрыт ниже.
Далее опишем конкретные реализации — строковую константу и подстановку (назовем ее «выражением»):
public class StringConstantElement : PatternElement
{
public string Value { get; set; }
public override int EstimatedLength { get { return Value == null ? 0 : Value.Length; } }
public override string GetNullValue()
{
return Value;
}
}
public class ExpressionElement : PatternElement
{
public string Path { get; set; }
public string FormatString { get; set; }
public string DefaultValue { get; set; }
public override int EstimatedLength { get { return Math.Max(20, DefaultValue == null ? 0 : DefaultValue.Length); } }
public override string GetNullValue()
{
return DefaultValue;
}
}
Также опишем интерфейс парсера IPatternParser, который принимает на входе текстовый шаблон, а выдает последовательность элементов:
public interface IPatternParser
{
IEnumerable<PatternElement> Parse(string pattern);
}
Парсер на основе фигурных скобок так и назовем — BracePatternParser. Не имея большого опыта написания синтаксических анализаторов (а именно это по сути делает парсер), я не буду углубляться в его реализацию.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Genesis.Patternizer
{
/// <summary>
/// парсер шаблона в фигурных скобочках
/// </summary>
public class BracePatternParser : IPatternParser
{
private object _lock = new object();
private HashSet<char> PATH_TERMINATOR_CHARS;
private HashSet<char> FORMAT_TERMINATOR_CHARS;
private HashSet<char> PATTERN_TERMINATOR_CHARS;
private string pattern; // шаблон
private int length; // длина строки
private int length_1; // длина строки минус один
private int index; // текущая позиция в строке
private StringBuilder constantBuilder;
private StringBuilder expressionBuilder;
/// <summary>
/// конструктор
/// </summary>
public BracePatternParser()
{
PATH_TERMINATOR_CHARS = new HashSet<char>(":|}".ToCharArray());
FORMAT_TERMINATOR_CHARS = new HashSet<char>("|}".ToCharArray());
PATTERN_TERMINATOR_CHARS = new HashSet<char>("}".ToCharArray());
}
/// <summary>
/// распарсить путь выражения
/// </summary>
/// <param name="chars"> символы-терминаторы </param>
/// <returns></returns>
private string ParsePatternPath(HashSet<char> chars)
{
// очищаем буфер выражения
expressionBuilder.Clear();
Stack<char> brackets = new Stack<char>();
bool ignoreBrackets = false;
for (index++; index < length; index++)
{
char c = pattern[index];
if (c == '(')
{
brackets.Push(c);
expressionBuilder.Append(c);
}
else if (c == ')')
{
if (brackets.Peek() == '(')
{
brackets.Pop();
}
else
{
// недопустимый символ
ignoreBrackets = true;
}
expressionBuilder.Append(c);
}
else if (c == '[')
{
brackets.Push(c);
expressionBuilder.Append(c);
}
else if (c == ']')
{
if (brackets.Peek() == '[')
{
brackets.Pop();
}
else
{
// недопустимый символ
ignoreBrackets = true;
}
expressionBuilder.Append(c);
}
else if (chars.Contains(c) && (ignoreBrackets || brackets.Count == 0))
{
// найден терминатор
break;
}
else
{
expressionBuilder.Append(c);
}
}
return expressionBuilder.Length == 0 ? null : expressionBuilder.ToString();
}
/// <summary>
/// распарсить часть выражения шаблона
/// </summary>
/// <param name="chars"> символы-терминаторы </param>
/// <returns></returns>
private string ParsePatternPart(HashSet<char> chars)
{
// очищаем буфер выражения
expressionBuilder.Clear();
for (index++; index < length; index++)
{
char c = pattern[index];
if (c == '\')
{
// знак экранирования в шаблоне
if (index < length_1)
{
expressionBuilder.Append(pattern[++index]);
}
}
else if (chars.Contains(c))
{
// найден терминатор
break;
}
else
{
expressionBuilder.Append(c);
}
}
return expressionBuilder.Length == 0 ? null : expressionBuilder.ToString();
}
/// <summary>
/// распарсить выражение шаблона
/// </summary>
/// <returns></returns>
private ExpressionElement ParsePattern()
{
string path = ParsePatternPath(PATH_TERMINATOR_CHARS);
if (path == null)
{
// выражение отсутствует
// пропускаем все до окончания шаблона (})
for (; index < length; index++)
{
char c = pattern[index];
if (c == '\')
{
index++;
}
else if (c == '}')
{
break;
}
}
return null;
}
else
{
ExpressionElement element = new ExpressionElement(path);
// читаем дополнительную информацию
if (index < length && pattern[index] == ':')
{
// строка формата
element.FormatString = ParsePatternPart(FORMAT_TERMINATOR_CHARS);
}
if (index < length && pattern[index] == '|')
{
// значение по умолчанию
element.DefaultValue = ParsePatternPart(PATTERN_TERMINATOR_CHARS);
}
return element;
}
}
/// <summary>
/// распарсить шаблон
/// </summary>
/// <param name="pattern"> шаблон </param>
/// <returns></returns>
public IEnumerable<PatternElement> Parse(string pattern)
{
lock (_lock)
{
if (pattern == null)
{
// нулевой шаблон
yield break;
}
else if (string.IsNullOrWhiteSpace(pattern))
{
yield return new StringConstantElement(pattern);
yield break;
}
// парсим шаблон
this.pattern = pattern;
// вспомогательные переменные
length = pattern.Length;
length_1 = length - 1;
index = 0;
// оптимизация
constantBuilder = new StringBuilder();
expressionBuilder = new StringBuilder();
// основной цикл парсера
for (; index < length; index++)
{
char c = pattern[index];
if (c == '{')
{
if (index < length_1 && pattern[index + 1] == c)
{
// экранированный символ '{'
constantBuilder.Append(c);
index++;
}
else
{
// начало управляющей конструкции
if (constantBuilder.Length != 0)
{
yield return new StringConstantElement(constantBuilder.ToString());
// очищаем буфер
constantBuilder.Clear();
}
var patternElement = ParsePattern();
if (patternElement != null)
{
yield return patternElement;
}
}
}
else if (c == '}')
{
if (index < length_1 && pattern[index + 1] == c)
{
// экранированный символ '}'
constantBuilder.Append(c);
index++;
}
else
{
// конец управляющей конструкции в неположенном месте, ошибкой считать не будем
constantBuilder.Append(c);
}
}
else
{
constantBuilder.Append(c);
}
}
// хвост тела шаблона
if (constantBuilder.Length != 0)
{
yield return new StringConstantElement(constantBuilder.ToString());
}
// очищаем данные
this.pattern = null;
constantBuilder = null;
expressionBuilder = null;
index = length = length_1 = 0;
}
}
}
}
Описанный выше парсер выполняет лишь часть общей задачи. Мало получить набор элементов шаблона, надо еще их обработать. Для этого опишем еще один интерфейс, представляющий главный по своей значимости компонент системы — IBuilderGenerator:
public interface IBuilderGenerator
{
Func<object, string> GenerateBuilder(List<PatternElement> pattern, Type modelType);
}
Для достижения наибольшего быстродействия, на каждый новый тип модели (modelType) будем создавать новый построитель и записывать его в хеш. Сам построитель представляет из себя обычную функцию, принимающую на входе object (модель) и возвращающую строку — заполненный шаблон. Конкретная реализация данного интерфейса будет приведена ниже, а перед этим рассмотрим последний компонент системы, связывающий все воедино.
Собственно шаблонизатор представляет из себя класс, связывающий шаблон, парсер и построитель. Его код также не представляет из себя ничего сверхинтересного.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BUILDER = System.Func<object, string>;
namespace Genesis.Patternizer
{
/// <summary>
/// мастер-шаблонизатор
/// </summary>
public class Patternizator
{
#region Declarations
private PatternizatorOptions _options; // опции шаблонизатора
private string _pattern; // шаблон
private List<PatternElement> _elements; // элементы шаблона
private Dictionary<Type, BUILDER> _builders; // словарь построителей
#endregion
#region Properties
/// <summary>
/// шаблон
/// </summary>
public string Pattern
{
get { return _pattern; }
set
{
_pattern = value;
PreparePattern();
}
}
#endregion
#region Constructors
/// <summary>
/// конструктор
/// </summary>
public Patternizator()
{
_options = PatternizatorOptions.Default;
_builders = new Dictionary<Type, BUILDER>();
}
/// <summary>
/// конструктор
/// </summary>
/// <param name="pattern"> шаблон </param>
public Patternizator(string pattern)
{
_options = PatternizatorOptions.Default;
Pattern = pattern;
}
/// <summary>
/// конструктор
/// </summary>
/// <param name="options"> опции шаблонизатора </param>
public Patternizator(PatternizatorOptions options)
{
_options = options;
_builders = new Dictionary<Type, BUILDER>();
}
/// <summary>
/// конструктор
/// </summary>
/// <param name="pattern"> шаблон </param>
/// <param name="options"> опции шаблонизатора </param>
public Patternizator(string pattern, PatternizatorOptions options)
{
_options = options;
Pattern = pattern;
}
#endregion
#region Private methods
/// <summary>
/// подготовить шаблон
/// </summary>
private void PreparePattern()
{
// парсим шаблон
_elements = _options.Parser.Parse(_pattern).ToList();
// сбрасываем хеш построителей
_builders = new Dictionary<Type, BUILDER>();
// для наглядности можно раскомментировать и посмотреть распарсенный шаблон в виде списка строк
//string template = string.Join(Environment.NewLine, _elements.Select(e => System.Text.RegularExpressions.Regex.Replace(e.ToString(), @"s+", " ").Trim()).ToArray());
}
#endregion
#region Public methods
/// <summary>
/// генерировать сообщение
/// </summary>
/// <param name="model"> модель </param>
/// <returns></returns>
public string Generate(object model)
{
// получаем тип модели и ее ключ
Type modelType = model == null ? null : model.GetType();
Type modelTypeKey = modelType ?? typeof(DBNull);
// ищем построитель данного типа модели
BUILDER builder;
if (!_builders.TryGetValue(modelTypeKey, out builder))
{
// построитель еще не создан
builder = _options.BuilderGenerator.GenerateBuilder(_elements, modelType);
_builders.Add(modelTypeKey, builder);
}
// запускаем генерацию
return builder(model);
}
#endregion
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Genesis.Patternizer
{
/// <summary>
/// опции шаблонизатора
/// </summary>
public class PatternizatorOptions
{
/// <summary>
/// парсер шаблона
/// </summary>
public IPatternParser Parser { get; set; }
/// <summary>
/// генератор построителя
/// </summary>
public IBuilderGenerator BuilderGenerator { get; set; }
#region Default
private static PatternizatorOptions _default;
/// <summary>
/// настройки по умолчанию
/// </summary>
public static PatternizatorOptions Default
{
get
{
if (_default == null)
{
_default = new PatternizatorOptions
{
Parser = new BracePatternParser(),
BuilderGenerator = new ReflectionBuilderGenerator(),
};
}
return _default;
}
}
#endregion
}
}
Опции (PatternizatorOptions) — это необязательный аргумент, в котором можно дать указание шаблонизатору использовать конкретную реализацию парсера или генератора построителя, например, если вы используете синтаксис шаблона, отличный от стандартного.
Пример использования шаблонизатора в стандартном исполнении:
// читаем шаблон
string pattern = GetPattern();
// готовим шаблонизатор
Patternizator patternizator = new Patternizator(pattern);
// создаем модель
User user = new User
{
Surname = RandomElement(rnd, SURNAMES),
Name = RandomElement(rnd, NAMES),
Patronymic = RandomElement(rnd, PATRONYMICS),
// дата рождения 1950 - 1990 гг
Birthdate = new DateTime(1950, 1, 1).AddDays(rnd.NextDouble() * 40.0 * 365.25)
};
var model = new
{
User = user,
UserName = user.Name,
Now = DateTime.Now,
};
// заполнение с использованием шаблонизатора
string text = patternizator.Generate(model);
В данном примере модель представляет из себя анонимный тип, но вас это не должно смущать. Даже при генерации элементов такого типа в цикле, построитель будет создан лишь один раз, при первом вызове метода Generate. Но к вопросу производительности вернемся в конце статьи, теперь же рассмотрим самое интересное, так сказать, гвоздь данной публикации.
Для начала произведем небольшой анализ. Как в теории можно решить данную задачу?
Напомню, у нас есть список элементов шаблона (константы и выражения) и тип модели. И надо получить функцию Func<object, string>, которая подставляет модель заданного типа в шаблон, получая на выходе строку.
Если с константами вопросов нет (просто кидаем их в StringBuilder), то с выражениями все сложнее.
Я вижу три возможных варианта, как получить значение выражения из модели:
Первый вариант явно страдает быстродействием, т.к. рефлексия всегда работает медленно. Мой вам совет — никогда не используйте рефлексию для операций, выполняющихся очень часто. Оптимальный вариант ее использования — это подготовка на этапе старта программы, т.е. что-то вида «пробежали по классам, нашли нужную информацию в атрибутах, построили какие-то связи (делегаты, события) и далее используем их, не обращаясь к рефлексии повторно». В общем, для данной задачи рефлексия явно не подходит.
Второй вариант очень хорош, изначально я хотел использовать именно его. Код функции построителя при этом выглядел бы примерно так (для шаблона, приведенного в начале):
public string Generate(object input)
{
if (input == null)
{
// модель не указана
return @"Здравствуйте, юзверь!
Вот фрагмент кода, который вы заказывали:
function PrintMyName()
{
Console.WriteLine("My name is {0}. I'm {1}.", "юзверь", 0);
}
Данное сообщение сформировано автоматически в ";
}
else {
Model model = input as Model;
StringBuilder sb = new StringBuilder();
sb.Append("Здравствуйте, "); // строковая константа
if (model.User != null) {
var m_GetFIO = model.User.GetFIO();
if (m_GetFIO != null)
{
sb.Append(m_GetFIO);
} else
{
sb.Append("юзверь"); // значение по умолчанию
}
} else {
sb.Append("юзверь"); // значение по умолчанию
}
sb.Append("!rnВот фрагмент кода, который вы заказывали:rnrn ..."); // строковая константа
\ и т.д.
return sb.ToString();
}
}
Конечно, код будет достаточно длинным, но кто его увидит? В общем, этот вариант был бы оптимальным, если бы не анонимные типы. В коде выше мы не смогли бы объявить переменную модели Model model = input as <?>;, если бы тип модели не имел имени.
Итак, остается третий вариант. Сгенерируем тот же код непосредственно в байт-коде. При написании шаблонизатора я сам впервые использовал динамические функции и генератор байт-кода, именно это подтолкнуло меня написать данную статью, чтобы у вас, уважаемые читатели, было меньше проблем, когда вы решитесь освоить эту технологию.
Динамические сборки, динамические функции и генератор байт-кода описаны в пространстве имен System.Reflection.Emit, и для их использования не надо подключать какие-либо дополнительные библиотеки.
Простейшая динамическая функция создается следующим образом:
// генерируем динамический метод
var genMethod = new DynamicMethod("<имя метода>", typeof(<тип результата>), new Type[] { typeof(<тип аргумента 1>), typeof(<тип аргумента 2>), ..., typeof(<тип аргумента N>) }, true);
// получаем генератор байт-кода (он же IL-генератор или CIL-генератор)
var cs = genMethod.GetILGenerator();
// генерируем тело метода
// ...
// конец метода
cs.Emit(OpCodes.Ret);
// конвертируем метод в делегат
return genMethod.CreateDelegate(typeof(<тип делегата>)) as <тип делегата>;
cs.Emit(OpCodes.Ret); — это операция записи команды в байт-коде. Кто не в курсе, байт-код [3] — это что-то вроде ассемблера для языков семейства .NET.
Если вы собрались силами и дочитали статью до этого абзаца, то у вас должен возникнут вопрос, как же я сгенерирую байт-код, если не знаю его команд? Ответ достаточно прост. Для этого нам понадобится дополнительная программка (проект есть в архиве), код которой приведен под спойлером.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
namespace ILDasm
{
class Program
{
#region Static
static void Main(string[] args)
{
new Program().Run();
}
#endregion
public void Run()
{
string basePath = AppDomain.CurrentDomain.BaseDirectory;
string exeName = Path.Combine(basePath, AppDomain.CurrentDomain.FriendlyName.Replace(".vshost", ""));
Process.Start(@"C:Program Files (x86)Microsoft SDKsWindowsv8.1AbinNETFX 4.5.1 Toolsx64ildasm.exe", string.Format(@"/item:ILDasm.TestClass::DoIt ""{0}"" /text /output:code.il", exeName));
}
}
public class TestClass
{
public string DoIt(object value)
{
StringBuilder sb = new StringBuilder();
return sb.ToString();
}
}
}
Смысл программы в том, что она запускает встроенный в студию дизассемблер ildasm [4] и натравливает его на функцию DoIt класса TestClass. Байт-код тела этой функции помещается в файл code.il, который затем можно открыть и проанализировать. Привожу байт-код функции DoIt (лишнее убрано):
IL_0000: newobj instance void [mscorlib]System.Text.StringBuilder::.ctor()
IL_0005: stloc.0
IL_0006: ldloc.0
IL_0007: callvirt instance string [mscorlib]System.Object::ToString()
IL_000c: ret
Вещество в черепной коробке с сочетании с методом проб и ошибок поможет сгенерировать код по аналогии, т.е. пишем тело функции DoIt наподобие того, что мы хотим получить в нашей сгенерированной функции, запускаем утилитку, смотрим код и воплощаем его в генераторе.
Все построено на стеке.
Если мы хотим выполнить операцию сложения a и b, надо поместить в стек значение переменной a, затем поместить в стек значение переменной b, затем вызывать команду сложения (add). При этом стек очищается от a и b, а на его вершину помещается результат сложения. Если после этого мы хотим умножить сумму на c, помещаем его значение в стек (помним, сейчас там уже есть сумма a+b) и вызываем операцию умножения (mul).
Итоговый байт-код:
IL_0000: ldarg.1
IL_0001: ldarg.2
IL_0002: add
IL_0003: ldarg.3
IL_0004: mul
А вот как это выглядит в C#:
cs.Emit(OpCodes.Ldarg_1);
cs.Emit(OpCodes.Ldarg_2);
cs.Emit(OpCodes.Add);
cs.Emit(OpCodes.Ldarg_3);
cs.Emit(OpCodes.Mul);
Аналогично вызываются методы и конструкторы (помещаем в стек аргументы и вызываем метод/конструктор). При этом для нестатических методов, первым в стек необходимо положить экземпляр класса, метод которого мы вызываем.
Данная статья не имеет целью полное обучение генерации байт-кода, так что продолжим рассуждения про генератор.
Ядро генератора заключено в функции, реализующей его интерфейс (IBuilderGenerator):
/// <summary>
/// сгенерировать функцию построителя
/// </summary>
/// <param name="pattern"> распарсеный шаблон </param>
/// <param name="modelType"> тип модели </param>
/// <returns></returns>
public virtual BUILDER GenerateBuilder(List<PatternElement> pattern, Type modelType)
{
if (modelType == null)
{
// модель не задана, следовательно возвращаем константную строку
StringBuilder sb = new StringBuilder();
foreach (PatternElement item in pattern)
{
string nullValue = item.GetNullValue();
if (nullValue != null)
{
sb.Append(nullValue);
}
}
string value = sb.ToString();
return (m) => value;
}
else
{
// создаем новый тип класса генератора
string methodName = "Generate_" + Guid.NewGuid().ToString().Replace("-", "");
// генерируем динамический метод
var genMethod = new DynamicMethod(methodName, typeof(string), new Type[] { typeof(object) }, true);
// получаем генератор кода
var cs = genMethod.GetILGenerator();
var sb = cs.DeclareLocal(typeof(StringBuilder));
var m = cs.DeclareLocal(modelType);
ReflectionBuilderGeneratorContext context = new ReflectionBuilderGeneratorContext
{
Generator = cs,
ModelType = modelType,
VarSB = sb,
VarModel = m,
};
// распаковываем модель
cs.Emit(OpCodes.Ldarg_0);
cs.Emit(OpCodes.Isinst, modelType);
cs.Emit(OpCodes.Stloc, m);
// создаем StringBuilder с начальной емкостью размера шаблона
cs.Emit(OpCodes.Ldc_I4, pattern.Sum(e => e.EstimatedLength));
cs.Emit(OpCodes.Newobj, typeof(StringBuilder).GetConstructor(new Type[] { typeof(int) }));
cs.Emit(OpCodes.Stloc, sb);
foreach (PatternElement item in pattern)
{
MethodInfo processor;
if (_dicProcessors.TryGetValue(item.GetType(), out processor))
{
// найден генератор
processor.Invoke(processor.IsStatic ? null : this, new object[] { context, item });
}
}
cs.Emit(OpCodes.Ldloc, sb);
cs.Emit(OpCodes.Callvirt, typeof(object).GetMethod("ToString", Type.EmptyTypes));
cs.Emit(OpCodes.Ret);
return genMethod.CreateDelegate(typeof(BUILDER)) as BUILDER;
}
}
Здесь нам как раз и пригодились метод GetNullValue() и свойство EstimatedLength элемента шаблона.
Особенностью генератора является его расширяемость, т.к. он не привязан к описанным в начале типам элементов шаблона — строковой константе и выражению. При желании вы можете придумать свои собственные элементы и, наследовав данный генератор, добавить функции, отвечающие за генерацию байт-кода для созданных вами типов элементов. Для этого в коде вы должны описать функцию с атрибутом PatternElementAttribute, например, генерация кода для строковой константы, включенная в стандартную реализацию генератора, описана так:
[PatternElement(typeof(StringConstantElement))]
protected virtual void GenerateStringConstantIL(ReflectionBuilderGeneratorContext context, StringConstantElement element)
{
if (element.Value != null)
{
WriteSB_Constant(context, element.Value);
}
}
/// <summary>
/// записать в StringBuilder строковое значение
/// </summary>
/// <param name="context"> контекст генерации </param>
/// <param name="value"> значение </param>
protected virtual void WriteSB_Constant(ReflectionBuilderGeneratorContext context, string value)
{
if (value != null)
{
var cs = context.Generator;
cs.Emit(OpCodes.Ldloc, context.VarSB);
cs.Emit(OpCodes.Ldstr, value);
cs.Emit(OpCodes.Callvirt, _dicStringBuilderAppend[typeof(string)]);
cs.Emit(OpCodes.Pop);
}
}
Код других методов приводить не стану, т.к. он очень громоздкий, но если у вас возникнут вопросы, постараюсь ответить на них отдельно.
Т.к. у меня нет возможности сравнить свой шаблонизатор с каким-либо другим, я проведу сравнение с захардкоденным генератором шаблона на основе string.Replace().
/// <summary>
/// запуск теста
/// </summary>
private void Run()
{
// вспомогательные переменные
string outputPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Result");
if (!Directory.Exists(outputPath)) Directory.CreateDirectory(outputPath);
Random rnd = new Random(0);
Stopwatch sw = new Stopwatch();
// читаем шаблон
string pattern = GetPattern();
// вспомогательные переменные
string text;
double patternTotal = 0; // общее время для заполнения шаблонов (шаблонизатор)
double patternInitialization; // инициализация (шаблонизатор)
double patternFirst = 0; // первый шаблон (шаблонизатор)
double manualTotal = 0; // общее время для заполнения шаблонов (вручную)
// готовим шаблонизатор
sw.Restart();
Patternizator patternizator = new Patternizator(pattern);
sw.Stop();
patternInitialization = sw.Elapsed.TotalMilliseconds;
Console.WriteLine("Сборка {0} (v. {1})", patternizator.GetType().Assembly.GetName().Name, patternizator.GetType().Assembly.GetName().Version);
// генерируем сообщения в цикле
for (int i = 0; i < COUNT_PATTERNIZATOR; i++)
{
// создаем модель
User user = new User
{
Surname = RandomElement(rnd, SURNAMES),
Name = RandomElement(rnd, NAMES),
Patronymic = RandomElement(rnd, PATRONYMICS),
// дата рождения 1950 - 1990 гг
Birthdate = new DateTime(1950, 1, 1).AddDays(rnd.NextDouble() * 40.0 * 365.25)
};
var model = new
{
User = user,
UserName = user.Name,
Now = DateTime.Now,
};
// заполнение с использованием шаблонизатора
sw.Restart();
text = patternizator.Generate(model);
sw.Stop();
patternTotal += sw.Elapsed.TotalMilliseconds;
if (i == 0)
{
patternFirst = sw.Elapsed.TotalMilliseconds;
}
// заполнение через замену строку
if (i < COUNT_MANUAL)
{
// ВНИМАНИЕ! Данный код сильно упрощен и не может быть использован в реальном приложении
// Его цель - сравнение скорости замены строк через Replace и динамической функции
sw.Restart();
{
StringBuilder sb = new StringBuilder(pattern);
DateTime now = DateTime.Now;
sb.Replace("{User.GetFIO()|юзверь}", model.User.GetFIO() ?? "юзверь");
sb.Replace("{UserName|юзверь}", model.UserName ?? "юзверь");
sb.Replace("{User.Age:0}", model.User.Age.ToString("0"));
sb.Replace("{Now:dd MMMM yyyy}", now.ToString("dd MMMM yyyy"));
sb.Replace("{Now:HH:mm:ss}", now.ToString("HH:mm:ss"));
text = sb.ToString();
}
sw.Stop();
manualTotal += sw.Elapsed.TotalMilliseconds;
}
}
WriteHeader("Шаблонизатор");
WriteElapsedTime("Инициализация шаблонизатора", patternInitialization);
WriteElapsedTime("Заполнение первого шаблона", patternFirst);
Console.WriteLine();
WriteElapsedTime(string.Format("Общее время для заполнения {0} шаблонов", COUNT_PATTERNIZATOR), patternTotal);
WriteElapsedTime("Среднее время заполнения шаблона", patternTotal / COUNT_PATTERNIZATOR);
WriteHeader("Вручную (хардкод)");
WriteElapsedTime(string.Format("Общее время для заполнения {0} шаблонов", COUNT_MANUAL), manualTotal);
WriteElapsedTime("Среднее время заполнения шаблона", manualTotal / COUNT_MANUAL);
Console.WriteLine();
Console.WriteLine("Нажмите любую клавишу для продолжения...");
Console.ReadKey();
}
Скриншот:

Понимаю, что статья получилась большой, возможно даже черезчур, но и тема, затронутая в ней не так проста. Поэтому прошу отписаться, какие вопросы в генерации байт-кода вы бы хотели узнать поподробнее. По этим вопросам я постараюсь написать отдельные статьи.
Автор: Doomer3D
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/84292
Ссылки в тексте:
[1] https://sourceforge.net/projects/open-genesis/?source=navbar: https://sourceforge.net/projects/open-genesis/?source=navbar
[2] Patternizer.zip: http://sourceforge.net/projects/open-genesis/files/Patternizer.zip/download
[3] байт-код: https://ru.wikipedia.org/wiki/Common_Language_Runtime
[4] ildasm: https://msdn.microsoft.com/ru-ru/library/f7dy01k1(v=vs.110).aspx
[5] Локализация проектов на .NET с интерпретатором функций: http://habrahabr.ru/post/190556/#first_unread
[6] Источник: http://habrahabr.ru/post/251765/
Нажмите здесь для печати.