Думаю, многие C# разработчики с нетерпением ждали в C# 6.0 появления первичных конструкторов и record’ов и были огорчены тем, что эта фича была отложена до 7-й версии. Под конец рабочего четверга желание иметь неизменяемые типы во что бы то ни стало пересилило во мне терпение и я решил написать утилиту, генерирующую их. Кому интересно — прошу под кат.
Постановка задачи видилась предельно ясно, record должен содержать:
- Свойства с публичными getter-ами
- Конструктор с параметрами для инициализации всех свойств
- Метод Copy() с таким же набором параметрв, но имеющий для каждого значение по умолчанию
- Перегрузки Equals и GetHashCode, реализацию IEquatable
- Операторы == и !=
В общем, всё как в case-классах в Scala.
Для описания record’ов был взят слегка упрощённый синтаксис C#:
namespace Records { using System; record Test { Int32 Id; String Name; Nullable<Decimal> Amount; } }
Разбор текста осуществляется с помощью Nemerle.PEG, получилась вот такая грамматика:
grammar { ANY = !['\u0000'..'\u001F'] !'\u007F' ['\u0000'..'\uFFFF']; ws : void = ("\r\n" / "\n" / "\r" / "\t" / ' ')*; letter = [Lu, Ll, Lt, Lm, Lo]; digit = ['0'..'9']; keyword = "using" / "record" / "namespace"; identifier : string = letter (letter / digit)*; path : string = identifier ("." identifier)*; genericTypeDefinition : string = identifier ws"<"ws (genericTypeDefinition / identifier)(ws","ws (genericTypeDefinition / identifier))* ws">"; property : PropertyDefinition = !keyword (genericTypeDefinition / identifier) ws identifier ws";"; properties : List[PropertyDefinition] = (ws property ws)+; import : ImportDefinition = "using" ws path";"; record : RecordDefinition = "record" ws identifier ws "{" ws property (ws property)* ws "}"; nmspace : NamespaceDefinition = "namespace" ws path ws "{" (ws import)* ws record (ws record)* ws "}" ws !ANY; }
По полученному в результате работы парсера DOM генерируется исходный код C# с помощью CodeDOM, который затем компилируется в сборку с помощью CSharpCodeProvider.
Для простоты реализации было внесено ограничение — в каждом файле должен находится новый namespace (в дальнейшем планирую убрать это ограничение). В остальном язык получился гибким: namespace можно сразу же импортировать в другие файлы, объявленые типы можно сразу же использовать как типы полей в других record’ах.
Приведу простой пример использования.
Создадим файл Units.rcs со сделующим содержанием:
namespace Units { using System; record Unit1 { Int32 Id; String Name; } record Unit2 { Int32 Id; Unit1 Unit; Decimal Amount; } }
а также Delivery.rsc
namespace Delivery { using System; using Units; record Address { String CityName; String Street; String House; } record Package { Address Destination; Unit2 Contents; } }
Для того, чтобы получить сборки нужно выполнить следующую команду:
RecSharp -i Units.rcs Delivery.rcs -o Records.dll
В результате будет получена сборка, которую можно подключить к проекту и пользоваться объектами.
Проект можно пощупать здесь:
RecSharp
(в Releases есть бинарники для тех, кто не хочет ставить Nemerle)
В перспективах возможно перееду с CodeDOM на Roslyn, но после первого беглого осмотра его API для кодогенерации выглядит сложнее, чем у CodeDOM.
Буду рад, если утилита будет кому-то полезна)
ссылка на оригинал статьи http://habrahabr.ru/post/269453/
Добавить комментарий