Olá pessoal!

Esses dias estava desenvolvendo uns exercícios em C#, até que em um deles notei um comportamento interessante de IEnumerable e IQueryable. Observe o seguinte código:

using System;
using System.Linq;
using System.Collections;

namespace reactiveCollection
{
    class Program
    {
        static void Main(string[] args)
        {
            IEnumerable collection = Enumerable.Range(0,10);

            foreach (int item in collection)
            {
                Console.WriteLine(item);
            }

            Console.ReadLine();
        }
    }
}

E se executarmos no terminal, a seguinte resposta é gerada:

root@1083f7cca7e3:/app# dotnet run
0
1
2
3
4
5
6
7
8
9

Bom, a resposta gerada era o que esperávamos… Mas o que acontece se trocarmos o IEnumerable por IQueryable?

using System;
using System.Linq;
using System.Collections;

namespace reactiveCollection
{
    class Program
    {
        static void Main(string[] args)
        {
            IEnumerable collection = Enumerable.Range(0,10).Select(x => x);

            foreach (int item in collection)
            {
                Console.WriteLine(item);
            }

            Console.ReadLine();
        }
    }
}
root@1083f7cca7e3:/app# dotnet run
0
1
2
3
4
5
6
7
8
9

Basicamente a mesma coisa. Vamos agora colocar uma
interação do usuário na expressão:

using System;
using System.Linq;
using System.Collections;

namespace reactiveCollection
{
    class Program
    {
        static void Main(string[] args)
        {
            IEnumerable collection = Enumerable
                .Range(0,10)
                .Select(x => 
                {
                    Console.ReadLine();

                    return x;
                });

            foreach (int item in collection)
            {
                Console.WriteLine(item);
            }

            Console.ReadLine();
        }
    }
}

Resultado:

root@1083f7cca7e3:/app# dotnet run


0

1

2

3

4

5

6

7

8

9

Algo interessante acontece… Eu normalmente esperaria pressionar 10x o Enter para então depois o foreach printar os números um por linha como nos testes anteriores.

Mas não é o que ocorre… A cada interação do foreach, a instrução de dentro do Select (isso é, o Console.Readline()) é executada… Isso acontece porque Enumerables (por consequência, IQueriables) só executam as expressões quando solicitadas através do yield (veja mais sobre o yield abaixo).

Um outro exemplo do que está acontecendo. Observa o seguinte código:


using System;
using System.Linq;
using System.Collections;

namespace reactiveCollection
{
    class Program
    {
        static void Main(string[] args)
        {
            IEnumerable collection = Enumerable
                .Range(0,10)
                .Select(x => 
                {
                    Console.WriteLine($"---------------------------Executando de dentro do Select da data { DateTime.Now.ToLongTimeString()}");

                    return x;
                });

            foreach (int item in collection)
            {
                Console.WriteLine($"Executando de fora do Select { DateTime.Now.ToLongTimeString()}");
            }

            Console.ReadLine();
        }
    }
}

E o resultado:


root@1083f7cca7e3:/app# dotnet run
---------------------------Executando de dentro do Select da data 15:00:12
Executando de fora do Select 15:00:13
---------------------------Executando de dentro do Select da data 15:00:13
Executando de fora do Select 15:00:14
---------------------------Executando de dentro do Select da data 15:00:14
Executando de fora do Select 15:00:15
---------------------------Executando de dentro do Select da data 15:00:15
Executando de fora do Select 15:00:16
---------------------------Executando de dentro do Select da data 15:00:16
Executando de fora do Select 15:00:17
---------------------------Executando de dentro do Select da data 15:00:17
Executando de fora do Select 15:00:18
---------------------------Executando de dentro do Select da data 15:00:18
Executando de fora do Select 15:00:19
---------------------------Executando de dentro do Select da data 15:00:19
Executando de fora do Select 15:00:20
---------------------------Executando de dentro do Select da data 15:00:20
Executando de fora do Select 15:00:21
---------------------------Executando de dentro do Select da data 15:00:21
Executando de fora do Select 15:00:22

Este resultado nos indica que, a cada interação, o método interno do Select é executado.

Este comportamento nos dá diversas vantagens em termos de processamento. Se o método interno, por exemplo, fosse uma chamada um pouco mais pesada em termos de recursos computacionais, ela seria executada sob necessidade. Se por ventura o loop fosse parado no meio, processamento desnecessário seria evitado.

Caso fosse preciso executar todo o método Select antes de percorrer-lo, basta apenas forçar a interação dele com métodos de transformação, como por exemplo o .ToList():


using System;
using System.Linq;
using System.Collections;

namespace reactiveCollection
{
    class Program
    {
        static void Main(string[] args)
        {
            IEnumerable collection = Enumerable
                .Range(0,10)
                .Select(x => 
                {
                    Console.WriteLine($"---------------------------Executando de dentro do Select da data { DateTime.Now.ToLongTimeString()}");

                    return x;
                })
                .ToList();

            foreach (int item in collection)
            {
                Console.WriteLine($"Executando de fora do Select { DateTime.Now.ToLongTimeString()}");
            }

            Console.ReadLine();
        }
    }
}

E o resultado vira:


root@1083f7cca7e3:/app# dotnet run
---------------------------Executando de dentro do Select da data 15:01:21
---------------------------Executando de dentro do Select da data 15:01:22
---------------------------Executando de dentro do Select da data 15:01:23
---------------------------Executando de dentro do Select da data 15:01:24
---------------------------Executando de dentro do Select da data 15:01:25
---------------------------Executando de dentro do Select da data 15:01:26
---------------------------Executando de dentro do Select da data 15:01:27
---------------------------Executando de dentro do Select da data 15:01:28
---------------------------Executando de dentro do Select da data 15:01:29
---------------------------Executando de dentro do Select da data 15:01:30
Executando de fora do Select 15:01:31
Executando de fora do Select 15:01:31
Executando de fora do Select 15:01:31
Executando de fora do Select 15:01:31
Executando de fora do Select 15:01:31
Executando de fora do Select 15:01:31
Executando de fora do Select 15:01:31
Executando de fora do Select 15:01:31
Executando de fora do Select 15:01:31
Executando de fora do Select 15:01:31

Podemos perceber que o tempo total entre as duas execuções é basicamente o mesmo. Mas o output para o console é bem diferente.

Esta é o codigo fonte do método .Select dentro de System.Linq. É possível observar a utilização do yield return que indica, basicamente, que aquele ponto é o ponto de retorno para o interação corrente, isso é, o retorno é executado quantas vezes for necessário de acordo com a quantidade da coleção.


private static IEnumerable<TResult> SelectIterator<TSource, TResult>(IEnumerable<TSource> source, Func<TSource, int, TResult> selector)
{
    int index = -1;
    foreach (TSource element in source)
    {
        checked
        {
            index++;
        }

        yield return selector(element, index);
    }
}

Mas por que podemos dizer que este código é Reativo?

Quando tratamos de consultar uma coleção, que é nosso exemplo, podemos ter dois comportamentos possíveis: Reativo e Pró-ativo.

O pró-ativo é o comportamento que calcula os resultados possíveis antes mesmo de serem requisitados. É o que aconteceu no nosso ultimo exemplo quando adicionamos o .ToList(). Toda a coleção foi iterada e calculada de modo que no foreach subsequente apenas o Consolte.Write de dentro do foreach foi executado.

Já a abordagem Reativa vai executando cada item da coleção e obtendo seu resultado à cada interação. Podemos dizer ao iterar há uma “Reação” interna de dentro da coleção que executa a posição corrente (e obtém seu resultado na hora, não anteriormente).

Um exemplo mais complexo

Para este exemplo estou usando a API do Pokémon por ser free e não necessitar autenticação. Vamos implementar a necessidade de listar os 10 primeiros pokémons, sendo que a listagem deve cessar assim que encontrar o primeiro pokémon do tipo “fogo”. Primeiro, vamos usar a abordagem pró-ativa:

using static Newtonsoft.Json.JsonConvert;
using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Diagnostics;

namespace reactiveCollection
{
    class Program
    {
        static void Main(string[] args)
        {
            var stopWatch = new Stopwatch();

            stopWatch.Start();

            using(var client = new HttpClient())
            {
                client.DefaultRequestHeaders.Accept.Clear();
                client.DefaultRequestHeaders.Accept.Add(
                    new MediaTypeWithQualityHeaderValue("application/json"));

                IEnumerable<Pokemon> pokemons = Enumerable
                .Range(1,11)
                .Select(x => 
                {
                    var result = client.GetAsync($"http://pokeapi.co/api/v2/pokemon/{x}").Result; 

                    return DeserializeObject<Pokemon>(result.Content.ReadAsStringAsync().Result);
                })
                .ToList();   

                foreach(var pokemon in pokemons)
                {
                    Console.WriteLine(pokemon.Name);

                    if(pokemon.Types.Any(x => x.Type.Name.ToLower() == "fire"))
                        break;
                }

            }

            stopWatch.Stop();

            Console.WriteLine($"Tempo de execução: {stopWatch.Elapsed.Seconds} segundos");
        }
    }
    public class Pokemon
    {
        public string Name { get; set; }
        public IList<PokemonTypeSlot> Types { get; set; }

        public class PokemonTypeSlot
        {
            public int Slot { get; set; }
            public PokemonType Type { get; set; }
            public class PokemonType {
                public string Name { get; set; }
            }
        }
    }
}

Resultado:

root@1083f7cca7e3:/app# dotnet run
bulbasaur
ivysaur
venusaur
charmander
Tempo de execução: 36 segundos

Agora, vamos apenas tirar o .ToList() e permitir a chamada à API por interação (mostrarei só o trecho):

...
                IEnumerable<Pokemon> pokemons = Enumerable
                .Range(1,11)
                .Select(x => 
                {
                    var result = client.GetAsync($"http://pokeapi.co/api/v2/pokemon/{x}").Result; 

                    return DeserializeObject<Pokemon>(result.Content.ReadAsStringAsync().Result);
                });   

                foreach(var pokemon in pokemons)
                {
                    Console.WriteLine(pokemon.Name);

                    if(pokemon.Types.Any(x => x.Type.Name.ToLower() == "fire"))
                        break;
                }
...

O resultando é bem diferente:

root@1083f7cca7e3:/app# dotnet run
bulbasaur
ivysaur
venusaur
charmander
Tempo de execução: 10 segundos

Vale lembrar que certamente há melhores maneiras de consumir esta API e obter o mesmo resultado, mas desenvolvi assim para explicar melhor o conceito.

Conclusão

Abordagens Reativas ou Pró-ativas são meios diferentes de se obter resultados numa linha de tempo. É errado perguntar quais das duas é melhor, pois dependendo do contexto é interessante utilizar um ou outro. O importante aqui é saber as diferenças e possibilidades de se trabalhar com ambas.

Caso você queira, deixei meu código aqui