среда, 9 ноября 2011 г.

Observable.Generate и перечисление списков

В библиотеке реактивных расширений (a.k.a. Rx – Reactive Extensions) существует вспомогательный метод Observable.Generate, который позволяет генерировать простые observable-последовательности.

IObservable<string> xs = Observable.Generate<int, string>(     
initialState: 0, // начальное значение
     condition: x => x < 10, // условие завершения генерации
     iterate: x => x + 1, // изменение значения
     resultSelector: x => x.ToString() // преобразование текущего значения в результат
     ); xs.Subscribe(x => Console.WriteLine("x: {0}", x));

ПРИМЕЧАНИЕ
Обратите внимание на явное указание параметров обобщенного метода. Причина такого странного поведения (ведь обобщенные параметры методов должны спокойно выводиться компилятором) в том, что в компиляторе языка C# есть известный баг, в результате которого, вывод типов плохо дружит с именованными параметрами. Подробнее об этом можно почитать на
stackoverflow или на connect-e. Да, кстати, это дело пофиксили в Visual Studio 11.

Метод Generate напоминает простой цикл for, который состоит из тех же самых этапов: инициализации начального значения, условия выхода из цикла и изменения переменной цикла. И если мы захотим сгенерировать обычную последовательность в памяти, то мы будем использовать именно цикл for внутри блока итераторов:

static IEnumerable<string> GenerateSequence()
{
     for (
         int x = 0; //initialState
         x < 10;  // condition
         x = x + 1  // iterate
         )
     {
         yield return x.ToString(); // resultSelector
     } } static void Main(string[] args) {
     var xs = GenerateSequence();
     foreach (var x in xs)
         Console.WriteLine("x: {0}", x); }

Оба варианта кода эквивалентны, но если в первом случае создается push-последовательность (в виде интерфейса IObservable of T), которая будет самостоятельно уведомлять о поступлении новых элементов, то во втором случае мы получаем pull-последовательность (в виде интерфейса IEnumerable of T), из которой нужно «плоскогубцами» эти элементы вытаскивать.

ПРИМЕЧАНИЕ
Если очень коротко, то pull-модель – это такая модель взаимодействия приложения с окружающим миром, когда ведущую роль выполняет именно приложение. Если посмотреть на интерфейс IEnumerable, то именно вызывающий код «управляет» потоком элементом, вызывая метод MoveNext для перехода к следующему. В push-модели действия разворачиваются по-другому: внешнее окружение само уведомляет приложение о некоторых событиях (например, о наличии новых данных), а приложение лишь «реагирует» на них. Подробнее об этих моделях, о дуализме интерфейсов IEnumerable/IObservable, а также о других возможностях библиотеки реактивных расширений можно почитать в статье:
Реактивные расширения и асинхронные операции.

Поскольку интерфейсы push и pull-последовательностей (IObservable of T и IEnumerable of T, соответственно) являются такими похожими, то к нам в голову легко может затесаться мысль о преобразовании из одной формы последовательности в другую:

int[] ai = new[] { 1, 2, 3 };
 
IObservable<int> oi = Observable.Generate(
     /*initialState:*/ ai.GetEnumerator(),
     /*condition:*/ e => e.MoveNext(),
     /*iterate:*/ e => e,
     /*resultSelector:*/ e => (int)e.Current
     ); oi.Subscribe(i => Console.WriteLine("i: {0}", i));

В целом ничего криминального, и при выполнении этого кода мы ожидаемо получим:

1
2
3

Но что, если вместо массива нам захочется сделать тоже самое со списком:

List<int> li = new List<int> {1, 2, 3};
IObservable<int> oi = Observable.Generate(
     /*initialState:*/ li.GetEnumerator(),
     /*condition:*/ e => e.MoveNext(),
     /*iterate:*/ e => e,
     /*resultSelector:*/ e => e.Current
     ); oi.Subscribe(i => Console.WriteLine("i: {0}", i));

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

ПРИМЕЧАНИЕ
Да, я знаю, что самым простым способом преобразования простой последовательности в observable-последовательность, является использование метода расширения Observable.ToObservable, который «расширяет» интерфейс IEnumerable. Но, предположим, что мы либо о нем не знаем, либо нам нужна более сложная логика, доступная в методе Generate.

Причина такого поведения кроется в том, что енумератор класса List of T (а также большинства других коллекций BCL) является структурой, а не классом. А, как нам известно, изменяемые структуры (ведь енумератор изменяет свое внутреннее состояние) не очень вяжется с передачей по значению. В результате этого мы постоянно пробуем изменить копию енумератора, а не исходный енумертор, переданный нами в методе Generate.

Изменяемые структуры – это зло, и только стремление к оптимизации кода для наиболее распространенного сценария использования енумераторов привели к тому, что разработчики BCL сделали их структурами, а не классами. Поскольку наиболее частым сценарием использования енумерторов в C# 2.0 было использование его в цикле foreach, который никак не страдал от проблем с изменяемыми структурами, то было принято решение сэкономить несколько тактов процессора и использовать для енумератора именно структуру, а не класс.

ПРИМЕЧАНИЕ
Подробности о том, почему это было сделано именно так, можно почитать
в одном из постов Sasha Goldshtein, а также в ответе Эрика Липперта на stackoverflow. О других же проблемах изменяемых значимых типов можно почитать в статье: О вреде изменяемых значимых типов.

Решается эта проблема довольно просто: для этого достаточно привести исходное значение енумератора к интерфейсу, что приведет к его упаковке и последующей модификации «общей» переменной, расположенной в управляемой куче, а не к изменению копии:

List<int> li = new List<int> {1, 2, 3};              
IObservable<int> oi = Observable.Generate(
     /*initialState:*/ (IEnumerator<int>)li.GetEnumerator(),
     /*condition:*/ e => e.MoveNext(),
     /*iterate:*/ e => e,
     /*resultSelector:*/ e => e.Current
     ); oi.Subscribe(i => Console.WriteLine("i: {0}", i));

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

  1. Ваша статья еще раз натолкнула на мысль о том, что нельзя вслепую использовать .Net Framework. Большое спасибо.

    ОтветитьУдалить
  2. Да уж. Если не знаешь, почему так - можно долго репу чесать :)

    ОтветитьУдалить
  3. @ritikov: да, тут можно вспомнить одну из предыдущих заметок о дырявых абстракциях. Вот и здесь, решение о реализации енумератора протекает и бьет по голове, если не знать о подводных камнях.

    @eugene: это точно.

    ОтветитьУдалить