Connect(); 2016

Volume 31 - Número 12

.NET Framework - Novidades no C# 7.0

De Mark Michaelis

Em dezembro de 2015, discuti o desenvolvimento do C# 7.0 (msdn.com/magazine/mt595758). Muitas coisas mudaram ao longo do último ano, mas a equipe está agora reduzindo o desenvolvimento no C# 7.0 e o Visual Studio 2017 Release Candidate está implantado virtualmente todos os novos recursos. (Digo “virtualmente” porque até que o Visual Studio 2017 seja realmente lançado, sempre haverá uma chance de mudanças adicionais.) Para obter uma breve visão geral, consulte a tabela de resumo em itl.tc/CSharp7FeatureSummary. Neste artigo, explorarei cada um dos novos recursos detalhadamente.

Desconstrutores

Desde o C# 1.0, é possível chamar uma função — o construtor — que combina parâmetros e os encapsula em uma classe. Porém, nunca houve um modo conveniente de desconstruir o objeto de volta às suas partes constituintes. Por exemplo, imagine uma classe PathInfo que toma cada elemento do nome de arquivo — nome do diretório, nome do arquivo, extensão — e os combina em um objeto, oferecendo suporte para em seguida manipular os vários elementos do objeto. Agora, imagine que você deseja extrair (desconstruir) o objeto de volta às suas partes.

No C# 7.0, isso se torna trivial por meio do desconstrutor, que retorna os componentes especificamente identificados do objeto. Preste atenção para não confundir um desconstrutor com um destruidor (desalocação e limpeza determinista de um objeto) ou com um finalizador (itl.tc/CSharpFinalizers).

Veja a classe PathInfo na Figura 1.

Figura 1 - Classe PathInfo com um Desconstrutor com Testes Associados

public class PathInfo
{
  public string DirectoryName { get; }
  public string FileName { get; }
  public string Extension { get; }
  public string Path
  {
    get
    {
      return System.IO.Path.Combine(
        DirectoryName, FileName, Extension);
    }
  }
  public PathInfo(string path)
  {
    DirectoryName = System.IO.Path.GetDirectoryName(path);
    FileName = System.IO.Path.GetFileNameWithoutExtension(path);
    Extension = System.IO.Path.GetExtension(path);
  }
  public void Deconstruct(
    out string directoryName, out string fileName, out string extension)
  {
    directoryName = DirectoryName;
    fileName = FileName;
    extension = Extension;
  }
  // ...
}

Obviamente, você pode chamar o método Desconstrutor da mesma forma que faria no C# 1.0. Porém, o C# 7.0 oferece um açúcar sintático que simplifica significativamente a invocação. Dada a declaração de um desconstrutor, você pode invocá-lo usando uma nova sintaxe “tipo tupla” do C# 7.0 (confira a Figura 2).

Figura 2 - Invocação e Atribuição do Desconstrutor

PathInfo pathInfo = new PathInfo(@"\\test\unc\path\to\something.ext");
{
  // Example 1: Deconstructing declaration and assignment.
  (string directoryName, string fileName, string extension) = pathInfo;
  VerifyExpectedValue(directoryName, fileName, extension);
}
{
  string directoryName, fileName, extension = null;
  // Example 2: Deconstructing assignment.
  (directoryName, fileName, extension) = pathInfo;
  VerifyExpectedValue(directoryName, fileName, extension);
}
{
  // Example 3: Deconstructing declaration and assignment with var.
  var (directoryName, fileName, extension) = pathInfo;
  VerifyExpectedValue(directoryName, fileName, extension);
}

Observe como, pela primeira vez, o C# permite a atribuição simultânea a várias variáveis de valores diferentes. Isso não é o mesmo que a declaração de atribuição null, na qual todas as variáveis são inicializadas ao mesmo valor (nulo):

string directoryName, filename, extension = null;

Em vez disso, com a nova sintaxe tipo tupla, para cada variável é atribuído um valor diferente, que corresponde não ao seu nome, mas à ordem em que ela aparece na declaração e na instrução de desconstrução.

Como esperado, o tipo dos parâmetros de retorno deve corresponder ao tipo das variáveis que estão sendo atribuídas, sendo que var é permitido porque o tipo pode ser inferido dos tipos de parâmetro de Desconstrução. Observe, porém, que embora você possa colocar uma única var fora dos parênteses como mostrado no Exemplo 3 na Figura 2, no momento não é possível obter uma cadeia, mesmo que todas as variáveis sejam do mesmo tipo.

Observe que, no momento, a sintaxe tipo tupla do C# 7.0 exige que pelo menos duas variáveis apareçam entre parênteses. Por exemplo, (FileInfo path) = pathInfo; não é permitido mesmo que haja um desconstrutor para:

public void Deconstruct(out FileInfo file)

Em outras palavras, você não pode usar a sintaxe de desconstrutor do C# 7.0 para métodos de Desconstrução com apenas um parâmetro de retorno.

Trabalhando com Tuplas

Como mencionei, cada um dos exemplos anteriores aproveitou a sintaxe tipo tupla do C# 7.0. A sintaxe é caracterizada pelos parênteses que envolvem as várias variáveis (ou propriedades) que são atribuídas. Uso o termo “tipo tupla” porque, na verdade, nenhum desses exemplos de desconstrutor usam algum tipo de tupla internamente. (Na verdade, a atribuição de tuplas por meio de uma sintaxe de desconstrutor não é permitida e é provavelmente desnecessária, uma vez que o objeto atribuído já é uma instância que representa as partes constituintes encapsuladas.)

Com o C# 7.0, agora existe uma sintaxe simplificada para trabalhar com tuplas, como mostrado na Figura 3. Essa sintaxe pode ser usada sempre que um especificador de tipo é permitido, inclusive em declarações, operadores cast e parâmetros de tipo.

Figura 3 - Declarando, Instanciando e Usando a Sintaxe Tupla do C# 7.0

[TestMethod]
public void Constructor_CreateTuple()
{
  (string DirectoryName, string FileName, string Extension) pathData =
    (DirectoryName: @"\\test\unc\path\to",
    FileName: "something",
    Extension: ".ext");
  Assert.AreEqual<string>(
    @"\\test\unc\path\to", pathData.DirectoryName);
  Assert.AreEqual<string>(
    "something", pathData.FileName);
  Assert.AreEqual<string>(
    ".ext", pathData.Extension);
  Assert.AreEqual<(string DirectoryName, string FileName, string Extension)>(
    (DirectoryName: @"\\test\unc\path\to",
      FileName: "something", Extension: ".ext"),
    (pathData));
  Assert.AreEqual<(string DirectoryName, string FileName, string Extension)>(
    (@"\\test\unc\path\to", "something", ".ext"),
    (pathData));
  Assert.AreEqual<(string, string, string)>(
    (@"\\test\unc\path\to", "something", ".ext"), (pathData));
  Assert.AreEqual<Type>(
    typeof(ValueTuple<string, string, string>), pathData.GetType());
}
[TestMethod]
public void ValueTuple_GivenNamedTuple_ItemXHasSameValuesAsNames()
{
  var normalizedPath =
    (DirectoryName: @"\\test\unc\path\to", FileName: "something",
    Extension: ".ext");
  Assert.AreEqual<string>(normalizedPath.Item1, normalizedPath.DirectoryName);
  Assert.AreEqual<string>(normalizedPath.Item2, normalizedPath.FileName);
  Assert.AreEqual<string>(normalizedPath.Item3, normalizedPath.Extension);
}
static public (string DirectoryName, string FileName, string Extension)
  SplitPath(string path)
{
  // See http://bit.ly/2dmJIMm Normalize method for full implementation.
  return (          
    System.IO.Path.GetDirectoryName(path),
    System.IO.Path.GetFileNameWithoutExtension(path),
    System.IO.Path.GetExtension(path)
    );
}

Para aqueles que não estão familiarizados com tuplas, trata-se de um modo de combinar vários tipos em um único tipo com uma sintaxe simples que está disponível fora do método de instanciação. É simples, pois, ao contrário de quando se define uma classe/struct, as tuplas podem ser “declaradas” em linha conforme necessário. Mas, ao contrário de tipos dinâmicos que também possibilitam a declaração e instanciação em linha, as tuplas podem ser acessadas fora do membro que as contêm e, além disso, podem ser incluídas como parte de uma API. Independentemente do suporte para APIs externas, as tuplas não têm modo de extensão compatível com versões (exceto se os parâmetros de tipo suportarem derivação) e, dessa forma, deve-se ter cuidado ao usá-las em APIs públicas. Portanto, uma abordagem melhor seria usar uma classe padrão para o retorno em uma API pública.

Antes do C# 7.0, o framework já possuía uma classe de tuplas, System.Tuple<…> (introduzida no Microsoft .NET Framework 4). O C# 7.0 é diferente da solução anterior, porém, porque ele insere a finalidade semântica na declaração, além de introduzir um tipo de valor de tupla:  System.ValueTuple<…>.

Vamos dar uma olhada na finalidade semântica. Observe que na Figura 3 a sintaxe tupla do C# 7.0 permite que você declare nomes de alias para cada elemento ItemX que a tupla contém. A instância de tupla pathData na Figura 3, por exemplo, tem propriedades definidas fortemente tipadas DirectoryName: string, FileName: string, e Extension: string, o que permite chamadas a pathData.DirectoryName, por exemplo. Trata-se de uma melhoria significativa, já que antes do C# 7.0 os únicos nomes disponíveis eram nomes ItemX, onde o X era incrementado para cada elemento.

Agora, embora os elementos para uma tupla do C# 7.0 sejam fortemente tipados, os nomes, porém, não têm distinção na definição de tipo. Portanto, você pode atribuir duas tuplas com nomes de alias diferentes e tudo o que acontecer será um aviso que informará que o nome no lado direito será ignorado:

// Warning: The tuple element name 'AltDirectoryName1' is ignored
// because a different name is specified by the target type...
(string DirectoryName, string FileName, string Extension) pathData =
  (AltDirectoryName1: @"\\test\unc\path\to",
  FileName: "something", Extension: ".ext");

Da mesma forma, você pode atribuir tuplas a outras tuplas que podem não ter todos os nomes de elemento de alias definidos:

// Warning: The tuple element name 'directoryName', 'FileNAme' and 'Extension'
// are ignored because a different name is specified by the target type...
(string, string, string) pathData =
  (DirectoryName: @"\\test\unc\path\to", FileName: "something", Extension: ".ext");

Para ser claro, o tipo e a ordem de cada elemento define a compatibilidade de tipo. Apenas os nomes de elemento são ignorados. Porém, mesmo que sejam ignorados quando são diferentes, eles ainda fornecem IntelliSense no IDE.

Observe que, estando um alias de nome de elemento definido ou não, todas as tuplas têm nome ItemX, onde X corresponde ao número do elemento. Os nomes ItemX são importantes porque possibilitam o uso das tuplas do C# 6.0, mesmo que os nomes de elemento de alias não estejam disponíveis.

Outro ponto importante ao qual se deve estar atento é que o tipo de tupla na base do C# 7.0 é um System.ValueTuple. Se esse tipo não estiver disponível na versão do framework que você está usando para a compilação, você poderá acessá-lo por meio de um pacote NuGet.

Para obter detalhes sobre o conteúdo das tuplas, confira intellitect.com/csharp7tupleiinternals.

Correspondência de Padrões com Expressões Is

Nesse caso você tem uma classe de base, Storage, por exemplo, e uma série de classes derivadas, DVD, UsbKey, HardDrive, FloppyDrive (lembra-se dessas?) e assim por diante. Para implementar um método de Ejeção para cada um, você tem várias opções:

  • Operador As
    • Converter e atribuir usando o operador as
    • Verificar o resultado para nulo
    • Realizar a operação de ejeção
  • Operador Is
    • Verificar o tipo usando o operador is
    • Converter e atribuir o tipo
    • Realizar a operação de ejeção
  • Cast
    • Converter explicitamente e atribuir
    • Tratar de uma possível exceção
    • Realizar a operação
    • Pois é!

Há um quarto método, significativamente melhor, que usa polimorfismo no qual você expede usando funções virtuais. Porém, isso só estará disponível se você tiver o código-fonte para a classe Storage e puder adicionar o método de Ejeção. Suponho que essa opção não esteja disponível para esta discussão, por isso a correspondência de padrões é necessária.

O problema com essas abordagens é que a sintaxe é consideravelmente longa e sempre exige várias instruções para cada classe que você deseja converter. O C# 7.0 oferece a correspondência de padrões como um modo de combinar o teste e a atribuição em uma única operação. Como resultado, o código na Figura 4 é simplificado como mostrado na Figura 5.

Figura 4 - Conversão de Tipo sem Correspondência de Padrões

// Eject without pattern matching.
public void Eject(Storage storage)
{
  if (storage == null)
  {
    throw new ArgumentNullException();
  }
  if (storage is UsbKey)
  {
    UsbKey usbKey = (UsbKey)storage;
    if (usbKey.IsPluggedIn)
    {
      usbKey.Unload();
      Console.WriteLine("USB Drive Unloaded.");
    }
    else throw new NotImplementedException();    }
  else if(storage is DVD)
  // ...
  else throw new NotImplementedException();
}

Figura 5 - Conversão de Tipo com Correspondência de Padrões

// Eject with pattern matching.
public void Eject(Storage storage)
{
  if (storage is null)
  {
    throw new ArgumentNullException();
  }
  if ((storage is UsbKey usbDrive) && usbDrive.IsPluggedIn)
  {
    usbDrive.Unload();
    Console.WriteLine("USB Drive Unloaded.");
  }
  else if (storage is DVD dvd && dvd.IsInserted)
  // ...
  else throw new NotImplementedException();  // Default
}

A diferença entre os dois não é muito radical, mas quando realizada frequentemente (par cada um dos tipos derivados, por exemplo), a sintaxe anterior é uma idiossincrasia inconveniente do C#. Essa melhoria no C# 7.0 — a combinação de teste, declaração e atribuição de tipo em uma única operação — faz com que a sintaxe anterior se torne obsoleta. Na sintaxe anterior, verificar o tipo sem atribuir um identificador faz com que a falha por meio de “default” seja inconveniente, no mínimo. Ao contrário, a atribuição permite condicionais adicionais além de apenas a verificação de tipo.

Observe que o código na Figura 5 começa com um operador is de correspondência de padrões com suporte para um operador de comparação null:

if (storage is null) { ... }

Correspondência de Padrões com a Instrução Switch

Embora o suporte da correspondência de padrões com o operador is seja uma melhoria, o suporte da correspondência de padrões para uma instrução switch é, possivelmente, ainda mais significativo, pelo menos quando há vários tipos compatíveis para os quais converter. Isso se dá porque o C# 7.0 inclui instruções case com correspondência de padrões e, além disso, se o padrão de tipo for satisfeito na instrução case, um identificador pode ser fornecido, atribuído e acessado dentro da instrução case. A Figura 6 oferece um exemplo.

Figura 6 - Correspondência de Padrões em uma Instrução Switch

public void Eject(Storage storage)
{
  switch(storage)
  {
    case UsbKey usbKey when usbKey.IsPluggedIn:
      usbKey.Unload();
      Console.WriteLine("USB Drive Unloaded.");
      break;
    case DVD dvd when dvd.IsInserted:
      dvd.Eject();
      break;
    case HardDrive hardDrive:
      throw new InvalidOperationException();
    case null:
    default:
      throw new ArgumentNullException();
  }
}

Observe no exemplo abaixo como as variáveis locais como usbKey e dvd são declaradas e atribuídas automaticamente dentro da instrução case. E, como esperado, o escopo é limitado para o que está contido na instrução case.

Porém, talvez tão importante quanto a declaração e atribuição da variável seja a condicional adicional que pode ser adicionada à instrução case com uma cláusula when. O resultado é que uma instrução case pode filtrar completamente um cenário inválido sem que haja um filtro adicional dentro da instrução case. Com isso, temos a vantagem adicional de permitir a avaliação da próxima instrução case se, de fato, a instrução case anterior não for completamente satisfeita. Isso também significa que as instruções case não estão mais limitadas a constantes e, além disso, uma expressão switch pode ser de qualquer tipo: não está mais limitada a bool, char, string, integral e enum.

Outra característica importante da funcionalidade da instrução switch de correspondência de padrões do novo C# 7.0 é que a ordem das instruções case é significante e validada no tempo de compilação. (Isso é diferente das versões anteriores da linguagem, nas quais, sem a correspondência de padrões, a ordem das instruções case não era significativa.) Por exemplo, se eu introduzisse uma instrução case para Armazenamento antes de uma instrução case de correspondência de padrões derivada de Armazenamento (UsbKey, DVD e HardDrive), o caso Armazenamento eclipsaria todas as outras correspondências de padrão de tipo (que derivam de Armazenamento). Uma instrução case de um tipo de base que eclipsa outras instruções case de tipo a partir da avaliação resultará em um erro de compilação na instrução case que foi eclipsada. Dessa forma, as exigências de ordem das instruções case são similares às instruções catch.

Os leitores se lembrarão que um operador is em um valor null retornará falso. Portanto, nenhuma instrução case de correspondência de padrões terá uma correspondência para uma expressão switch com valor null. Por essa razão, a ordem da instrução case com valor null não importa. Esse comportamento corresponde a instruções switch antes da correspondência de padrões.

Além disso, para dar suporte à compatibilidade com instruções switch antes do C# 7.0, o caso padrão é sempre avaliado em último lugar, independentemente do seu lugar na ordem de instruções case. (Dito isso, para uma leitura mais fácil, é recomendável colocá-lo no final, já que é sempre avaliado por último.) Além disso, instruções goto case ainda funcionam apenas para rótulos de caso constante, e não para correspondência de padrões.

Funções Locais

Embora já seja possível declarar um delegado e atribuir uma expressão a ele, o C# 7.0 leva esse passo mais adiante permitindo a declaração completa de uma função local em linha dentro de outro membro. Observe a função IsPalindrome na Figura 7.

Figura 7 - Exemplo de Função Local

bool IsPalindrome(string text)
{
  if (string.IsNullOrWhiteSpace(text)) return false;
  bool LocalIsPalindrome(string target)
  {
    target = target.Trim();  // Start by removing any surrounding whitespace.
    if (target.Length <= 1) return true;
    else
    {
      return char.ToLower(target[0]) ==
        char.ToLower(target[target.Length - 1]) &&
        LocalIsPalindrome(
          target.Substring(1, target.Length - 2));
    }
  }
  return LocalIsPalindrome(text);
}

Nesta implementação, em primeiro lugar verifiquei se o argumento passado por IsPalindrome não é null ou apenas espaços brancos. (Poderia ter usado a correspondência de padrões com “o texto é nulo” para a verificação de null.) Em seguida, declarei a função LocalIsPalindrome, na qual comparei o primeiro e o último caractere recursivamente. A vantagem dessa abordagem é que não preciso declarar a função LocalIsPalindrome dentro do escopo da classe onde ela poderia ser chamada erroneamente, o que contorna a verificação IsNullOrWhiteSpace. Em outras palavras, as funções locais possibilitam uma restrição adicional do escopo, mas apenas dentro da função que está em volta.

O cenário de validação de parâmetros na Figura 7 é um dos casos de uso comuns de funções locais. Outro caso que encontro com frequência ocorre em testes de unidade, como ao testar a função IsPalindrome (confira a Figura 8).

Figura 8 - O Teste de Unidade Usa Funções Locais com Frequência

[TestMethod]
public void IsPalindrome_GivenPalindrome_ReturnsTrue()
{
  void AssertIsPalindrome(string text)
  {
    Assert.IsTrue(IsPalindrome(text),
      $"'{text}' was not a Palindrome.");
  }
  AssertIsPalindrome("7");
  AssertIsPalindrome("4X4");
  AssertIsPalindrome("   tnt");
  AssertIsPalindrome("Was it a car or a cat I saw");
  AssertIsPalindrome("Never odd or even");
}

Funções de iteração que retornam IEnumerable<T> e produzem elementos de retorno são outro caso de uso comum de funções locais.

Para resumir o assunto, a seguir são expostos alguns outros pontos que é bom saber para usar funções locais:

  • As funções locais não permitem o uso de um modificador de acessibilidade (público, privado, protegido).
  • As funções locais não suportam sobrecarga. Você não pode ter duas funções locais no mesmo método com o mesmo nome, mesmo que as assinaturas não se sobreponham.
  • O compilador criará um aviso para as funções locais que nunca são invocadas.
  • As funções locais podem acessar todas as variáveis no escopo delimitado, inclusive variáveis locais. Esse comportamento é o mesmo com expressões lambda definidas localmente, exceto que as funções locais não alocam um objeto que representa o fechamento, como as expressões lambda definidas localmente fazem.
  • As funções locais estão no escopo para todo o método, sejam elas invocadas antes ou depois da declaração.

Retorno por Referência

Desde o C# 1.0, é possível passar argumentos para uma função por referência (ref). O resultado é que qualquer alteração no parâmetro será passada de volta ao chamador. Considere a seguinte função Swap:

static void Swap(ref string x, ref string y)

Nesse cenário, o método chamado pode atualizar as variáveis originais do chamador com os novos valores, trocando, dessa forma, o que está contido no primeiro e no segundo argumento.

Desde o C# 7.0, também é possível passar uma referência de volta por meio do retorno da função, e não apenas do parâmetro ref. Considere, por exemplo, uma função que retorna o primeiro pixel em uma imagem que está associada com olhos vermelhos, como mostrado na Figura 9.

Figura 9 - Retorno de Ref e Declaração Local de Ref

public ref byte FindFirstRedEyePixel(byte[] image)
{
  //// Do fancy image detection perhaps with machine learning.
  for (int counter = 0; counter < image.Length; counter++)
  {
    if(image[counter] == (byte)ConsoleColor.Red)
    {
      return ref image[counter];
    }
  }
  throw new InvalidOperationException("No pixels are red.");
}
[TestMethod]
public void FindFirstRedEyePixel_GivenRedPixels_ReturnFirst()
{
  byte[] image;
  // Load image.
  // ...
    // Obtain a reference to the first red pixel.
  ref byte redPixel = ref FindFirstRedEyePixel(image);
  // Update it to be Black.
  redPixel = (byte)ConsoleColor.Black;
  Assert.AreEqual<byte>((byte)ConsoleColor.Black, image[redItems[0]]);
}

Ao retornar uma referência à imagem, o chamador poderá atualizar o pixel para uma cor diferente. A verificação da atualização por meio da matriz demonstra que o valor é agora preto. A alternativa ao uso de um parâmetro por referência é, poderia-se dizer, menos óbvia e de leitura mais difícil:

public bool FindFirstRedEyePixel(ref byte pixel);

Existem duas restrições importantes quanto ao retorno por referência, ambas devido ao tempo de vida do objeto. Uma delas é que referências de objeto não podem ser coletadas no lixo enquanto ainda estiverem sendo referenciadas, e a outra é que não devem consumir memória quando não houver mais referências. Em primeiro lugar, você só pode retornar referências para campos, outras propriedades ou funções de retorno de referência ou objetos que foram passados como parâmetros para a função de retorno por referência. Por exemplo, FindFirst­RedEyePixel retorna uma referência a um item na matriz da imagem, que era um parâmetro para a função. Da mesma forma, se a imagem foi armazenada como um campo dentro de uma classe, você pode retornar o campo por referência:

byte[] _Image;
public ref byte[] Image { get {  return ref _Image; } }

Em segundo lugar, locais ref são inicializados para uma localização de armazenamento específica na memória e não podem ser modificados para apontar para uma localização diferente. (Você não pode ter um ponteiro para uma referência e modificar a referência, ou seja, um ponteiro para um ponteiro para aqueles que conhecem C++.)

Há várias características do retorno por referência que devem ser conhecidas: 

  • Se você está retornando uma referência, obviamente você deve retorná-la. Isso significa que, no exemplo da Figura 9, mesmo que não exista nenhum pixel de olhos vermelhos, você ainda precisa retornar um byte ref. O único modo de contornar isso seria por meio de uma exceção. Ao contrário, a abordagem do parâmetro por referência permite que você não precise alterar o parâmetro e retorne bool, indicando sucesso. Em muitos casos, isso pode ser preferível.
  • Ao declarar uma variável local de referência, é necessária a inicialização. Isso envolve atribuir a ela um retorno ref de uma função ou uma referência a uma variável:
ref string text;  // Error
  • Embora seja possível declarar uma variável local de referência no C# 7.0, não é permitido declarar um campo de tipo de ref:
class Thing { ref string _Text;  /* Error */ }
  • Você não pode declarar um tipo por referência para uma propriedade autoimplementada:
class Thing { ref string Text { get;set; }  /* Error */ }
  • Propriedades que retornam uma referência são permitidas:
class Thing { string _Text = "Inigo Montoya"; 
  ref string Text { get { return ref _Text; } } }
  • Uma variável local de referência não pode ser inicializada com um valor (como null ou uma constante). Ela deve ser atribuída de um membro de retorno por referência ou uma variável local/campo: 
ref int number = null; ref int number = 42;  // ERROR

Variáveis Out

Desde o primeiro lançamento do C#, a invocação de métodos que contêm parâmetros out sempre requer a pré-declaração do identificador de argumento out antes da invocação do método. O C# 7.0 remove essa idiossincrasia e permite a declaração do argumento out em linha com o método de invocação. A Figura 10 mostra um exemplo.

Figura 10 - Declaração Em Linha de Argumentos Out

public long DivideWithRemainder(
  long numerator, long denominator, out long remainder)
{
  remainder = numerator % denominator;
  return (numerator / denominator);
}
[TestMethod]
public void DivideTest()
{
  Assert.AreEqual<long>(21,
    DivideWithRemainder(42, 2, out long remainder));
  Assert.AreEqual<long>(0, remainder);
}

Observe como no método DivideTest, a chamada para DivideWith­Remainder de dentro do teste inclui um especificador de tipo após o modificador out. Além disso, observe como o restante continua a estar no escopo do método automaticamente, como mostrado pela segunda invocação Assert.AreEqual. Ótimo!

Aprimoramentos literais

Ao contrário das versões anteriores, o C# 7.0 inclui um formato literal binário numérico, como o exemplo a seguir demonstra:

long LargestSquareNumberUsingAllDigits =
  0b0010_0100_1000_1111_0110_1101_1100_0010_0100;  // 9,814,072,356
long MaxInt64 { get; } =
  9_223_372_036_854_775_807;  // Equivalent to long.MaxValue

Observe também o suporte para o sublinhado “_” como um separador de dígitos. É usado simplesmente para melhorar a facilidade de leitura e pode ser colocado em qualquer lugar entre os dígitos do número: binário, decimal ou hexadecimal.

Tipos de Retorno Assíncrono Generalizado

No caso da implementação de um método assíncrono, é possível retornar o resultado sincronicamente, gerando um curto-circuito em uma operação de execução longa, porque o resultado é virtualmente instantâneo ou até mesmo já conhecido. Considere, por exemplo, um método assíncrono que determina o tamanho total dos arquivos em um diretório (bit.ly/2dExeDG). Se, de fato, não houver arquivos no diretório, o método pode retornar imediatamente sem nunca executar uma operação de execução longa. Até o C# 7.0, as exigências da sintaxe assíncrona determinavam que o retorno de um método desse tipo deveria ser Task<long> e, portanto, que uma Task fosse instanciada mesmo que nenhuma instância Task fosse necessária. (Para obter isso, o padrão geral é retornar o resultado de Task.FromResult<T>.)

No C# 7.0, o compilador não limita mais os retornos do método assíncrono para o vazio, Task ou Task<T>. Agora você pode definir tipos personalizados, como o struct System.Threading.Tasks.ValueTask<T> fornecido pelo .NET Core Framework, que é compatível com um retorno de método assíncrono. Para obter mais informações, confira itl.tc/GeneralizedAsyncReturnTypes.

Mais Membros de Expressão Incorporada

O C# 6.0 introduziu membros de expressão incorporada para funções e propriedades, permitindo uma sintaxe simplificada para a implementação de métodos e propriedades triviais. No C# 7.0, as implementações de expressão incorporada são adicionadas a construtores, acessadores (implementações das propriedades get e set) e até mesmo a finalizadores (confira a Figura 11).

Figura 11 - Usando Membros de Expressão Incorporadas em Acessadores e Construtores

class TemporaryFile  // Full IDisposible implementation
                     // left off for elucidation.
{
  public TemporaryFile(string fileName) =>
    File = new FileInfo(fileName);
  ~TemporaryFile() => Dispose();
  Fileinfo _File;
  public FileInfo File
  {
    get => _File;
    private set => _File = value;
  }
  void Dispose() => File?.Delete();
}

É esperado que o uso de membros de expressão incorporada seja particularmente comum para finalizadores, já que a implementação mais comum é chamar o método Dispose, como mostrado.

Fico feliz em ressaltar que o suporte adicional para membros de expressão incorporada foi implementado pela comunidade C# e não pela equipe Microsoft C#. Viva o software livre!

Cuidado: Esse recurso não foi implementado no Visual Studio 2017 RC.

Expressões Throw

A classe Temporary na Figura 11 pode ser melhorada para incluir validação de parâmetros dentro dos membros de expressão incorporada. Dessa forma, posso atualizar o construtor para:

public TemporaryFile(string fileName) =>
  File = new FileInfo(filename ?? throw new ArgumentNullException());

Sem as expressões throw, o suporte do C# para membros de expressão incorporada não pode realizar nenhuma validação de parâmetro. Porém, com o suporte do C# 7.0 para throw como uma expressão, não apenas uma instrução, a informação de erros em linha dentro de expressões contidas maiores torna-se possível.

Cuidado: Esse recurso não foi implementado no Visual Studio 2017 RC.

Conclusão

Confesso que, quando comecei a escrever este artigo, achava que ele seria bem mais curto. Porém, conforme passei mais tempo programando e testando os recursos, descobri que havia muito mais no C# 7.0 do que pressupus ao ler os títulos dos recursos e ao acompanhar o desenvolvimento da linguagem. Em muitos casos — ao declarar variáveis, literais binários, expressões throw e assim por diante —, não há muita coisa envolvida no entendimento e uso dos recursos. Porém, em muitos casos, como no retorno por referência, desconstrutores e tuplas, por exemplo, é necessário muito mais para aprender o recurso do que o esperado inicialmente. Nesses casos, não se trata apenas da sintaxe, mas também é necessário saber quando o recurso é relevante.

O C# 7.0 continua a ser reduzido com a diminuição rápida da lista de idiossincrasias (identificadores out pré-declarados e a falta de expressões throw), ao mesmo tempo em que se alarga para incluir o suporte a recursos que, anteriormente, não eram vistos no nível da linguagem (tuplas e correspondência de padrões).

Esperamos que esta introdução ajude você a começar a programar em C# 7.0 imediatamente. Para obter mais informações sobre os desenvolvimentos do C# 7.0 após este artigo, visite meu blog em intellitect.com/csharp7, assim como a versão mais atualizada do meu livro, “Essential C# 7.0” (cujo lançamento é esperado logo depois que o Visual Studio 2017 seja enviado para produção).


Mark Michaelis é fundador da IntelliTect, onde atua como arquiteto técnico principal e instrutor. Há quase 20 anos trabalha como Microsoft MVP, e é Diretor Regional da Microsoft desde 2007. Michaelis atua em diversas equipes de análise de design de software da Microsoft, incluindo C#, Microsoft Azure, SharePoint e Visual Studio ALM. Ele dá palestras em conferências de desenvolvedores e escreveu diversos livros, incluindo o mais recente, “Essential C# 6.0 (5th Edition)” (itl.tc/EssentialCSharp). Você pode entrar em contato com ele pelo Facebook, em facebook.com/Mark.Michaelis, pelo seu blog IntelliTect.com/Mark, no Twitter: @markmichaelis ou pelo email mark@IntelliTect.com.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Kevin Bost (IntelliTect), Mads Torgersen (Microsoft) e Bill Wagner (Microsoft)


Discuta esse artigo no fórum do MSDN Magazine