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

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

Archive for the ‘.NET’ Category

Проект CallSharp: I/O Call Instrumentation на платформе .NET

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

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

callsharp_0_1_1

Сначала простой пример: у вас есть "abc", нужно получить "cba". Ниже я представил это схематично, и далее в статье я буду продолжать использовать такие заголовки.

ff0

Этот пример идеально иллюстрирует проблему, т.к. в .NET у строки нету метода Reverse(), и решений этой задачи – несколько. Например, можно написать вот так:

new string(input.Reverse().ToArray())

Следовательно, хотелось бы получить программу, которая сама выводила бы эти цепочки вызовов на основе входных и выходных данных, гуляя по .NET BCL API, делая все возмножные вызовы и проверяя их на соответствие. Звучит немного фантастично, да?

Давайте возьмем для начала простой пример:

ff1

В нашем случае abc – это точно строка, и ничто иное. Теперь нам предстоит понять что на abc можно вызвать чтобы получить ABC.

Нам повезло что строки в .NET немутабельны, и нам не нужно проверять изменения оригинальной строки после вызовов на ней – только выходного значения. А следовательно, мы можем взять и поискать все методы (а также свойства, которые в .NET тоже методы с приставкой get_), которые

  • Являются нестатическими методами класса string (System.String, если быть педантичными)

  • Могут не принимать ни одного аргумента

  • Возвращают строку

Примечательно, что «могут не принимать ни одного аргумента» – это три раздельных случая, а именно

  • Функция не имеет параметров вообще, т.е. Foo()

  • Функция может и имеет параметры, но у всех них есть дефолтные значения, т.е. Foo(int n = 0)

  • Функция берет упакованый список, т.е. Foo(params char[] letters)

Если руководствоваться этими критериями, мы получим список фунций string, которых было бы неплохо вызвать на строке "abc":

Normalize 
ToLower 
ToLowerInvariant 
ToUpper 
ToUpperInvariant 
ToString 
Trim
TrimStart
TrimEnd

Берем каждую из этих функций, вызываем на "abc", смотрим на результат. Подходят только две функции:

input.ToUpper()
input.ToUpperInvariant()

Ура, первая миссия выполнена!

ff2

Как понять, что за тип у числа 3 справа? Я предлагаю вот такой алгоритм:

  • Через reflection, берем все типы у которых есть метод TryParse().

  • Вызываем на всех данных. Если возвращает true – делаем боксинг распаршенного (неологизм?) объекта, возвращая его как object.

  • Не забываем, что любой ввод это как минимум string. А если ввод имеет длину 1, то это еще и char.

Согласно этому алгоритму, тройка (3) справа может быть и string и char (а также float или даже TimeSpan!), но в текущем примере, мы допустим что это все же Int32 или просто int.

Используя все тот же линейный поиск по нестатическим методам, мы моментально находим

input.Length

Естественно, что на самом деле это вызов функции get_Length(), но CallSharp заранее удаляет все ненужные декорации для удобства пользователя.

ff3

Читерский пример. Если бы я взял true, мне бы попался IsNormalized(), а так на не-статике вариантов нет. Что же, придется расширить наш алгоритм – теперь будем перебирать ещё и статические методы, которые

  • Не обязательно являются членами класса (в нашем случае – строки), но тем не менее попадают в список одобренных типов. Причина: я не хочу произвольно вызывать File.Delete(), например

  • Возвращают нужный нам тип (в данном случае – bool)

Расширив наш поиск до статики, мы получили два вполне корректных результата:

string.IsNullOrEmpty(input)
string.IsNullOrWhiteSpace(input)

Прекрасно! Давайте что-нибудь посложнее уже!

ff4

Ухх, тут ситуация посложнее – "abc ", то есть два пробела на конце: это одной функцией уже не получить. Надо делать цепочку вызовов. В данном случае цепочка не должна быть stringstringstring, она может быть stringчто угодноstring, т.к. промежуточные данные нам не важны.

Именно на этом этапе происходит комбинаторный взрыв. Ну, а что вы хотели? Зато мы на наших входных данных получаем очень много вариантов:

string.Concat(input.Split()).ToUpper()
string.Concat(input.Split()).ToUpperInvariant()
input.ToUpper().Trim()
input.ToUpper().TrimEnd()
input.ToUpperInvariant().Trim()
input.ToUpperInvariant().TrimEnd()
input.Trim().ToUpper()
input.Trim().ToUpperInvariant()
input.TrimEnd().ToUpperInvariant()
input.TrimEnd().ToUpper() // + lots more solutions

Я не стал выкладывать все решения, их достаточно много. Как видите, все варианты являются более-менее правильными, но не учтена коммутативность вызовов: ведь по сути не важно, вызывать нам .Trim().ToUpper() или .ToUpper().Trim(), но программа этого не знает.

Это не ужасная проблема когда вызовов 2, но когда их 3 или больше, количество излишней работы, которую вынуждена делать программа, весьма внушительно.

ff5

Мы пока что обсуждали только «няшные» функции которые можно вызывать без аргументов. Всё – такое дело больше не прокатит. Чтобы удалить bbb на конце нужно вызвать что-то, что жестко выпиливает или b или bbb или удаляет 3 последние буквы в тексте.

Естественно, что все аргументы вызова должны как-то коррелировать с объектом, на котором идет вызов. Для этого сделан страшный и ужасный FragmentationEngine – класс-дробитель, который умеет дробить другие типы на составные части. (Тут должна быть картинка Дробителя из Hearthstone.)

Давайте возьмем строку aaabbb. Ее можно раздробить так:

  • Все возмоджные буквы (в данном случае – 'a' и 'b')

  • Все возможные подстроки (в т.ч. пустая строка). Это реально болезненная операция, т.к. на длинной строке их очень много.

  • Все возможные числа в пределах длины самой строки. Это нужно для вызовов всяких Substring().

Надробив строку на всякие объекты, мы ищем методы – статические или нет – которые берут эти объекты. Тут все более менее предсказуемо, за иключением того что

  • Вызовы с 2+ аргументами делают нехилый комбинаторный взрыв. Простой пример – это Substring().

  • Вызовы функций которые берут params[] теоретически создают ничем не ограниченный комбинаторный взрыв, поэтому их нужно или лимитировать или не вызывать вообще.

CallSharp, конечно, справляется с нашим синтетическим примером и выдает нам

input.Trim('b')
input.TrimEnd('b')

Как вы уже наверное догадались, комбинаторные взрывы могут поднабросить на вентилятор очень много вариантов которые, будучи корректными, являются излишне сложными. Вот например:

ff6

Хмм, казалось бы, нужно всего лишь удалить er ну или e и r по отдельности. Если запустить CallSharp на этом примере, мы получим

input.Trim('e','r')
input.Trim('r','e')
input.Trim('a','e','r')
input.Trim('a','r','e')
input.Trim('e','a','r')
input.Trim('e','r','a')
input.Trim('r','a','e')
input.Trim('r','e','a')
input.TrimEnd('e','r')
input.TrimEnd('r','e') 
// 30+ more options

Как видите, первые два варианта – единственные, которые хотелось бы использовать. Все остальные обладают излишней информацией, которая не делает никому погоды. Или вот еще

ff7

Тут вариантов меньше, вот они:

input.Replace("aabb", "aa")
input.Replace("bb", "")
input.Replace("bbcc", "cc")

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

Еще одно интересное наблюдение – это то, что иногда интересные решения кроются на глубине, а не на поверхности. Вот например

ff8

Тут можно просто удалить пробел, но CallSharp дает много вариантов, например

input.Replace(" ", string.Empty)
input.Replace(" b ", "b")
input.Replace("a b ", "ab")
input.Replace(" b c", "bc")
input.Replace("a b c", "abc")
// at greater depth,
string.Concat(input.Split())

Лучшие варианты – первый и, возможно, последний – он хоть и подороже с точки зрения выполнения (наверное, не проверял, интуиция подсказывает), но выглядит элегантно. Это хороший пример того, как программа может вывести то, что человек сразу не увидит.

Резюмируя

Сейчас CallSharp работает, скажем так, небыстро. Проблема в основном в использовании reflection (в частности, MethodInfo.Invoke()) а также в комбинаторных взрывах связанных с глубиной вызовов и количеством аргументов и их вариаций.

Текущие проблемы с перформансом отчасти решатся при переезде от динамического до статического reflection (предполагается сделать всё на T4). Оптимизаций можно делать очень много – я бы например хотел сделать аннотации для разметки «коммутативности» как наборов функций, так и аргументов в функциях (например, порядок букв в Trim() не важен).

CallSharp – open source проект, лежит на GitHub. Там же есть его релизы – по ссылке click here установится ClickOnce дистрибутив, которые самообновляется по мере выхода новых версий.

Для тех, кому хочется чуть более живого повествования, ниже представлен мой доклад на Петербургской .NET User Group:

Спасибо за внимание!

Written by Dmitri

17 декабря 2016 at 14:34

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

Tagged with ,

Неожиданные изменения в экосистеме .NET

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

Думаю многие из вас смотрели вчерашний Connect(); — там Microsoft в очередной раз показывали что происходит в плане технологий и инструментария для разработки. Но мало кто ожидал что .NET (конечно не весь, а только некое «ядро») будет выпущен в опенсорс, да еще с гарантией отказа от судебного преследования.

На текущий момент у нас, правда, уже много всего в Open Source, включая языки C#/F#/VB.NET, ASP.NET а также (удивительно) Entity Framework, не говоря о более мелких проектах вроде MEF. Так вот, теперь о ядро дотнета, включая такие сакральные вещи как GC (сборщик мусора, чей алгоритм держался в строжайшем секрете) теперь тоже идут на GitHub (да-да, не на CodePlex а туда, где реально сидят разработчики).

.NET Core

Вот что уже было выложено на GitHub:

  • Immutable Collections — те самые немутабельные коллекции о которых я писал.

  • SIMD — т.е. поддержка длинных регистров которые используются для SSE/AVX. Что ж, хорошо! Пока что доступ к SIMD возможен только через С++, правда основная головная боль по использованию SIMD уже ушла благодаря умным компиляторам. Писать assembler или использовать intrinsics нужно только в очень крайних случаях.

  • AssemblyMetadataReader — всякие там CustomAttribute и иже с ним. Странно как-то отделять это от всего остального, ну да ладно.

  • XDocument и XmlDocument — ну то есть поддержка Xml и в частности моего любимого (хоть и бажного, увы) System.Xml.Linq.

Одна из идей всего этого – нечто под названием Core CLR, т.е. возможность шипить некое ядро CLR вместе с вашей собственной программой. Это даст некую отвязку от ситуации когда ваше ПО не поставить, не выкачав предварительно из интернета еще 100Мб файлов .NET Framework 4 (по собственному опыту). И да, урезанная версия .NET Framework проблему эту не решила, увы.

Visual Studio Community

Основная пробема была и есть что VS бесплатной редакции не дает шипить ПО и не дает ставить плагины. Проехали! Теперь VS Community – новая редакция, которая

  • Поддерживает расширения

  • Позволяет распостранять ПО в коммерческих целях

Единственный нюанс – эта штука имеет ограничения по размеру компании. То есть это как бы для «небольшого бизнеса». На самом деле, все немного туманно:

In non-enterprise organizations, up to 5 users can use Visual Studio Community. In enterprise organizations (meaning those with >250 PCs or > $1MM in annual revenue), no use is permitted beyond the open source, academic research, and classroom learning environment scenarios described above.

Понятие «enterprise» еще нужно объяснить, но по крайней мере любая команда из 5 человек может смело брать и использовать, и при этом поставить на студии ReSharper без каких-либо проблем.

ASP.NET vNext

Дино Эспозито обещал на киноуте .NEXT как раз и рассказать поподробнее про ASP.NET vNext, но тот факт что эта технология теперь едет и на Linux/OSX — это серьезно. У меня, например, все сайты используют ASP.NET (большинство – MVC, но один старый сайт 2003 года все еще на WebForms), и мне развитие ASP.NET только на руку – я вряд ли буду изучать такие вещи как Node/Ruby/PHP/whatever.

С другой стороны, я признаю что все классные фреймворки где-то за пределами .NET области: я про WordPress по сравнению с Orchard, например. Мне разработчики написали, что Orchard не будет работать с моим IIS 6 (да, у меня старый IIS, и что?), а мои попытки использовать .NETные блогодвижки, в т.ч. всякий адъ вроде N2… не, ну хорошо что такие фреймворки есть, но я честно, не фанат.

Что меня радует так то, что скорее всего мы увидим C# Native на не-Windows системах. А вот это уже интересно, т.к. нужно оно не только для вебного быстродействия, но и для того чтобы в некоторых случаях можно было не страдать с С++ (хотя я уже практически привык).

C++

Да, над С++ тоже поработали (что неудивительно), добавив помимо всего прочего поддержку мобильной разработки на С++ (не забываем, что теперь и Microsoft производит Android-телефоны, если что), ну и конечно же некоторые подвижки в плане C++11/14/17, такие как generic лямбды, await, и много всего другого.

Заключение

Не знаю насчет C++, а вот за .NET беспокоиться не стоит. Такая демократизация на руку всем, и я доволен что >10 лет назад я выбрал направление, которое пока и не думает умирать.

Если вас впечатляют эти подвижки и хочется «проникнуться» средой .NET, то все просто: приходите на .NEXT 8 декабря. Это большая .NET конфа в Москве. Там буду я, коллеги из комманды ReSharper, многие известные эксперты (в т.ч. много людей из «топа» .NET-секции Хабра), и много .NET разработчиков. До встречи!

Written by Dmitri

13 ноября 2014 at 12:30

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

Что нового в F# 3.1

leave a comment »

Недавно в пререлиз VS2013 была залита версия F# 3.1 в которой, как ни странно, есть несколько полезняшек которые немного упрощают жизнь. В частности:

  • Наконец-то кейсы дискриминированных объединений можно именовать. То есть если мы раньше писали
    type Stuff =
      | Pair of float * float
    

    то теперь можно дать имена аргументам, т.е.:

    type Stuff =
      | Pair of first : float * second : float
    

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

    match myPair with
    | Pair(fisrt = 0.0f) -> ⋮
    
  • F# явно пытается стать чем-то вроде MATLAB-а в плане работы с матрицами – в 3.0 они сделали так чтобы матрицы можно выпиливать кусочки например так foo.[2..5,*] что в MATLAB выглядело бы как foo(2:5,:), а в 3.1 можно еще и брать отдельные элементы, например
  • foo.[2,*] // 2й ряд
    bar.[*,5] // 5я колонка
    
  • Вывод типов для LINQ. Давно пора. Пока вы в уютном сишарпике писали foo.Where(x => x.Bar), в F# творилось плохое приходилось навешшивать аннотации типов на параметр. Кажись пофиксили, теперь можно просто писать foo.Where(fun x -> x.Bar). Э-эх, если бы они еще fun убрали для лаконичности, хотя это наверное не реально.
  • Раньше, если вы импортировали из C# методы расширения с дженериками (например монаду Maybe — хотя зачем?) то F# их не видел. Теперь в 3.1 все должно работать и, более того, можно самому делать подобные расширения. Правда F# как всегда отличился и добавил анти-ООПшного изврата в сей процесс путем не давания возможности ограничить generic-параметр (в стиле where foo : Bar, new(). Обещают пофиксить в будущем, а пока рекомендуют пользоваться [<Extension>].
  • В констрантных выражениях теперь можно писать вещи вроде
    [<Literal>]
    let foo = "bar" + "baz"
    

Это что касается языка. Помимо этого, есть какие-то попытки улучшить поддержку VS, но если честно — улучшение тултипов и выравнивания кода не даст мне возможность лучше идентифицировать ошибки или делать рефакторинги. Более того, все мои дискуссии с F# community сводятся к тому что все с пеной у рта бегут доказывать что «ReSharper не нужен»,ну и следовательно продолжать диалог сложно.

Что ж, скажем спасибо что язык хотя бы не стоит на месте. Хотя хотелось бы конечно большего. Вот ссылка на пост где помимо языковой части еще затронут инструментарий и либы – может будет интересно. ■

Written by Dmitri

27 сентября 2013 at 11:33

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

Tagged with ,

Немутабельные коллекции

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

Если переключить NuGet в пререлизный режим, один из пакетов который вы найдете называется Microsoft Immutable Collections. Немутабельность магически приходит в .NET в основном под влиянием F#: тут, относительная неизменяемость встроена в язык, хотя по-хорошему, что касается коллекций, у F# те же проблемы что и у C# т.к. массивы мутабельны: x.[0] = 123; вполне валидное выражение, и let mutable для этого писать необязательно.

Что такое немутабельная коллекция?

Если коротко, то

  • Немутабельную коллекцию нельзя менять

  • Но методы изменения все-таки есть!

  • И каждый метод создает и возвращает новый инстанс коллекции

Естественный вопрос зачем? Классическая реплика насчет потокобезопасности как бы не работает, т.к. есть System.Collections.Concurrent или как там. Но с другой стороны, существуют вполне реалистичные ситуации, в которых нужно и обновить коллекцию и тем не менее оставить предыдущий вариант. Хотите пример? Пожалуйста!

Надуманный пример

Есть такая штука – брокер событий. Это фактически специализированный service bus для приложения, с тем намеком что все события системы кладутся на некую шину через Publish() и потом снимаются с нее с помощью Subscribe(). Соответственно класть в него событие – кошерно, а вот комманду (привет CQRS) – это уже на любителя.

Так вот, у брокера событий есть набор подписок:

private readonly List<Subscription> subscribers = new List<Subscription>();

Есть метод для подписок

public IDisposable Subscribe(IObserver<EventArgs> subscriber)
{
  Subscription sub = new Subscription(this, subscriber);
  if (!subscribers.ToArray().Any(s => s.Subscriber == subscriber))
  {
    subscribers.Add(sub);
  }
  return sub;
}

и для рассылки событий:

public void Publish<T>(T args) where T : EventArgs
{
  foreach (var s in subscribers.ToArray())
    s.Subscriber.OnNext(args);
}

Заметили использование ToArray()? С чего бы это все материализовывать? А с того что подписки и отписки фактически могут меняться в процессе, и материализация — единственный процесс, который спасает нас от этого. Другая альтернатива — как раз немутабельные коллекции. Cам список можно опеределить вот так:

private ImmutableHashSet<Subscription> subscriptions = ImmutableHashSet.Create<Subscription>();

Уже нет readonly т.к. вся суть этой переменной в том, чтобы ее перезаписывать — напомню что .NET гарантирует атомарность этой операции. Соответственно, метод подписки выглядит вот так:

public IDisposable Subscribe(IObserver<EventArgs> subscriber)
{
  var s = new Subscription(this, subscriber);
  subscriptions = subscriptions.Add(s);
}

А соответственно метод рассылки вот так:

public void Publish<T>(T args) where T : EventArgs
{
  foreach (var s in subscriptions)
    s.Subscriber.OnNext(args);
}

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

Что есть в коробке?

Доступен полный набор коллекций – array, list, stack, dictionary, sorted dictionary, queue, hashset, sorted set.

Также доступен ImmutableInterlocked, содержит методы для атомарного присваивания, замены, вообщем все как Interlocked для немутабельных коллекций. В принципе, в примере выше я должен был пометить поле как volatile и воспользоваться этим классом для полного счастья.

Вообще, у меня не так много юз-кейсов всего этого. Если они есть у вас, напишите в комментариях.

Written by Dmitri

20 июля 2013 at 23:48

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

Tagged with

Организация зависимостей в UI после генерации модели

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

Как все уже наверное знают, у меня есть система, которая генерит полноценные приложения из Excel таблиц. Пока что эти приложения – только код, но хочется еще и UI впридачу. Причем хочется чтобы поведение UI соответствовало поведению Excel, т.е. чтобы был мгновенный пересчет всего и вся.

Чтобы все это работало правильно, нужно чтобы при изменении чего либо, все другие элементы тоже обновлялись. После обсуждения задачи с @controlflow и другими людьми, стало понятно что есть несколько вариантов, в частности:

  • Переписывать всю модель с использованием INotifyPropertyChanged, т.е. генерить классы и переменные которые все делают соответствующие вызовы (а зависимости резодвить чем-то вроде Obtics). Это конечно вариант, но серьезно нарушает принципы целостности – ведь затаскивание поведения представления в модель – в корне плохо.

  • Делать обертки (вьюхи) поверх существующих POCO классов и привязываться уже к ним. При этом инфа о зависимостях прописывается статически.

  • Писать свой собственный INPC-аналогичный фреймворк который позволяет выводить из выражений информацию о зависимостях. Это подразумевает навязывание пользователю специального API для работы с моделью.

Я решил выбрать второй вариант как меньшее из всех 3-х зол.

Вью поверх модели

Итак, представим что есть

public class Person
{
  public int Age;
  public bool CanVote()
  {
    return Age >= 16;
  }
}

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

Итак, класс есть, трогать его нельзя. Пишем вьюшку. Точнее, поскольку плодить вызов нотификаций не хочется, тупо делаем абстрактный базовый класс. (Который можно потом менять, например на NotificationObject из Prism.)

public abstract class View : INotifyPropertyChanged
{
  public event PropertyChangedEventHandler PropertyChanged;
  [NotifyPropertyChangedInvocator]
  protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
  {
    PropertyChangedEventHandler handler = PropertyChanged;
    if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
  }
}

Первое, наивное приближение вьюшки у меня выглядит вот так:

public class PersonView : View
{
  private Person person;
  public PersonView(Person person)
  {
    this.person = person;
  }
  public int Age
  {
    get
    {
      return person.Age;
    }
    set
    {
      if (value == Age) return;
      person.Age = value;
      OnPropertyChanged();
    }
  }
  public bool CanVote()
  {
    return person.CanVote();
  }
}

И в этой вьюшке два недостатка:

  • Непонятно как биндить функцию на UI.

  • При изменении Age, CanVote() должен пересчитываться.

Прикручиваем к формочке

Итак, делаем простую форму для всего этого:

Прописываем простой биндинг:

personView = new PersonView(new Person());
var ageBinding = new Binding("Text", personView, "Age");
tbAge.DataBindings.Add(ageBinding);

Интересно что в WinForms, конверсия из string в int происходит автоматически, в отличии от WPF где нужны конвертеры. Так или иначе, биндинг работает, только вот с CanVote все непонятно.

На самом деле, мы конечно неправильно сделали – на CanVote тоже нужно было навесить свойство, что-то вроде:

public bool CanVote
{
  get
  {
    return person.CanVote();
  }
}

В этом случае, можно привязываться:

var canVoteBinding = new Binding("Text", personView, "CanVote");
lblCanVote.DataBindings.Add(canVoteBinding);

Нерешенным остается только вопрос о том как сделать NotifyPropertyChanged. Вот так нельзя:

public int Age
{
  get
  {
    return person.Age;
  }
  set
  {
    if (value == Age) return;
    person.Age = value;
    OnPropertyChanged();
    OnPropertyChanged("CanVote"); // FAIL
  }
}

Изменение выше работает, но не масштабируется на уровне 20-страничной экселевой простыни с огромным количеством зависимостей. Ведь зависимости могут быть между объектами, и соответственно между вьюшками.

Таблица зависимостей

Давайте в наш базовый View вклиним таблицу зависимостей (а-а, строковые литералы!) обо всех зависимостях внутри одной вьюшки:

protected Dictionary<string,List<string>> localDependencies = new Dictionary<string, List<string>>();
protected void AddLocalDependency(string from, string to)
{
  if (localDependencies.ContainsKey(from))
    localDependencies[from].Add(to);
  else
  {
    localDependencies.Add(from, new List<string>{to});
  }
}

Теперь, перепишем OnPropertyChanged так, чтобы нотификации уходили не только на «основное» свойство, но и на все что от него зависят:

protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
  PropertyChangedEventHandler handler = PropertyChanged;
  if (handler != null)
  {
    handler(this, new PropertyChangedEventArgs(propertyName));
    if (localDependencies.ContainsKey(propertyName))
      foreach (var to in localDependencies[propertyName])
        handler(this, new PropertyChangedEventArgs(to));
  }
}

Теперь, в конструкторе вьюхи можно автогенерировать соответствующий enlisting для пары Age-CanVote, и все должно работать. И оно работает.

Многовьюшные зависимости

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

public class VotingSystem
{
  public int VotingAge;
}
public class VotingSystemView : View
{
  private VotingSystem votingSystem;
  public VotingSystemView(VotingSystem votingSystem)
  {
    this.votingSystem = votingSystem;
  }
  public int VotingAge
  {
    get { return votingSystem.VotingAge; }
    set
    {
      if (value == votingSystem.VotingAge) return;
      votingSystem.VotingAge = value;
      OnPropertyChanged();
    }
  }
}

Теперь Person (именно модель, а не вью) переписывается с использованием VotingSystem:

public class Person
{
  private VotingSystem votingSystem;
  public Person(VotingSystem votingSystem)
  {
    this.votingSystem = votingSystem; // save for later
  }
  public int Age;
  public bool CanVote()
  {
    return Age >= votingSystem.VotingAge; // refer to value
  }
}

Наша цель простая – мы хотим чтобы CanVote() пересчитывался за счет изменений в VotingSystem.VotingAge. Точнее, мы хоти чтобы VotingSystemView мог сказать PersonView «обнови-ка ты свой Age».

Прощайте локальные зависимости

Думаю уже понятно, что точно так же как Person берет VotingSystem в качестве параметра в конструкторе (что полезно для dependency injection), мы делаем то же самое в конструкторе вью, т.е.:

public PersonView(Person person, VotingSystemView votingSystemView)
{
  this.person = person;
}

Кэшировать эту штуку необязательно. Сейчас самое важное – переписать базовый класс так, чтобы нотификации отсылались правильной вьюшке. Тут полезно создать как-то упростить концепцию вью-и-поле:

using VPN = System.Tuple<WindowsFormsApplication1.View.View,string>;

А потом мы меняем наш Dictionary, ну и соответственно механизм вызова тоже меняется:

if (handler != null)
{
  handler(this, new PropertyChangedEventArgs(propertyName));
  var self = new VPN(this, propertyName); // need a pair here
  if (dependencies.ContainsKey(self))
    foreach (var to in dependencies[self])
      to.Item1.OnPropertyChanged(to.Item2); // notify there
}

Ну и собственно enlisting в этот механизм выглядит вот так:

var canVoteTuple = Tuple.Create((View) this, "CanVote");
AddDependency(Tuple.Create((View)this, "Age"), canVoteTuple);
AddDependency(Tuple.Create((View)votingSystemView, "VotingAge"), canVoteTuple);

Постмортем

Есть два варианта как смотреть на все что изложено выше. С одной стороны – это все адов адъ, ведь в действительности у типичного класса может быть 15 зависимостей, что означает что в конструктор воткнется именно столько других объектов, и в конструктор вьюшки тоже.

С другой стороны – оно работает, и это самое главное. Я уверен, что большинство людей, которые сгенерят из Excel-евой простыни полноценное декстоп или веб-приложение не захотят потом кардинально переписывать бизнес-логику. Хотя, кто знает… ■

Written by Dmitri

16 июня 2013 at 14:09

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

Tagged with , , ,

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

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

Нужен ли математикам статический анализ?

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

Есть такая профессия, в которой люди на 50% процентов занимаются программированием, но при этом сидят на порой весьма “несовременных” технологиях, пишут в основном на С++, и никаких особых бенефитов от IDE вообще не имеют. И при этом не жалуются. В этом посте – про то, кто эти люди, чем они заниматся и чем мы можем сделать их жизнь лучше.

Знакомьтесь – квонты

Квонт, он же quant (quantitative analyst – численный, так сказать, аналитик) — это человек, который занимается применением математики в финансах, часто – для создания торговых систем, т.е. программ, которые занимаются автоматизированной торговлей на бирже. Эти люди – чаще всего магистры или PhD в области физики (да-да) или математики (в частности, с дипломами вроде MFE – Masters in Financial Engineering), в некоторых случаях, с дипломом CQF (один из очень небольшого кол-ва “неакадемических” дипломов) или даже с MBA Finance или а ля.

Квонты (я бы называл их квантами) программируют в очень узком ключе – их в большинстве случаев не волнуют новые тренды в Asp.Net или новых языках вроде Scala или D. Они живут в совершенно другой реальности, где доминантным маст-хэв языком был, есть и будет С++. Конечно, есть варинты (например, вспомним Jane Street и их любовь к OCaml), но суть остается одна – эти разработчики привыкли работать с достаточно ограниченным tool support, и не особо от этого страдают. Ведь согласитесь, для того чтобы эффективно выполнять математические рассчеты нужно лишь чтобы у вас худо-бедно работал intellisense, и то, это не так критично если нужно вызвать что-то вроде std::min().

Соответственно, текущее положение вещей наводит на мысль, что людям которые используют в основном C++ (а также MATLAB, VBS, R, ну и другие языки на вкус) tool support как бы не очень нужен.

И все же…

Приведу конкретный пример. На многих графических устройствах, операция a*x+b оптимизирована и проходит быстрее, чем умножение и сложение отдельно. Соответственно, когда я работаю с GPU.NET, у меня есть в R2P контекстное действие, которое превращает все, что похоже на “умножение-в-сложении” на вызов вроде DeviceMath.MultiplyAdd(a, x, b). Мелочь, а приятно, и программу ускоряет.

И это далеко не частный случай. Вот еще пример – допустим что вы попросили студента за еду реализовать вам ряд формул в коде, и студент в качестве реализации написал что-то вроде y = a * Math.Pow(x, 2.0) + b * x. Бред, не так ли?

И тут снова можно включить “электронный мозг” статического анализатора и начать помогать несчастному разработчику. Для начала ему нужно объяснить, что считать ряды Тэйлора в подобном случае раз, эээ, в 50 медленнее чем сделать x*x. Но даже объяснив это, вы получите вот такой результат:

y = a * x * x + b * x;

Это тоже не очень-то эффективно. Ведь тут целых 3 умножения, хотя можно обойтись двумя:

y = x * (a * x + b);

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

А это имеет смысл?

Действительно, кому нужна скорость? Ну, наблюдения показывают что ряд задач (например, Монте-Карло симуляции) очень любят кушать CPU. Микрооптимизация вычислений – это как раз то, что позволит аналитику протестировать свою стратегию быстрее, на большем объеме данных.

Еще одна полезная, хотя и тривиальная вещь – это рефакторинги с уклонов в сторону параллелизации – например рефакторинг for и foreach циклов в параллельные с сохранением правильной семантики. То есть, если разработчик написал

int [] elems = new int[] { ⋮ };
int sum = 0;
foreach (var e in elems) sum += e;

то почему бы нам не дать ему возможность отрефакторить этот цикл в параллельный:

Parallel.ForEach(elems, () => 0, 
                 (n, loopState, localSum) => 
                 {
                   localSum += n;
                   return localSum;
                 },
                 localSum => Interlocked.Add(ref sum, localSum));

У меня в R2P уже реализованы некоторые приведения простых циклов в параллельные, но это большая задача, и за один день ее не решить.

Другие языки?

Я привел пример поддержки C#, т.к. не знаю доступной инфраструктуры для написания рефакторингов для С++ или (это было бы еще интересней) для CUDA C. Было бы интересно услышать мнение читателей насчет применимости всех этих идей.

Written by Dmitri

18 августа 2011 at 22:22

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

Tagged with , ,