Заметки с тегом
Dotnet
Сравнение async await и Task.ContinueWith()

Это краткая выжимка из рабочего доклада по работе с async/await в C#. Для наглядности, параллельно рассматривается подход с использованием блока ContinueWith.

Основные паттерны асинхронного программирования

async/await — синтаксическая обертка над задачами

  • await ставит на паузу текущий метод, ожидая выполнения задачи.
  • Выглядит как блокирующая (синхронная) операция.
  • Не блокирует текущий поток.
  • Выполнение продолжается в том же контексте, из которого была вызвана задача, если явно не указано иное.
  • Ключевое слово async указывается, чтобы среда исполнения воспринимала await как ключевое слово.
  • await метод начинает выполняться синхронно. Если он уже закончил свое выполнение то новый поток не создается. Все продолжается в том же потоке. Подробнее в ответе на stackoverflow.com.
  • await работает с любым типом, для которого реализован метод GetAwaiter(). Подробнее в статье - await anything.

Демонстрационное приложение

Примеры показываются на тестовом Windows Form приложении. GitHub репозиторий с приложением.

async/await тестовое окно

Асинхронные действия лежат в PeopleRepositoryAsync:

public class PeopleRepositoryAsync
{
    public async Task<List<string>> GetPeopleList()
    {        
        await Task.Delay(2000);                       
        return new List<string>
        {
            "John Smith",  
            "Ivan Ivanov",
            "Joao Fetucini"
        };
    }
}

Метод GetPeopleList() асинхронно ожидает 2 секунды и возвращает список пользователей.

Первое сравнение TAP и async/await подхода

Реализация с Task и ContinueWith

Добавим код для получения списка пользователей в обработчик нажатия кнопки “Fetch Data (with Task)” - buttonTask_Click:

Task<List<string>> peopleTask = Repository.GetPeopleList();
List<string> people = peopleTask.Result;

Этот код не будет выполняться асинхронно. Он будет ожидать завершение задачи peopleTask в основном потоке, поэтому UI заморозится. Добавим конструкцию ContinueWith(t => { }):

peopleTask.ContinueWith(t =>
{
  List<string> people = peopleTask.Result;
});

Теперь задача по получению пользователей выполнится асинхронно. После её завершения выполнится код в блоке ContinueWith. Добавим в ContinueWith отображение полученных имен в textBoxMain:

textBoxMain.AppendText($"{Environment.NewLine}Person list:{Environment.NewLine}");
foreach (var person in people)
{
    textBoxMain.AppendText($"- {person}{Environment.NewLine}");
}

Если запустить приложение и нажать на “Fetch Data (with Task)”, то возникнет ошибка. Все потому, что код в блоке ContinueWith выполняется в потоке, отличном от того где находится SynchronizationContext UI потока. Для выполнения в нужном потоке добавим в вызов метода ContinueWith аргумент TaskScheduler.FromCurrentSynchronizationContext():

peopleTask.ContinueWith(t => { ... }, TaskScheduler.FromCurrentSynchronizationContext());

Теперь приложение работает корректно. Перейдем к реализации этого кода с помощью ключевых слов async и await.

Реализация с async/await

Основное отличие от предыдущей реализации — код будет похож на синхронный. Перейдем в обработчик нажатия кнопки “Fetch Data (with await)” - buttonAwait_Click. Добавим код для получения списка пользователей:

List<string> people = await Repository.GetPeopleList();

В объявление метода добавим слово async, чтобы среда исполнения поняла что await это ключевое слово, а не просто переменная. Вставим без изменений код из ContinueWith:

textBoxMain.AppendText($"{Environment.NewLine}Person list:{Environment.NewLine}");
foreach (var person in people)
{
    textBoxMain.AppendText($"- {person}{Environment.NewLine}");
}

Все. Приложение работает так же как и в предыдущем пункте. Код выглядит как синхронный. Выполнение продолжается в том же потоке, из которого и было вызвано.

Обработка ошибок

Для демонстрации добавим в метод GetPeopleList код вызова ошибки (после Task.Delay(2000)):

throw new NotImplementedException("Метод не реализован!");

Обработка ошибок для await метода

Сначала рассмотрим самый простой случай. Отлов и обработка ошибок для метода с await происходит как в синхронном коде:

try
{
    // Получение и вывод значений из репозитория
}
catch (Exception ex)
{
    MessageBox.Show(ex.Message, "ОШИБКА");
}
finally
{
    // Критичный к выполнению код
}

Всё. Дополнительно писать ничего не надо, ошибка будет поймана.

Обработка ошибок для Task метода

Для метода с блоком ContinueWith обработать ошибки можно несколькими способами.

Первый, использовать еще один вызов ContinueWith на задаче. Вызовем ContinueWith с 2 дополнительными параметрами:

peopleTask.ContinueWith(t =>
    {
        // Получение и вывод значений из репозитория
    },
    CancellationToken.None,
    TaskContinuationOptions.OnlyOnRanToCompletion,
    TaskScheduler.FromCurrentSynchronizationContext());

Главное — аргумент TaskContinuationOptions.OnlyOnRanToCompletion. Он указывает, что блок кода выполнится только если в задаче не было ошибок.

Теперь, код для обработки ошибки:

peopleTask.ContinueWith(t =>
    {
        foreach (var exception in t.Exception.Flatten().InnerExceptions)
        {
            MessageBox.Show(exception.Message);
        }
    },
    CancellationToken.None,
    TaskContinuationOptions.OnlyOnFaulted,
    TaskScheduler.FromCurrentSynchronizationContext());

Опция OnlyOnFaulted указывает, что код в блоке выполниться только при ошибке в задаче. Оператор foreach разворачивает ошибки в «плоское» состояние, т. к. все ошибки представляются в виде иерархии и оборачиваются в AggregateException.

Для имитации блока finally, напишем:

peopleTask.ContinueWith(t =>
    {
        // Критичный к выполнению код
    },
    CancellationToken.None);

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

peopleTask.ContinueWith(t =>
    {
        if (t.Status == TaskStatus.RanToCompletion)
        {
            // Получение и вывод значений из репозитория
        }
        if (t.Status == TaskStatus.Faulted)
        {
            // Обработка ошибок
        }
        // (finally) Критичный к выполнению код
    },
    TaskScheduler.FromCurrentSynchronizationContext());

Отмена действий

Обновим метод GetPeopleList класса Repository. Добавим параметр CancellationToken и точку отмены после вызова Task.Delay(2000):

public async Task<List<string>> GetPeopleList(CancellationToken cancellationToken = new CancellationToken())
{
    await Task.Delay(1500, cancellationToken);
    cancellationToken.ThrowIfCancellationRequested();
    return new List<string> { ... };
}

Обратит внимание, что отмена произойдет только после ожидания в 2 секунды. Само действие Task.Delay не прерывается.

Для операции отмены используем кнопку “Cancel request” с обработчиком buttonCancel_Click. Объекту CancellationToken можно задать значение только при инициализации. Поэтому создадим переменную CancellationTokenSource. Она позволяет генерировать токены и изменять их состояние во время выполнения.

В класс MainForm добавим поле:

private CancellationTokenSource _tokenSource;

А в обработчик buttonCancel_Click код для подачи токену сигнала отмены:

_tokenSource.Cancel();

Обработка отмены для async метода

В обработчике buttonAwait_Click изменим вызов метода GetPeopleList(), добавив инициализацию _tokenSource и передав сгенерированный токен в качестве аргумента:

_tokenSource = new CancellationTokenSource();
List<string> people = await Repository.GetPeopleList(_tokenSource.Token);

Добавим обработку операции отмены:

catch (OperationCanceledException ex)
{
    MessageBox.Show(ex.Message, "Canceled");
}

Обработка отмены для Task метода

Для buttonTask_Click добавим похожий код для передачи токена:

_tokenSource = new CancellationTokenSource();
Task<List<string>> peopleTask = Repository.GetPeopleList(_tokenSource.Token);

Для обработки операции отмены в блок ContinueWith добавим:

if (t.Status == TaskStatus.Canceled)
{
  MessageBox.Show("Operation Canceled", "Canceled");
}

Deadlocks

Добавим в класс Repository метод DeadlockTestAsync():

public async Task DeadlockTestAsync()
{
    await Task.Delay(1500);
    Console.WriteLine("Done!");
}

Вызовем этот метод в обработчике кнопки “Deadlock”:

async void buttonDeadlock_Click
{
    Repository.DeadlockTestAsync().Wait();
}

Все. При нажатии на кнопку возникнет Deadlock. Почему? Рассмотрим по пунктам:

  1. DeadlockTestAsync() вызывается на потоке с UI.
  2. Task.Delay() запускается в новом потоке.
  3. await захватывает SynchronizationContext и подключает continuation для выполнения действий после завершения.
  4. Вернемся к вызову DeadlockTestAsync().
  5. Wait() ждет завершение задачи в UI потоке.
  6. Task.Delay() ожидает выполнить продолжение на UI потоке.
  7. Но поток в ожидание - Дедлок!
  8. Все потому, что задача не вернется из DeadlockTestAsync(), пока не выполнится “продолжение”.

Для избежания подобной ситуации, в библиотеках, лучше писать .ConfigureAwait(false):

await Task.Delay(1500).ConfigureAwait(false);

Это позволит выполнить “продолжение” в том же потоке, в котором работала задача. В моем примере это будет поток, отличный от UI потока.

Полезные ссылки

Перетаскивание кода в окно Watch в Visual Studio

Маленькая хитрость, которую я открыл совсем недавно. Для добавления переменной в окно Watch, необязательно вводить туда значение или копировать и вставлять из редактора. Просто перетащите её:

Перетаскивание переменной в окно watch

Второе окно Watch 2 обрело для меня смысл и, главное, легкость в использовании:

Перетаскивание переменной в окно watch 2

Сайт с полезными советами для .Net разработчиков — dailydotnettips.com.

Новый оператор ?. в C# 6

Одно из нововведений в С# 6 — оператор ?.. Давайте рассмотрим, где и как его использовать.

Преобразование цепочки вызовов

С новым оператором уменьшается количество проверок на null в цепочке вызовов:

var documentName = taskManager.CurrentTask == null ? null :
  (taskManager.CurrentTask.GetDocument() == null ? null :
    taskManager.CurrentTask.GetDocument().Name);

Перепишем в одну строчку используя ?.:

var documentName = taskManager.CurrentTask?.GetDocument()?.Name;

documentName примет значение null, если CurrentTask, GetDocument() или Name вернет null.

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

Использование с индексатором

Рассмотрим получение значения из коллекции по индексу, с проверкой набора значений на null:

var tasks = (taskManager.Tasks != null) ? taskManager.Tasks[index] : null;

С новым оператором проверка на null условным оператором не расписывается. Бонус — выражение в 2 раза короче:

var tasks = taskManager.Tasks?[index];

Оборачивание в нулевые типы

Результат выполнения String.Equals()bool. Скомпилируется ли следующий код?

public void Main()
{
  String str = "x";
  bool result = str?.Equals("x");
}

Нет. Возникнет ошибка: Cannot implicitly convert type ’bool?’ to ’bool’. Такое поведение ожидаемо, так как str?.Equals("x") вернет null если переменная str не инициализирована и bool в остальных случаях.

В подобных ситуациях, при использовании оператора ?., возвращаемый тип функции T будет оборачиваться в Nullable<T>.

Использование в условии

Есть задача — проверить коллекцию на наличие в ней элементов. Типичный код:

if (taskManager.Tasks != null && taskManager.Tasks.Any())
  Console.Write("Tasks has items!");

Попробуем переписать:

if (taskManager.Tasks?.Any())
  Console.Write("Tasks has items!");

Код не скомпилируется. Как написано в предыдущем пункте, taskManager.Tasks?.Any() вернет Nullable<bool> тип, который нельзя однозначно трактовать в условии if. Дополним код:

if (taskManager.Tasks?.Any() == true)
  Console.Write("Tasks has items!");

Работает. И на одно условие стало меньше.

Комбинируя с оператором ??, перепишем в эквивалентный код:

if (taskManager.Tasks?.Any() ?? false)
  Console.Write("Tasks has items!");

Комбинация с оператором ??

Оператор ?? называется оператором объединения со значением null. Если операция возвращает null, оператор ?? подставит значение из правой части выражения.

var documentName = taskManager.CurrentTask?.GetDocument()?.Name ?? "No Name";

Если CurrentTask, GetDocument() или Name вернет null, то переменная примет значение "No Name".

Использование с делегатами

Стандартный код для вызова делегата:

var handler = this.PropertyChanged;
if (handler != null)
  handler()

Используя ?., больше не надо каждый раз писать такой код, делегат вызывается одной строкой:

PropertyChanged?.Invoke(e)

Компилятор создает код для вычисления PropertyChanged только один раз, запоминая результат во временной переменной.

Обратите внимание, следующий код вызовет ошибку:

PropertyChanged?(e)

Вызов метода Invoke обязателен.

Полезные ссылки

Что такое монады на примере C#

Конспект — вольный перевод одного из лучших циклов статей о монадах. Эрик Липперт, на протяжении 13 глав, отвечает на вопрос:

Я C# разработчик без опыта в «функциональном программировании». Что такое «монада» и как можно её использовать для себя?

Оригинальный цикл статей доступен по тегу monads.

Часть первая

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

«Шаблон монады» это еще один шаблон для типов. Как, например, одиночка (синглтон).

Часть вторая

  • Nullable<T> — представляет объект типа T, который может быть null (в дальнейшем, подразумевается что Nullable<T> может работать с любым типом данных).
  • Func<T> — представляет объект типа T, который будет вычислен отложено (в дальнейшем, для большей ясности, будет использоваться делегат delegate T OnDemand<T>();).
  • Lazy<T> — представляет объект типа T, который будет вычислен отложено в первый раз, а после, закеширован.
  • Task<T> — представляет объект типа T, который будет вычислен асинхронно и будет доступен в будущем, если уже не вычислен.
  • IEnumerable<T> — представляет упорядоченную, доступную только для чтения последовательность от нуля и более элементов типа T.

Часть третья

Первое требование для монад: «если M<T> это тип-монада, тогда должен быть простой путь по превращению любого значение типа T в значение типа M<T>». Например:

static Nullable<T> CreateSimpleNullable<T>(T item) { return new Nullable<T>(item); }
static OnDemand<T> CreateSimpleOnDemand<T>(T item) { return () => item; }
static IEnumerable<T> CreateSimpleSequence<T>(T item) { yield return item; }

Кажется, что второе требование просто сформулировать: «из монады M<T>можно получить значение типа T». Но не все так однозначно. Начнем с очень специфичного вопроса. Можно легко прибавить единицу к целочисленному типу, но как «прибавить единицу» к типу-монаде обернутого вокруг целочисленного типа?

Для Nullable<T>:

static Nullable<int> AddOne(Nullable<int> nullable)
{
  if (nullable.HasValue)
  {
    int unwrapped = nullable.Value;
    int result = unwrapped + 1;
    return CreateSimpleNullable(result);
  }
  else  
    return new Nullable<int>();
}

То есть можно развернуть, произвести операцию и завернуть? Не совсем, если проделать ту же операцию для OnDemand<T>(), который обернут вокруг DateTime.Now.Seconds, то получится статическое значение. Поэтому проделанную операцию вместе с разворачиванием необходимо завернуть в функцию, как показано здесь:

static OnDemand<int> AddOne(OnDemand<int> onDemand)
{
  return () =>
  {
    int unwrapped = onDemand();
    int result = unwrapped + 1;
    return result;
  };
}

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

Для Lazy<T>:

static Lazy<int> AddOne(Lazy<int> lazy)
{
  return new Lazy<int>(() =>
  {
    int unwrapped = lazy.Value;
    int result = unwrapped + 1;
    return result;
  });
}

Для Task<T>:

async static Task<int> AddOne(Task<int> task)
{
  int unwrapped = await task;
  int result = unwrapped + 1;
  return result;
}

И, наконец, для IEnumerable<T>:

static IEnumerable<int> AddOne(IEnumerable<int> sequence)
{
  foreach(int unwrapped in sequence)
  {
    int result = unwrapped + 1;
    yield return result;
  }
}

Таким образом, одно из полезных правил для шаблона монад — добавление единицы к завернутому целочисленному типу производит другой завернутый целочисленный тип, с сохранением всех особенностей.

Часть четвертая

Напишем метод, который позволит делать оболочку над любыми Nullable<T> функциями, а не только операцией по добавлению единицы:

static Nullable<T> ApplyFunction<T>(
  Nullable<T> nullable,
  Func<T, T> function)
{
  if (nullable.HasValue)
  {
    T unwrapped = nullable.Value;
    T result = function(unwrapped);
    return new Nullable<T>(result);
  }
  else
    return new Nullable<T>();
}

Теперь метод AddOne(...) будет выглядеть так:

static Nullable<int> AddOne(Nullable<int> nullable)
{
  return ApplyFunction(nullable, (int x) => x + 1);
}

Но, допустим, мы хотим функцию которая принимает тип int и возвращает double. Например, поделить 2 целых числа и получить результат типа double. Для этого, перепишем метод ApplyFunction в следующий вид:

static Nullable<R> ApplyFunction<A, R>(
  Nullable<A> nullable,
  Func<A, R> function)
{
  if (nullable.HasValue)
  {
    A unwrapped = nullable.Value;
    R result = function(unwrapped);
    return new Nullable<R>(result);
  }
  else
    return new Nullable<R>();
}

Для остальных типов, можно сделать по аналогии. По сути, получился способ превращения типов из A в R в монадические типы из М<А> в М<R> такие, что сохраняется действие функции и значения, предоставляемые в монадическом («расширенном») типе.

Часть пятая

Ранее было указано, что можно взять любую функцию с одним параметром и любым не пустым возвращаемым типом и применить эту функцию к монаде с возвращаемым типом M<R>. Любой возвращаемый тип, так? Предположим, что есть функция с одним параметром:

static Nullable<double> SafeLog(int x)
{
  if (x > 0)
    return new Nullable<double>(Math.Log(x));
  else
    return new Nullable<double>();
}

Обычная функция с одним параметром. Значит, ее можно применить к Nullable<int> и получить обратно… Nullable<Nullable<double>>! Это неправильно.

Создадим новую версию ApplyFunction, которая избегает описанной проблемы:

static Nullable<R> ApplySpecialFunction<A, R>(
  Nullable<A> nullable,
  Func<A, Nullable<R>> function)
{
  if (nullable.HasValue)
  {
    A unwrapped = nullable.Value;
    Nullable<R> result = function(unwrapped);
    return result;
  }
  else
    return new Nullable<R>();
}

Просто, не так ли? Создадим функции для остальных операторов:

static OnDemand<R> ApplySpecialFunction<A, R>(
  OnDemand<A> onDemand,
  Func<A, OnDemand<R>> function)
{
  return () =>
  {
    A unwrapped = onDemand();
    OnDemand<R> result = function(unwrapped);
    return result();
  };
}

static Lazy<R> ApplySpecialFunction<A, R>(
  Lazy<A> lazy,
  Func<A, Lazy<R>> function)
{
  return new Lazy(() =>
  {
    A unwrapped = lazy.Value;
    Lazy<R> result = function(unwrapped);
    return result.Value;
  };
}

static async Task<R> ApplySpecialFunction<A, R>(
  Task<A> task,
  Func<A, Task<R>> function)
{
  A unwrapped = await task;
  Task<R> result = function(unwrapped);
  return await result;
}

static IEnumerable<R> ApplySpecialFunction<A, R>(
  IEnumerable<A> sequence,
  Func<A, IEnumerable<R>> function)
{
  foreach(A unwrapped in sequence)
  {
    IEnumerable<R> result = function(unwrapped);
    foreach(R r in result)
      yield return r;
  }
}

В итоге для «шаблона монады» имеются 3 правила:

  1. Всегда существует возможность преобразовать тип T в тип M<T>.

    static M<T> CreateSimpleM<T>(T value)
  2. Если существует функция, преобразующая A в R, тогда можно применить эту функцию к экземпляру M<A> и получить экземпляр M<R>.

    static M<R> ApplyFunction<A, R>(
    M<A> wrapped,
    Func<A, R> function)
  3. Если существует функция, преобразующая A в M<R>, тогда можно применить эту функцию к экземпляру M<A> и получить экземпляр M<R>.

    static M<R> ApplySpecialFunction<A, R>(
    M<A> wrapped,
    Func<A, M<R>> function)

Но, правило 2 является частным случаем правила 3. Его можно представить в как комбинацию 1 и 3 правила:

static M<R> ApplyFunction<A, R>(
  M<A> wrapped,
  Func<A, R> function)
{
  return ApplySpecialFunction<A, R>(
    wrapped,
    (A unwrapped) => CreateSimpleM<R>(function(unwrapped)));
}

Остается всего два правила. Они являются полными правилами «шаблона монады»? В принципе, да.

Часть шестая

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

Пусть имеются 2 метода:

static M<T> CreateSimpleM<T>(T t) { ... }
static M<R> ApplySpecialFunction<A, R>(
  M<A> monad, Func<A, M<R>> function) {...}

Тогда, результат следующего выражения:

ApplySpecialFunction(someMonadValue, CreateSimpleM)

по значению идентичен someMonadValue, а результат следующего выражения:

ApplySpecialFunction(CreateSimpleM(someValue), someFunction)

по значению идентичен:

someFunction(someValue)

Часть седьмая

Допустим, имеются 2 функции:

Func<int, Nullable<double>> log = x => x > 0
    ? new Nullable<double>(Math.Log(x))
    : new Nullable<double>();
Func<double, Nullable<decimal>> toDecimal = y => Math.Abs(y) < decimal.MaxValue
    ? new Nullable<decimal>((decimal)y)
    : new Nullable<decimal>();

Тогда, с помощью определенного ранее метода ApplySpecialFunction можно написать следующий метод-помощник:

static Func<X, Nullable<Z>> ComposeSpecial<X, Y, Z>(
  Func<X, Nullable<Y>> f,
  Func<Y, Nullable<Z>> g)
{
  return x => ApplySpecialFunction(f(x), g);
}

который позволяет объединить определенные выше функции в одну:

Func<int, Nullable<decimal>> both = ComposeSpecial(log, toDecimal);

Отсюда следует последнее правило — метод ApplySpecialFunction должен гарантировать работу композиции. Пример:

Func<X, M<Y>> f = whatever;
Func<Y, M<Z>> g = whatever;
M<X> mx = whatever;
M<Y> my = ApplySpecialFunction(mx, f);
M<Z> mz1 = ApplySpecialFunction(my, g);
Func<X, M<Z>> h = ComposeSpecial(f, g);
M<Z> mz2 = ApplySpecialFunction(mx, h);

Значения mz1 и mz2 должны быть одинаковыми.

Наконец, можно полностью описать «шаблон монады» в C#:

Монада — это обобщенный тип M<T>, такой что:

  • Для нее существует конструирующий механизм, который принимает на вход переменную типа T и возвращает M<T>:

    static M<T> CreateSimpleM<T>(T t)
  • Если существует способ преобразования значения типа A в M<R>, то можно применить эту функцию к экземпляру M<A> и получить экземпляр M<R>:

    static M<R> ApplySpecialFunction<A, R>(
    M<A> monad, Func<A, M<R>> function)

Оба этих метода должны подчиняться следующим законам:

  • Применение функции создающую простую монаду (правило-метод 1) к конкретному экземпляру монады должно приводить к логически идентичному экземпляру монады.
  • Применение функции к результату функции создающей простую монаду из определенного значения и применение этой функции к определенному значению напрямую должно приводить к логически идентичным экземплярам монад.
  • Результат применения к значению первой функции второй функции и результат применения первоначального значения к функции-композиции первых двух функций должен приводить к двум логически идентичным экземплярам монад.

Часть восьмая

Традиционное имя для функции CreateSimpleunit. В Haskell — return.

Традиционное имя для функции ApplySpecialFunctionbind. В Haskell она является встроенной функцией, для того чтобы применить функцию f на экземпляр монады m необходимо написать m >>= f.

Фактически функция привязки берет неизменный рабочий процесс и операцию над ним и возвращает новый рабочий процесс.

Мой конспект на этом оканчивается. В последующих частях серии рассматривается практическое применение монад в коде.

  • Часть 9. О простых монадах «присоединяющих дополнительные данные к значению».
  • Часть 10. О запросах и LINQ на примере SelectMany.
  • Часть 11. Дополнения к предыдущей главе. Аддитивная монада.
  • Часть 12. Продолжение про запросы и SelectMany.
  • Часть 13. О Task монадах.
C# Enum и Атрибут Flags

Возникают ситуации, когда переменная должна хранить несколько значений типа перечисления. Например, используемые области логирования: Warning + Info, или сочетания цветов: Red + Blue + Green.

Для хранения в переменной нескольких флагов, значениям енама присваиваются степени двойки:

[Flags]
public enum MyColors
{
    Yellow = 1,
    Green = 2,
    Red = 4,
    Blue = 8
}

Значения 2, 4, 8 используются для операторов смещения, таких как побитовое И (AND), ИЛИ (OR) и исключающее ИЛИ (XOR).

Операции над переменной

Логическое ИЛИ (|) применяется для помещения нескольких значений флагов в одну переменную:

myProperties.AllowedColors = MyColor.Red | MyColor.Green | MyColor.Blue;

Логическое И (&) помогает при нахождении значения флага:

if((myProperties.AllowedColors & MyColor.Yellow) == MyColor.Yellow)
{
    // Yellow has been set...
}

if((myProperties.AllowedColors & MyColor.Green) == MyColor.Green)
{
    // Green has been set...
}

Начиная с .Net 4 можно использовать сокращенную версию, без явного указания &:

if (myProperties.AllowedColors.HasFlag(MyColor.Yellow))
{
    // Yellow has been set...
}

Операция XOR (’^’) исключает значения из переменной:

myProperties.AllowedColors = MyColor.Red | MyColor.Green | MyColor.Blue;
// Удаляем значение используя оператор смещения XOR.
myProperties.AllowedColors = myProperties.AllowedColors ^ MyColor.Green;
Console.WriteLine("My colors are {0}", myProperties.AllowedColors);
// Output: My colors are Red, Blue

Атрибут Flags

Атрибут [Flags] необязательный и используется для красивого вывода при вызове .ToString():

enum Colors { Yellow = 1, Green = 2, Red = 4, Blue = 8 }
[Flags] enum ColorsFlags { Yellow = 1, Green = 2, Red = 4, Blue = 8 }
...
var str1 = (Colors.Yellow | Colors.Red).ToString(); // "5"
var str2 = (ColorsFlags.Yellow | ColorsFlags.Red).ToString(); // "Yellow, Red"

Так же, атрибут [Flags] не присваивает значениям степень двойки. Если не проставить вручную, то значения инициализируются как в обычном енаме.

Неправильное объявление:

[Flags]
public enum MyColors
{
    Yellow,
    Green,
    Red,
    Blue
}

Присвоенные значения: Yellow = 0, Green = 1, Red = 2, Blue = 3. Они не подходят для использования операций смещения.

Битовое представление

Описанное выше работает благодаря битовому представлению значений флагов при проставлении степени двойки:

Yellow: 00000001
Green:  00000010
Red:    00000100
Blue:   00001000

Значение переменной AllowedColors после присваивания Red, Green и Blue c использованием операции ИЛИ (|):

myProperties.AllowedColors: 00001110

Теперь, для проверки вхождения значения Green в переменную используем операцию смещения И (&):

myProperties.AllowedColors: 00001110
             MyColor.Green: 00000010
             -----------------------
                            00000010 // Это то же самое, что и MyColor.Green!

Полезные ссылки