Дмитpий Hecтepук

Блог о программировании — C#, F#, C++, архитектура, и многое другое

Сокращенный генератор C# в стиле Zen Coding

12 комментариев

Уверен что много разработчиков слышали про такую вещь как Zen Coding. Если нет – скажу лишь что это методика очень быстро верстать HTML. Лично я ее использую и безумно доволен. (Все мои статьи написаны через zen coding, многие используют лично написанные расширения.)

Так вот, пришла идея сделать то же самое на C#. Зачем? Отчасти потому что идея SharpWizard как дизайнера классов если не провалилась то по крайней мере не пользуется лично у меня (как автора) никаким успехом. О причинах не знаю, знаю лишь что люблю работать чисто с кодом, а отвлекаться на интерфейс (пусть и красивый WPF-интерфейс с анимацией) – это не моё. Поэтому давайте потеоретизируем и подумаем, можно ли сделать Zen Coding для C#.

Макетирование сущностей

Возьмем мой любимый демонстрационный класс:

public class Person
{
  public string Name { get; set; }
  public int Age { get; set; }
}

Избыточность повсюду, коллеги. Давайте сначала возьмем классификаторы видимости (в данном случае public) и заменим их на UML-аналоги. Забыли что это такое? Напоминаю:

  • +public; n.b.: на +ы можно ‘забить’, т.е. считаю что без плюса все равно должно быть public а не private как считает c#. А отсутствие modifier-а будем считать моветоном и казнить неверных.

  • /protected

  • -private

  • \internal

  • /\protected internal

Ухх, веселья-то! Я сюда могу даже воткнуть классификаторы для abstract, static, ну вы поняли. Класс отрефакторился до вот такого:

+c:Person
{
  + string Name { get; set; }
  + int Age { get; set; }
}

Теперь давайте рефакторить дальше. Свойства тоже можно закодировать по методике применения, например вот так:

  • p – свойство с автоматически созданным полем

  • a – автосвойство с { get; set; }

  • dp, ap – угадайте :) ага – dependency property и attached property

Ну и так далее – юмор думаю понятен. Рефакторим снова:

+c:Person
{
  +ap:string Name
  +ap:int Age
}

Ну и наконец, когда быстро прототипируешь, нужно еще иметь возможность обернуть все это в namespace и добавить импорты по вкусу. Переносы строк и скобочки – явно лишнее, т.к. ситуция тут очевидная – в пространстве имен классы, в классе свойства (в свойстве яйцо, в яйце игла, и т.д.)

ns: i:s i:scg +c:person +ap:string name +ap:int age

N.b.: я могу даже пойти дальше и заменить string на s, int на i ну и так далее. А что—имею право.

Заметили как весь класс стал lowercase? Это чтобы ускорить печатанье. Вот HTML-верстальщикам все равно, нам конечно нет, но тут капитализация поможет. Итак, попробую расшифровать что же мы написали.

  • ns: – декларация пространства имен. ns:my_company.product тоже сработало бы, а ns: просто делает дефолтный неймспейс для проекта

  • i:s – превращается в import System

  • i:scg – хмм, угадайте… угу, становится import System.Collections.Generic

  • +c:person становится public class Person

  • Ну дальше надеюсь понятно =)

Моя главная проблема – быстрое создание сущностей. Но методы тоже важны. Поэтому написав -m:bool can_vote получим public bool CanVote() { ... }. И так далее. Это если без параметров. Если с параметрами, тогда будет вот так:

+m:i add i a i b
// уже догадались? правильно, это то же самое что и
public int Add(int a, int b)
{
}

Что еще может быть в классе? Делегаты? События? Дженерики? Все эти кейсы разруливаются и вообще не попадают в тот 95% use case о котором я сейчас думаю. Если что – можно и ручками написать. Или сниппетом воспользоваться.

Константы и прочие сокращения

Ах да, забыл что можно все еще больше сократить. Во-первых есть константы. Тут можно тип заменить на значение, т.к. только ежику непонятно что строка обернутая в кавычки – действительно строка. Вот что я имею ввиду:

// пишем (f = field, c = constant)
+fc:name "Dmitri"
// а получаем
public const string name = "Dmitri";

Конечно, на совести парсера распознавать типы, но на то у нас есть x.TryParse() и иже с ним. Все разрулят.

Теперь к слеующей «прелести», а именно тупой инициализации объекта – да-да, той которая все равно будет «перекинута» в конструктор. Так вот, тут все еще более примитивно – берем и пишем new перед типом. Результат таков:

// пишем (ht = hashtable)
-f:new ht cache
// а получаем
private Hashtable cache = new Hashtable();

Имплементация

Итак, предлагается сделать парсер (и встроить поддержку в Visual Studio 2010) для того, чтобы быстро определять сущности с помощью магического языка (shorthand notation), который знают только я и вы. А дальше можно расширять.

В этой ситуации у меня как всегда возникает желание погенерировать чего-нибудь на ANTLR/fslexx/fsayy, но надо себя сдерживать – пожалуй этот минипарсер я напишу на старом добром C#.

Старый добрый CodeBuilder

Собирать код используя StringBuilder непрактично, но с другой стороны это хорошая основа. Мне нужен, собственно, лишь вот такой интерфейс:

internal interface IStringBuilder
{
  IStringBuilder AppendLine(string text);
  IStringBuilder Append(string text);
  IStringBuilder Indent();
  IStringBuilder Unindent();
}

Реализация этого интерфейса базируется на StringBuilder но также поддерживает индентацию. Вот собственно что у меня получилось:

internal class CodeBuilder : IStringBuilder
{
  private readonly StringBuilder sb = new StringBuilder();
  private int indentLevel = 0;
  private const int indentSize = 2;
  private string IndentText
  {
    get
    {
      return string.Empty.PadRight(indentLevel*indentSize);
    }
  }
  public IStringBuilder AppendLine(string text)
  {
    sb.Append(IndentText).AppendLine(text);
    return this;
  }
  public IStringBuilder Append(string text)
  {
    sb.Append(IndentText).Append(text);
    return this;
  }
  public IStringBuilder Indent()
  {
    ++indentLevel;
    return this;
  }
  public IStringBuilder Unindent()
  {
    --indentLevel;
    return this;
  }
  public override string ToString()
  {
    return sb.ToString();
  }
}

Посетитель

Сам сборщик, будучи объектно-ориентированным, использует паттерн Visitor. Интерфейс примитивен:

internal interface IVisitable
{
  void Visit(IStringBuilder sb, ConversionOptions co);
}

Типичная реализация интерфейса (прошу заметить что я не использую CodeDom) выглядит примерно вот так:

internal class Property : IVisitable
{
  internal string Name { get; set; }
  internal string Type { get; set; }
  internal Visibility Visibility { get; set; }
  public void Visit(IStringBuilder sb, ConversionOptions co)
  {
    sb.AppendLine("{0} {1} {2} { get; set; }".ƒ(Visibility.AsString(), Type.type(), Name.cap()));
  }
}

Двойные скобки вокруг get и set поел TypograFix.

Странная буква ƒ – это fluent замена для string.Format().

Разбор дерева ведется рекурсивно, поэтому параметры sb (собственно посетитель) и co (плейсхолдер для опций конверсии) пробрасываются рекурсивно по всем элементам. Например:

internal class Namespace : IVisitable
{
  ...
  public void Visit(IStringBuilder sb, ConversionOptions co)
  {
    sb.AppendLine("namespace {0}".ƒ(Name.cap()));
    sb.AppendLine("{").Indent();
    foreach (var c in Classes)
      c.Visit(sb, co);
    sb.Unindent().Append("}");
  }
}

Парсер

Парсер – это самый сложный аспект всего дела, поэтому я покажу лишь кусочек. Суть в том, что получив строку, поделить ее на токены.

IVisitable context = null;
IVisitable rootContext = null;
List<string> terms = new List<string>(
  input.Split(new[]{' '}, StringSplitOptions.RemoveEmptyEntries));
bool needInstruction = true;
for (int i = 0; i < terms.Count; i++)
{
  string term = terms[i];
  ...
}

Каждый токен потом тихо парсится в зависимости от того, что он из себя представляет. Например, если это ns:Xxx, то это простанство имен. И так далее. Вот собственно пример более полного разбора:

string[] parts = term.Split(':');
string classifier = parts[0];
string vis = ExtractVisibility(ref classifier);
if (classifier == "ns")
{
  // namespace
  Namespace n = new Namespace();
  if (parts.Length > 1)
    n.Name = parts[1];
  else
    n.Name = GetDefaultNamespace();
  context = n;
  if (rootContext == null)
    rootContext = n;
}

Для других элементов все чуть посложнее, но не настолько чтобы оправдать использование ANTLR или аналогичных методов.

Немного о мэппингах

Все-таки аттрибуты в C# крайне неудобны в плане работы. Мне например пришла в голову идея декорировать перечисление Visibility различными строковыми константами, для чего я создал следующий аттрибут:

internal class ShorthandAttribute : Attribute
{
  internal string Shorthand { get; private set; }
  internal string Expanded { get; private set; }
  internal ShorthandAttribute(string shorthand, string expanded)
  {
    Shorthand = shorthand;
    Expanded = expanded;
  }
}

После написания этого аттрибута я, довольный, определил мою структуру Visibility согласно правилам которые описал в начале этого поста:

internal enum Visibility
{
  [Shorthand("+", "public")]
  Public,
  [Shorthand(@"\", "internal")]
  Internal,
  [Shorthand(@"/\", "protected internal")]
  ProtectedInternal,
  [Shorthand("/", "protected")]
  Protected,
  [Shorthand("-", "private")]
  Private,
}

Но когда мне потреботвалось получить данные их этих аттрибутов, пришлось лезть в Google только для того чтобы распознать, как же вытаскивать эту информацию. Естественно, все делается через reflection – вот пример разбора:

static CSharpShorthandParser()
{
  ...
  Type VisibilityType = typeof(Visibility);
  var props = VisibilityType.GetProperties().Where(p => p.PropertyType.IsEnum);
  foreach (var prop in props)
  {
    object [] atts = prop.GetCustomAttributes(typeof (ShorthandAttribute), false);
    ShorthandAttribute sa = atts[0] as ShorthandAttribute;
    ...
  }
}

Как-то все это неопрятно, мне кажется. Механизм должен работать более эффективно, так чтобы не нужно было «гуглить» варианты использования. Вообще лучше когда это все поддерживается через API, например через те же extension-методы. Хмм, а это идея.

Идеологическая составляющая

В свое время я тоже думал что Zen Coding – это идиотизм. А сейчас пишу с помощью его этот пост. В основном этот подход экономит время когда нужно сделать что-то крупное, т.к. количество церемонии при создании крупных структур соответственно больше.

Возьмем несколько примеров. (Где тут мои объекты?) Ну вот например, перечисление visibility, если не считать аттрибутов (а они, я признаю, были откровенно плохой идеей), в нашем shorthand notation выглядат вот так:

\e:visibility public internal protected_internal protected private

Возьмем еще пример – помните наш интерфейс IStringBuilder? Определение его вообще минимально

\i:IStringBuilder m:. append_line m:. append s text m: indent m: unindent

Точка (.) в примере выше просто ссылается на имя класса, тем самым позволяя быстро делать fluent-интерфейсы.

Futures

На данный момент код сыроват, потестить его можно в оконном приложении скачав его с BitBucket. Если же говорить о возможных расширениях, то у меня в голове несколько идей, в частности:

  • Делать автореализацию популярных интерфейсов. Еще не знаю как это можно сделать, но например если я пишу, скажем, c:npc:person, то можно было бы сделать чтобы этот класс реализовывал INotifyPropertyChanged и чтобы все свойства посылали обновления.

  • Поддержка автогенерации некоторых фич, например полноинициализируещего (слово-то какое) конструктора. То есть если у нас есть person { name, age }, делать конструктор public Person(string name, int age). Можно даже использовать дефолтные параметры (C# 4).

  • Ну и естественно поддержка всяких других фич C#, которых просто море, хотя не то чтобы все из них нужны в этой shorthand-нотации.

Вот пока и все. Я допишу поддержку в Visual Studio (2010 конечно, а вы что думали?), потестирую, и если этот подход «прокатит» (а я еще точно не знаю), то обязательно напишу об этом. ■

Advertisements

Written by Dmitri

3 марта 2010 в 19:21

Опубликовано в C#

комментариев 12

Subscribe to comments with RSS.

  1. Dmitri, поясни плз, а чем не устраивает обычные snippet’ы в студии, на их базе получается можно сформировать множество таких правил.

    А так не вижу особого смысла в таком подходе, когда есть IntelliSense и R#, имхо я и так пишу мало кода.

    outcoldman

    3 марта 2010 at 20:25

    • И правила такие уже сформированы! Например сниппет prop уже встроен. Но я скорее не про это. Во-первых, в сниппетах нельзя использовать исполняемый код. То есть в R# можно если реализовывать свои макросы, но это напряжно. Во-вторых, все-таки по скорости недотягивает до шагания Tab’ом и заполнения плейсхолдеров.

      Я сам в свое время создал проекто по генеративным сниппетам, на csn.codeplex.com. Но все это слишком медленно и непрактично.

      Для меня подход изложенный выше это попытка сделать proof of concept и жить с этим. В свое время я вручную реализовал HTML Zen для TypograFix, и это одна из лучших фич этого редактора. (Особенно если учесть что я могу например писать теги по-русски, тем самым не переключая раскладку.)

      As they say, don’t knock it till you try it :)

      Dmitri

      3 марта 2010 at 20:33

      • Понял. Ну в общем то да, сложно сказать будет ли полезно пока не попробуешь :) Что ж, держи в курсе ;)

        outcoldman

        3 марта 2010 at 21:34

  2. По поводу скорости с решарпером и «шагания табом». С решарпером получается довольно быстро. Достаточно «вывернуть» работу на созданием класса. Я обычно не создаю классы вручную перед тем как их использовать. Начинаю писать тест и в нём например:
    var p = new Person^();
    p.Name^ = «zkot»;
    p.Age^ = 28;
    После того как закончил этот код. Возвращаюсь в места помеченные ^, нажимаю Alt-Enter, пару движений стрелками и затем Enter. Класс готов. Если бы мне нужно было написать метод:
    p.SomeMethod(10, «qwe»), опять таки нажму Alt-Enter+Стрелка вниз+Enter и «шагая табами» укажу названия параметров.
    Впрочем, хочется увидеть Ваш подход в действии. Сравнить. Подожду рабочей версии для vs2010 :)

    zkot

    3 марта 2010 at 23:10

    • Ок, думаю что когда все будет готово, я опубликую вебкаст того как это работает. Для наглядности, так сказать.

      Dmitri

      3 марта 2010 at 23:39

  3. Понравилась идейка. Сам тоже сначала Zen Coding не понимал, а сейчас ворвался — очень удобно.
    На досуге подумаю, какие фичи в Zen Coding для C# нужны мне — может что-нибудь полезное скажу =)

    Игорь

    4 марта 2010 at 0:33

  4. Одно дело интерпретивный недо-язык HTML, а другое дело декларативный C#.

    iNEOi

    4 марта 2010 at 18:31

  5. Beh, non è così semplice, Dimitri.
    Effettivamente il linguaggio HTML va «studiato» un po’ e quindi praticato con sagacia.
    Ci ho messo quasi 3 settimane a capire il Template del mio blog… ed ancora adesso ho diversi problemi. A volte si fa decisamente più in fretta a chiedere il supporto di una persona che lo fa per mestiere!
    Comunque sono contenta di avere letto le sue informazioni qui su wordpress.
    La ringrazio ed auguro un sereno fine settimana

    :-)claudine

    Claudine Giovannoni

    6 марта 2010 at 22:26

    • Non sono sicuro di ciò che il tuo commento indirizzi. Il mio post qui non ha nulla a che fare con HTML, ero semplicemente sottolineare che lo Zen di codificazione è una semplificazione del linguaggio HTML, che può trovare il suo corollario in C#.

      Dmitri

      11 марта 2010 at 15:45

  6. В Entrprise Architect protected модификатор обозначается как #:
    http://www.sparxsystems.com/resources/uml2_tutorial/uml2_classdiagram.html

    Alex@Net

    14 марта 2010 at 17:27

    • Чтобы напечатать # нужно нажать на shift, а цель моей нотации — экономить время.

      Dmitri

      14 марта 2010 at 20:12


Оставить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: