As novidades do C# 6

Com o recente lançamento do Visual Studio 2015, foi lançada a versão 6 da linguagem de programação para a plataforma .NET C#.

Como neste lançamento o enfoque principal foi na nova plataforma de compiladores (“Roslyn”), os melhoramentos e adições à linguagem foram escassos mas, tal como os melhoramentos e adições das versões anteriores, tornarão a vida de quem desenvolve usando a linguagem de programação C# muito melhor.

1. Melhoramentos em auto-propriedades

As propriedades implementadas automaticamente (ou, abreviando, auto-propriedades) são propriedades não abstratas e não externas com acessores com corpo apenas com ponto e virgula.

Quando uma propriedade é implementada automaticamente, é criado um campo escondido para dar suporte à propriedade e os acessores de leitura e escrita são são implementados para, respetivamente, ler e escrever desse campo.

1.1. Inicializadores para auto-propriedades

Passa a ser possível declarar a inicialização de auto-propriedades da mesma forma que se inicializam os campos:

public class Person
{
    public string First { get; set; } = "Jane";
    public string Last { get; set; } = "Doe";
}

Com esta sintaxe, o inicializador inicializa diretamente o campo que dá suporte à propriedade sem recorre ao setter da propriedade.

Os inicializadores de propriedades são executados, tal como e juntamente com, os inicializadores de campos.

Tal como acontece com os inicializadores de campos, os inicializadores de propriedades não podem fazer referência a this porque, tal como acontece com os inicializadores dos campos, correm antes dos objetos estarem devidamente inicializados.

A implementação desta nova funcionalidade é feita usando funcionalidades tradicionais da linguagem tornado possível a utilização do código gerado em versões anteriores da plataforma .NET. Na verdade o código anterior é traduzido pelo compilador para o seguinte código C# 1:

public class Person
{
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    public string k__BackingField = "Jane";
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    public string k__BackingField = "Doe";

    public string First
    {
        [CompilerGenerated]
        get { return k__BackingField }
        [CompilerGenerated]
        set { k__BackingField = value; }
    }
    public string Last
    {
        [CompilerGenerated]
        get { return k__BackingField }
        [CompilerGenerated]
        set { k__BackingField = value; }
    }
}

Note-se que os campos k__BackingField e k__BackingField têm nomes que não são válidos em C#. Tal acontece para que não haja qualquer hipótese de colisão entre os atribuídos pelo programador e os nome atribuídos pelo compilador.

1.2. Auto-propriedades apenas de leitura

As auto-propriedades passam a dispensar o acessor de escrita passando, por isso, a poder ser apenas de leitura:

public class Person
{
    public string First { get; } = "Jane";
    public string Last { get; } = "Doe";
}

Neste caso, o campo gerado é declarado implicitamente como readonly (embora isto apenas tenha importância para efeitos de reflexão – reflection).

À semelhança do caso anterior, o código gerado será:

public class Person
{
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly string k__BackingField = "Jane";
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly string k__BackingField = "Doe";

    public string First
    {
        [CompilerGenerated]
        get { return k__BackingField; }
    }
    public string Last
    {
        [CompilerGenerated]
        get { return k__BackingField; }
    }
}

Tal como acontece com os campos apenas de leitura, no caso das auto-propriedades apenas de leitura é possível inicializar o seu valor no construtor:

public class Person
{
    // ...

    public Person(string first, string last)
    {
        First = first;
        Last = last;
    }
}

E, mais uma vez, o compilador gera código equivalente a C# 1:

public class Person
{
    // ...

    public Person(string first, string last)
    {
        k__BackingField = first;
        k__BackingField = last;
    }
}

2. Membros função com corpo em formato expressão

Passam a poder ser usadas expressões semelhantes às funções lambda para definir corpos de funções com apenas de uma instrução (statement) ou bloco trazendo às funções membros de tipos a mesma clareza e simplicidade.

2.1. Corpos em formato expressão em membros do tipo método

Métodos, assim como operadores definidos pelo utilizador e conversões, podem ter o seu corpo definito por uma expressão usando a “seta das lambdas”:

public Point Move(int dx, int dy) => new Point(x + dx, y + dy);
public static Complex operator +(Complex a, Complex b) => a.Add(b);
public static implicit operator string (Person p) => p.First + " " + p.Last;

O efeito é exatamente o mesmo que se os métodos tivessem apenas uma instrução de return. Os exemplos acima são convertidos pelo compilador para:

public Point Move(int dx, int dy)
{
    return new Point(x + dx, y + dy);
}
public static Complex operator +(Complex a, Complex b)
{
    return a.Add(b);
}
public static implicit operator string (Person p)
{
    return p.First + " " + p.Last;
}

Para métodos cujo tipo de retorno seja void (ou Task para métodos assíncronos) a sintaxe da seta ainda se aplica, mas a expressão que se segue tem de ser uma instrução (à semelhança do que já acontece com as lambdas):

public void Print() => Console.WriteLine(First + " " + Last);

que será traduzido para:

public void Print()
{
    Console.WriteLine(First + " " + Last);
}

2.2. Corpos em formato expressão em membros do tipo propriedade

Os corpos do tipo expressão também podem ser usados para definir o corpo de propriedades e indexadores apenas de leitura:

public string Name => First + " " + Last;
public Person this[long id] => store.LookupPerson(id);

Note-se a ausência da palavra-chave get, que se torna implícita pela sintaxe de expressão.

Os exemplos anteriores são traduzidos pelo compilador para:

public string Name
{
    get
    {
        return First + " " + Last;
    }
}
public Person this[long id]
{
    get
    {
        return store.LookupPerson(id);
    }
}

3. Diretiva using static

À semelhança do que acontece com a diretiva using para espaços de nomes (namespaces), a diretiva using static adiciona os membros estáticos da classe ou enumerado dado como argumento ao espaço de nomes global, permitindo a sua utilização sem a necessidade de qualificação com o nome da classe:

using static System.Console;
using static System.Math;
using static System.DayOfWeek;
class Program
{
    static void Main()
    {
        WriteLine(Sqrt(3 * 3 + 4 * 4));
        WriteLine(Friday - Monday);
    }
}

O código anterior será traduzido pelo compilador para:

class Program
{
    static void Main()
    {
        System.Console.WriteLine(System.Math.Sqrt(3 * 3 + 4 * 4));
        System.Console.WriteLine(System.DayOfWeek.Friday - System.DayOfWeek.Monday);
    }
}

Esta funcionalidade é ótima quando se tem um conjunto de funções relacionadas com um determinado domínio que se usa frequentemente, de que System.Math é um bom exemplo. Permite também especificar individualmente os nomes de um enumerado, como os membros de System.DayOfWeek no exemplo acima.

3.1. Métodos de extensão

Os métodos de extensão são métodos estáticos, mas a intenção é de que sejam usados como métodos de instância dos tipos que estendem. Em vez de trazer esses métodos para o âmbito global, a funcionalidade using static faz com que esses métodos estejam disponíveis como métodos de extensão:

using static System.Linq.Enumerable; // The type, not the namespace
class Program
{
    static void Main()
    {
        var range = Range(5, 17);                // Ok: not extension
        var odd = Where(range, i => i % 2 == 1); // Error, not in scope
        var even = range.Where(i => i % 2 == 0); // Ok
    }
}

Isto faz com que alterar um método para que passe a ser método de extensão passe a ser uma modificação fraturante, o que não era o caso anteriormente. Mas os métodos de extensão são geralmente chamados como métodos estáticos nos casos raros em que existe uma ambiguidade e, nesses casos, parece legítimo que sejam qualificados com o nome da classe.

4. Operadores condicionados por null

É frequente a necessidade de ter código salpicado de verificação para null. Os operadores condicionados por null permitem o acesso a membros e elementos apenas quando o recetor não é null, retornando um resultado null caso contrário:

int? length = people?.Length; // null se people é null
Person first = people?[0];    // null se people é null

O código anterior será traduzido para:

int? nullable = (people != null) ? new int?(people.Length) : null;
Person person = (people != null) ? people[0] : null;

Os operadores condicionados por null pode ser muito conveniente quando usado com o operador de coalescência de null (??):

int length = people?.Length ?? 0; // 0 se people é null

Os operadores condicionados por null têm um comportamento de curto-circuito, em que a cadeia de acesso a membros, elementos ou invocações imediatamente a seguir apenas são executados se o recetor original não for null:

int? first = people?[0].Orders.Count();

O exemplo anterior é, na essência, equivalente a:

int? first = (people != null) ? people[0].Orders.Count() : (int?)null;

Com a exceção de que people é avaliado apenas uma vez. Nenhum dos acessos a membros ou elementos e invocações que se seguem ao operador ? são executados se o valor de people for null.

E nada impede que os operadores condicionados por null sejam encadeados, no caso de ser necessária alguma verificação de null mais que uma vez na cadeia:

int? first = people?[0]?.Orders.Count();

A invocação (uma lista de argumentos entre parêntesis) não pode ser precedida imediatamente pelo operador ? – isso levaria a demasiadas ambiguidades. Assim sendo, a esperada invocação de um delegate caso este não seja null não funciona. Contudo, o delegate pode sempre ser invocado via o seu método Invoke:

if (predicate?.Invoke(e) ?? false) { … }

Uma utilização muito comum desta funcionalidade é o disparo de eventos:

PropertyChanged?.Invoke(this, args);

Que é traduzido para:

var handler = PropertyChanged;
if (handler != null)
{
    handler.Invoke(this, args);
}

Que é uma forma segura para threads de verificar se o evento tem subscritores porque apenas avalia o lado esquerdo da invocação uma vez e mantém o seu valor numa variável temporária.

5. Interpolação de strings

O método String.Format com as suas variadas versões é muito versátil e útil, mas a sua utilização é um bocado desajeitada e sujeita a erros devido aos marcadores numéricos ({0}) que têm de corresponder à posição dos argumentos fornecidos em separado:

var s = string.Format("{0} tem {1} ano{{s}}.", p.Name, p.Age);

A interpolação de strings permite substituir diretamente no literal string os índices por “buracos” com as expressões que correspondem aos valores:

var s = $"{p.Name} tem {p.Age} ano{{s}}.";

Tal como acontece com o método String.Format, é possível a especificação de alinhamentos e formatos:

var s = $"{p.Name,20} tem {p.Age:D3} ano{{s}}.";

O conteúdo dos buracos pode ser qualquer expressão, incluindo strings:

var s = $"{p.Name} tem {p.Age} ano{(p.Age == 1 ? "" : "s")}.";

Note-se que a expressão condicional está entre parêntesis, para que : "s" não seja confundido com o especificador de formato.

5.1. strings formatáveis

Quando não é especificado um provedor de formatação na invocação do método String.Format, é usada a cultura corrente do thread corrente e isso nem sempre é o desejado. Por isso, à semelhança do que acontece com as expressões lambda, o compilador traduz a string interpolada de forma diferente consoante o tipo do recetor da expressão.

Se o recetor da expressão for do tipo IFormattable:

IFormattable christmas = $"{new DateTime(2015, 12, 25):f}";

o compilador gera o seguinte código:

IFormattable christmas = FormattableStringFactory.Create("{0:f}", new DateTime(2015, 12, 25));

que pode ser usado da seguinte forma:

var christamasText = christmas.ToString(new CultureInfo("pt-PT"));

5.1.1. FormattableString

O tipo concreto retornado por FormattableStringFactory.Create é derivado de:

namespace System
{
    public abstract class FormattableString : IFormattable
    {
        protected FormattableString();
        public abstract int ArgumentCount { get; }
        public abstract string Format { get; }
        public static string Invariant(FormattableString formattable);
        public abstract object GetArgument(int index);
        public abstract object[] GetArguments();
        public override string ToString();
        public abstract string ToString(IFormatProvider formatProvider);
    }
}

Isto permite, não só acesso a formato mas também aos argumentos da string formatável.

5.1.2. Retrocompatibilidade

As funcionalidades introduzidas pelo C# 6 são compatíveis com as plataformas .NET anteriores. No entanto, esta funcionalidade em particular necessita dos tipos System.Runtime.CompilerServices.FormattableStringFactory e System.FormattableString que só foram introduzidos na versão 4.6 da plataforma. A boa notícia é que o compilador não está preso à localização destes tipos numa determinada assembly e, caso se pretenda usar esta funcionalidade numa versão anterior da plataforma, basta adicionar a implementação destes tipos.

6. Expressões nameof

Ocasionalmente é necessário providenciar uma string com o nome de alguns elementos do programa:

  • Quando se lança uma System.ArgumentNullException,
  • Quando se dispara um evento PropertyChanged.
  • etc.

Usar literais string para isto é simples, mas sujeito a erros. Pode haver erros de escrito, ou uma refatorização do código pode ter mudado o nome do artefacto.

As expressões nameof são uma espécie de literal do tipo string em que o compilador valida a existência de algo com aquele nome. Uma vez que passa a ser uma referência ao artefacto, o Visual Studio sabe a que se refere e navegação e refatorização do código funcionarão.

No essencial, código como o seguinte:

if (x == null) throw new ArgumentNullException(nameof(x));
var s = nameof(person.Address.ZipCode);

será convertido em:

if (x == null) throw new ArgumentNullException("x");
var s = "ZipCode";

6.1. Código fonte vs. metadados

Os nomes usados pelo compilador são os nomes do código fonte e não os nomes dos metadados dos artefactos, pelo que, o seguinte código:

using S = System.String;
class C
{
    void M<T>(S s)
    {
        var s1 = nameof(T);
        var s2 = nameof(S);
    }
}

é convertido em:

using S = System.String;
class C
{
    void M<T>(S s)
    {
        var s1 = "T";
        var s2 = "S";
    }
}

6.2. Tipos primitivos

Não é permitida a utilização de tipos primitivos (int, long, char, bool, string, etc.) em expressões nameof.

7. Métodos de extensão Add em inicializadores de coleções

Quando os inicializadores de coleções foram introduzidos na linguagem C#, os métodos Add chamados não podiam ser métodos de extensão. O Visual Basic acertou na sua implementação à primeira ao permitir a sua utilização, mas isso parece ter ficado esquecido para o C#.

Nesta versão a falha foi corrigida e é possível agora usar métodos de extensão Add em inicializadores de coleções.

Não é uma grande funcionalidade, mas é útil e, em termos de implementação do compilador, tratou-se apenas de remover a verificação da condição que o impedia.

8. Inicializadores de índices

A inicialização de objetos e coleções são úteis para inicializar declarativamente os campos e propriedades de objetos ou, no caso das coleções, um conjunto inicial de elementos.

A inicialização de dicionários, por outro lado, não era tão elegante, obrigando à existência de um método Add que recebesse como argumento a chave e o valor correspondente a essa chave. Se uma implementação em particular de dicionário não tivesse um método Add com as características mencionadas, não seria possível usar um inicializador.

A partir de agora, passa a ser possível usar inicializadores em que são usados indexadores:

var numbers = new Dictionary<int, string>
{
    [7] = "sete",
    [9] = "nove",
    [13] = "treze"
};

que serão traduzidos para:

var dictionary = new Dictionary<int, string>();
dictionary[7] = "sete";
dictionary[9] = "nove";
dictionary[13] = "treze";
var numbers = dictionary;

9. Filtros de exceções

Os filtros de exceção são uma funcionalidade da CLR já disponibilizada pelo Visual Basic e pelo F# e que passa agora a estar disponível também no C#:

try
{
    ...
}
catch (Exception ex) when (SomeFilter(ex))
{
    ...
}

Se a avaliação da expressão entre parêntesis a seguir à palavra-chave when resultar no valor true, a exceção é apanhada. Caso contrário, o bloco catch é ignorado.

Isto permite que sejam definidos mais que um bloco catch para o mesmo tipo de exceção:

try
{
    //...
}
catch (SqlException ex) when (ex.Number == 2)
{
    // ...
}
catch (SqlException ex)
{
    // ...
}

No exemplo anterior o primeiro bloco catch apenas é executado se ocorrer um exceção do tipo SqlException em que o valor da propriedade Number seja 2. Caso contrário é executado o bloco seguinte.

É considerado aceitável e comum o “abuso” de filtros de exceções com efeitos colaterais, como logging.

ATENÇÃO: Os filtros de exceção são executados no contexto do lançamento da exceção (throw) e não do contexto do seu tratamento (catch).

10. await em blocos catch e finally

No C#5 não era permitida a utilização da palavra-chave await em blocos catch e finally porque, na altura da implementação da funcionalidade async-await, a equipa pensou que isto não seria possível implementar. Mas agora descobriram que afinal não era impossível.

Passa a ser possível escrever código como este:

Resource res = null;
try
{
    res = await Resource.OpenAsync();      


catch (ResourceException e)
{
    await Resource.LogAsync(res, e);        
}
finally
{
    if (res != null) await res.CloseAsync();         
}

11. Melhorias na resolução de sobrecarga de métodos

Foram introduzidas algumas melhorias na resolução de sobrecarga de métodos por forma a tornar mais expectável a forma como o compilador decide qual o método de sobrecarga a usar.

Onde isto se fará notar mais (ou deixar de notar) é na escolha de métodos de sobrecarga que recebam tipos valor nullable. Ou quando se passa um grupo de métodos (em vez de uma lambda) para métodos de sobrecarga que recebem delegates.

Recursos

  1. New Language Features in C# 6
  2. C# 7 Work List of Features
  3. Interpolated Strings (C# and Visual Basic Reference)

Publicado na edição 50 (PDF) da Revista PROGRAMAR.