C# – Novas Funcionalidades do C# 6.0 – Antevisão de Abril de 2014

Introdução

No passado evento //Build/, a Microsoft disponibilizou uma versão de antevisão da versão 6.0 da linguagem de programação C#.

Trata-se de uma versão preliminar, pelo que algumas funcionalidades poderão sofre alterações, não fazer parte da versão final ou novas funcionalidades poderão ainda ser adicionadas.

Esta nova versão da linguagem inaugura também uma nova postura da Microsoft no que diz respeito ao código aberto e participação da comunidade.

De facto, em direto numa sessão de abertura (keynote do segundo dia, aproximadamente a 01:15:50), Anders Hejlsberg publicou no CodePlex código fonte da plataforma de compiladores e os compiladores de C# e Visual Basicprojeto Roslyn.

Além do código fonte, foram publicadas as notas das reuniões de desenho, onde se pode consultar o racional por detrás de cada nova funcionalidade.

Estado da Implementação das Funcionalidades

O estado atualizado da implementação das funcionalidades de ambas as linguagens pode ser consultado aqui.

Funcionalidades Implementadas

Comecemos por percorrer, sem qualquer ordem especial, as funcionalidades já implementadas e disponibilizadas na versão de antevisão anteriormente referida.

Inicialização de AutoPropriedades

Passa a ser possível adicionar inicializadores a auto-propriedades (auto-property) da mesma forma que se pode adicionar a campos (field):

public class Cliente
{
    public string Nome { get; set; } = "José";
    public string Apelido { get; set; } = "Silva";
}

O inicializador inicializa diretamente o campo que dá suporte à propriedade. Por essa razão passa a fazer sentido existirem auto-propriedades sem assessores de escrita (setter):

public class Cliente
{
    public string Nome { get; } = "José";
    public string Apelido { get; } = "Silva";
}

Auto-propriedades apenas de leitura só são permitidas se existir um inicializador. Caso contrário nunca poderiam conter qualquer valor para além do valor por omissão do seu tipo.

Tal como acontece com os inicializadores dos campos, os inicializadores de auto-propriedades não podem referenciar this – afinal são executados antes do objeto estar devidamente inicializado. Isto faria com que não houvesse muitas escolhas interessantes para inicializar as auto-propriedades. Especialmente as auto-propriedades apenas de leitura pareceriam inúteis se não pudessem, de alguma forma, ser inicializadas a partir de valores passados no construtor do objeto. No entanto, os construtores primários veem mudar isto.

Construtores Primários

Os construtores primários (primary constructors) permitem que os parâmetros do construtor sejam declarados diretamente na classe (class) ou estrutura (struct) sem a necessidade da declaração explícita do construtor no corpo da declaração do tipo. Estes parâmetros estão disponíveis como nomes simples sendo o seu âmbito (scope) toda a declaração do tipo.

Nota: O desenho desta funcionalidade foi alterado e segue-se a descrição do novo desenho. No entanto, não foi possível ter estas alterações na versão de antevisão, pelo que, poderão ser encontradas situações que correspondem à semântica anterior: os parâmetros são capturados implicitamente e não existe sintaxe para a captura explícita.

Parâmetros em Classes e Estruturas

Aqui está um exemplo de uma classe com um construtor primário:

public class Cliente(string nome, string apelido)
{
    public string Nome { get; } = nome;
    public string Apelido { get; } = apelido;
}

Obter o mesmo efeito sem construtores primários teria requerido campos privados para guardar os valores de nome e apelido, um construtor explícito para os inicializar e um corpo de leitura para Nome e Apelido para os expor:

public class Cliente
{
    private string nome;
    private string apelido;
    public Cliente(string nome, string apelido)
    {
        this.nome = nome;
        this.apelido = apelido;
    }
    public string First { get { return nome; } }
    public string Last { get { return apelido; } }
}

No extrato de código anterior foram evidenciados os pedaços que se tornam desnecessários com a introdução dos construtores primários e auto-propriedades apenas de leitura inicializadas.

O objetivo desta funcionalidade é expressar os tipos de forma mais concisa. Note-se, no entanto, que também remove uma diferença importante na linguagem entre tipos mutáveis e tipos imutáveis: as auto-propriedades eram uma forma abreviada apenas disponível se se estivesse na disposição de tornar a classe mutável e, por isso, a tentação para o fazer por omissão era enorme. Agora, com auto-propriedades apenas de leitura, mutável e imutável passam a estar em pé de igualdade.

De momento não existe uma forma de explicitar um corpo para o construtor primário. O comité não conseguiu chegar a uma boa sintaxe e pensa que não será muito importante porque a maioria dos corpos de construtores são constituídos apenas por  inicializações. No entanto, muitos são os que se têm manifestado em favor da sua existência. Qual a tua opinião?

Parâmetros Campo

Nota: Os campos parâmetro (field parameters) não funcionam na corrente versão de antevisão.

Por omissão, os parâmetros de um construtor primário existem apenas em tempo de inicialização. Membros como as propriedades e métodos não se podem a eles referir porque, quando são chamados, o objeto já foi construído e o parâmetro desapareceu.

Foi considerado (e experimentado) deixar os parâmetros serem implicitamente “capturados” em campos privados gerados pelo compilador se fossem usados depois da construção, mas isso levava a código supérfluo e sujeito a erros onde muitas vezes os parâmetros eram inadvertidamente capturados em campos.

Claro que se podem declarar campos privados e inicializalos com um parâmetro:

public class Cliente(
  string nome,
  string apelido,
  DateTime aniversário)
{
    public string Nome { get; } = nome;
    public string Apelido { get; } = apelido;
    private DateTime _aniversário = aniversário;
}

Infelizmente o campo privado não poderia usar o mesmo nome do parâmetro e necessita de um nome alternativo, por exemplo prefixá-lo com uma barra inferior.

Para evitar isto, existe uma sintaxe para capturar explicitamente em campos os parâmetros dos construtores primários:

public class Cliente(
  string nome,
  string apelido,
  private  DateTime aniversário)
{
    public string Nome { get; } = nome;
    public string Apelido { get; } = apelido;
}

Quando se usa o modificador de acessibilidade (o mais provável é ser private) num construtor primário isso significa a existência de um parâmetro e um campo com o mesmo nome e a inicialização do campo com o parâmetro. Desta forma, vai ser possível referir-se a esse campo em métodos e propriedades após a inicialização:

public int Idade { get { return CalcularIdade
                                (aniversário); } }

Construtores Explícitos

Uma declaração de classe com construtor primário continua a poder definir outros construtores. No entanto, para assegurar que os argumentos são mesmo passados ao construtor primário, todos os construtores devem chamar um inicializador this(...):

public Ponto()
    : this(0, 0) // chama o construtor primário
{
}

Construtores explícitos podem chamar-se entre si, mas direta ou indiretamente terão de chamar o construtor primário no final porque é o único construtor que pode chamar o inicializador base(...).

No caso de estruturas (struct) com construtores primários, os construtores explícitos não podem chamar o construtor por omissão (sem parâmetros): têm de chamar direta ou indiretamente o construtor primário.

Incializador Base

O construtor primário chama sempre, implícita ou explicitamente, o inicializador base. Se não for especificado um inicializador base, como com todos os construtores, será chamado o construtor base sem parâmetros.

A forma de explicitamente chamar o inicializador base é passando uma lista de argumentos na especificação da classe base:

class BufferFullException()
    : Exception("Buffer full")
{
}

Tipos Parciais

Se um tipo for declarado em múltiplas partes, apenas uma das partes pode declarar parâmetros de construtor primário e apenas essa parte pode declarar argumentos na especificação da classe base.

Using Static

Nota: a implementação corrente desta funcionalidade não reflete o desenho atual. Mais detalhes abaixo.

Esta funcionalidade permite especificar um tipo numa cláusula using, tornando todos os membros estáticos acessíveis desse tipo sem qualificação no código subsequente:

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

Esta funcionalidade tem a capacidade de tornar o código mais curto. Existe alguma preocupação de que possa causar alguma confusão no namespace e causar ambiguidades, especialmente em classes que não foram desenhadas para serem “abertas” desta forma. Com quantos métodos Create(...) pode uma pessoa lidar se ficar confusa com o que está a criar?

Métodos de Extensão

Os métodos de extensão são métodos estáticos. Isto levanta a questão de como uma diretiva using static os vai expor. Como um método de topo no escopo? Como um método de extensão? Ambos? Vejamos um exemplo:

using System.Linq.Enumerable; // Apenas o tipo, e não todo o namespace
class Program
{
    static void Main()
    {
        var range = Range(5, 17);                // (1)
        var odd = Where(range, i => i % 2 == 1); // (2)
        var even = range.Where(i => i % 2 == 0); // (3)
    }
}

Range é um normal método estático, portanto a chamada marcada com (1) pode e deve ser permitida. A implementação atual não faz nada de especial com métodos de extensão, portanto permite a chamada aWhere como método estático em (2), mas não como método de extensão em (3).

Isto não é o que é pretendido. Permitir (3) satisfaria o longo pedido para tornar disponíveis métodos de extensão mais seletivamente pelo tipo em que estão definidos e não apenas pelo namespace. Por outro lado, os métodos de extensão raramente foram desenhados para serem usados como métodos estáticos (é apenas um situação de recurso para os raros casos em que existem ambiguidades) e não há interesse em venham a causar confusão no namespace de topo. Por isso, numa futura versão, (2) não será permitido.

Expressões de Declaração

As expressões de declaração permitem declarar variáveis locais no meio de expressões, com ou sem inicialização. Aqui estão alguns exemplos:

if (int.TryParse(s, out int i)) { … }
GetCoordinates(out var x, out var y);
Console.WriteLine("Result: {0}", (int x = GetValue()) * x);
if ((string s = o as string) != null) { ... s ... }

Isto é particularmente útil para parâmetros de saída (out), onde deixa de ser necessário declará-los previamente, numa linha separada. Isto é agradável na maioria dos casos, mas em cenários onde apenas expressões são permitidas, é necessário para a utilização de parâmetros de saída. Em cláusulas de consulta, por exemplo:

from s in strings
select int.TryParse(s, out int i) ? i : -1;

Intuitivamente, o escopo das variáveis declaradas numa expressão extende-se para o bloco exterior mais próximo, instrução estruturada (como if ou while) ou instruções embebidas (como o corpo de um if ou while). Também corpos de expressões lambda, cláusulas de consulta e inicializadores de campos e propriedades atuam como limites de escopo.

Na maioria dos casos, as expressões de declaração permitem var apenas se existir um inicializador de que se possa inferir o tipo, tal como nas instruções de declaração. No entanto, se a declaração da expressão é passado num parâmetro de saída (out), será tentada a resolução de sobrecargas (overlaod resolution) sem o tipo desse argumento e inferido o tipo a partir do método selecionado:

void GetCoordinates(out int x, out int y) { … }
GetCoordinates(out var x, out var y); // inferido int

As expressões de declaração podem requerer alguma habituação e pode ser sujeitas a abusos em novos e interessantes modos. Experimentem e digam o que pensam.

Filtros de Exceções

Os filtros de exceções já existiam no Visual Basic e no F# e agora existem também no C#. Este é o aspeto que terão:

try
{ ... }
catch (Exception e) if (myfilter(e))
{
    ...
}

Se a expressão entre parêntesis avaliar para verdadeiro, o bloco catch é executado, caso contrário a exceção continua o seu caminho.

Os filtros de exceções são preferíveis a apanhar e relançar porque deixam o stack inalterado. Se a exceção levar posteriormente a que haja um dump do stack, pode-se ver de onde foi originalmente lançada em vez de de onde foi relançada.

Literais Binários e Separadores de Dígitos

Nota: Na atual versão de antevisão, esta funcionalidade está disponível apenas para Visual Basic.

Pretende-se permitir literais binários em C#. Já não vai ser necessário pertencer-se à irmandade secreta do Hex para especificar vetores de bits ou valores do tipo flag!

var bits = 0b00101110;

Para literais longos (e estes novos literais binários podem facilmente ser muito longos) a possibilidade de especificar dígitos em grupos pode ser muito útil. O C# permitirá o uso da barra inferior em literais para separar grupos de dígitos:

var bits = 0b0010_1110;
var hex = 0x00_2E;
var dec = 1_234_567_890;

Podem se usar quantos se quiser e onde se quiser, exceto no início.

Membros Indexados e Inicializadores de Elementos

Os inicializadores de objetos e coleções podem ser muito úteis para inicializar campos e propriedades de objetos de forma declarativa ou para dar a uma coleção um conjunto inicial de elementos. A inicialização de dicionários não é tão elegante. Nesta versão é adicionada uma nova sintaxe aos inicializadores de objetos que permite atribuir valores diretamente a chaves através de qualquer indexador que o novo objeto possua:

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

No caso de as chaves serem strings, algumas vezes quer-se pensar nelas quase como sendo um tipo fraco de membros do objeto. Este é o caso, por exemplo, quando se tem dados semiestruturados em formatos de comunicação como JSON, que é como se fosse um objeto mas não é fortemente tipado em C#. É adicionada nesta versão uma sintaxe para “membros indexados”, que permitem fazer de conta que uma chave literal string é como se fosse um membro:

var customer = new JsonData
{
    $first = "Anders",  // => ["first"] = "Anders"
    $last = "Hejlsberg" // => ["last"] = "Hejlsberg"
};
string first = customer.$first; // => customer["first"]

Os membros indexados são identificados com o símbolo $ e podem ser usados em inicializadores de objetos e acesso a membros. São simplesmente traduzidos para a forma inferior que é indexar com literais do tipo string podendo, portanto, ser utilizados onde quer que o objeto rector tenha um indexador que receba uma única chave do tipo string. No entanto, dada a fraca receção pela comunidade desta funcionalidade, foi decidida a sua remoção na versão final.

Await em Blocos Catch e Finally

No C# 5.0 a palavra-chave await não era permitida em blocos catch e finally porque se pensava não ser possível a sua implementação. Finalmente descobriu-se como poderia ser implementado.

Isto era uma limitação significativa e obrigava a código nada bonito para dar a volta e compensar esta falta. Tal já não é mais necessário:

Resource res = null;
try
{
    // Podia-se fazer isto.
    res = await res.OpenAsync(…);       
    ...
}
catch (ResourceException e)
{
    // Agora pode-se fazer isto ...
    await res.LogAsync(res, e);              
}
finally
{
    // ... e isto
    if (res != null) await res.CloseAsync();
}

A implementação é algo complicada, mas não temos de nos preocupar com isso. É essa a razão de ter async na linguagem.

Métodos de Extensão Add em Inicializadores de Coleções

Quando inicialmente se implementaram os inicializadores de coleções no C#, os métodos Add que são chamados não podiam ser métodos de extensão. No Visual Basic isto foi uma possibilidade desde o início e o C# parece ter sido esquecido. Isto foi resolvido: o código gerado para um inicializador de coleção poderá usar um método de extensão chamado Add. Não é uma grande funcionalidade, mas é útil e, afinal, a sua implementação no novo compilador consistiu em remover a validação que a impedia.

Funcionalidades Não Implementadas

Algumas funcionalidades não foram ainda implementadas mas estão já planeadas ou em consideração para versão final do C# 6.0. Sendo assim, algumas funcionalidades poderão ainda sofrer alterações ou ser retiradas em função do feedback da comunidade.

Declaração de Corpo de Membros por Expressão

Esta funcionalidade planeada consiste na declaração de propriedades apenas de leitura e métodos através de uma simples expressão:

public class Point(int x, int y)
{
    public int X => x;
    public int Y => y;
    public double Dist => Math.Sqrt(x* x + y* y);
    public Point Move(int dx, int dy) => new Point(x + dx, y + dy);
}

Inicializadores de Eventos

Esta funcionalidade planeada vai permitir a inicialização de eventos nos inicializadores de objetos:

var botão = new Button
{
    ...
    Click += (source, e) => ...
    ...
};

Operador ;

Esta funcionalidade ainda em consideração permitiria que uma sequência de instruções seja combinada de forma a formar uma única instrução lógica:

(var x = Foo(); Write(x); x*x)

Params IEnumerable

Esta funcionalidade planeada vai permitir a utilização, para além de arrays, de enumeradores em conjugação com o modificador params:

int Avg(params IEnumerable<int> numbers) { ... }

Interpolação de Strings

Esta funcionalidade ainda em consideração permitiria a utilização de identificadores na construção de strings:

"\{p.First} \{p.Last} is \{p.Age} years old."

Trata-se de uma funcionalidade que tem gerado alguma discussão que pode ser seguida aqui.

Operador NameOf

Esta funcionalidade ainda em consideração permitiria a obtenção do nome de um membro de um tipo à semelhança de tipeof:

string s = nameof(Console.Write);

Propagação de Null

Esta funcionalidade planeada vai permitir escrever código mais conciso quando está em causa um acesso encadeado em que podem ocorrer valores null.

No caso mais simples, irá permitir quando agora se escreve:

var temp = GetCustomer();
string name = (temp == null) ? null : temp.name;

passar a escrever:

var temp = GetCustomer()?.name;

Mas como é que este novo operador (?.) se vai comportar em sequências? Existem duas hipóteses: ou é associativo à esquerda ou é associativo à direita.

Associatividade à Esquerda

Esta opção significaria que sequências do tipo a?.b?.c seriam equivalentes a ((a?.b)?.c). Uma consequência desta abordagem é que, uma vez usado o operador de propagação de null (?.), teria de ser usado em todas os acessos posteriores na sequência porque o valor sobre o qual se estaria a aceder ao membro seguinte poderia ser nulo (null).

Associatividade à Direita

Esta opção significaria que sequências do tipo a?.b?.c seriam equivalentes a (a?(.b?.c)). Uma consequência desta abordagem é que o operador de propagação de null (?.) pode ser usado em combinação com o operador . em qualquer combinação. Código como o seguinte seria válido:

int? l = cliente?.Nome.Length;

Discussão e Opção Escolhida

Após longa discussão pública, optou-se pela associatividade à direita por ser a que a maioria dos intervenientes achou mais “natural” e útil.

Private Protected

A combinação dos modificadores de acessibilidade protected e internal em C# corresponde ao modificador de acesso FamillyOrAssembly da CLR. A aplicação deste modificador significa que este membro ou tipo apenas é acessível por tipos que estejam na mesma assembly do tipo declarante ou sejam derivados deste.

Uma vez que a CLR também tem um modificador de acesso FamillyAndAssembly que faz com que o membro seja acessível apenas por tipos que estejam na mesma assembly do tipo declarante e sejam derivados deste.

A proposta inicial foi a utilização combinada dos modificadores private e protected, mas não foi bem recebida pela comunidade. Após longa discussão pública, ainda não há, à data da escrita deste artigo, uma decisão definitiva. Foi criado um inquérito para escolher a opção mais consensual, mas poderá já não estar disponível na altura que este artigo for publicado.

Conclusão

Esta versão não introduz funcionalidades fraturantes como foi o caso do LINQ ou async-await porque os esforços da equipa têm estado na nova plataforma de compiladores. Mas mesmo assim foram introduzidas algumas funcionalidades úteis e a nova plataforma permite explorar mais facilmente novas funcionalidades.

A grande novidade é a abertura ao público em geral da discussão e desenvolvimento da linguagem e plataforma de compiladores. Participem na discussão.

Recursos

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