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

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

Archive for the ‘C#’ Category

Что нового в C# 8

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

«Восьмерка» еще даже не вышла RTM а я уже пишу про нее пост. Зачем? Ну, основная идея что тот, кто предупрежден — вооружен. Так что в этом посте будет про то что известно на текущий момент, а если это все посдстава, ну, поделом.

Nullable Reference Types

Я не знаю, в курсе вы или нет, но дизайнеры всех современных языков «продолбали» как минимум несколько важных аспектов. Один из основных продолбов — это неинициализированные объекты, указатель на которые имеет значение NULL в С и nullptr в современном С++. Другой аспект — это zero-terminated strings, когда длина строки вычисляется за О(n). Но мы сейчас говорим за null.

C# это конечно тоже зацепило в расных испостасях, вот например:

public class Person
{
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string MiddleName { get; set; }
  public Person(string first, string last, string middle) =>
    (FirstName, LastName, MiddleName) = (first, last, middle);
  public string FullName =>
    $"{FirstName} {MiddleName[0]} {LastName}";
}

Пример выше очень хорошо иллюстрирует проблему. У человека может быть отчество но у меня, например, в паспорте его нет и следовательно непонятно что писать в это поле. Дефолтное значение строки в C# это null. Если интерполировать это значение в пустую строку ($ за кулисами делает string.Format()), ничего страшного не будет. Но если вы попробуете получить первую букву null-значения, вы получите исключение NRE (NullReferenceException).

Кардинального решения этой проблемы нет, т.к. если внезапно запретить null, придется написать 100500 статических проверок на инициализацию всех объектов во всех брэнчах конструктора. Или перейти полностью на Optional типы что, собственно, тоже не совсем решает проблему.

Системы статического анализа вроде Решарпера уже давно пытаются как-то облегчить участь разработчика, предупреждая о возможных косяках. Собственно для этого придумали аннотации (NuGet пакет JetBrains.Annotations) которые можно добавить в проект вот таким вот образом:

public class Person
{
  public string FirstName { get; set; }
  public string LastName { get; set; }
  [CanBeNull] public string MiddleName { get; set; }
  public Person(string first, string last, [CanBeNull] string middle) =>
    (FirstName, LastName, MiddleName) = (first, last, middle);
  public string FullName =>
    $"{FirstName} {MiddleName[0]} {LastName}";
}

Код выше заставляет системы статического анализа ругаться на возможный NRE в точке MiddleName[0].

Но Microsoft… как всегда берет то, что делает другие и банально копирует. Вообще есть шутка, что в долгосрочной перспективе VS просто скопирует все фичи Решарпера. А потом запретит плагины. Поэтому хорошо что есть райдер.

Короче, МС естественно не стали менять язык. Точнее как не стали, они конечно его поменяли. Вместо решарперных аннотаций, в C#8 можно написать вот так:

#nullable enable

Просто написав от эту штуку наверху файла вы меняете поведения компилятора. Теперь, при написании чего-то вроде

var p = new Person("Dmitri", "Nesteruk", null);

вы получите следующий warning:

1>NullableReferenceTypes.cs(26,48,26,52): warning CS8625: Cannot convert null literal to non-nullable reference or unconstrained type parameter.

Да-да, просто предупреждение, а не ошибку (хотя treat errors as warnings никто не отменял).

Ну да ладно, а что дальше? А дальше нам хочется как-то все-таки сказать C#, что теоретически MiddleName таки может быть null поэтому доступ с индексом [0] тоже нужно проверять.

Для этого мы меняем поле на вот такое:

public string? MiddleName;

Хмм, что это? Кому-то может показаться что string? эквивалентно Nullable<T>, но напомню что в этом типе, T : struct, так что очевидно это что-то другое. Этот вопросик — это всего лишь подсказка компилятору, т.к. по факту тип поля все еще обычный string.

Теперь компилятор выдаст вам еще один варнинг на доступ по индексу ноль. И чтобы оно заработало вам придется как-то перестраховаться, например написав:

public string FullName =>  $"{FirstName} {MiddleName?[0]} {LastName}";

Теперь самый важный вопрос: что же поменялось в IL? С точки зрения исполняемого кода — ничего! Но с точки зрения метаданных поменялось конечно: теперь в типе который использует nullable аннотации все типы которые являются nullable проаннотированы атрибутом [Nullable]. Сделано это по понятным причинам: чтобы потребители вашего кода могли использовать ваши аннотации.

Чувствительность к проверкам

Аннотации в C#8 работают в какой-то мере как Котлиновские смарт-касты. Иначе говоря, если у меня есть апи который выдает nullable-тип

string? s = GetString();

То конечно при попытке достучаться до первой буквы я получу warning. Но если я напишу так:

if (s != null){  char c = s[0];}

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

Предотвращение лишних проверок

Есть два способа отключить проверки на null. Первый — это просто писать код без «вопросиков», тем самым констатировав тот факт что все твои поля, параметры и так далее вообще ни при каких условиях null-ами быть не могут.

Второй подход — это явно сказать компилятору что в этой точке проверка не нужна. Вот несколько примеров:

  • (null as Person).FullName конечно выдаст нам warning

  • (null as Person)!.FullName warning уже не выдаст, т.к. мы явно просим не проверять выражение

  • (null as Person)!!!!!!!!!!!!!.FullName тоже является валидным выражением и тоже отключает проверки

  • (null as Person)!?.FullName тоже валидно и все же делает в этом случае проверку на null; примечательно что обратное использование, ?!, не скомпилируется

Проверки в либах

Естественно, то вся эта кухня имеет хоть какой-то смысл только при условии что BCL и прочие популярные библиотеки проаннотированны этими аннотациями. Ведь сейчас я могу написать

Type t = Type.GetType("abracadabra");Console.WriteLine(t.Name);

и не получить никакого предупреждения. Я-то знаю что Type.GetType() возвращает null когда даешь невалидное название типа, но поскольку BCL пока еще не размечена nullable аннотациями, компилятор это съедает.

И нет, мы не можем «форсировать» подобные проверки кодом вроде

Type t = Type.GetType("abracadabra");
Type? u = t;
Console.WriteLine(u.Name);

Код выше все равно не выдаст предупреждение. Очевидно, компилятор считает что t != null, следовательно u тоже не может быть равно null, сколько его не декорируй.

Итого

Nullable reference types — сомнительной полезности фича, которую еще рано использовать. Оригинальности в ней мало. В долгосрочной перспективе она, конечно, должна помочь нам как-то бороться с рисками nullability но глобально она проблему, как вы понимаете, не решает.

Индексы и Диапазоны

На матстатистке есть такое хороше упражнение: брать диапазон чисел, обладающих тем или иным распределением, преобразовывать его, и потом считать статистику по результату. Например, если X~N(0,1), чему равны E[X²] и V[X²]?

Диапазоны это очень хорошо, но дизайнерами сишарпа явно хотелось получить диапазоны в стиле Питона, а для этого пришлось ввести понятие «с конца».

Итак, у нас за кулисами появляются два новых типа: Index и Range.

Index

Вы никогда не задумывались, почему это индекс в массив обязательно int а не uint? Все просто: этот ваш пресловутый индекс это просто отступ от указателя на начало массива (привет Си). Поэтому он теоретически может быть отрицательным, хотя конечно в C# запись x[-1] лишена всякого смысла.

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

В C# пошли другим путем и ввели новый синтаксис. Но, все по порядку. Для начала, ввели новый тип под названием Index который представляет индекс элемента в массиве, строке или вашей собственной коллекции:

Index i0 = 2; // implicit conversion

Индекс выше, как вы понимаете, будет ссылаться на третий элемент с начала той или иной коллекции.

У самого типа Index есть два свойства (зачем сделали свойства а не поля — тот еще вопрос):

  • Value, то есть сколько элементов нужно отсчитать

  • IsFromEnd — булевое значение, показывающее, нужно ли отсчитывать от конца коллекции а не от начала

Структуру можно инициализировать просто вызвав конструктор:

Index i1 = new Index(0, false);

Код выше, как вы поняли, берет первый элемент сначала. А вот последний элемент (то есть нулевой, но с конца) можно взять вот так:

var i2 = ^0; // Index(0, true)

Опаньки! Многие из вас наверное хотели, чтобы оператор ^ сделали возведением в степень (мне как математику эта идея комфортнее, LaTeX, все такое), но по факту это теперь специальный синтаксис для создания индекса «с конца».

Встроенные типы, такие как массивы или строки, конечно же поддерживают индексер (operator this[]) для типа Index, так что смело можно писать

var items = new[] { 1, 2, 3, 4, 5 };
items[^2] = 33; // 1, 2, 33, 4, 5

Range

Следующий кусочек этого паззла — это тип Range, который представляет из себя линейный, направленный строго по возрастанию диапазон индексов, строго с шагом 1. Для него тоже введен новый синтаксис:

X..Y

Что означает «все элементы от X включительно до Y». При этом, включает ли диапазон значение с индексом Y или нет — нельзя сказать не посмотрев на Y. Об этом очень скоро.

Итак, вот несколько примеров:

  • var a = i1..i2; // Range(i1, i2) — полноценный диапазон с началом и концом

  • var b = i1..; // Range(i1, new Index(0, true)); — диапазон от i1 и до конечного элемента

  • var c = ..i2; // Range(new Index(0, false), i2) — диапазон от самого первого элемента и до индекса i2

  • var e = ..; — вообще весь диапазон, то есть от первого и до последнего элемента

  • Range.ToEnd(2); — эта и подобные статический функции — как раз то, что использует компилятор за кулисами; все это можно лицезреть, если открыть сборку в dotPeek, ilSpy или другом декомпиляторе

Включается ли конечный элемент?

Окей, знаете что в С++ толстым слоем разборсано неопределенное поведение (undefined behavior?). Ну так вот, в реализации Range в C# тоже затаился сюрприз. Суть примерно в следующем: последний элемент включается только если он взят «с конца».

Представьте массив x = {1, 2, 3}. Если взять x[0..2] вы получите {1, 2}, а вот если взять x[..2] или например x[..], вы получите {1, 2, 3}.

Это немного выносит мозг т.к. элементы x[2] и x[^1] — это одинаковые элементы, но семантика их поведения как часть Range-а разная!

Еще немного семантики

Во-первых, диапазон X..Y будет работать только если X <= Y (надеюсь вы включили лигатуры?). Если же вы решили сделать нисходящий диапазон (например 7..3), то вы просто получите ArgumentOutOfRangeException. Это очень неприятно т.к. часто хочется получить индексы массивы как некоторые выходные значения из функций, и проверять что диапазон восходящий ну совсем не хочется.

Во-вторых, «шаг» не включен в спеку, то есть нельзя написать 1..2..100 и получить только нечетные числа. А было бы удобно. Эта фича реализована в языках вроде MATLAB.

Range ведет себя в стандартных типах вот так:

  • В массивах, он дает копию подмассива, прям копируя каждый элемент

  • В строках происходит вызов Substring(). Строки в C# иммутабельные так что создается новая строка.

  • На коллекциях можно вызывать AsSpan() передавая ему Range.

  • В Span тоже можно засунуть RangeSpan.Slice(), получив под-диапазон.

Конечно, вы можете также запилить поддержку Index/Range в своих собственных типах с помощью переопределения operator this[]. При этом нужно понимать, что не во всех коллекциях понятие «с конца» работает хорошо. Например, если у вас односвязный список (singly linked list), то брать что-то «с конца» — плохая идея.

Итого

Полезные в целом фичи, которые являются просто слоем синтаксического сахара который компилятор разворачивает в инициализацию разных struct-ов. Включительность-исключительность диапазонов в зависимости от того, «хвостовой» ли закрывающий индекс понравится не всем, как и невозможность обходить коллекции задом наперед.

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

Default Interface Members

Никто не рискует породить столько ненависти сколько реализация дефолтных интерфейс мемберов, то есть бархатное преврашение интрефейсов в абстрактные классы. И первый вопрос, на который нужно ответить — зачем?

Ну типичная мотивация такая. Вот допустим вы хотите сделать Enumerable.Count() — его конечно можно делать через while (x.MoveNext()) но это немного бредово для коллекций, чья длина известна заранее. Поэтому мы втыкаем в Count() проверки типа:

if (x is IList<T> list)
  return list.Count;

Логично? А как насчет IReadOnlyList<T>, для него такая оптимизация будет? Что значит «не сделали»?!?

Ситуация, приведенная выше наводит нас на вынужденное, очень грубое нарушение open-closed principle да и других принципов SOLID, т.к. делать проверки на все возможные типы ради оптимизация — это провальная затея.

А что можно сделать? Ну, можно было бы, чисто теоретически, как-то взять и добавить реализацию Count(), специфичную для IReadOnlyList<T>, прямо в интерфейс, возможно как метод расширения. Только это тоже не сработает! Откуда мы знаем что экстеншн-метод IReadOnlyList<T>.Count() является перегрузкой метода IEnumerable<T>.Count()? Правильно, не знаем! Нужен другой подход.

Именно этот подход и реализуют дефолтные методы интерфейсов. Они позволяют реализовать нечто, функционально-эквивалентное экстеншн-методам, но также поддающееся правилам наследования и виртуальных вызовов.

Тонкости использования

Но давайте для начала посмотрим на более приземленный пример:

public interface IHuman
{
  string Name { get; set; }
    
  public void SayHello() 
  {
    Console.WriteLine($"Hello, I am {Name}");
  }
}public class Human : IHuman
{
  public string Name { get;set; }
}

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

Human human = new Human() { Name = "John" };
human.SayHello(); // will not compile

Странно да? Вроде бы, валидный код. На самом деле нет — дело в том, что конкретный класс, хоть он и реализует тот или иной интерфейс, понятия не имеет о дефолтных методах этого интерфейса.

Почему, спросите вы? Потому, что вся соль этих методов в том, чтобы добавлять их пост фактум, когда вашим интерфейсом уже пользуются. А что если за это время класс Human обзавелся собственным SayHello()? Правильно, будет конфликт.

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

IHuman human = new Human() { Name = "John" };
human.SayHello();
((IHuman)new Human { … }).SayHello();

Наследование интерфейсов

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

public interface IHuman
{
  string Name { get; set; }
  
  void SayHello() 
  {
    Console.WriteLine($"Hello, I am {Name}");
  }
}
public interface IFriendlyHuman : IHuman
{
  void SayHello()
  {
    Console.WriteLine(      $"Greeting, my name is {Name}");
  }
}
((IHuman)new Human()).SayHello();
// Hello, I am John
((IFriendlyHuman)new Human()).SayHello();
// Greeting, my name is John

Заметьте, что в коде выше IFriendlyHuman.SayHello() как бы должен override-ить IHuman.SayHello(), но этого не происходит! Что же нужно сделать чтобы вызов SayHello() стал действительно виртуальным? Нужно явно сказать об этом:

public interface IFriendlyHuman : IHuman
{
  void IHuman.SayHello()
  //   ↑↑↑↑↑↑
  {
    Console.WriteLine(      $"Greeting, my name is {Name}");
  }
}

Вот в этом случае вызов SayHello() на любом интерфейсе, будь то IHuman или IFriendlyHuman будет виртуальным и уже не важно, к какому из них вы скастовались:

((IHuman)new Human()).SayHello();
Greeting, my name is John
((IFriendlyHuman)new Human()).SayHello();
Greeting, my name is John

Diamond Inheritance

Естественно, в ситуации когда вы можете иметь два «честных override-а» в двух интерфейсах-наследниках породит конфликт в случае, если вы попытаетесь реализовать их оба:

interface ITalk { void Greet(); }
interface IAmBritish : ITalk
{
  void ITalk.Greet() => WriteLine("Good day!");
}
interface IAmAmerican : ITalk
{
  void ITalk.Greet() => WriteLine("Howdy!");
}
class DualNational : IAmBritish, IAmAmerican {}
// Error CS8705 Interface member 'ITalk.Greet()' does not have a most specific implementation. Neither 'IAmBritish.ITalk.Greet()', nor 'IAmAmerican.ITalk.Greet()' are most specific.

Проблема тут в том, что компилятор не может найти «более специфичный» (то есть, ниже в иерархии наследования) интерфейс для использования и, в результате равнозначности, не поймет что нужно делать если кто-то вызовет какой-нибудь IAmAmerican.Greet() — ведь по идее нужно гулять по виртуальной таблице, а куда идти-то, если варианта два?

Итого

Фича для писателей АПИ. Обычным пользователям скорее всего не стоит беспокоиться, особенно если вы, как я, контролируете весь свой код и вам не страшно в любой момент менять его API. Единственный реальный кейс — это когда вот прям нужно оверрайдить экстеншн-методы. У вас есть подобные юз-кейсы?

Pattern Matching

Эту фичу нагло крадут из F# уже на протяжении нескольких минорных релизов. Конечно — в F# ведь эта фича очень удобна, но она идет рука об руку с теми фичами, которых в С# нет, а именно алгебраические типы и функциональные списки.

Но несмотря на это, аналогом F#ного match стал С#ный switch. В C#8 все это обрастает дополнительными возможностями.

Property Matching

Если у объекта есть поля или свойства, можно мэтчить по ним:

struct PhoneNumber{
  public int Code, Number;
}
var phoneNumber = new PhoneNumber();
var origin = phoneNumber switch {
  { Number: 112 } => "Emergency",
  { Code: 44 } => "UK"
};

Код выше анализирует структуру объекта phoneNumber, проверяя его поля на конкретные значения. Заметьте что у нас не switch statement а switch expression, то есть выражение которое делает возврат лямбда-образным синтаксисом.

Несмотря на всю лаконичность, код выше — бажный, так как не отлавливает все кейсы. У дизайнеров было два выбора: либо молча сглатывать неохват паттерна делая default-init (иначе говоря, возвращать default(T) из свича у которого не замэтчился ни один паттерн) или же бросать исключение. Микрософт выбрали второе, так что если запустить код выше, мы просто получим:

Ну и системы статического анализа кода тоже в долгу не останутся:

Загадка

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

var origin = phoneNumber switch {
  { Number: 112 } => "Emergency",
  { Code: 44 } => "UK",
  { } => "Indeterminate",
  _ => "Missing"
};

Что это значит? Да то, что теперь есть 2 кейса: _ (подчеркивание), которое словит абсолютно любой результат, и {} которое отловит любой аргумент который не null.

Пример выше определенно покрывает все кейсы, так что исключение мы на нем не словим. А что насчет вот такого?

var origin = phoneNumber switch {
  { Number: 112 } => "Emergency",
  { Code: 44 } => "UK",
  { } => "Unknown"
};

Покрывает ли пример выше все варианты? Если phoneNumber это struct — то конечно да, но представим что это class. С одной стороны кто-то может сказать что да, не покрыли ситуацию с null, но… ведь есть nullable reference types!

Вот и получается, что покрытие всех вариантов зависит не только от типа объекта но еще и от контекста компилятора.

Рекурсивные паттерны

Все-таки с неймингом у МС получается ооочень плохо, прям фееричный булшит. Сначала nullable reference types — масло масляное, ибо референсные типы являются nullable по определению, но термин recursive patterns — это еще на порядок хуже.

В F#, поскольку там есть функциональные списки, можно рекурсивно проверять подсписки списка на соответствие паттернам. В C# же, этот термин значит совсем другое, причем как я уже сказал, нейминг этой фичи в C# — просто шлак.

Если коротко, «рекурсивные паттерны» в C# это возможность углубиться внутри структуры того или иного объекта и проверить еще и поля-полей, если так можно выразиться:

var personsOrigin = person switch {
  { Name: "Dmitri" } => "Russia",
  { PhoneNumber: { Code: 46 } } => "Sweden",
  { Name: var name } => $"No idea where {name} lives"
};

В примере выше проиллюстрированы сразу две идеи. Первая — это то, что можно углубиться в объект person.PhoneNumber и промэтчить его поле Code на значение 46. Вторая — это то что вместо мэтчинга можно задекларировать переменную и в нее записать найденное значение чтобы им с последствии пользоваться. Это тоже иногда бывает удобно, и конечно же работает в контексте этой надуманной «рекурсивности».

Валидация

Хорошее применение всей этой кухне: комплесная валидация разных аспектов одного сложного объекта в одном switch-е. Вот например:

var error = person switch {
  null => "Object missing",
  { PhoneNumber: null } => "Phone number missing entirely",
  { PhoneNumber: { Number: 0 } } => "Actual number missing",
  { PhoneNumber: { Code: var code } } when code < 0 => "WTF?",
  { } => null // no error
};
if (error != null)
  throw new ArgumentException(error);

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

Интеграция с проверками на типы

Проперти-паттерны можно «поженить» с проверками на типы которые появились одним сишарпом ранее. Теперь можно проверить на тип, а потом еще и распаковать структуру:

IEnumerable<int> GetMainOfficeNumbers()
{
  foreach (var pn in numbers)
  {
    if (pn is ExtendedPhoneNumber { Office: "main" })
      yield return pn.Number;
  }
}

В примере выше мы сначала проверяем тип номера телефона и, если он подходит, распаковываем его и проверяем что речь идет именно про главный офис. Очень удобно.

Деконструкция

Надеюсь вы не забыли про деконструкцию — фичу языка которая позволяет, по сути, распаковывати тип в кортеж. Для этого тип должен реализовывать метод Deconstruct() который просто выгружает содержимое типа в out-параметры.

Так вот, эту фичу тоже можно поженить с паттерн-мэтчингом и получить следующее:

var type = shape switch
{
  Rectangle((0, 0), 0, 0) => "Point at origin",
  Circle((0, 0), _) => "Circle at origin",
  Rectangle(_, var w, var h) when w == h => "Square",
  Rectangle((var x, var y), var w, var h) =>
    $"A {w}×{h} rectangle at ({x},{y})",
  _ => "something else"
};

Код выше распаковывает содержимое прямоугольника или круга в кортежные структуры, при этом есть варианты: либо заниматься паттерн-мэтчингом, либо просто деструктурировать объекты в переменные, как показано в последнем примере. Заметьте что этот процесс тоже является «рекурсивным» — у прямоугольника есть точка начала, которая деструктурируется в круглых скобках. Прочерк (_) используется для тех аспектов деструктуризации, которые нам не интересны.

Итого

Нужные и полезные возможности, которые найдут свое применение. Валидация и похожий анализ типов становится очень лаконичным.

Заключение

Что, я не все фичи показал? Ну, я и не обещал, вообщем-то. Для начала хватит. Пока язык не релизнули я могу еще чуток поисследовать. А если и вы хотите поисследовать, вам потребуется VS 2019 Preview (да-да, preview версия а не RTM), .NET Core 3 (многое из описанного выше попросту не поддерживается в .NET Framework), ну и dotPeek тоже будет полезен чтобы понять, что же там за кулисами.

У меня пока все. Продолжение (возможно) следует. ■

Реклама

Written by Dmitri

1 июня 2019 at 21:16

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

Tagged with

Использование ImpromptuInterface в реализациях паттернов проектирования

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

Вы наверняка уже знаете, что если отнаследоваться от DynamicObject, можно получить разные плюшки от DLR, такие как например возможность вызывать методы класса даже если у класса их нет, и все такое. Но знаете ли вы что можно «вывесить» этот ваш DynamicObject как абсолютно любой интерфейс? Зачем это надо, спросите вы — ну, для того чтобы реализовывать всякие паттерны проектирования. Вот парочка примеров.

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

После добавления NuGet пакета ImpromptuInterface, у вас появится extension-метод ActLike<T>(), который будет доступен на всех объектах типа object. Параметр T в данном случае – это именно тип интерфейса который хочется вывесить наружу.

За кулисами этого вызова создается, конечно же, динамическая прокся, которая является одновременно и проксёй для оригинального объекта и при этом реализует нужный интерфейс.

А теперь давайте посмотрим для чего это надо.

Паттерн Null Object

Самый простой пример — это паттерн Null Object, т.е. объект, который ровным счетом ничего не делает.

Вот например, у вас есть интерфейс логирования

public interface ILog
{
  void Info(string msg);
  void Warn(string msg);
}

Потом вы используете этот механизм в каком-то другом классе, например логируете операции с банковским счетом:

public class BankAccount
{
  private ILog log;
  private int balance;
  public BankAccount(ILog log)
  {
    this.log = log;
  }
  public void Deposit(int amount)
  {
    balance += amount;
    // check for null everywhere
    log?.Info($"Deposited ${amount}, balance is now {balance}");
  }
  public void Withdraw(int amount)
  {
    if (balance >= amount)
    {
      balance -= amount;
      log?.Info($"Withdrew ${amount}, we have ${balance} left");
    }
    else
    {
      log?.Warn($"Could not withdraw ${amount} because balance is only ${balance}");
    }
  }
}

Представьте теперь, что логирование вам нафиг не сдалось. Что делать? Передавать null нельзя, будут NRE везде где вызовы, ведь safe call operator ?. повсеместно разработчики, как правило, не используют. Ждем non-nullable types в C#8 или позже.

Один вариант — просто сделать фейковый класс:

public sealed class NullLog : ILog
{
  public void Info(string msg)
  {
    
  }
  public void Warn(string msg)
  {
    
  }
}

Это и есть «каноническая» реализация паттерна Null Object, но она получается легко только когда членов у класса мало, или же ReSharper под рукой. В целом, не хочется писать всю эту муть, особенно если вас не так сильно волнует перформанс.

Итак, третий, «ядерный» вариант с использование ImpromptuInterface. Делаем класс со следующей сигнатурой:

public class Null<T> : DynamicObject where T:class
{
  ⋮

T параметр выше – как раз тип интерфейса. Теперь можно сделать из класса этакий псевдо-singleton (на самом деле нет, создаем каждый раз):

⋮
public static T Instance 
{
  get
  {
    if (!typeof(T).IsInterface)
      throw new ArgumentException("I must be an interface type");
    return new Null<T>().ActLike<T>();
  }
}
⋮

Магия в том, что мы делаем Null<T>, сохраняя тип аргумента, но вывешиваем его не как Null<T> а просто как T.

А что дальше? Дальше нам нужно перехватывать вызовы методов, например:

  ⋮
  public override bool TryInvokeMember(InvokeMemberBinder binder, 
    object[] args, out object result)
  {
    result = Activator.CreateInstance(binder.ReturnType);
    return true;
  }
}

Код выше смотрит на тип возвращаемого значения метода и берет его дефолтную реализацию, т.е. вызывает new T() через System.Activator — это конечно же чревато тем, что если у класса нет дефолтного конструктора, то result будет равен null что очень грустно но не критично если вы не будете пытаться этим результатом пользоваться.

Вот собственно и всё, наш null object готов:

var log = Null<ILog>.Instance;
var ba = new BankAccount(log);
ba.Deposit(100); // логирования не будет

Динамическая Прокси

А теперь давайте представим «обратный» сценарий: у вас есть класс банковского счета, вы хотите добавить логирование «со стороны».

public interface IBankAccount
  {
    void Deposit(int amount);
    bool Withdraw(int amount);
    string ToString();
  }
  public class BankAccount : IBankAccount
  {
    private int balance;
    private int overdraftLimit = -500;
    public void Deposit(int amount)
    {
      balance += amount;
      WriteLine($"Deposited ${amount}, balance is now {balance}");
    }
    public bool Withdraw(int amount)
    {
      if (balance - amount >= overdraftLimit)
      {
        balance -= amount;
        WriteLine($"Withdrew ${amount}, balance is now {balance}");
        return true;
      }
      return false;
    }
    public override string ToString()
    {
      return $"{nameof(balance)}: {balance}";
    }
  }

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

public class Log<T> : DynamicObject where T : class, new()
{
  private readonly T subject;
  private Dictionary<string, int> methodCallCount =
    new Dictionary<string, int>();
  protected Log(T subject)
  {
    this.subject = subject;
  }
  ⋮

Далее можно сварганить фабричный метод, который будет делать тип Log<T> и выдывать его как интерфейс I. Например:

⋮
public static I As<I>() where I : class
{
  if (!typeof(I).IsInterface)
    throw new ArgumentException("I must be an interface type");
  return new Log<T>(new T()).ActLike<I>();
}
⋮

Теперь нам нужно сделать так, чтобы при вызове метода, метод-то вызывался, но кол-во методов тоже записывалось. Для этого нужно перегрузить TryInvokeMember():

⋮
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
  try
  {
    // logging
    WriteLine($"Invoking {subject.GetType().Name}.{binder.Name} with arguments [{string.Join(",", args)}]");
    // more logging
    if (methodCallCount.ContainsKey(binder.Name)) methodCallCount[binder.Name]++;
    else methodCallCount.Add(binder.Name, 1);
    result = subject.GetType().GetMethod(binder.Name).Invoke(subject, args);
    return true;
  }
  catch
  {
    result = null;
    return false;
  }
}
⋮

Что делает код выше? Ну, помимо логирования количества вызовов, он через reflection вызывает нужный метод на объекте и сохраняет результат. Если вдруг падает исключение — что поделать, бывает, пишет в результат null, и всё.

Ну и наконец, если вам вдруг надо чтобы работал честный ToString() на объекте, но он декорировал существующий ToString() того типа, на котором ведется логирование, то это можно сделать вот так:

  ⋮
  public string Info
  {
    get
    {
      var sb = new StringBuilder();
      foreach (var kv in methodCallCount)
        sb.AppendLine($"{kv.Key} called {kv.Value} time(s)");
      return sb.ToString();
    }
  }
  
  // will not be proxied automatically
  public override string ToString()
  {
    return $"{Info}{subject}";
  }
}

Вот собственно и всё, теперь можно юзать вот эту проксю как-то вот так:

var ba = Log<BankAccount>.As<IBankAccount>();
ba.Deposit(100);
ba.Withdraw(50);
WriteLine(ba);

Заключение

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

Если вам интересны паттерны, у меня есть более 10 часов видосиков по паттернам в C#/.NET. Enjoy!

Written by Dmitri

4 мая 2017 at 12:41

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

.NET-ный enum в стиле Rust

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

Как вы уже наверное знаете, у разных языков (C#, Java, Rust, …) понимание того что такое enum (перечисление) абсолютно разное. Иногда мне в C# хочется чтобы было как в Rust, да и в C++ тоже. Поэтому это рассказ про то, как сделать «хорошо» на примере C#.

Однородный тип

Давайте начнем. Например, чтобы писать

Color color = Color.Red;

нужно чтобы Color.Red имел тот же тип что и Color. То есть, если утрировать, можно и так написать:

public class Color
{
  private byte r, g, b, a;
  public Color(byte r, byte g, byte b, byte a)
  {
    this.r = r;
    this.g = g;
    this.b = b;
    this.a = a;
  }
  public static Color Red = new Color(255, 0, 0, 255);
}

Вопрос теперь только в том как генерить все это многообразие. В Rust ничего особо писать не надо, а в C#, как видите, нужно сделать очень много телодвижений. Но результат практически тот же.

Что-то мы потеряли, а что-то приобрели. Потеряли мы все Enum.GetValues(), т.е. возможность получить все предопределенные цвета. Как их вернуть? Ну наверное как-то вот так:

public static Color Red = new Color(255, 0, 0, 255);
public static Color Blue = new Color(0, 0, 255, 255);
public static Color[] Values = new[] {Red, Blue};

Что ещё продолбано? Ну, имена этих переменных. Ой! А вот это непросто будет сделать:

private BiDictionary<string, Color> colors = new BiDictionary<string, Color>
{
  ["Red"] = Red,
  ["Blue"] = Blue
};
public IEnumerable<string> Names => colors.Keys;
public IEnumerable<Color> Values => colors.Values;
public Color this[string name] => colors[name];
public string this[Color color] => colors.Reverse[color]; // возможно излишне?
private string ToStringImpl()
{
  return $"{nameof(r)}: {r}, {nameof(g)}: {g}, {nameof(b)}: {b}, {nameof(a)}: {a}";
}
public override string ToString()
{
  if (colors.Reverse.ContainsKey(this)) return colors.Reverse[this];
  return ToStringImpl();
}

BiDictionary как вы поняли – это словарь который работает в обе стороны, чтобы можно было быстро искать. Реализации подобного есть в тырнете, плюс самому на коленке написать тоже несложно.

Итак, всё ли мы реализовали? Да вроде да, можно использовать

Color red = Color.Red;
Color my = new Color(255, 0, 255, 0);

и вывод будет вот такой:

Red
r: 255, g: 0, b: 255, a: 0

Дискриминированное объединение

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

type IntOrBool =
  | Boolean of bool
  | Integer of int // это не раст, это F#

Ха! То есть смотрите, у вас есть два абсолютно разных типа, с разными данными, и они должны приводиться к некому типу IntOrBool. Первое что приходит в голову — это абстрактный класс вроде

public abstract class IntOrBool
{
  public int Tag => this is Integer ? 0 : 1;
  public bool IsInteger => this is Integer;
  public bool IsBoolean => this is Boolean;
  public static Boolean NewBoolean(bool value) => new Boolean(value);
  public static Integer NewInteger(int value) => new Integer(value);
}

Ну и соответственно каждый из наследников должен выглядеть как-то вот так:

public class Integer : IntOrBool
{
  public int Value { get; set; }
  public Integer(int value)
  {
    Value = value;
  }
  // делаем вид что мы int
  public static implicit operator Integer(int value)
  {
    return new Integer(value);
  }
  public static implicit operator int(Integer integer)
  {
    return integer.Value;
  }
}

Но дальше у нас те же проблемы что и ранее — как сделать ToString(), как вывести список всех возможных типов объединения? Мы точно знаем как это делает F# — он для этого использует атрибуты, и по сути поиск всех классов превращается в использование Reflection. Что уныло. А что если сделать вот так?

public static Type[] Cases = new[] {typeof(Boolean), typeof(Integer)};

В конечном счете, что мы теряем? Ладно, пора уже попробовать использовать все это в типичном (насколько это возможно для такого синтетического примера) сценарии:

IntOrBool foo = 0; // FAIL :(

Опа, уже нифига не работает. Хотели удобство а получилось не очень. Что, еще операторов? Пожалуйста

public static implicit operator IntOrBool(int value) => NewInteger(value);
public static implicit operator IntOrBool(bool value) => NewBoolean(value);

ок, теперь можно инициализировать, а можно ли проверять if-ом?

IntOrBool foo = 0;
if (foo is Integer bar)
{
  int i = bar;
  WriteLine(i);
}

Кривоватенько, но работает. foo is int bar конечно же писать нельзя. Ну и выборку по шаблону в стиле case Integer(i) пока тоже в C# не сделали.

Да, насчет ToString() — тут будет просто GetType().ToString() для кейсов у которых нет собственных данных. То есть будут порождаться типы даже там, где они не нужны. Для F# это как бы норма (в F# например, любой переданный оператор превращается в struct), но в C# это не очень хорошо, хотя опять же, ничего критичного.

Вообщем вот такой финт ушами, не дающий особых плющек, т.к. ко всему этому нужен умный switch и полноценный pattern matching. Да, и Deconstruct(), если вы его реализуете, не поможет для типов которые имеют один member. Сорри! ■

Written by Dmitri

14 апреля 2017 at 0:47

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

Tagged with

Что нового в C# 7

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

Ну что, всех можно поздравить с релизом VS2017? Для начала, я пожалуй признаюсь что я давно уже (время наверное на месяцы пошло) использую Rider (и даже запилил по нему видеокурс), но релиз VS2017 — это не только релиз слегка медленной, 32-битной Windows-only IDE которая даже не умеет правильно рисовать лигатуры. Это еще и релиз новых версий компиляторов — именно того, что Microsoft нам обещали делать out-of-band, но обещание оказалось наглой ложью, как для C++ так и для C#.

Что же, давайте посмотрим что там в новом сишарпике…

Локальные методы

Вообще, проблемы с локальными методами толком не было, т.к. в любой функции можно декларировать лямбда-переменную, что и есть по сути метод. Так что в C#7 это разве что привели в более удобоваримый вид:

public static Tuple<double,double> Solve(double a, double b, double c)
{
  double CalculateDiscriminant()
  {
    return b * b - 4 * a * c;
  }
  var disc = CalculateDiscriminant();
  return Tuple.Create(
    (-b + Math.Sqrt(disc)) / (2 * a),
    (-b - Math.Sqrt(disc)) / (2 * a)
  );
}

Что мы можем заметить в примере выше? Правильно, вложенная функция умеет захватывать окружение, в т.ч. аргументы внешней функции. И конечно же дать функции CalculateDiscriminant аргументы a,b,c не получилось бы, т.к. эти имена уже заняты.

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

Касательно полезности фичи: мне норм. Этот подход позволяет плодить меньше членов на уровне класса, а баловаться с лямбдами напряжно т.к. для любой лямбды нужно определить тип Func<int,Foo,string> и это порой бывает утомительно — хотя в некоторых случаях Решарпер помогает. Вообще было бы лучше если бы нам позволяли еще делать локальные статические переменные прямо в методах, как это позволяет делать C++.

Out-переменные

Обычно как было: декларация переменной это statement. Все, теперь это по сути expression т.к.:

string s;
if (DateTime.TryParse(s, out DateTime dt))
{
  // используем dt
}

Это чисто «сахарная» фича, и резонным будет вопрос — а что если DateTime не пропарсится а мы все равно попытаемся ей воспользоваться? Ну, тут все просто — у этой штуки будет дефолтное значение. Но зато заметьте, dt имеет тот же scope, что и строка s.

Кортежи

Кортежи уже были в C#, но вставлены они были криво: все эти Item1, Item2 не добавляли к удобству пользования, и кому-то могло показаться что рано или поздно нам вообще подсунут variadic templates. Но разрабы поймали себя и все-таки сделали что-то годное:

public static (double x1, double x2) Solve(double a, double b, double c)
{
  var disc = CalculateDiscriminant();
  double CalculateDiscriminant()
  {
    return b * b - 4 * a * c;
  }
  return (
    (-b + Math.Sqrt(disc)) / (2 * a), 
    (-b - Math.Sqrt(disc)) / (2 * a)
  );
}

И вызвать можно это вот так:

var (x1, x2) = QuadraticEquationSolver.Solve(1, 10, 16);
Console.WriteLine(x1 + ", " + x2);

Ну а если декларировать кортежи на месте, то правила те же самые, и есть варианты — давать элементам кортежа имена или нет:

var names = (first: "Dmitri", last: "Nesteruk");
Console.WriteLine(names.first);

Как видите, кортеж определяется как тип через скобочки и список типов (т.е. (double,double) условно-эквивалентно ValueTuple<double,double>). Но ложка дегтя тут огромная: для всего этого нужен внешний пакет System.ValueTuple. Что?!?!?

Спокойно, очевидно что просто эту тему недопилили и ValueTuple<T, ...>, который должен был быть в .NET 4.6.2 BCL просто туда не попал. Что это за класс? Да просто еще одна реализация кортежа, но в этот раз вся ее суть закрыта синтаксическим сахаром вроде того что я привел выше. Если кому интересно что под капотом, вот сорцы.

Expression-bodied members

На момент C#6, некоторые вещи можно было писать как expression bodies, т.е например void Foo(x) => x+1; делал вполне себе валидный метод. Сейчас же, это счастье распространилось помимо методов и пропертей еще на конструкторы/деструкторы, геттеры и сеттеры:

class Person
{
  Person() => Names = new[]{"Nameless One"};
  public int FirstName
  {
    get => Names[0]; // ну вы поняли
  }
}

Вообще вся эта expression bodied тематика экономит хорошо так символов. Единственное что мне не нравится это каша вроде

class Person
{
  bool CanVote => Age <= 16; // без лигатур - коряво :(
}

но это просто один конкретный пример того как можно получить нечитаемый код.

Throw Expressions

Тут мне 2017 студия осмелилась подправить Решарперный код. Было что-то вроде этого:

class Foo
{
  Foo(Bar bar)
  {
    if (bar == null) throw new SomeException();
    this.bar = bar;
  }
}

но теперь оказывается можно писать вот так

class Foo
{
  Foo(Bar bar)
  {
    this.bar = bar ?? throw new SomeException();
  }
}

Интересный ход конем, однако. Хорошо ли бросать из места где ожидается конкретный return value? Ну, если компилятор может это безопасно обработать, почему бы и нет — главное потом не пытаться прочитать поле, которое мы так и недописали.

Pattern matching

Когда-то я писал про самопальный pattern matching, но тут сделали реальную реализацию.

Начнем с простого — теперь проверку на тип можно сделать прямо в if-е и сразу получить и проверку и приведенный тип:

if (shape is Rectangle rc)
{
  var area = rc.Width * rc.Height;
}

Ну и потом нам разрешили делать switch на этом месте:

Shape s = new Rectangle { width = 0, height = 0 };
switch (s)
{
  case Rectangle rc when (rc.width == rc.height):
    Console.WriteLine("It's a square!");
    break;
  case Circle c:
    break;
}

Заметьте ключевое слово when выше. Ничего не напоминает? Правильно, F#. Но конечно F# все еще лидирует в этом плане. Хотя бы потому, что в F# можно делать алгебраические типы данных, что очень полезно когда ты, например, что-нибудь структурированное парсишь, например MathML.

Ну хорошо, смотрите, хоть что-то сделали. Конечно, нельзя мэтчить на Rectangle(42, double h) — это откинули на будущее. И switch на кортежах тоже пока не работает. Грусть-печаль.

Ref returns

Как вернуть элемент массива by reference? Ну строго говоря, можно запинить весь массив и попытаться вернуть указатель в unsafe. Но теперь есть более гуманный способ — ref на чем угодно.

int[] numbers = new int[] { 1, 2, 3 };
ref int refToSecond = ref numbers[1];
refToSecond = 123;
Console.WriteLine(string.Join(",",numbers)); // 1, 123, 3

Хмм, ну это хорошо конечно, но со списком такое не пройдет

List<int> numbers = new List<int> { 1, 2, 3 };
ref int second = ref numbers[1]; // A property or indexer cannot be used⋮

Хмм, да, полезность сей затеи стремится к нулю. Нельзя например сослаться на букву в строке.

Все это — простой aliasing, то есть еще одно имя для той же самой переменной. Это не «ссылка на область памяти» т.к. это обычно требует слово fixed чтобы вменяемо работать в unsafe контексте, а у нас все тут safe.

Да, теперь можно и из метода ref на переданный ref возвращать, то есть возможно вот такое мракобесие:

static ref int Min(ref int x, ref int y) {
  if (x < y)
    return ref x;
  else
    return ref y;
}
⋮
int a = 1, b = 2;
 ref int c = ref Min(ref a, ref b);
c = 123; // a == 123!!!

Holy s~t, посмотрите сколько тут раз ref написано. Даже в вызове используется ref Min(...) потому что если ты опустишь этот ref это значит что тебе не нужен референс на значение, а нужно само значение.

int d = Min(ref a, ref b); // вполне валидно, d == 1

Проблема тут в том, что этот ref — это то же самое что референсы в C++, из-за которых теперь в уютном сишарпике можно писать треш вроде Min(ref a, ref b) += 123 (по сути делает a += 123), что абсолютно отвратительно для читания и понимания. К тому же, неопытные программисты тут же будут ломиться и возвращать адреса локальных переменных, что делать нельзя:

static ref int Foo()
{
  int x = 0;
  return ref x; // не скомпилируется - cannot return local by reference
}

Хотя помнится что году так в 2012, Липперт угрожал что они могут и этот сценарий поддержать.

Что еще тут?

Теперь можно делать подчеркивания в литералах: int x = 123_456;. Также появились бинарные литералы — теперь можно писать var literal = 1100_1011_____0101_1010_1001;. Подчеркиваний может быть сколько угодно.

Pattern matching — все самое вкусное не попало в C#7, ждем следующих версий. Должны быть всякие вот такие штуки поддержаны:

switch (foo)
{
  case 123: ⋮
  case MyEnum.Stuff: ⋮
  case string s: ⋮
  case Point(int x, 321) where x > 0: ⋮
  case Point(12, 23) : ⋮
  default: ⋮
}

Record types — не вошли, будут в C#8 наверное.

Ну вообщем как-то так. А вы заметили насколько шустрее стал компилятор C#7? Серьезно, видимо опять что-то подкрутили с инкрементальностью. Кстати, в VS2015 сделали обалденные улучшения инкрементальности C++, а тут кажется улучшили C#. У меня «горячая» компиляция летает, хотя «холодная» все еще тормозит как и раньше.

Наверняка что-то еще из фич забыл, напишите в комментариях. ■

Written by Dmitri

13 марта 2017 at 10:54

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

Мысли о «новом» обещанном нам C#

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

Как вы понимаете, Microsoft сейчас толком не до языковых фич т.к. сам компилятор переделали и даже уже сами переключились на Roslyn в процессе разработки, что как бы намекает на большие планы на 2014. Соответственно, те фичи которые Мадс осветил на NDC London не являются системообразующими, поэтому получите список:

  • Первичный конструктор в стиле class Size(int cx, int cy) { ... }, где как вы догадались, cx и cy это значения которые потом можно присвоить пропертям. Позволяет писать new Size(320,240), и вообще своровано с F#.
  • Возможность задавать значения readonly свойствам в стиле public int Width { get; } = cx. Даже не знаю что тут сказать – у меня часто свойства еще и в UI используются, а там все равно будет поле, так что нет разницы. (Ждите от Roslyn автореализации INPC? Не дождетесь.)
  • Property expressions, т.е. возможность писать property get без собственно блока get{}, return и прочих излишеств:
    public int Area => Width * Height;
    
  • Та же тема для методов. Фактически, просто удаление скобочек и return, но я уже вижу как новички пихают в эти стейтменты всякий мусор:
    public Point Move(int dx, int dy) => new Point(X + dx, Y + dy);
    
  • params IEnumerable, потому что когда хочется неограниченное число аргументов, то IEnumerable удобнее чем массив? На самом деле, могли как в D попробовать делать вещи через convention over configuration, т.е. вместо ключевого слова params просто сказать, что если есть только один параметр и он IEnumerable<T> или T[], то давать пользователям передавать массив через Foo(a,b,c) – и все были бы довольны.
  • Monadic null checking означает что вместо person.With(x => x.Address).With(x => x.HouseName) можно написать person?.Address?.HouseName, и везде будут проверки на null. Шикарно конечно, может немного поздновато, к тому же как показала практика, проверка на null – это не единственный concern, которых можно вкладывать в цепочки из лямбд.
  • Вывод типов для конструкторов, т.е. вместо Tuple.Create(foo,bar) можно наконец-то писать new Tuple(foo,bar)
  • Инлайновые декларации для out параметров по мне так самый большой фейл, пожалуй. Идея в том чтобы можно было писать
    if (foo.TryGetValue(out int x))
    {
      // use x, it's already been declared!
    }
    

    Ну и конечно идея в том что если параметров возврата несколько, то это как бы упрощает жизнь. Знаете что еще упрощает жизнь?

    let (x1,x2) = SolveQuadratic(1,10,16);
    

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

  • Импорт типа в пространство имен: уже хорошо, сделаем using System.Math чтобы не пользоваться поистине невменяемыми с точки зрения здравого смысла конструкциями вроде Math.Sin(). Хотя это только полумера: еще нужно удалить заглавные буквы (ну зачем они?), добавить оператор возведения в степень (с целочисленной перегрузкой), и уже будет можно дышать. Но разве ж кто-то об этом думает?

Ничего из списка выше особо не впечатляет. Гораздо интересней сам Roslyn (все же надеюсь что он будет «готов» к VS2014), ну и как это не странно, С++ сейчас развивается как-то быстрее, причем без всякого переписывания компилятора. ■

Written by Dmitri

21 декабря 2013 at 13:36

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

Управление подписками на события

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

Итак, небольшая заметка на тему того, как я работаю с подписками на события. Проблема такая: у меня есть XMPP-клиент, он генерирует тучу событий, которые хочется отловить, но помимо этого, еще иногда нужно делать reset этому клиенту, напрочь удаляя все подписки и создавая новые.

Для начала, я создаю класс DisposableCollection который умеет вызывать Dispose() на каждом элементе коллекции:

public sealed class DisposableCollection : Collection<IDisposable>, IDisposable
{
  public void Dispose()
  {
    foreach (var obj in this)
      obj.SafeDispose();
  }
}

SafeDispose() в коде выше – это всего лишь Dispose() с проверкой на null. Метод также возвращает «флаг успешности», что порой бывает полезно.

public static bool SafeDispose(this IDisposable thіs)
{
  if (thіs != null)
  {
    thіs.Dispose();
    return true;
  }
  return false;
}

Еще один extension method — это инверсия потока управления для добавления элементов в коллекцию:

public static T AddTo<T>(this T thіs, Collection<T> coll)
{
  coll.Add(thіs);
  return thіs;
}

А теперь всем этим можно пользоваться. Для подписок используем ReactiveExtensions.

public sealed class XmppConnectionManager : IResetable
{
  private XmppClient client;
  private DisposableCollection subscriptions;
  private void InitializeEvents()
  {
    // wire up events
    Observable.FromEventPattern<ExceptionEventArgs>(x => client.OnError += x, x => client.OnError -= x)
      .Subscribe(x => OnError(x.EventArgs))
      .AddTo(subscriptions);
    Observable.FromEventPattern<EventArgs>(x => client.OnLogin += x, x => client.OnLogin -= x)
      .Subscribe(x => OnLogin(x.EventArgs))
      .AddTo(subscriptions);
    ...
  }
}

Соответственно, если нужно вдруг отписаться от всех подписок, это делается очень быстро:

subscriptions.SafeDispose();

А теперь загадка: почему методы расширения, приведенные выше, компилируются несмотря на название параметра thіs? Дам намек: как ключевое слово имя параметра не подсветилось.

Written by Dmitri

2 августа 2011 at 6:27

Опубликовано в .NET, C#, rx

Хранение preset-ов в setting-ах

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

Сегодня я зарелизил версию 1.7.2 Типографикса, добавив всего одну фичу – возможность держать несколько наборов настроек. Сделал я это для того, чтобы можно было сохранять настройки редактора для разных платформ – в моем случае, для этого блога и devtalk, а также для Хабра, ГДН, CodeProject’а и других систем.

Примечательно то, что простенькая задача сохранить в словарике несколько пресетов и потом записать все это в Properties.Settings превратилась в непростую затею.

Первая попытка

Сначала я сделал все правильно: создал Dictionary<string, ConversionOptions>, то есть мэп названий пресетов на их значения, и тупо попробовал записать все это в настройки. Ничего не получилось. Оказывается, большое количество классов в WPF (такие как Color или Thickness) не сериализуются! Это значит, что по сути дела чтобы сериализовать тип мне нужно их выкинуть!

Следующей проблемой оказалось то, что .Net бросал исключение при попытке сериализовать событие PropertyChanged. Это правда очень быстро релилось прописыванием [field: NonSerialized], но толком делу не помогло.

Вторая попытка

Проблема в том, что несмотря на то, что все работало, настройки отказались записывать Dictionary<>. Никаких исключений не было, тип просто не сохранялся в Properties.Settings и воответственно при попытке считать, возвращался null.

Самое простое в этом случае (точно так же как и в случае с несериализуемыми типами) – это сконвертировать структуру не в XML а в… JSON! (Формат выбран произвольно.) Соответственно, я скачал ServiceStack.Text и заменил тип свойств в Settings на string. В результате, сериализация происходит вот так:

Settings.Default["OptionPresets"] =
  JsonSerializer.SerializeToString(OptionPresets, typeof (Dictionary<string, ConversionOptions>));

Ну а что касается тех «несериализуемых» полей то, увы, приходится делать строковые или аналогичные backing fields, со всемы вытекающими последствиями.

Written by Dmitri

5 июня 2011 at 23:22

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