Não gosto de C #, mas gosto de coletar todos os padrões e todo o açúcar que eles oferecem de versão para versão.
No terceiro dia, assisti ao discurso de Bill Wagner nas conferências da NDC , onde ele mostrou que você precisa escrever código de uma nova maneira (TM).
Ele mostra muitos exemplos de boa refatoração, o código se torna mais legível, mas a partir daquele momento percebi que a linguagem precisa de um arquiteto são.
Açúcar não vai ajudar em nada
Pegue um pedaço de código mal escrito escrito por um amador em seus joelhos. Este método verifica o estado da instância da classe e retorna verdadeiro se estiver ok e falso se não estiver.
internal bool GetAvailability()
{
if (_runspace.RunspacePoolAvailability == RunspacePoolAvailability.Available) { return true;}
if (_runspace.RunspacePoolAvailability == RunspacePoolAvailability.Busy) { return true;}
return false;
}
O programador tentou, nem mesmo uma única coisa no método. Mas somos experientes, vamos refatorá-lo, remover os ifs e transformá-lo em um ternark:
internal bool GetAvailability()
{
return _runspace.RunspacePoolAvailability == RunspacePoolAvailability.Available ||
_runspace.RunspacePoolAvailability == RunspacePoolAvailability.Busy;
}
Ficou muito melhor, 2 linhas de código em vez de 5, mas o ternark pode ser transformado em um padrão:
internal bool GetAvailability()
{
return _runspace.RunspacePoolAvailability is RunspacePoolAvailability.Available or RunspacePoolAvailability.Busy;
}
No total, deixamos uma bela linha de código. Tudo! A refatoração está completa! (não)
internal void Invoke()
{
if (!GetAvailability()) return;
PowerShell _powershell = PowerShell.Create();
_powershell.RunspacePool = _runspace;
_powershell.Invoke()
}
Chamar _powershell'a em um _runspace'e inacessível lançará uma exceção.
Todas as implementações são igualmente ruins porque ela resolve um problema - protege o código por trás de si mesmo para que não lance uma exceção, e sua aparência já é a décima coisa.
O código não estava atualizado, mas seu significado não mudou.
Mais exceções!
Quando um programa encontra a realidade, é claro, surgem situações excepcionais. Arquivo não encontrado, arquivo com formato errado, conteúdo errado, sem conteúdo, Streamreader leu nulo ou passou para uma string vazia. Duas linhas de código foram escritas e ambas quebradas, mas assisti à palestra e consegui enxergar.
“Mas se preocupe, agora, ao fazer sua própria classe ou biblioteca, você não precisa pensar no código defensivo, o compilador faz a verificação de tipo para nós e verificar se há null nunca foi tão fácil!
Basta jogar tudo para o usuário da biblioteca e escrever o código. Lançar exceções e colocar um programa agora se tornou prestigioso! Eu jogo e você pega! "
Que, como entendi a palestra de Bill Wagner - NDC Conferences 2020,
fiquei tão inspirado por este conceito, e pelo trabalho de .net em geral, então vou contar a vocês a verdadeira história do desenvolvimento das classes RunspacePool e Powershell do System .Management.Automation, que encontrei recentemente:
Smoker # 1 faz Powershell
Em primeiro lugar, é claro, para manter o controle do estado, criamos um campo booleano que muda para true quando o método Dispose é chamado.
Geralmente não é seguro mostrar o campo IsDisposed, porque se o CLR coletar lixo, você poderá capturar uma referência nula.
class PowershellInNutshell() : IDisposable
{
private static bool IsDisposed = false;
private static RunspacePoolInTheNuttshell;
public static void Invoke()
{
if (IsDisposed) throw new ObjectDisposedException();
Console.WriteLine("I was invoked");
}
public void Dispose()
{
if (IsDisposed) throw new ObjectDisposedException("Invoke"," , , ");
IsDisposed = true;
Console.WriteLine("I was invoked");
GC.SuppressFinalize(this);
}
}
Quando chamamos Dispose ou outro método novamente, lançamos uma exceção e deixamos o outro programador também monitorar o estado da instância ou capturar exceções com seu código, mas esses não são meus problemas.
Fumante nº 2 ganha RunspacePooll
Aqui também criamos o campo IsDisposed, mas desta vez o fazemos com um getter público, para que a pessoa que usa a biblioteca não precise escrever mais código de segurança.
class RunspacePoolInTheNuttshell() : IDisposable
{
public static bool IsDisposed = false;
public void Dispose()
{
if (IsDisposed) return;
IsDisposed = true;
GC.SuppressFinalize(this);
Console.WriteLine("I was invoked");
}
}
Se Dispose foi chamado, volte e pronto. Claro que ao acessar novamente o campo ele receberá nullref, pois o objeto já estará retirado da memória, mas isso é problema meu.
Uma pessoa saudável usa a biblioteca:
Aqui está uma exceção, aqui não há exceção, aqui embrulhamos o peixe. Ambas as classes vêm no mesmo pacote e têm comportamentos diferentes. As classes lançam o mesmo tipo de exceção por motivos diferentes.
- Senha incorreta? InvalidRunspacePoolStateException!
- Sem conexão? InvalidRunspacePoolStateException!
Acontece que em um lugar você precisa lidar com ObjectDisposedException, em outra NullReferenceException na terceira InvalidRunspacePoolStateException, e tudo está cheio de surpresas.
A exceção não é uma solução
Antes da comunhão das sagradas ordenanças, eu li o arquivo da maneira antiga:
public static void Main()
{
string txt = @"c:\temp\test.txt";
if (File.Exists(txt)) return;
string readText = File.ReadAllText(txt);
Console.WriteLine(readText);
}
Mas depois de assistir ao vídeo, comecei a fazer de uma nova maneira:
public static void Main()
{
string txt = @"c:\temp\test.txt";
try
{
string readText = File.ReadAllText(txt);
Console.WriteLine(readText);
}
catch (System.IO.FileNotFoundException)
{
Console.WriteLine("File was not found");
}
}
Ou é uma nova maneira?
public static void Main()
{
string txt = @"c:\temp\test.txt";
if (!File.Exists(txt))
{
throw new NullReferenceException();
}
string readText = File.ReadAllText(txt);
Console.WriteLine(readText);
}
Como exatamente é de uma nova maneira? Onde termina a responsabilidade do desenvolvedor e começa a responsabilidade do usuário?
Em geral, a ideia é clara, se você é o autor da biblioteca, pode imediatamente chamar o método e enviar dados incorretos para ele, você conhece a área de assunto perfeitamente e descreveu todos os casos de comportamento incorreto, descartou exceções que sentir e lidar com eles você mesmo.
internal class NewWay
{
public static string _a;
public static string _b;
public static string _c;
public static void NewWay(string a, string b, string c)
{
string _a = a ?? throw new NullReferenceException("a is null");
string _b = b ?? throw new NullReferenceException("b is null");
string _c = c ?? throw new NullReferenceException("c is null");
}
public void Print()
{
if (String.Compare(_a, _b) != 0)
{
throw new DataException("Some Other Ex");
}
Console.WriteLine($"{_a + _b + _c}");//
}
}
try
{
NewWay newway = new(stringThatCanBeNull, stringThatCanBeNull, stringThatCanBeNull);
newway.Print();
}
catch (NullReferenceException ex)
{
Console.WriteLine(" ");
}
catch (DataException ex)
{
Console.WriteLine(" ");
}
Os mais engenhosos já entenderam para onde estou levando. A organização da correção de erros com base em blocos try catch levará apenas a um aninhamento de código mais profundo.
Usando esse padrão, em qualquer caso, nos recusamos a executar o código, mas de forma mais educada.
Em geral, nada de novo, há 10 anos as pessoas começaram a suspeitar que o C # estava sobrecarregado de padrões e de ano para ano eles não diminuíam. Conheça outro, lançar exceções ficou mais fácil.
E finalmente - os operadores
Os operadores não devem lançar nada em qualquer lugar.
Um exemplo de JS que você provavelmente conhece:
console.log('2'+'2'-'2');
// 20
Os designers de JS consideraram que um operador de adição separado e um operador de concatenação separado não são necessários, portanto, fazer matemática em JS não é seguro.
A fonte desse bug em JS é a conversão implícita do tipo de string para o tipo int usando o operador. É assim que o excesso de açúcar se torna um bug.
C # também sofre de conversão implícita de tipo, embora com muito menos frequência. Tomemos, por exemplo, a entrada do usuário, que, após a atualização da biblioteca, passou a ser mapeada para string em vez de int, como antes, e o operador (+) e o operador matemático e o operador de concatenação.
Alterar o tipo de int para string não quebrou o código, mas quebrou a lógica de negócios.
Portanto, vou deixar aqui, e você tenta adivinhar o resultado da execução sem executar.
class Program
{
static void Main(string[] args)
{
Console.WriteLine($"{'2' + '2' - '2' }");
}
}