Коли немає сил чекати Record's

Думаю, багато хто 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;
Unit Unit1;
Decimal Amount;
}
}

а також Delivery.rsc
namespace Delivery {
using System;
using Units;

record Address {
String CityName;
String Street;
String House;
}

record Package {
Destination Address;
Unit2 Contents;
}
}

Для того, щоб отримати складання потрібно виконати наступну команду:
RecSharp -i Units.rcs Delivery.rcs -o Records.dll

У результаті буде отримана збірка, яку можна підключити до проекту і користуватися об'єктами.
Проект можна помацати тут:
RecSharp
(у Releases є бінарники для тих, хто не хоче ставити Nemerle)

У перспективах можливо переїду з CodeDOM на Roslyn, але після першого побіжного огляду його API для кодогенерации виглядає складніше, ніж у CodeDOM.

Буду радий, якщо утиліта буде комусь корисна)

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.