Как стать автором
Обновить

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

Время на прочтение3 мин
Количество просмотров18K
Думаю, многие 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

В результате будет получена сборка, которую можно подключить к проекту и пользоваться объектами.
Так-же можно воспользоваться расширением для VisualStudio, генерирующим исходники наподобие T4.
Проект можно пощупать здесь:
RecSharp
(в Releases есть бинарники для тех, кто не хочет ставить Nemerle)
Расширение для VisualStudio:
RecSharp.VisualStudio
(опять таки, в релизах лежит собраный .vsix)

В перспективах возможно перееду с CodeDOM на Roslyn, но после первого беглого осмотра его API для кодогенерации выглядит сложнее, чем у CodeDOM.

Буду рад, если утилита будет кому-то полезна.
Теги:
Хабы:
Всего голосов 19: ↑16 и ↓3+13
Комментарии54

Публикации

Истории

Работа

Ближайшие события