Когда нет сил ждать 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.
Буду рад, если утилита будет кому-то полезна)