C#

Como o C# 6.0 simplifica, esclarece e condensa o seu código

Mark Michaelis

O C# 6.0 não é uma revolução radical na programação C#. Ao contrário da introdução dos genéricos em C# 2.0, C# 3.0 e seu caminho inovador para programar coleções com o LlNQ, ou a simplificação dos padrões de programação assíncrona em C# 5.0, o C# 6.0 não transformará o desenvolvimento. Dito isso, o C# 6.0 mudará a forma de escrever o código C# em cenários específicos, devido aos recursos que são muito mais eficientes, que você provavelmente esquecerá que havia outra forma de codificá-los. Ele introduz novos atalhos de sintaxe, reduz a quantidade de cerimônia na ocasião, e em última análise, faz escrever o código C# de forma mais enxuta. Neste artigo eu estou indo aprofundar os detalhes do novo conjunto de recursos do C# 6.0, que tornam tudo isso possível. Especificamente, eu focarei nos itens descritos no Mapa Mental mostrados na Figura 1.

Mapa Mental do C# 6.0
Figura 1 Mapa Mental do C# 6.0

Observe que muitos dos exemplos aqui vêm da próxima edição do meu livro, "Essential C# 6.0" (Addison-Wesley Professional).

Usando Static

Muitos dos recursos do C# 6.0 podem ser aproveitados nos programas de Console mais básicos. Por exemplo, usar agora é suportado em classes específicas em um recurso chamado de diretiva estática de uso, o que torna os métodos estáticos disponíveis no escopo global, sem um prefixo de tipo de qualquer tipo, conforme mostrado na Figura 2. Pelo fato do System.Console ser uma classe estática, ele fornece um ótimo exemplo de como esse recurso pode ser aproveitado.

Figura 2 A diretiva estática de uso reduz o ruído dentro do seu código

using System;
using static System.ConsoleColor;
using static System.IO.Directory;
using static System.Threading.Interlocked;
using static System.Threading.Tasks.Parallel;
public static class Program
{
  // ...
  public static void Main(string[] args)
  {
    // Parameter checking eliminated for elucidation.
    EncryptFiles(args[0], args[1]);
  }
  public static int EncryptFiles(
    string directoryPath, string searchPattern = "*")
  {
    ConsoleColor color = ForegroundColor;
    int fileCount = 0;
    try
    {
      ForegroundColor = Yellow
      WriteLine("Start encryption...");
      string[] files = GetFiles(directoryPath, searchPattern,
        System.IO.SearchOption.AllDirectories);
      ForegroundColor = White
      ForEach(files, (filename) =>
      {
        Encrypt(filename);
        WriteLine("\t'{0}' encrypted", filename);
        Increment(ref fileCount);
      });
      ForegroundColor = Yellow
      WriteLine("Encryption completed");
    }
    finally
    {
      ForegroundColor = color;
    }
    return fileCount;
  }
}

Neste exemplo, existem diretivas estáticas de uso para System.ConsoleColor, System.IO.Directory, System.Threading.Interlocked e System.Threading.Tasks.Parallel. Estes permitem a invocação de vários métodos, propriedades e enums diretamente: ForegroundColor, WriteLine, GetFiles, Incrementar, Amarelo, Branco e ForEach. Em cada caso, isso elimina a necessidade de qualificar o membro estático com seu tipo. (Para aqueles de vocês usa o Visual Studio 2015 Preview ou anterior, a sintaxe não inclui a adição da palavra-chave "estático" depois de usar, então só está "usando o System.Console", por exemplo. Além disso, não até depois que o Visual Studio 2015 Preview faz com que a diretiva estática de uso funcione para enums e estruturas, além de classes estáticas.)

Para a maior parte, eliminar o tipo de qualificador não reduz significativamente a clareza do código, mesmo que haja menos código. WriteLine em um programa de console é bastante óbvio como é a chamada para GetFiles. E, pelo fato da adição da diretiva estática de uso no System.Threading.Tasks.Parallel ter sido obviamente intencional, o ForEach é uma maneira de definir um Loop ForEach paralelo que, com cada versão do C#, parece (se você está olhando apenas para a direita) mais e mais como uma declaração ForEach do C#.

O cuidado óbvio com a diretiva estática de uso é tomar cuidado para que a clareza não seja sacrificada. Por exemplo, considere a função Criptografar definida na Figura 3.

Figura 3 O ambíguo existe na invocação (com o Operador nameof)

private static void Encrypt(string filename)
  {
    if (!Exists(filename)) // LOGIC ERROR: Using Directory rather than File
    {
      throw new ArgumentException("The file does not exist.", 
        nameof(filename));
    }
    // ...
  }

Parece que a chamada para Existir é exatamente o que é necessário. No entanto, de forma mais explícita, a chamada é Directory.Exists quando, na verdade, o File.Exists é o que é necessário aqui. Em outras palavras, embora o código esteja certamente legível, está incorreto e, pelo menos neste caso, evitar a sintaxe estátic de uso é provavelmente melhor.

Observe que, se as diretivas estáticas de uso são especificadas tanto para System.IO.Directory como para System.IO.File, o compilador emite um erro ao chamar o Existir, forçando o código a ser modificado com um prefixo de desambiguação do tipo, a fim de resolver a ambiguidade.

Um recurso adicional da diretiva estática de uso é o seu comportamento com os métodos de extensão. Os métodos de extensão não são movidos em âmbito global, como aconteceria com métodos estáticos normalmente. Por exemplo, uma diretiva ParallelEnumerable estática de uso não colocaria o método Selecionar no âmbito global e você não poderia chamar Select(files, (filename) => { ... }). Essa restrição é por projeto. Primeiro, os métodos de extensão são destinados a serem processados como métodos de instância em um objeto (files.Select((filename)=>{ ... }) por exemplo) e não é um padrão normal chamá-los como métodos estáticos diretamente do tipo. Segundo, há bibliotecas como System.Linq, com tipos como Enumerable e ParallelEnumerable que têm nomes de métodos de sobreposição, como Selecionar. Para ter todos esses tipos adicionados ao escopo global, força uma desordem desnecessária no IntelliSense e, possivelmente, introduz uma invocação ambígua (embora não no caso de classes com base no System.Linq).

Embora os métodos de extensão não se coloquem em escopo global, o C# 6.0 ainda permite classes com métodos de extensão nas diretivas estáticas de uso. A diretiva estática de uso alcança o mesmo enquanto uma diretiva (namespace) de uso faz com exceção apenas para a classe específica dirigida pela diretiva estático de uso. Em outras palavras, a estática de uso permite que o desenvolvedor estreite os métodos de extensão que estão disponíveis para a classe particular identificada, em vez de todo o namespace. Por exemplo, considere o trecho na Figura 4.

Figura 4 Apenas métodos de extensão do ParallelEnumerable estão em escopo

using static System.Linq.ParallelEnumerable;
using static System.IO.Console;
using static System.Threading.Interlocked;
// ...
    string[] files = GetFiles(directoryPath, searchPattern,
      System.IO.SearchOption.AllDirectories);
    files.AsParallel().ForAll( (filename) =>
    {
      Encrypt(filename);
      WriteLine($"\t'{filename}' encrypted");
      Increment(ref fileCount);
    });
// ...

Observe que não há declaração System.Linq em uso. Em vez disso, existe a diretiva System.Linq.ParallelEnumerable estática de uso, fazendo com que apenas os métodos de extensão de ParallelEnumerable estejam no escopo como métodos de extensão. Todos os métodos de extensão nas classes como System.Linq.Enumerable não estarão disponíveis como métodos de extensão. Por exemplo, o files.Select(...) falhará na compilação porque o Selecionar não está no escopo em uma matriz de cadeia de caracteres (ou até mesmo a <cadeia de caracteres> IEnumerable). Em contraste, AsParallel está no escopo artavés do System.Linq.ParallelEnumerable. Em resumo, a diretiva estática de uso em uma classe com métodos de extensão trará os métodos de extensão de classe no escopo como métodos de extensão. (Os métodos de não-extensão na mesma classe serão trazidos para o escopo global normalmente.)

Em geral, a melhor prática é limitar o uso da diretiva estática de uso para algumas classes que são usadas repetidamente ao longo do escopo (ao contrário do Paralelo), tais como System.Console ou System.Math. Da mesma forma, ao usar o estático para enums, certifique-se de que os itens enum são auto-explicativos, sem o seu identificador do tipo. Por exemplo, e, talvez o meu favorito, especifique usando Microsoft.VisualStudio.TestTools.UnitTesting.Assert em arquivos de testes de unidade para habilitar invocações de afirmação de teste, tais como IsTrue, AreEqual<T>, Fail e IsNotNull.

O Operador nameof

A Figura 3 inclui um outro recurso novo em C# 6.0, o operador nameof. Esta é uma nova palavra-chave contextual para identificar uma cadeia de caracteres literal que extrai uma constante para (em tempo de compilação) o nome não qualificado de qualquer identificador que é especificado como um argumento. Na Figura 3, nameof(filename) devolve "filename", o nome do parâmetro do método de Criptografia. No entanto, o nameof funciona com qualquer identificador programático. Por exemplo, a Figura 5 aproveita o nameof para passar o nome da propriedade para INotifyPropertyChanged.PropertyChanged. (por faar nisso, usar o atributo CallerMemberName para recuperar um nome da propriedade para a invocação PropertyChanged ainda é uma abordagem válida para recuperar o nome da propriedade; consulte itl.tc/?p=11661.)

Figura 5 Usando o Operador nameof para INotifyPropertyChanged.PropertyChanged

public class Person : INotifyPropertyChanged
{
  public event PropertyChangedEventHandler PropertyChanged;
  public Person(string name)
  {
    Name = name;
  }
  private string _Name;
  public string Name
  {
    get{ return _Name; }
    set
    {
      if (_Name != value)
      {
        _Name = value;
        PropertyChangedEventHandler propertyChanged = PropertyChanged;
        if (propertyChanged != null)
        {
          propertyChanged(this,
            new PropertyChangedEventArgs(nameof(Name)));
        }
      }
    }
  }
  // ...
}
[TestClass]
public class PersonTests
{
  [TestMethod]
  public void PropertyName()
  {
    bool called = false;
    Person person = new Person("Inigo Montoya");
    person.PropertyChanged += (sender, eventArgs) =>
    {
      AreEqual(nameof(CSharp6.Person.Name), eventArgs.PropertyName);
      called = true;
    };
    person.Name = "Princess Buttercup";
    IsTrue(called);
  }   
}

Observe que se apenas o "nome" não qualificado é fornecido (porque está no escopo) ou o identificador CSharp6.Person.Name totalmente qualificado é usado como no teste, o resultado é apenas o identificador final (o último elemento de um nome pontilhado).

Ao alavancar o operador nameof, é possível eliminar a grande maioria das cadeias de caracteres "mágicas" que se referem a identificadores de código, enquanto elas estão no escopo. Isto não só elimina erros do tempo de execução, devido a erros de ortografia dentro das cadeias de caracteres mágicas, que nunca são verificadas pelo compilador, mas também permite refatorar as ferramentas como Renomear para atualizar todas as referências para o identificador de mudança de nome. E, se um nome muda sem uma ferramenta de refatoração, o compilador emitirá um erro indicando que o identificador não existe mais.

Interpolação da cadeia de caracteres

A Figura 3 poderia ser melhorada, não apenas especificando a mensagem de exceção indicando que o arquivo não foi encontrado, mas também por meio da exibição do próprio nome do arquivo. Antes do C# 6.0, você faria isso usando string.Format para incorporar o nome do arquivo na cadeia de caracteres literal. No entanto, a formatação composta não era a mais legível. A formatação do nome de uma pessoa, por exemplo, requer a substituição dos espaços reservados com base na ordem dos parâmetros, conforme mostrado na atribuição da mensagem da Figura 6.

Figura 6 Composição da formatação da cadeia de caracteres versus interpolação da cadeia de caracteres

[TestMethod]
public void InterpolateString()
{
  Person person = new Person("Inigo", "Montoya") { Age = 42 };
  string message =
    string.Format("Hello!  My name is {0} {1} and I am {2} years old.",
    person.FirstName, person.LastName, person.Age);
  AreEqual<string>
    ("Hello!  My name is Inigo Montoya and I am 42 years old.", message);
  string messageInterpolated =
    $"Hello!  My name is {person.FirstName} {person.LastName} and I am
    {person.Age} years old.";
  AreEqual<string>(message, messageInterpolated);
}

Observe a abordagem alternativa para a formatação composta com a atribuição para messageInterpolated. Neste exemplo, a expressão atribuída a messageInterpolated é uma cadeia de caracteres literal prefixada com um código de identificação de "$" e colchetes que está incorporado em linha dentro da cadeia de caracteres. Neste caso, as propriedades da pessoa são usadas para fazer esta cadeia de caracteres significativamente mais fácil de ler do que uma cadeia de caracteres composta. Além disso, a sintaxe de interpolação da cadeia de caracteres reduz erros causados por argumentos seguindo a cadeia de caracteres do formato que está em ordem incorreta, ou inexistente e causando uma exceção. (No Visual Studio 2015 Preview não há caractere $ e, em vez disso, cada colchete deixado requer uma barra antes dele. As versões seguintes do Visual Studio 2015 Preview são atualizadas para usar o $ na frente da sintaxe da cadeia de caracteres literal em seu lugar.)

A interpolação da cadeia de caracteres é transformada em tempo de compilação para invocar uma chamada string.Format equivalente. Isso deixa o suporte local para a localização como antes (embora ainda com cadeias de caracteres no formato tradicional) e não introduz qualquer injeção de pós-compilação do código através das cadeias de caracteres.

A Figura 7 mostra mais dois exemplos de interpolação da cadeia de caracteres.

Figura 7 Uso da interpolação da cadeia de caracteres no lugar do string.Format

public Person(string firstName, string lastName, int? age=null)
{
  Name = $"{firstName} {lastName}";
  Age = age;
}
private static void Encrypt(string filename)
{
  if (!System.IO.File.Exists(filename))
  {
    throw new ArgumentException(
      $"The file, '{filename}', does not exist.", nameof(filename));
  }
  // ...
}

Observe que, no segundo caso, a instrução de posição, tanto a interpolação da cadeia de caracteres como o operador nameof são alavancados. A interpolação da cadeia de caracteres é o que faz com que a mensagem ArgumentException inclua o nome do arquivo (ou seja, "O arquivo, 'c:\data\missingfile.txt' não existe"). O uso do operador nameof é para identificar o nome do parâmetro Criptografar ("nome do arquivo"), o segundo argumento do construtor ArgumentException. O Visual Studio 2015 está plenamente consciente da sintaxe de interpolação da cadeia de caracteres, fornecendo um código de cores e IntelliSense para os blocos de código embutidos dentro da cadeia de caracteres interpolada.

Operador nulo condicional

Embora eliminado na Figura 2 para maior clareza, praticamente todos os métodos principais que aceitam argumentos requerem a verificação do parâmetro para nulo antes de invocar o membro Comprimento para determinar quantos parâmetros foram passados. De modo mais geral, é um padrão muito comum verificar a nulidade antes de invocar um membro, a fim de evitar um System.NullReferenceException (que quase sempre indica um erro na lógica da programação). Devido à frequência deste padrão, o C# 6.0 introduz o operador "?" conhecido como o operador nulo condicional:

public static void Main(string[] args)
{
  switch (args?.Length)
  {
  // ...
  }
}

O operador nulo condicional traduz a verificação se o operando é nulo antes de invocar o método ou a propriedade (Comprimento, neste caso). O código explícito equivalente logicamente seria (embora na sintaxe C# 6.0 do valor de args seja avaliada apenas uma vez):

(args != null) ? (int?)args.Length : null

O que torna o operador nulo condicional especialmente conveniente é que ele pode ser encadeado. Se, por exemplo, você invoca string[] names = person?.Name?.Split(' '), a Divisão só será chamada se a pessoa e o person.Name não forem nulos. Quando encadeado, se o primeiro operando é nulo, a avaliação de expressão é de curto-circuito, e nenhuma invocação dentro da cadeia de chamada de expressão ocorrerá. Cuidado, porém, você não negligencia involuntariamente os operadores nulos condicional adicionais. Considere, por exemplo, names = person?.Name.Split(' '). Se existe uma instância de pessoa, mas Nome é nulo, um NullReferenceException ocorrerá mediante invocação de Divisão. Isso não significa que você deve usar uma cadeia de operadores nulos condicionais, mas sim que você deve ser intencional sobre a lógica. No caso da Pessoa, por exemplo, se o Nome for validado e nunca pode ser nulo, nenhum operador nulo condicional adicional é necessário.

Uma coisa importante a observar sobre o operador nulo condicional é que, quando utilizado com um membro que devolve um tipo de valor, ele sempre devolve uma versão nula desse tipo. Por exemplo, args?.Length devolve um int?, não simplesmente um int. Embora talvez um pouco peculiar (em comparação com outro comportamento do operador), a devolução de um tipo de valor anulável ocorre apenas no final da cadeia de chamada. O resultado é que chamar o operador do ponto (".") no Comprimento só permite a invocação de membros int (não int?). No entanto, encapsular args?.Length entre parênteses - obrigando o resultado int? através da precedência do operador do parênteses - invocará a devolução do int? e torna os membros específicos Nullable<T> (HasValue e Value) disponíveis.

O operador nulo condicional é um ótimo recurso por conta própria. No entanto, ao usá-lo em combinação com uma invocação delegada resolve um problema encontrado no C# que existe desde o C# 1.0. Observe como na Figura 5 eu atribuo o manipulador de eventos PropertyChange a uma cópia local (propertyChanged) antes de verificar o valor para o nulo e, finalmente, disparar o evento. Esta é a maneira mais fácil do segmento seguro para invocar eventos sem o risco de uma anulação do evento ocorrer entre o momento em que a verificação para nulo ocorra e quando o evento é disparado. Infelizmente, isso não é intuitivo e eu frequentemente me deparo com um código que não segue esse padrão, com o resultado de NullReferenceExceptions inconsistente. Felizmente, com a introdução do operador nulo condicional no C# 6.0, este problema foi resolvido.

Com o C# 6.0, o trecho de código muda a partir de:

PropertyChangedEventHandler propertyChanged = PropertyChanged;
if (propertyChanged != null)
{
  propertyChanged(this, new PropertyChangedEventArgs(nameof(Name)));
}

para simplificar:

PropertyChanged?.Invoke(propertyChanged(
  this, new PropertyChangedEventArgs(nameof(Name)));

E, por causa de um evento ser apenas um delegado, o mesmo padrão de invocar um delegado através do operador nulo condicional e uma invocação é sempre possível. Esse recurso, talvez mais do que qualquer outro no C# 6.0, é a certeza de mudar a forma de escrever o código C# no futuro. Uma vez que você alavanca operadores nulos condicionais em delegados, as chances são de que você e o código nunca voltarão à maneira antiga (a não ser, é claro, que você esteja preso em um mundo pré-C# 6.0).

Os operadores nulos condicionais também podem ser usados em combinação com um operador de índice. Por exemplo, quando você os usa em combinação com um Newtonsoft.JObject, você pode atravessar um objeto JSON para recuperar elementos específicos, conforme mostrado na Figura 8.

Figura 8 Um exemplo de configuração de cor do console

string jsonText =
    @"{
      'ForegroundColor':  {
        'Error':  'Red',
        'Warning':  'Red',
        'Normal':  'Yellow',
        'Verbose':  'White'
      }
    }";
  JObject consoleColorConfiguration = JObject.Parse(jsonText);
  string colorText = consoleColorConfiguration[
    "ForegroundColor"]?["Normal"]?.Value<string>();
  ConsoleColor color;
  if (Enum.TryParse<ConsoleColor>(colorText, out color))
  {
    Console.ForegroundColor = colorText;
  }

É importante observar que, ao contrário da maioria das coleções dentro do MSCORLIB, o JObject não lança uma exceção se um índice é inválido. Se, por exemplo, o ForegroundColordoesn't existe, o JObject devolve nulo ao invés de lançar uma exceção. Isso é significativo porque usar o operador nulo condicional em coleções que lançam um IndexOutOfRangeException quase sempre é desnecessário e pode implicar em segurança quando não existe tal segurança. Voltar ao trecho que mostra o exemplo principal e args, considere o seguinte:

public static void Main(string[] args)
{
  string directoryPath = args?[0];
  string searchPattern = args?[1];
  // ...
}

O que torna este exemplo perigoso é que o operador nulo condicional dá uma falsa sensação de segurança, o que implica que se args não for nulo, o elemento deve existir. É claro, este não é o caso, porque o elemento pode não existir, mesmo se o args não for nulo. Pelo fato de verificar se o elemento conta com os args?.Length, já se verifica se os args não são nulos, você nunca realmente precisa usar também o operador nulo condicional ao indexar a coleção após o comprimento da verificação. Em conclusão, evite usar o operador nulo condicional em combinação com o operador de índice, se o operador de índice lança um IndexOutOfRangeException para índices não existentes. Fazer isso leva a uma falsa sensação de validade do código.

Construtores padrão nas estruturas

Outro recurso do C# 6.0 para estar ciente é o suporte para um construtor padrão (sem parâmetros) em um tipo de valor. Isso foi anteriormente anulado porque o construtor não seria chamado ao inicializar matrizes, padronizando um campo do tipo estrutura, ou inicializando uma instância com o operador padrão. No C# 6.0, no entanto, os construtores padrão agora são permitidos com a ressalva de que eles são apenas invocados quando o tipo de valor é instanciado com o novo operador. Tanto a inicialização de matriz como a atribuição explícita do valor padrão (ou a inicialização implícita de um tipo de campo de estrutura) contornarão o construtor padrão.

Para compreender como o construtor padrão é alavancado, considere o exemplo da classe ConsoleConfiguration mostrado na Figura 9. Com base em um construtor, e a sua invocação através do novo operador, conforme mostrado no método CreateUsingNewIsInitialized, as estruturas serão completamente inicializadas. Como seria de esperar, e, conforme é demonstrado na Figura 9, o encadeamento do construtor é totalmente suportado, onde um construtor pode chamar outro usando a palavra-chave "this"seguindo a declaração do construtor.

Figura 9 Declarando um construtor padrão em um tipo de valor

using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
public struct ConsoleConfiguration
{
  public ConsoleConfiguration() :
    this(ConsoleColor.Red, ConsoleColor.Yellow, ConsoleColor.White)
  {
    Initialize(this);
  }
  public ConsoleConfiguration(ConsoleColor foregroundColorError,
    ConsoleColor foregroundColorInformation,
    ConsoleColor foregroundColorVerbose)
  {
    // All auto-properties and fields must be set before
    // accessing expression bodied members
    ForegroundColorError = foregroundColorError;
    ForegroundColorInformation = foregroundColorInformation;
    ForegroundColorVerbose = foregroundColorVerbose;
  }
   private static void Initialize(ConsoleConfiguration configuration)
  {
    // Load configuration from App.json.config file ...
  }
  public ConsoleColor ForegroundColorVerbose { get; }
  public ConsoleColor ForegroundColorInformation { get; }
  public ConsoleColor ForegroundColorError { get; }
  // ...
  // Equality implementation excluded for elucidation
}
[TestClass]
public class ConsoleConfigurationTests
{
  [TestMethod]
  public void DefaultObjectIsNotInitialized()
  {
    ConsoleConfiguration configuration = default(ConsoleConfiguration);
    AreEqual<ConsoleColor>(0, configuration.ForegroundColorError);
    ConsoleConfiguration[] configurations = new ConsoleConfiguration[42];
    foreach(ConsoleConfiguration item in configurations)
    {
      AreEqual<ConsoleColor>(default(ConsoleColor),
        configuration.ForegroundColorError);
      AreEqual<ConsoleColor>(default(ConsoleColor),
        configuration.ForegroundColorInformation);
      AreEqual<ConsoleColor>(default(ConsoleColor),
        configuration.ForegroundColorVerbose);
    }
  }
  [TestMethod]
  public void CreateUsingNewIsInitialized()
  {
    ConsoleConfiguration configuration = new ConsoleConfiguration();
    AreEqual<ConsoleColor>(ConsoleColor.Red,
      configuration.ForegroundColorError);
    AreEqual<ConsoleColor>(ConsoleColor.Yellow,
      configuration.ForegroundColorInformation);
    AreEqual<ConsoleColor>(ConsoleColor.White,
      configuration.ForegroundColorVerbose);
  }
}

Existe um ponto-chave para se lembrar sobre estruturas: todos os campos de instância e auto-propriedades (porque eles têm campos de backup) devem ser totalmente inicializados antes de invocar outros membros de instância. Como resultado, no exemplo na Figura 9, o construtor não pode chamar o método de inicialização até que todos os campos e auto-propriedades sejam atribuídos. Felizmente, se um construtor encadeado lida com toda a inicialização necessária e é invocado por meio de uma invocação "this", o compilador é inteligente o suficiente para detectar que não é necessário iniciar os dados novamente a partir do corpo deste construtor não invocado, conforme mostrado na Figura 9.

Melhorias de auto-propriedade

Observe também na Figura 9 que as três propriedades (para as quais não existem campos explícitos) são todas declaradas como auto-propriedades (sem corpo) e apenas um elemento. Estes auto-propriedades apenas do elemento são um recurso do C# 6.0 para declarar propriedades de somente leitura que são apoiados (internamente) por um campo de somente leitura. Como tal, estas propriedades só podem ser modificadas a partir do construtor.

As auto-propriedades somente dos elementos estão disponíveis em ambas as estruturas e declarações de classe, mas elas são especialmente importantes para estruturas por causa da orientação de práticas recomendadas que as estruturas são imutáveis. Ao invés de seis ou mais linhas necessárias para declarar uma propriedade de somente leitura e inicializar antes do C# 6.0, agora uma declaração de linha única e a atribuição a partir do construtor são tudo o que é necessário. Assim, a declaração de estruturas imutáveis é agora não só o padrão de programação correto para estruturas, mas também o mais simples padrão - uma mudança muito apreciada de sintaxe anterior onde a codificação correta requer mais esforço.

Um segundo recurso de auto-propriedade introduzido no C# 6.0 é o suporte para os inicializadores. Por exemplo, eu posso adicionar uma auto-propriedade do DefaultConfig estático com inicializador para ConsoleConfiguration:

// Instance property initialization not allowed on structs.
static private Lazy<ConsoleConfiguration> DefaultConfig{ get; } =
  new Lazy<ConsoleConfiguration>(() => new ConsoleConfiguration());

Como uma propriedade, deveria fornecer um padrão de fábrica de instância única para acessar uma instância do ConsoleConfigurtion padrão. Observe que em vez de atribuir o elemento apenas na auto-propriedade a partir do construtor, este exemplo alavanca o System.Lazy<T> e o instancia como um inicializador durante a declaração. O resultado é que uma vez que o construtor for concluído, a instância do Lazy<ConsoleConfiguration> será imutável e uma invocação de DefaultConfig sempre devolverá a mesma instância do ConsoleConfiguration.

Observe que os inicializadores da auto-propriedade não são permitidos nos membros de instância de estruturas (embora eles certamente permitam nas classes).

Métodos incorporado da expressão e auto-propriedades

Outro recurso introduzido no C# 6.0 é expressão de membros incorporados. Este recurso existe para ambas as propriedades e métodos e permite o uso do operador de seta (=>) para atribuir uma expressão de qualquer uma das propriedades ou métodos no lugar de um corpo de declaração. Por exemplo, porque a propriedade DefaultConfig no exemplo anterior é tanto privada como do tipo Lazy<T>, recuperar a instância padrão real do ConsoleConfiguration requer um método getDefault:

static public ConsoleConfiguration GetDefault() => DefaultConfig.Value;

No entanto, neste trecho, observe que não há corpo de método do tipo bloco de declaração. Em vez disso, o método é implementado com apenas uma expressão (não uma declaração) prefixada com o operador de seta lambda. A intenção é fornecer uma implementação simples de uma linha sem toda a cerimônia, e uma que seja funcional com ou sem parâmetros na assinatura do método:

private static void LogExceptions(ReadOnlyCollection<Exception> innerExceptions) =>
  LogExceptionsAsync(innerExceptions).Wait();

No que diz respeito às propriedades, observe que os corpos de expressão funcionam apenas para propriedades de somente leitura (somente elementos). Na verdade, a sintaxe é praticamente idêntica à dos métodos de corpo de expressão, com exceção dos que não existem parênteses após o identificador. Voltando ao exemplo da Pessoa anterior, eu poderia implementar propriedades FirstName and LastName de somente leitura usando corpos de expressão, conforme mostrado a Figura 10.

Figura 10 Auto-propriedades de corpos de expressão

public class Person
{
  public Person(string name)
  {
    Name = name;
  }
  public Person(string firstName, string lastName)
  {
    Name = $"{firstName} {lastName}";
    Age = age;
  }
  // Validation ommitted for elucidation
  public string Name {get; set; }
  public string FirstName => Name.Split(' ')[0];
  public string LastName => Name.Split(' ')[1];
  public override string ToString() => "\{Name}(\{Age}";
}

Além disso, as propriedades de expressão do corpo também podem ser usadas nos membros de índice, devolvendo um item de uma coleção interna, por exemplo.

Dicionário do Inicializador

As coleções do tipo de dicionário são um ótimo meio para definir um par de valores do nome. Infelizmente, a sintaxe para inicialização está um pouco abaixo do ideal:

{ {"First", "Value1"}, {"Second", "Value2"}, {"Third", "Value3"} }

Para melhorar isso, o C# 6.0 inclui uma nova sintaxe de tipo de atribuição do dicionário:

Dictionary<string, Action<ConsoleColor>> colorMap =
  new Dictionary<string, Action<ConsoleColor>>
{
  ["Error"] =               ConsoleColor.Red,
  ["Information"] =        ConsoleColor.Yellow,
  ["Verbose"] =            ConsoleColor.White
};

Para melhorar a sintaxe, a equipe de linguagem introduziu o operador de atribuição como um meio de associar um par de itens que fazem um par de valor de pesquisa (nome) ou mapa. A pesquisa é qualquer uma que seja o valor do índice (e tipo de dados) que o dicionário declara.

Melhorias de exceção

Para não ficar atrás, as exceções também tinham um par de pequenos ajustes de linguagem no C# 6.0. Primeiro, é possível agora usar cláusulas de espera dentro de ambos os blocos de captura e finais, conforme mostrado na Figura 11.

Figura 11 Usando Espera dentro dos blocos de captura e dinais

public static async Task<int> EncryptFilesAsync(string directoryPath, string searchPattern = "*")
{
  ConsoleColor color = Console.ForegroundColor;
  try
  {
  // ...
  }
  catch (System.ComponentModel.Win32Exception exception)
    if (exception.NativeErrorCode == 0x00042)
  {
    // ...
  }
  catch (AggregateException exception)
  {
    await LogExceptionsAsync(exception.InnerExceptions);
  }
  finally
  {
    Console.ForegroundColor = color;
    await RemoveTemporaryFilesAsync();
  }
}

Desde a introdução da espera no C# 5.0 suporta a espera em blocos de captura e finais, acabou sendo muito mais procurado do que o inicialmente esperado. Por exemplo, o padrão de invocar métodos assíncronos de dentro de um bloco de captura ou final é bastante comum, especialmente quando se trata de limpeza ou de registro durante esses tempos. Agora, no C# 6.0, é finalmente possível.

O segundo recurso relacionado com a exceção (que está disponível no Visual Basic desde o 1.0) é o suporte para filtros de exceção como este na parte superior da filtragem por um tipo de exceção particular, agora é possível especificar uma cláusula "if" para restringir ainda mais se a exceção for capturada pelo bloco de captura ou não. (Na ocasião, esse recurso também foi aproveitado para efeitos secundários, tais como exceções de logon como uma exceção "flies by" sem realmente executar qualquer processamento de exceção). Um cuidado a observar sobre esse recurso é que, se há alguma chance de seu aplicativo poder ser localizado, evite expressões condicionais de captura que operam via mensagens de exceção, porque elas deixarão de funcionar sem mudanças após a localização.

Conclusão

Um ponto final ao falar sobre todos os recursos do C# 6.0 é que, embora obviamente, exija o compilador do C# 6.0, incluído no Visual Studio 2015 ou posterior, eles não exigem uma versão atualizada do Microsoft .NET Framework. Portanto, você pode usar os recursos do C# 6.0, mesmo se você estiver compilando no .NET Framework 4, por exemplo. A razão pela qual isso é possível é que todos os recursos são implementados no compilador, e não tem nenhuma dependência do .NET Framework.

Com esse rápido tour, eu encerro minha pesquisa no C# 6.0. As únicas duas funções restantes que não serão discutidas são o suporte para a definição de um método de extensão de Adicionar personalizado para ajudar com inicializadores de coleção, e algumas resoluções de sobrecarga menores, mas melhoradas. Em resumo, o C# 6.0 não muda radicalmente seu código, pelo menos não da maneira que os genéricos ou LINQ fez. O que ele faz, no entanto, é fazer os padrões de codificação corretos mais simples. O operador nulo condicional de um delegado é provavelmente o melhor exemplo disso, mas assim também, são muitos dos outros recursos, incluindo interpolação de cadeia de caracteres, o operador nameof e melhorias de auto-propriedade (especialmente para propriedades de somente leitura).

Para obter informações adicionais, aqui estão algumas referências adicionais:

Além disso, embora não esteja disponível até o segundo trimestre de 2015, procure pelo próximo lançamento do meu livro, “Essential C# 6.0” (intellitect.com/EssentialCSharp).

No momento em que você lê isso, as discussões dos recursos do C# 6.0 provavelmente estarão fechadas. No entanto, existe pouca dúvida de que uma nova Microsoft esteja emergindo, que está empenhada em investir no desenvolvimento entre plataforma usando as práticas recomendadas de código aberto, que permitem que a comunidade de desenvolvimento compartilhe na criação de um grande software. Por esta razão, em breve você será capaz de ler sobre as discussões iniciais do projeto no C# 7.0, porque desta vez a discussão terá lugar em um fórum de código aberto.


Mark Michaelis (itl.tc/Mark) é o fundador do IntelliTect. Ele também atua como arquiteto técnico chefe e treinador. Desde 1996, ele tem sido um MVP da Microsoft para C#, Visual Studio Team System (VSTS) e SDK do Windows e ele foi reconhecido como Diretor Regional da Microsoft em 2007. Ele também atua em diversos projetos de softwares da Microsoft - equipes de revisão de design, incluindo C#, a Divisão de Sistemas Conectados, e VSTS. Michaelis faz palestra em conferências de desenvolvedores, tem escrito vários artigos e livros e atualmente está trabalhando na próxima edição do "Essential C#" (Addison-Wesley Professional).

Agradecemos ao seguinte especialista técnico da Microsoft pela revisão deste artigo: Mads Torgersen