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

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

Posts Tagged ‘resharper

Лайфхак с использованием строковых литералов

4 комментария

Хочу рассказать про лайфхак с использованием стороковых литералов. Строковый литерал сам по себе – достаточно унылая вещь, но есть один нюанс – лексер языкового сервиса (если у вас такой имеется) хорошо понимает начало и конец литерала, и может разом выдать содержимое как System.String. А получив полноценную CLR-строку, с ней можно очень много всего сделать.

Да, сразу подчеркну – лайфхак этот не очень-то зависит от языка, хотя конечно я его использую в C#.

Строка как магический псевдоконструктор

Первое наблюдение – это то что многие объекты поддерживают метод Parse() – не только int или decimal, но такие объекты как DateTime, TimeSpan, XElement и так далее. Это значит что написав строку, отдаленно похожую на дату, я могу сконвертировать ее в правильный вызов конструктора DateTime:

Особенно эффектно это выглядит для XML, где сложный кусок этого языка может превратиться в красивую цепочку из конструкторов XElement, XAttribute и так далее.

Строка как неживая часть кода

Языки можно мешать – например HTML и C# в MVCшных вьюшках. И если вы не боитесь временно – примерно на долю секунду которая нужна чтобы выполнить контекстное действие – потерять контроль над кодом, то кто мешает вам, например, делать string splicing в PHP-стиле, но в коде C#?

Вот что я имею ввиду:

В примере выше, мы вставили идентификаторы C# прямо в строку, а потом вызвали контекстное действие, которое заменило всю строку на правильный вызов String.Format().

Строка как мимолетно существующий DSL

В погоне за оператором ?. (цепочная процерка на null), многие сели на Roslyn и аналогичные вещи. Но ведь проблема цепочной проверки может очень просто решиться если вместо введения оператора мы дороворимся, что любое выражение в форме a.b.c может быть написано как строковый литерал и, мановением волшебной палочки превратиться в

a == null ? null : (a.b == null ? null : a.b.c)

Ну или что-то в этом духе. Опять же, строка которая толком ничего не означает может быть развернута вот в такое. Код, конечно, не очень читабельный. Но всяко лучше чем пытаться делать то же самое руками.

Внимательные читатели заметят, что то же самое можно извлечь и из вполне валидного идентификатора. Что ж, не спорю. Мне просто лень по нему гулять – для меня сделать string.Split() намного проще :)

Подобных встроенных DSLов можно сделать море. Например, можно взять нотацию HTML Zen и из этого строкового литерала создать декларацию XLinq которая соответсвует исходному коду. Возможности безграничны.

Как это работает

Сразу скажу – реализовать подобное тривиально. Для Решарпера, нужно просто контекстное действие, которое говорит вам что вы «на строке», создает новый кусок кода, и делает обмен:

[ContextAction(Group = "C#", Name = "Hide string value",
  Description = "Ensures the string is not human-readable in code.",
  Priority = 15)]
public class StringHideRealValueCA : IContextAction
{
  private readonly IList<IBulbItem> items = new List<IBulbItem>();
  private readonly ICSharpContextActionDataProvider provider;
  public StringHideRealValueCA(ICSharpContextActionDataProvider provider)
  {
    this.provider = provider;
  }
  public bool IsAvailable(IUserDataHolder cache)
  {
    var e = provider.GetSelectedElement<ICSharpLiteralExpression>(true, true);
    if (e != null && e.IsConstantValue())
    {
      if (e.ConstantValue.IsString())
      {
        var s = (string)e.ConstantValue.Value;
        if (!string.IsNullOrEmpty(s))
          items.Add(new StringHideRealValueImpl(provider, e));
      }
    }
    return items.Count > 0;
  }
  public IBulbItem[] Items
  {
    get { return items.ToArray(); }
  }
}

В коде выше, StringHideRealValueProvider прячет естественную строку, подменяя ее Base64-закодированной строкой. Вот, собственно, реализация:

private class StringHideRealValueImpl
{
  // очевидные методы опущены
      protected override Action<ITextControl> ExecutePsiTransaction(ISolution solution, IProgressIndicator progress)
  {
    CSharpElementFactory factory = CSharpElementFactory.GetInstance(provider.PsiModule, true);
    var value = literal.ConstantValue.Value as string;
    if (value != null)
    {
      string encoded = Convert.ToBase64String(Encoding.Unicode.GetBytes(value));
      ICSharpExpression ex = factory.CreateExpressionAsIs(
        string.Format("System.Text.Encoding.Unicode.GetString(System.Convert.FromBase64String(\"{0}\"))", encoded));
      literal.ReplaceBy(ex);
    }
    return
      tc => provider.PsiFile.OptimizeImportsAndRefs(
        provider.Document.DocumentRange.CreateRangeMarker(provider.Document), false, true, progress);
  }
}

Если коротко, в замену существующему литералу создается выражение (через вызов CreateExpressionAsIs), а потом банально втыкается в замен существующей строки. На базе этого вы можете писать свои механизмы подмены – это тривиально!

Заключение

Пример выше – это маленький лайфхак который иногда помогает писать код быстрее. String splicing особенно полезен, хотя нужно приучить себя им пользоваться. Помните – эти строки только несколько секунд «магические», а потом перестают существовать совсем. Так что ничего страшного.

Written by Dmitri

17 декабря 2011 at 1:30

Опубликовано в .NET

Tagged with

Горячая подмена в .Net приложениях

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

Дело было вечером, делать было нечего, вот и решил я написать еще один сугубо технический пост про то, как делать “горячую подмену” сборок в приложениях. Напомню, что это уже второй пост на эту тему (вот первый), но в этом посте я хочу более детально рассказать (не вдаваясь в самые мясистые детали) о том, что же такое горячая подмена и как она делается.

Суть задачи

Я пишу плагин для Решарпера. Чтобы заниматься отладкой этого плагина, мне нужно запускать дополнительный инстанс Visual Studio, который занимает нехилое кол-во времени при стартапе. И понятное дело что при каждом изменении с моей стороны нужно запускать этот процесс снова, благо Edit & Continue – это миф, и там реально ничего не работает.

Мне это не нравится. Я хочу чтобы было лучше.

В частности, я хочу редактировать плагин прямо при исполнении и чтобы “все работало”. Сразу. Без задержек. И тут-то возникают проблемы.

Как это решается сейчас

Допустим у меня есть некий класс который наследует от BulbItemImpl — не важно что это за класс, суть не важна. Важно, в основном то, что этот класс создают другие мои классы в плагине примерно вот так:

// вот мой класс
class MyItem : BulbItemImpl
{
  ...
}
 
// и вот как я его создаю
var i = new MyItem(...);

Соответственно если я хочу подменить реализацию MyItem, я не смогу больше делать new, т.к. в том случае будет создаваться старый MyItem, а не новый.

Вклиниваем контейнер

Следовательно, я делаю небольшой трик: без особого детерминизма я втыкаю Unity как статический класс с соответствующим набором extension методов:

public static class ContainerExtensions
{
  static UnityContainer container;
  
  static ContainerExtensions()
  {
    container = new UnityContainer();
  }
  
  static TBase Get<TBase,TImpl>(this object o, params object [] args)
  {
    return container.Resolve<TBase>(typeof(TImpl).Name, new InjectionConstructor(args));
  }
  
  static void Set<TBase>(this object o, Type implType)
  {
    container.RegisterType(typeof(TBase), implType, typeof(TImpl).Name);
  }
}

Вот такое вот шаманство. Ничего странного не находите? Как насчет typeof(TImpl).Name? Давайте поясню что тут происходит. У нас много классов наследуют от BulbItemImpl, следовательно нам как бы некошерно просто делать container.RegisterType<BulbItemImpl,MyItem> потому что эта регистрация будет 100 раз перекрыта. Поэтому мы делаем именную регистрацию по имени типа конечного результата. Это будет полезно дальше.

Делаем Impl-прослойку

Итак, у нас есть класс который выглядит, например, вот так:

public sealed class RequireOrEnsureBulbItem : BulbItemImpl
{
    // Fields
    private readonly string block;
    private readonly IMethodDeclaration method;
    private readonly ICSharpContextActionDataProvider provider;
    private readonly string text;
    // Methods
    public RequireOrEnsureBulbItem(ICSharpContextActionDataProvider provider, IMethodDeclaration method, string text, string block);
    protected override Action<ITextControl> ExecuteTransaction(ISolution solution, IProgressIndicator progress);
    // Properties
    public override string Text { get; }
}

Соответственно, мы берем и переименовываем этот класс, добавляя префикс Impl.

Теперь, берем и делаем его копию. Копия эта является прозрачным прокси поверх оригинала. Копия содержит инстанс Impl-типа:

private BulbItemImpl impl;

который инициализируется в конструкторе:

public RequireOrEnsureBulbItem(ICSharpContextActionDataProvider provider,
  IMethodDeclaration method, string text, string block)
{
  impl = this.Get<BulbItemImpl,RequireOrEnsureBulbItemImpl>(
    provider, method, text, block);
}

и используется для всех пробросов вызовов методов (в т.ч. свойств), например

public override string Text { get { return impl.Text; } }

Что дальше?

Мы только что разделили один класс на интерфейс, который загружается единожды, и реализацию, которую можно, в принципе, менять в контейнере. Но как? На самом деле – очень просто. Для начала давайте решим как мы будем знать что можно редактировать а что нет. Лучший способ – пометить это типы (это сгенерированные Impl-типы) каким-нибудь аттрибутом вроде [Editable]. Тогда мы будем всегда знать что можно генерировать а что нет.

Теперь берем, и как я уже описал, открываем редактор на этот Impl-тип. Следует помнить что редактируем мы тип без названия Impl, т.к. только для него у нас есть сорцы. Мы ничего не переименовываем, а просто создаем новый тип (наследующий, в данном примере, от BulbItemImpl) и регистрируем его в контейнере.

this.Set<BulbItemImpl>(GeneratedType);

Теперь, по новому запросу будет выдаваться новый тип, в то время как если старый тип загружен где-то в памяти, мы будет продолжать им пользоваться.

Заключение

То что я описал выше – это и есть “горячая подмена” функционала. С помощью нее я могу редактировать плагин прямо в момент исполнения, проверять API, тестировать новые идеи и при этом не напрягаться с перезапуском отладчика. Понятное дело что отладка динамически сгенерированных сборок у меня не работает (не напрягаюсь я на тему PDB), но это не так критично как получить ответ на простой вопрос “работает оно или нет?”.

Written by Dmitri

29 декабря 2010 at 1:03

Опубликовано в .NET

Tagged with