Когда нет сил ждать Record’ы

от автора

Думаю, многие 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *