C# 7.0

Generalizando tipos de retornos assíncronos no C# 7.0

Até a versão 6.0 do C#, os métodos assíncronos poderiam retornar os tipos Task, Task<T> e void.

Por se tratar de um objeto, em algumas situações os retornos de Task<T>, podem apresentar problemas de performance. Alocado na memória heap e coletado pelo garbage coletor, ao fazer uso intenso de métodos assíncronos, a performance de uma aplicação pode ser impactada pela volumosa alocação dos objetos Task<T>.

Para resolver este problema, na versão 7.0 do C#, foi adicionado o recurso que permite criar tipos de retorno para os métodos assíncronos.

Regras para a criação de um tipo Task<T>

Ao ler a documentação sobre os tipos de retorno assíncrono, é dito que qualquer tipo que contenha um método GetAwaiter público, que retorne um objeto que implemente a interface System.Runtime.CompilerServices.ICriticalNotifyCompletion, pode ser utilizado. Mas não é apenas isso.

Além deste detalhe, os tipos retornados por um método assíncrono, devem seguir um padrão “task type”. Pode ser uma classe ou estrutura, mas precisam estar associados a um builder type, identificado com o atributo System.Runtime.CompilerServices.AsyncMethodBuilderAttribute. Caso retornem valores, deve ser definido como um tipo genérico.

O “tipo task” em si, só necessita implementar o método GetAwaiter público, mas o objeto retornado por este método, deve implementar a interface ICriticalNotifyCompletion, o método GetResult e possuir uma propriedade pública chamada IsCompleted.

Já o builder type a ser criado, deve corresponder a classe ou estrutura definida. Não se pode utilizar um existente e deve possuir a estrutura abaixo:

class MyTaskMethodBuilder<T>
{
    public static MyTaskMethodBuilder<T> Create();

    public void Start<TStateMachine>(ref TStateMachine stateMachine)
        where TStateMachine : IAsyncStateMachine;

    public void SetStateMachine(IAsyncStateMachine stateMachine);
    public void SetException(Exception exception);
    public void SetResult(T result);

    public void AwaitOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine;
    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : ICriticalNotifyCompletion
        where TStateMachine : IAsyncStateMachine;

    public MyTask<T> Task { get; }
}

Para tipos não genéricos, o método SetResult não possui parâmetro.

A classe acima será utilizada pelo compilador para transformar tipo assíncrono como um método assíncrono na compilação para o MSIL.

Você pode ver mais detalhes do funcionamento desta classe, aqui.

Parece complicado? Vamos a um exemplo para entender estes pontos.

Criando um tipo assíncrono/Task

Inicialmente criarei uma classe que implementará a interface ICriticalNotifyCompletion, que chamarei de MyTaskAwaiter:

public class MyTaskAwaiter<TResult> : ICriticalNotifyCompletion
{
    //Retorna se a operação foi concluída
    public bool IsCompleted => _value.IsCompleted;
    private readonly MyTask<TResult> _value;

    //Inicializa a classe com a classe Task criada
    public MyTaskAwaiter(MyTask<TResult> value)
    {
        this._value = value;
    }

    //Retorna o resultado
    public TResult GetResult() => _value.GetResult();

    //Define uma continuação.
    public void OnCompleted(Action continuation)
    {
        _value.AsTask().ConfigureAwait(continueOnCapturedContext: true).GetAwaiter().OnCompleted(continuation);
    }

    //Define uma continuação.
    public void UnsafeOnCompleted(Action continuation)
    {
        _value.AsTask().ConfigureAwait(continueOnCapturedContext: true).GetAwaiter().UnsafeOnCompleted(continuation);
    }
}

Os métodos mais importantes desta classe são, o GetResult e o OnCompleted, que, respectivamente, retorna o resultado da “Task”, e informa quando ela foi concluída.

Note que a “Task” é uma classe customizada, que será definida com o seguinte código:

[AsyncMethodBuilder(typeof(MyTaskBuilder<>))]
public class MyTask<TResult>
{
    //Define uma propriedade Task para indicar quando uma operação assincrona foi finalizada
    private Task<TResult> _task;

    //O resultado retornado pela classe
    private TResult _result;

    //Indica que a operação foi finalizada ou não
    public bool IsCompleted => _task == null || _task.IsCompleted;

    //Inicializando a classe com o resultado da operação bem sucedida
    public MyTask(TResult result){
        this._task = null;
        this._result = result;
    }

    //Obtém o "Awaiter" da classe
    public MyTaskAwaiter<TResult> GetAwaiter() => new MyTaskAwaiter<TResult>(this);

    //Retorna uma task, ou cria uma para o resultado
    public Task<TResult> AsTask() => _task ?? Task.FromResult(_result);

    //Retorna o resultado
    public TResult GetResult() =>
        _task == null ? 
            _result : 
            _task.GetAwaiter().GetResult();
}

Mesmo necessitando apenas de um método GetAwaiter, note que a classe definiu outros métodos e propriedades para facilitar o seu uso.

Por fim, é necessário definir o builder type para a classe acima, que deve ter o código abaixo:

public class MyTaskBuilder<TResult>
{
    private AsyncTaskMethodBuilder<TResult> _methodBuilder;
    private TResult _result;
    private bool _haveResult;
    private bool _useBuilder;

    //Cria o Buider Type
    public static MyTaskBuilder<TResult> Create() => new MyTaskBuilder<TResult>() { _methodBuilder = AsyncTaskMethodBuilder<TResult>.Create() };

    //Inicia o Buider Type
    public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine { _methodBuilder.Start(ref stateMachine); }

    //Define o State Machine para o Buider Type
    public void SetStateMachine(IAsyncStateMachine stateMachine) { _methodBuilder.SetStateMachine(stateMachine); }

    //Define o resultado
    public void SetResult(TResult result)
    {
        if (_useBuilder)
        {
            _methodBuilder.SetResult(result);
        }
        else
        {
            _result = result;
            _haveResult = true;
        }
    }

    //Gera uma exceção
    public void SetException(Exception exception) => _methodBuilder.SetException(exception);

    //Retorna a task vinculado ao builder type
    public MyTask<TResult> Task {
        get {
            if (_haveResult)
            {
                return new MyTask<TResult>(_result);
            }
            else
            {
                _useBuilder = true;
                return new MyTask<TResult>(_methodBuilder.Task.Result);
            }
        }
    }

    //Define o comportamento quando for necessário aguardar a conclusão da operação assincronona
    public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine 
    {
        _useBuilder = true;
        _methodBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine);
    }

    //Define o comportamento quando for necessário aguardar a conclusão da operação assincronona
    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine { 
        _useBuilder = true;
        _methodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
    }
}

Com a definição dessas três classes, podemos retornar o tipo MyTask em métodos assíncronos.

Utilizando o tipo assíncrono

O uso desta classe não difere do uso das classes Task ou Task em um método assíncrono:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Retorno: " + MyMethod().GetResult());
    }

    public static async MyTask<int> MyMethod(){
        var result = await MyMethodAsync();

        return result;
    }

    public static MyTask<int> MyMethodAsync()
    {
        return new MyTask<int>(5);
    }
}

Mesmo permitindo a criação de tipos assíncronos, criar um não é algo recomendado. Tudo que vimos acima só deve ser utilizado em casos específicos. Se o objetivo for apenas melhorar a performance da aplicação, basta substituir o tipo Task<T>, pelo ValueTask<T>.

ValueTask<T>

O tipo System.Threading.Tasks.ValueTask<T> foi adicionado na versão 7.0 do C#, como uma forma de superar os gargalos da classe Task<T>. Definido como uma estrutura, os objetos de ValueTask<T> não são salvos na memória heap, o que já contorna o problema com alocação de memória da classe Task<T>.

O seu uso é bem simples:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Retorno: " + MyMethod().Result);
    }

    public static async ValueTask<int> MyMethod(){
        var result = await MyMethodAsync();

        return result;
    }

    public static ValueTask<int> MyMethodAsync()
    {
        return new ValueTask<int>(5);
    }
}

E por não ser removida da memória pelo garbage collector, ela permite até a criação de “cache”:

class Program
{
    static void Main(string[] args)
    {
var task1 = CachedMethod();
        Console.WriteLine("Retorno da primeira chamada: " + task1.Result);

        var task2 = CachedMethod();
        Console.WriteLine("Retorno da segunda chamada: " + task2.Result);

        var task3 = CachedMethod();
        Console.WriteLine("Retorno da segunda chamada: " + task3.Result);
    }

    public static ValueTask<int> CachedMethod()
    {
        return (cache) ? new ValueTask<int>(cacheResult) : new ValueTask<int>(LoadCache());
    }
    private static bool cache = false;
    private static int cacheResult;
    private static async Task<int> LoadCache()
    {
        // simulando um processamento assincrono:
        await Task.Delay(100);
        cacheResult = new Random().Next(5000, 10000);
        cache = true;
        return cacheResult;
    }
}

A execução do código acima será:

Resultado da execução do código

Conclusão

Como recomendado na documentação, no C# 7.0, procure sempre utilizar a classe ValueTask<T>, pois o seu uso já se provou ser mais performático que a classe Task<T>.

Pattern Matching no C# 7.0

O operador is existe desde a primeira versão do C# e ele sempre foi utilizado para verificar o tipo de um objeto em tempo de execução.

Neste tipo de verificação é muito comum o uso de operações “cast“. Por exemplo, vamos supor uma classe base, com duas classes filhas:

public class Forma() {}

public class Triangulo : Forma{
    public int Largura { get; set; }
    public int Altura { get; set; }
    public int Base { get; se; }

    public Triangulo(int largura, int altura, int base) {
        this.largura = largura;
        this.altura = altura;
        this.base = base;
    }

    public void Perimetro (){
        Console.WriteLine("O perímetro do triângulo é {0}", largura + altura + base);
    }
}

public class Retangulo : Forma {
    public int Largura { get; set; }
    public int Altura { get; set; }

    public Retangulo(int largura, int altura) {
        this.largura = largura;
        this.altura = altura;
    }

    public void Area(){
        Console.WriteLine("A área do retângulo é {0}", largura * altura);
    }
}

Caso houvesse uma array com essas classes, para chamar cada método delas, seria necessário realizar uma conversão:

public class Program {
    public static void Main(){
        Forma[] formas = { new Triangulo(10, 12, 10), new Retangulo(7, 4), new Triangulo(17, 15, 16), new Retangulo(25, 12) };

        foreach (var item in formas)
        {
            if (item is Triangulo)
                ((Triangulo)item).Perimetro();
            if (item is Retangulo)
                ((Retangulo)item).Area();
        }
    }
}

Repare que ao descobrir o tipo da variável, ainda é necessário realizar o cast:

if (item is Triangulo)
    ((Triangulo)item).Perimetro();

Para evitar este cast, no C# 7.0 foi introduzido o conceito de Pattern Matching:

Pattern Matching

O Pattern Matching adiciona mais poder ao operador is e a cláusula switch.

Agora com o operador is, após realizar uma verificação de tipo, é possível atribuir o resultado a uma variável:

public class Program {
    public static void Main(){
        Forma[] formas = { new Triangulo(10, 12, 10), new Retangulo(7, 4), new Triangulo(17, 15, 16), new Retangulo(25, 12) };

        foreach (var item in formas)
        {
            if (item is Triangulo t)
                t.Perimetro();
            if (item is Retangulo r)
                r.Area();
        }
    }
}

Com isso, não é mais necessário realizar o cast do objeto dentro do condicional. Este tipo de situação também nos permite uma verificação mais elaborada:

if (item is Retangulo r and r.Largura > 0)
    r.Area();

Assim, apenas se item for um objeto de Retangulo e a propriedade Largura deste objeto for maior que 0, que o bloco do condicional será executado.

Não temos essas situações no exemplo acima, mas agora com o operador is também é possível verificar um valor literal:

if(item is 10)
    Console.WriteLine("Item é 10");

Null:

if(item is null)
    Console.WriteLine("Item é nulo");

Ou mesmo aplicar o objeto a uma variável:

if(item is var i)
    Console.WriteLine("Item é do tipo {0}", i?.GetType().Name);

Desta forma, é possível descobrir o tipo do objeto quando esta informação não é conhecida.

Pattern Matching com switch

Além do operador is, o switch também recebeu novos recursos para ser aplicado Pattern Matching. Agora é possível comparar o tipo de um objeto dentro de um bloco switch:

foreach (var item in formas)
{
    switch(item){
        case Triangulo t:
            t.Perimetro();
            break;
        case Retangulo r:
            r.Area();
            break;
        case 10:
            Console.WriteLine("Item é 10");
            break;
        case null:
            Console.WriteLine("Item é nulo");
            break;
        case Retangulo r when r.Largura > 0:
            r.Area();
            break;
        case var i:
            Console.WriteLine("Item é do tipo {0}", i?.GetType().Name);
            break;
    }
}

Algumas das opções acima não se aplicam ao nosso exemplo, mas elas foram adicionadas para mostrar que as mesmas opções que vimos com o condicional if, podem ser aplicadas ao switch.

Conclusão

Agora o processo de checagem de tipo e cast de objetos está mais simples e dinâmico. Assim, caso esteja utilizando a versão 7.0 do C#, não hesite em utilizar este recurso. Ele irá melhorar a legibilidade do seu código.

JUNTE-SE A MAIS DE 150.000 PROGRAMADORES