Nós usamos o .NET desde o seu início. Temos soluções escritas em todas as versões do framework em produção: desde o primeiro até o mais recente .NET Core 3.1.
A história do .NET, que temos acompanhado de perto todo esse tempo, está acontecendo diante de nossos olhos: a versão do .NET 5, que está planejada para ser lançada em novembro, acaba de ser lançada na forma de Release Candidate 2. Há muito que fomos avisados de que a quinta versão vai marcar época: ela vai acabar Esquizofrenia .NET, quando havia dois ramos do framework: clássico e core. Agora eles irão se fundir em êxtase, e haverá um .NET contínuo.
Lançado RC2você já pode começar a usá-lo totalmente - nenhuma nova mudança é esperada antes do lançamento, haverá apenas uma correção dos bugs encontrados. Além disso: RC2 já possui um site oficial dedicado ao .NET.
E apresentamos uma visão geral das inovações em .NET 5 e C # 9. Todas as informações com exemplos de código foram retiradas do blog oficial dos desenvolvedores da plataforma .NET (bem como de muitas outras fontes) e verificadas pessoalmente.
Novos tipos nativos e apenas novos
C # e .NET adicionaram tipos nativos simultaneamente:
- nint e nuint para C #
- seus System.IntPtr e System.UIntPtr correspondentes em BCL
O ponto para adicionar esses tipos são operações com APIs de baixo nível. E o truque é que o tamanho real desses tipos é determinado já em tempo de execução e depende da quantidade de bits do sistema: para os de 32 bits, seu tamanho será de 4 bytes, e para os de 64 bits, respectivamente, de 8 bytes.
Muito provavelmente você não encontrará esses tipos no trabalho real. Como, no entanto, com outro novo tipo: Half. Este tipo existe apenas em BCL, não há analogia para ele em C # ainda. É um tipo de 16 bits para valores de ponto flutuante. Pode ser útil para os casos em que a precisão infernal não é necessária e você pode ganhar um pouco de memória para armazenar valores, porque os tipos flutuam e duplos ocupam 4 e 8 bytes. O mais interessante é que, para esse tipo em geral, até agoraas operações aritméticas não são definidas e você não pode nem mesmo adicionar duas variáveis do tipo Half sem convertê-las explicitamente em float ou double. Ou seja, o propósito desse tipo agora é puramente utilitário - economizar espaço. No entanto, eles planejam adicionar aritmética a ele na próxima versão do .NET e C #. Em um ano.
Atributos para funções locais
Anteriormente, eles eram proibidos e isso criava alguns inconvenientes. Em particular, era impossível travar os atributos dos parâmetros das funções locais. Agora você pode definir atributos para eles, tanto para a própria função quanto para seus parâmetros. Por exemplo, assim:
#nullable enable
private static void Process(string?[] lines, string mark)
{
foreach (var line in lines)
{
if (IsValid(line))
{
// Processing logic...
}
}
bool IsValid([NotNullWhen(true)] string? line)
{
return !string.IsNullOrEmpty(line) && line.Length >= mark.Length;
}
}
Expressões lambda estáticas
O objetivo do recurso é fazer com que as expressões lambda não possam capturar nenhum contexto e variáveis locais que existam fora da própria expressão. Em geral, o fato de que eles podem capturar o contexto local muitas vezes é útil no desenvolvimento. Mas às vezes isso pode ser a causa de erros difíceis de detectar.
Para evitar esses erros, as expressões lambda agora podem ser marcadas com a palavra-chave estática. E, neste caso, eles perdem o acesso a qualquer contexto local: das variáveis locais a este e a base.
Aqui está um exemplo de uso bastante abrangente:
static void SomeFunc(Func<int, int> f)
{
Console.WriteLine(f(5));
}
static void Main(string[] args)
{
int y1 = 10;
const int y2 = 10;
SomeFunc(i => i + y1); // 15
SomeFunc(static i => i + y1); // : y1
SomeFunc(static i => i + y2); // 15
}
Observe que as constantes capturam lambdas estáticos muito bem.
GetEnumerator como método de extensão
Agora, o método GetEnumerator pode ser um método de extensão, que permitirá a você iterar por meio de foreach, mesmo que não pudesse ser iterado antes. Por exemplo - tuplas.
Aqui está um exemplo quando se torna possível iterar sobre ValueTuple por meio de foreach usando o método de extensão escrito para ele:
static class Program
{
public static IEnumerator<T> GetEnumerator<T>(this ValueTuple<T, T, T, T, T> source)
{
yield return source.Item1;
yield return source.Item2;
yield return source.Item3;
yield return source.Item4;
yield return source.Item5;
}
static void Main(string[] args)
{
foreach(var item in (1,2,3,4,5))
{
System.Console.WriteLine(item);
}
}
}
Este código imprime números de 1 a 5 no console.
Padrão de descarte em parâmetros de expressões lambda e funções anônimas
Micro-aperfeiçoamento. Caso você não precise de parâmetros em uma expressão lambda ou em uma função anônima, você pode substituí-los por um sublinhado, ignorando assim:
Func<int, int, int> someFunc1 = (_, _) => {return 5;};
Func<int, int, int> someFunc2 = (int _, int _) => {return 5;};
Func<int, int, int> someFunc3 = delegate (int _, int _) {return 5;};
Instruções de nível superior em C #
Esta é uma estrutura de código C # simplificada. Agora, escrever o código mais simples realmente parece simples:
using System;
Console.WriteLine("Hello World!");
E tudo vai compilar perfeitamente. Ou seja, agora você não precisa criar um método no qual a instrução de saída do console deve ser colocada, você não precisa descrever nenhuma classe na qual o método deve ser colocado e não há necessidade de definir o namespace no qual a classe deve ser criada.
By the way, no futuro, os desenvolvedores C # estão pensando em desenvolver um tópico com sintaxe simplificada e tentando se livrar do uso de System; em casos óbvios. Enquanto isso, você pode se livrar dele simplesmente escrevendo assim:
System.Console.WriteLine("Hello World!");
E realmente será um programa de trabalho de linha única.
Opções mais complexas podem ser usadas:
using System;
using System.Runtime.InteropServices;
Console.WriteLine("Hello World!");
FromWhom();
Show.Excitement("Top-level programs can be brief, and can grow as slowly or quickly in complexity as you'd like", 8);
void FromWhom()
{
Console.WriteLine($"From {RuntimeInformation.FrameworkDescription}");
}
internal class Show
{
internal static void Excitement(string message, int levelOf)
{
Console.Write(message);
for (int i = 0; i < levelOf; i++)
{
Console.Write("!");
}
Console.WriteLine();
}
}
Na realidade, o próprio compilador envolverá todo esse código nos namespaces e classes necessários, você simplesmente não saberá sobre isso.
Claro, esse recurso tem limitações. A principal delas é que isso só pode ser feito em um arquivo de projeto. Como regra, faz sentido fazer isso no arquivo onde você criou anteriormente o ponto de entrada para o programa na forma da função Main (string [] args). Ao mesmo tempo, a função principal em si não pode ser definida lá - esta é a segunda limitação. Na verdade, esse arquivo com uma sintaxe simplificada é a função Main, e ainda contém a variável args implicitamente, que é uma matriz com parâmetros. Ou seja, este código também compilará e exibirá o comprimento da matriz:
System.Console.WriteLine(args.Length);
Em geral, o recurso não é o mais importante, mas para fins de demonstração e treinamento é bastante adequado para si. Detalhes aqui .
Correspondência de padrões em uma declaração if
Imagine que você precise verificar se uma variável de objeto não é de um determinado tipo. Até agora, era necessário escrever assim:
if (!(vehicle is Car)) { ... }
Mas com C # 9.0, você pode escrever de forma humana:
if (vehicle is not Car) { ... }
Também foi possível registrar de forma compacta alguns cheques:
if (context is {IsReachable: true, Length: > 1 })
{
Console.WriteLine(context.Name);
}
Esta nova notação é equivalente à boa e velha assim:
if (context is object && context.IsReachable && context.Length > 1 )
{
Console.WriteLine(context.Name);
}
Ou você também pode escrever a mesma coisa de uma maneira relativamente nova (mas isso já é ontem):
if (context?.IsReachable && context?.Length > 1 )
{
Console.WriteLine(context.Name);
}
Na nova sintaxe, você também pode usar os operadores booleanos e, ou e não, mais parênteses para priorizar:
if (context is {Length: > 0 and (< 10 or 25) })
{
Console.WriteLine(context.Name);
}
E essas são apenas melhorias na correspondência de padrões em um if regular. O que adicionamos à correspondência de padrões para a expressão switch - continue a ler.
Melhor correspondência de padrões na expressão de switch
A expressão switch (não deve ser confundida com a instrução switch) tem grandes melhorias na correspondência de padrões. Vejamos exemplos da documentação oficial . Os exemplos são dedicados ao cálculo da tarifa de um determinado transporte em um determinado horário. Aqui está o primeiro exemplo:
public decimal CalculateToll(object vehicle) =>
vehicle switch
{
Car c => 2.00m,
Taxi t => 3.50m,
Bus b => 5.00m,
DeliveryTruck t => 10.00m,
{ } => throw new ArgumentException("Unknown vehicle type", nameof(vehicle)),
null => throw new ArgumentNullException(nameof(vehicle))
};
As duas últimas linhas da instrução switch são novas. Os colchetes representam qualquer objeto que não seja nulo. E agora você pode usar a palavra-chave correspondente para corresponder a nulo.
Isso não é tudo. Observe que, para cada mapeamento para um objeto, você deve criar uma variável: c para carro, t para táxi e assim por diante. Mas essas variáveis não são usadas. Nesses casos, você já pode usar o padrão de descarte em C # 8.0:
public decimal CalculateToll(object vehicle) =>
vehicle switch
{
Car _ => 2.00m,
Taxi _ => 3.50m,
Bus _ => 5.00m,
DeliveryTruck _ => 10.00m,
// ...
};
Mas começando com a nona versão do C #, você não pode escrever nada nesses casos:
public decimal CalculateToll(object vehicle) =>
vehicle switch
{
Car => 2.00m,
Taxi => 3.50m,
Bus => 5.00m,
DeliveryTruck => 10.00m,
// ...
};
As melhorias na expressão switch não param por aí. Agora é mais fácil escrever expressões mais complexas. Por exemplo, muitas vezes o resultado retornado deve depender dos valores de propriedade do objeto passado. Agora, isso pode ser escrito de forma mais curta e conveniente do que uma combinação de if:
public static decimal CalculateToll(object vehicle) =>
vehicle switch
{
Car { Passengers: 0 } => 2.00m + 0.50m,
Car { Passengers: 1 } => 2.0m,
Car { Passengers: 2 } => 2.0m - 0.50m,
Car => 2.00m - 1.0m,
// ...
};
Preste atenção nas três primeiras linhas do switch: na verdade, o valor da propriedade Passengers é verificado e, em caso de igualdade, o resultado correspondente é retornado. Se não houver correspondência, o valor da variante geral será retornado (a quarta linha dentro do switch). A propósito, os valores das propriedades são verificados apenas se o objeto do veículo passado não for nulo e for uma instância da classe Car. Ou seja, você não deve ter medo da Exceção de Referência Nula ao verificar.
Mas isso não é tudo. Agora, na expressão switch, você pode até escrever expressões para uma correspondência mais conveniente:
public static decimal CalculateToll(object vehicle) =>
vehicle switch
{
Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
Bus => 5.00m,
DeliveryTruck t when (t.GrossWeightClass >= 5000) => 10.00m + 5.00m,
DeliveryTruck t when (t.GrossWeightClass >= 3000 && t.GrossWeightClass < 5000) => 10.00m,
DeliveryTruck => 8.00m,
// ...
};
E isso não é tudo. A sintaxe da expressão switch foi estendida para expressões switch aninhadas para facilitar ainda mais a descrição de condições complexas:
public static decimal CalculateToll(object vehicle) =>
vehicle switch
{
Car c => c.Passengers switch
{
0 => 2.00m + 0.5m,
1 => 2.0m,
2 => 2.0m - 0.5m,
_ => 2.00m - 1.0m
},
// ...
};
Como resultado, se você colar completamente todos os exemplos de código já fornecidos, terá esta imagem com todas as inovações descritas de uma vez:
public static decimal CalculateToll(object vehicle) =>
vehicle switch
{
Car c => c.Passengers switch
{
0 => 2.00m + 0.5m,
1 => 2.0m,
2 => 2.0m - 0.5m,
_ => 2.00m - 1.0m
},
Taxi t => t.Fares switch
{
0 => 3.50m + 1.00m,
1 => 3.50m,
2 => 3.50m - 0.50m,
_ => 3.50m - 1.00m
},
Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
Bus => 5.00m,
DeliveryTruck t when (t.GrossWeightClass >= 5000) => 10.00m + 5.00m,
DeliveryTruck t when (t.GrossWeightClass >= 3000 && t.GrossWeightClass < 5000) => 10.00m,
DeliveryTruck => 8.00m,
null => throw new ArgumentNullException(nameof(vehicle)),
_ => throw new ArgumentException(nameof(vehicle))
};
Mas isso também não é tudo. Aqui está outro exemplo: uma função comum que usa o mecanismo de expressão switch para determinar a carga com base no tempo passado: hora do rush da manhã / noite, períodos do dia e da noite:
private enum TimeBand
{
MorningRush,
Daytime,
EveningRush,
Overnight
}
private static TimeBand GetTimeBand(DateTime timeOfToll) =>
timeOfToll.Hour switch
{
< 6 or > 19 => TimeBand.Overnight,
< 10 => TimeBand.MorningRush,
< 16 => TimeBand.Daytime,
_ => TimeBand.EveningRush,
};
Como você pode ver, no C # 9.0 também é possível usar os operadores de comparação <,>, <=,> =, bem como os operadores lógicos e, ou e não, na correspondência.
Mas isso, droga, não é o fim. Agora você pode usar ... tuplas na expressão switch. Aqui está um exemplo completo de código que calcula um certo coeficiente para a tarifa, dependendo do dia da semana, hora do dia e direção da viagem (de / para a cidade):
private enum TimeBand
{
MorningRush,
Daytime,
EveningRush,
Overnight
}
private static bool IsWeekDay(DateTime timeOfToll) =>
timeOfToll.DayOfWeek switch
{
DayOfWeek.Saturday => false,
DayOfWeek.Sunday => false,
_ => true
};
private static TimeBand GetTimeBand(DateTime timeOfToll) =>
timeOfToll.Hour switch
{
< 6 or > 19 => TimeBand.Overnight,
< 10 => TimeBand.MorningRush,
< 16 => TimeBand.Daytime,
_ => TimeBand.EveningRush,
};
public static decimal PeakTimePremiumFull(DateTime timeOfToll, bool inbound) =>
(IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
{
(true, TimeBand.MorningRush, true) => 2.00m,
(true, TimeBand.MorningRush, false) => 1.00m,
(true, TimeBand.Daytime, true) => 1.50m,
(true, TimeBand.Daytime, false) => 1.50m,
(true, TimeBand.EveningRush, true) => 1.00m,
(true, TimeBand.EveningRush, false) => 2.00m,
(true, TimeBand.Overnight, true) => 0.75m,
(true, TimeBand.Overnight, false) => 0.75m,
(false, TimeBand.MorningRush, true) => 1.00m,
(false, TimeBand.MorningRush, false) => 1.00m,
(false, TimeBand.Daytime, true) => 1.00m,
(false, TimeBand.Daytime, false) => 1.00m,
(false, TimeBand.EveningRush, true) => 1.00m,
(false, TimeBand.EveningRush, false) => 1.00m,
(false, TimeBand.Overnight, true) => 1.00m,
(false, TimeBand.Overnight, false) => 1.00m,
};
O método PeakTimePremiumFull usa tuplas para correspondência, e isso se tornou possível na nova versão do C # 9.0. A propósito, se você olhar atentamente para o código, então duas otimizações se sugerirão:
- as últimas oito linhas retornam o mesmo valor;
- o tráfego diurno e noturno têm o mesmo coeficiente.
Como resultado, o código do método pode ser bastante reduzido usando o padrão de descarte:
public static decimal PeakTimePremiumFull(DateTime timeOfToll, bool inbound) =>
(IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
{
(true, TimeBand.MorningRush, true) => 2.00m,
(true, TimeBand.MorningRush, false) => 1.00m,
(true, TimeBand.Daytime, _) => 1.50m,
(true, TimeBand.EveningRush, true) => 1.00m,
(true, TimeBand.EveningRush, false) => 2.00m,
(true, TimeBand.Overnight, _) => 0.75m,
(false, _, _) => 1.00m,
};
Bem, se você olhar ainda mais de perto, você pode reduzir esta opção, retirando o coeficiente 1,0 no caso geral:
public static decimal PeakTimePremiumFull(DateTime timeOfToll, bool inbound) =>
(IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
{
(true, TimeBand.Overnight, _) => 0.75m,
(true, TimeBand.Daytime, _) => 1.5m,
(true, TimeBand.MorningRush, true) => 2.0m,
(true, TimeBand.EveningRush, false) => 2.0m,
_ => 1.0m,
};
Por precaução, deixe-me esclarecer: as comparações são feitas na ordem em que são listadas. Na primeira correspondência, o valor correspondente é retornado e nenhuma outra comparação é feita.
Atualizar
tuplas na expressão de switch também pode ser usado em C # 8.0. O desenvolvedor inútil que escreveu este artigo ficou um pouco mais esperto.
E, finalmente, aqui está outro exemplo louco que demonstra a nova sintaxe para correspondência com tuplas e propriedades do objeto:
public static bool IsAccessOkOfficial(Person user, Content content, int season) =>
(user, content, season) switch
{
({Type: Child}, {Type: ChildsPlay}, _) => true,
({Type: Child}, _, _) => false,
(_ , {Type: Public}, _) => true,
({Type: Monarch}, {Type: ForHerEyesOnly}, _) => true,
(OpenCaseFile f, {Type: ChildsPlay}, 4) when f.Name == "Sherlock Holmes" => true,
{Item1: OpenCaseFile {Type: var type}, Item2: {Name: var name}}
when type == PoorlyDefined && name.Contains("Sherrinford") && season >= 3 => true,
(OpenCaseFile, var c, 4) when c.Name.Contains("Sherrinford") => true,
(OpenCaseFile {RiskLevel: >50 and <100 }, {Type: StateSecret}, 3) => true,
_ => false,
};
Tudo isso parece bastante incomum. Para um entendimento completo, recomendo que você olhe no código - fonte , há um exemplo completo do código.
Nova digitação de destino, bem como basicamente aprimorada
Muito tempo atrás, em C #, tornou-se possível escrever var em vez de um nome de tipo, porque o próprio tipo poderia ser determinado a partir do contexto (na verdade, isso é chamado de tipagem de destino). Ou seja, em vez da seguinte entrada:
SomeLongNamedType variable = new SomeLongNamedType();
tornou-se possível escrever de forma mais compacta:
var variable = new SomeLongNamedType()
E o compilador irá adivinhar o próprio tipo de variável. Ao longo dos anos, a sintaxe reversa foi implementada:
SomeLongNamedType variable = new ();
Agradecimento especial pelo fato de que esta sintaxe funciona não apenas ao declarar uma variável, mas também em muitos outros casos onde o compilador pode adivinhar imediatamente o tipo. Por exemplo, ao passar parâmetros para um método e retornar um valor do método:
var result = SomeMethod(new (2020,10,01));
//...
public Car SomeMethod(DateTime p)
{
//...
return new() { Passengers = 2 };
}
Neste exemplo, ao chamar SomeMethod, o parâmetro do tipo DateTime é criado usando a sintaxe abreviada. O valor retornado do método é criado da mesma maneira.
Onde realmente haverá um benefício para esta sintaxe é ao definir coleções:
List<DateTime> datesList = new()
{
new(2020, 10, 01),
new(2020, 10, 02),
new(2020, 10, 03),
new(2020, 10, 04),
new(2020, 10, 05)
};
Car[] cars =
{
new() {Passengers = 2},
new() {Passengers = 3},
new() {Passengers = 4}
};
A ausência da necessidade de escrever o nome completo do tipo ao listar os elementos da coleção torna o código um pouco mais limpo.
Operadores de destino digitados ?? e?:
O operador ternário ?: foi aprimorado no C # 9.0. Anteriormente, ele exigia conformidade total dos tipos de retorno, mas agora é mais inteligente. Aqui está um exemplo de uma expressão que era inválida nas versões anteriores da linguagem, mas bastante legal na nona:
int? result = b ? 0 : null; // nullable value type
Anteriormente, era necessário converter explicitamente de zero para int? .. Agora, não é necessário.
Além disso, na nova versão da linguagem, é permitido usar a seguinte construção:
Person person = student ?? customer; // Shared base type
Os tipos de cliente e aluno, embora derivados de Pessoa, são tecnicamente diferentes. A versão anterior da linguagem não permitia o uso de tal construção sem conversão de tipo explícita. Agora, o compilador entende perfeitamente bem o que isso significa.
Substituindo o tipo de retorno de métodos
No C # 9.0, era permitido substituir o tipo de retorno dos métodos substituídos. Há apenas um requisito: o novo tipo deve ser herdado do original (covariante). Aqui está um exemplo:
abstract class Animal
{
public abstract Food GetFood();
...
}
class Tiger : Animal
{
public override Meat GetFood() => ...;
}
Na classe Tiger, o valor de retorno do método GetFood foi redefinido de Food to Meat. Agora está tudo bem se a carne for derivada de alimentos.
propriedades init não são realmente membros somente leitura
Um recurso interessante apareceu na nova versão da linguagem: init-properties. Essas são propriedades que só podem ser definidas durante a inicialização inicial do objeto. Parece que existem apenas membros da classe para isso, mas na verdade são coisas diferentes que permitem que você resolva problemas diferentes. Para entender a diferença e a beleza das propriedades init, aqui está um exemplo:
Person employee = new () {
Name = "Paul McCartney",
Company = "High Technologies Center",
CompanyAddress = new () {
Country = "Russia",
City = "Izhevsk",
Line1 = "246, Karl Marx St."
}
}
Esta sintaxe para declarar uma instância de classe é muito conveniente, especialmente quando há mais objetos entre as propriedades da classe. Mas essa sintaxe tem limitações: as propriedades da classe correspondente devem ser mutáveis . Isso ocorre porque a inicialização dessas propriedades ocorre após a chamada para o construtor. Ou seja, a classe Person do exemplo deve ser declarada assim:
class Person {
//...
public string Name {get; set;}
public string Company {get; set;}
public Address CompanyAddress {get; set;}
//...
}
No entanto, na verdade, a propriedade Name é imutável. Atualmente, a única maneira de tornar essa propriedade somente leitura é declarar um setter privado:
class Person {
//...
public string Name {get; private set;}
//...
}
Mas, neste caso, perdemos imediatamente a capacidade de usar a sintaxe conveniente para declarar uma instância de classe, atribuindo valores às propriedades entre chaves. E podemos definir o valor da propriedade Name apenas passando-o em parâmetros para o construtor da classe. Agora imagine que a propriedade CompanyAddress também seja imutável em significado. Em geral, me encontrei muitas vezes nessa situação, e sempre tive que escolher entre dois males:
- construtores sofisticados com um monte de parâmetros, mas todas as propriedades da classe somente leitura;
- sintaxe conveniente para criar um objeto, mas todas as propriedades da classe de leitura e gravação, e devo me lembrar disso e não alterá-las acidentalmente em algum lugar.
Neste ponto, alguém pode se lembrar dos membros da classe somente leitura e sugerir estilizar a classe Person desta forma:
class Person {
//...
public readonly string Name;
public readonly string Company;
public readonly string CompanyAddress;
//...
}
Ao que respondo que este método não está apenas de acordo com o Feng Shui, mas também não resolve o problema da inicialização conveniente: membros somente leitura também podem ser definidos no construtor, como propriedades com um configurador privado.
Mas no C # 9.0 este problema é resolvido: se você definir uma propriedade como uma propriedade init, você obtém uma sintaxe conveniente para criar um objeto e uma propriedade que é realmente imutável no futuro:
class Person {
public string Name { get; init; }
public string Company { get; init; }
public Address CompanyAddress { get; init; }
}
A propósito, em init-properties, como no construtor, você pode inicializar membros de classe somente leitura e pode escrever assim:
public class Person
{
private readonly string name;
public string Name
{
get => name;
init => name = (value ?? throw new ArgumentNullException(nameof(Name)));
}
}
O registro é um DTO's legalizado
Dando continuidade ao tópico das propriedades imutáveis, chegamos à principal, na minha opinião, inovação da linguagem: o tipo de registro. Esse tipo é projetado para criar convenientemente estruturas imutáveis inteiras, não apenas propriedades. A razão para o surgimento de um tipo separado é simples: trabalhando de acordo com todos os cânones, criamos constantemente DTOs para isolar diferentes camadas do aplicativo. DTOs geralmente são apenas uma coleção de campos, sem qualquer lógica de negócios. E, como regra, os valores desses campos não mudam durante o tempo de vida desses DTOs.
.
DTO – Data Transfer Object. (DAL, BL, PL) - . «». -DTO' DAL BL, , DTO-, , DTO-, - ( HTML- JSON-).
— DTO-, - -, .
DTO- - . DTO-, AutoMapper - .
, DTO- .
Então, depois de muitos, muitos anos, os desenvolvedores C # finalmente chegaram ao aprimoramento realmente necessário: eles legalizaram os modelos DTO como um tipo de registro separado.
Até agora, todos os modelos DTO que criamos (e os criamos em grandes quantidades) eram classes comuns. Para o compilador e para o tempo de execução, eles não eram diferentes de todas as outras classes, embora não fossem no sentido clássico. Poucas pessoas usaram estruturas para modelos DTO - isso nem sempre era aceitável por vários motivos.
Agora podemos definir registro (doravante denominado registro) - uma estrutura especial projetada para criar modelos DTO imutáveis. A gravação ocupa um lugar intermediário entre estruturas e classes em seu sentido usual. É uma subclasse e superestrutura. Um registro ainda é um tipo de referência com todas as consequências decorrentes. Os registros quase sempre se comportam como uma classe regular, eles podem conter métodos, eles permitem a herança (mas apenas de outros registros, não de objetos, embora se o registro não herde explicitamente de nada, então ele herda do objeto tão implicitamente quanto tudo em C # ) pode implementar interfaces. Além disso, você não precisa fazer registros completamente imutáveis. E onde, então, está o significado e qual é a diferença?
Vamos apenas criar uma entrada:
public record Person
{
public string LastName { get; }
public string FirstName { get; }
public Person(string first, string last) => (FirstName, LastName) = (first, last);
}
Agora, aqui está um exemplo de como usá-lo:
Person p1 = new ("Paul", "McCartney");
Person p2 = new ("Paul", "McCartney");
System.Console.WriteLine(p1 == p2);
Este exemplo imprimirá true para o console. Se Person fosse uma classe, false seria impresso no console porque os objetos são comparados por referência: duas variáveis de referência são iguais apenas se elas se referem ao mesmo objeto. Mas não é o caso das gravações. Os registros são comparados pelo valor de todos os seus campos, incluindo os privados.
Continuando com o exemplo anterior, vamos examinar este código:
System.Console.WriteLine(p1);
No caso de uma classe, receberíamos o nome completo da classe no console. Mas, no caso de registros, veremos isso no console:
Person { LastName = McCartney, FirstName = Paul}
O fato é que, para registros, o método ToString () é substituído implicitamente e exibe não o nome do tipo, mas uma lista completa de campos públicos com valores. Da mesma forma, para registros, os operadores == e! = São redefinidos implicitamente, o que torna possível alterar a lógica de comparação.
Vamos brincar com a herança de registros:
public record Teacher : Person
{
public string Subject { get; }
public Teacher(string first, string last, string sub)
: base(first, last) => Subject = sub;
}
Agora vamos criar duas postagens de tipos diferentes e compará-las:
Person p = new("Paul", "McCartney");
Teacher t = new("Paul", "McCartney", "Programming");
System.Console.WriteLine(p == t);
Embora o registro do Professor seja herdado de Person, as variáveis p e t não serão iguais, false será impresso no console. Isso ocorre porque a comparação é feita não apenas para todos os campos de registros, mas também para tipos, e os tipos aqui são claramente diferentes.
E embora a comparação de tipos de registro herdados seja permitida (mas inútil), a comparação de diferentes tipos de registro em geral não é permitida em princípio:
public record Person
{
public string LastName { get; }
public string FirstName { get; }
public Person(string first, string last) => (FirstName, LastName) = (first, last);
}
public record Person2
{
public string LastName { get; }
public string FirstName { get; }
public Person2(string first, string last) => (FirstName, LastName) = (first, last);
}
// ...
Person p = new("Paul", "McCartney");
Person2 p2 = new("Paul", "McCartney");
System.Console.WriteLine(p == p2); //
As entradas parecem iguais, mas haverá um erro de compilação na última linha. Você só pode comparar registros do mesmo tipo ou tipos herdados.
Outro recurso interessante dos registros é a palavra-chave with, que facilita a criação de modificações em seus modelos de DTO. Dê uma olhada em um exemplo:
Person me = new("Steve", "Brown");
Person brother = me with { FirstName = "Paul" };
Neste exemplo, para o registro irmão, os valores de todos os campos serão preenchidos a partir do registro me, exceto para o campo FirstName - ele será alterado para Paul.
Até agora, você viu a maneira clássica de criar registros - com definições completas de construtores, propriedades e assim por diante. Mas agora também há uma forma lacônica:
public record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName,
string Subject)
: Person(FirstName, LastName);
public sealed record Student(string FirstName,
string LastName, int Level)
: Person(FirstName, LastName);
Você pode definir os registros de forma abreviada e o compilador criará as propriedades e o construtor para você. No entanto, esse recurso tem um recurso adicional - você não pode apenas usar uma notação abreviada para definir propriedades e um construtor, mas, ao mesmo tempo, pode adicionar seu próprio método à entrada:
public record Pet(string Name)
{
public void ShredTheFurniture() =>
Console.WriteLine("Shredding furniture");
}
public record Dog(string Name) : Pet(Name)
{
public void WagTail() =>
Console.WriteLine("It's tail wagging time");
public override string ToString()
{
StringBuilder s = new();
base.PrintMembers(s);
return $"{s.ToString()} is a dog";
}
}
Nesse caso, as propriedades e o construtor dos registros também serão criados automaticamente. Cada vez menos código clichê, mas aplicável apenas a postagens. Isso não funciona para classes e estruturas.
Além de tudo o que já foi dito, o compilador também pode criar automaticamente um desconstrutor de registros:
var person = new Person("Bill", "Wagner");
var (first, last) = person; //
Console.WriteLine(first);
Console.WriteLine(last);
No entanto, no nível de IL, os registros ainda são uma classe. No entanto, há uma suspeita para a qual nenhuma confirmação ainda foi encontrada: com certeza, no nível de tempo de execução, os registros serão otimizados em algum lugar. Muito provavelmente, devido ao fato de que será sabido de antemão que um determinado registro é imutável. Isso abre oportunidades de otimização, pelo menos em um ambiente multi-threaded, e o desenvolvedor nem mesmo precisa colocar esforços especiais para isso.
Nesse ínterim, estamos reescrevendo todos os modelos DTO de classes a registros.
Geradores de fonte .NET
Source Generator (doravante referido simplesmente como um gerador) é um recurso bastante interessante. Um gerador é um trecho de código executado na fase de compilação, tem a capacidade de analisar o código já compilado e pode gerar código adicional que também será compilado. Se não estiver totalmente claro, então aqui está um exemplo bastante relevante quando um gerador pode estar em demanda.
Imagine um aplicativo da Web C # / .net que você escreve no ASP.NET Core. Quando você inicia este aplicativo, há uma grande quantidade de trabalho de fundo de inicialização para analisar do que este aplicativo é feito e o que ele deve fazer. A reflexão é usada freneticamente. Como resultado, o tempo desde o lançamento do aplicativo até o início do processamento da primeira solicitação pode ser obscenamente longo, o que é inaceitável em serviços de alta carga. O gerador pode ajudar a reduzir esse tempo: mesmo na fase de compilação, ele pode analisar sua aplicação já compilada e, adicionalmente, gerar o código necessário que irá inicializá-la muito mais rápido na inicialização.
Também há um número bastante grande de bibliotecas que usam reflexão para determinar em tempo de execução os tipos de objetos usados (entre eles, há muitos pacotes Nuget principais). Isso abre um amplo escopo para otimização usando geradores, e os autores deste recurso estão esperando melhorias apropriadas dos desenvolvedores da biblioteca.
Geradores de código são um tópico novo e muito incomum para caber no escopo deste post. Além disso, você pode ver um exemplo do mais simples "Olá, mundo!" gerador nesta revisão .
Existem dois novos recursos associados aos geradores de código, que são descritos a seguir.
Métodos parciais
As classes parciais em C # já existem há muito tempo, seu propósito original é separar o código gerado por um certo designer do código escrito pelo programador. Métodos parciais foram adaptados em C # 9.0. Eles se parecem com isto:
public partial class MyClass
{
public partial int DoSomeWork(out string p);
}
public partial class MyClass
{
public partial int DoSomeWork(out string p)
{
p = "test";
System.Console.WriteLine("Partial method");
return 5;
}
}
Este exemplo substituto demonstra que os métodos parciais não são essencialmente diferentes dos comuns: eles podem retornar valores, podem aceitar variáveis de saída e podem ter modificadores de acesso.
A partir das informações disponíveis, os métodos parciais estarão intimamente relacionados aos geradores de código, onde devem ser usados.
Inicializadores de módulo
Existem três razões para a introdução desta funcionalidade:
- Permitir que as bibliotecas tenham alguma forma de inicialização única na inicialização com sobrecarga mínima e sem necessidade explícita de o usuário chamar algo;
- a funcionalidade existente de construtores estáticos não é muito adequada para essa função, porque o tempo de execução deve primeiro descobrir se uma classe com um construtor estático é usada (essas são as regras), e isso dá atrasos mensuráveis;
- os geradores de código devem ter algum tipo de lógica de inicialização que não precisa ser chamada explicitamente.
Na verdade, o último ponto parece ter se tornado decisivo para que o recurso fosse incluído no lançamento. Como resultado, criamos um novo atributo de que precisamos para revestir o método que é a inicialização:
using System.Runtime.CompilerServices;
class C
{
[ModuleInitializer]
internal static void M1()
{
// ...
}
}
Existem algumas restrições ao método:
- deve ser estático;
- não deve ter parâmetros;
- não deve retornar nada;
- não deve funcionar com genéricos;
- deve ser acessível a partir do módulo que o contém, ou seja:
- deve ser interno ou público
- não precisa ser um método local
E funciona assim: assim que o compilador encontra todos os métodos marcados com o atributo ModuleInitializer, ele gera um código especial que chama todos eles. A ordem de invocação dos métodos inicializadores não pode ser especificada, mas será a mesma em cada compilação.
Conclusão
Já tendo publicado o post, percebemos que ele é mais voltado para as notícias na linguagem C # 9.0 do que para as notícias do próprio .NET. Mas acabou bem.