C # divertido. Cinco exemplos de coffee breaks

Depois de escrever mais de um artigo sobre a Veeam Academy , decidimos abrir uma pequena cozinha interna e oferecer alguns exemplos em C # que estamos analisando com nossos alunos. Ao compilá-los, fomos guiados pelo fato de que nosso público é desenvolvedor iniciante, mas também pode ser interessante para programadores experientes olharem sob o gato. Nosso objetivo é mostrar a profundidade da toca do coelho e, ao mesmo tempo, explicar os recursos da estrutura interna do C #.

Por outro lado, ficaremos felizes em ouvir comentários de colegas experientes que apontarão falhas em nossos exemplos ou compartilharão os seus. Eles gostam de usar essas perguntas em entrevistas, por isso, com certeza, todos temos algo a dizer.

Esperamos que nossa seleção seja útil para você, ajude a atualizar seu conhecimento ou apenas sorria.

imagem

Exemplo 1


Estruturas em C #. Com eles, mesmo desenvolvedores experientes costumam ter perguntas, que costumam ser usadas por todos os tipos de testes online.

Nosso primeiro exemplo é um exemplo de atenção plena e conhecimento do que o bloco usando se expande. E também um tópico bastante para comunicação durante a entrevista.

Considere o código:

public struct SDummy : IDisposable { private bool _dispose; public void Dispose() { _dispose = true; } public bool GetDispose() { return _dispose; } static void Main(string[] args) { var d = new SDummy(); using (d) { Console.WriteLine(d.GetDispose()); } Console.WriteLine(d.GetDispose()); } } 

O que o método Main imprimirá no console?
Observe que o SDummy é uma estrutura que implementa a interface IDisposable, para que variáveis ​​do tipo SDummy possam ser usadas no bloco using.

De acordo com a especificação da linguagem C #, o uso da instrução para tipos significativos em tempo de compilação se expande para um bloco try-finally:

  try { Console.WriteLine(d.GetDispose()); } finally { ((IDisposable)d).Dispose(); } 

Portanto, em nosso código, o método GetDispose () é chamado dentro do bloco using, que retorna o campo booleano _dispose, cujo valor ainda não foi definido para o objeto d (ele é definido apenas no método Dispose (), que ainda não foi chamado) e, portanto, o valor é retornado O padrão é Falso. O que vem a seguir?

E então o mais interessante.
Executando uma linha em um bloco finalmente
  ((IDisposable)d).Dispose(); 

normalmente leva ao boxe. Não é difícil ver, por exemplo, aqui (no canto superior direito em Resultados, primeiro selecione C # e, em seguida, IL):

imagem

Nesse caso, o método Dispose já é chamado para outro objeto, e não para o objeto d.
Execute o nosso programa e veja se o programa realmente exibe "False False" no console. Mas é assim tão simples? :)

De fato, NENHUMA EMBALAGEM ESTÁ ACONTECENDO. De acordo com Eric Lippert, o que é feito para fins de otimização (veja aqui e aqui ).
Mas, se não houver embalagem (o que por si só pode parecer surpreendente), por que “Falso Falso” e não “Falso Verdadeiro” na tela, porque Dispose agora deve ser aplicado ao mesmo objeto?!?

E aqui não para isso!
Dê uma olhada no que o compilador C # expande nosso programa para:

 public struct SDummy : IDisposable { private bool _dispose; public void Dispose() { _dispose = true; } public bool GetDispose() { return _dispose; } private static void Main(string[] args) { SDummy sDummy = default(SDummy); SDummy sDummy2 = sDummy; try { Console.WriteLine(sDummy.GetDispose()); } finally { ((IDisposable)sDummy2).Dispose(); } Console.WriteLine(sDummy.GetDispose()); } } 


Há uma nova variável sDummy2, à qual o método Dispose () é aplicado!
De onde veio essa variável oculta?
Vamos voltar às especificações novamente :
Uma instrução using do formulário 'using (expression) statement' tem as mesmas três expansões possíveis. Nesse caso, ResourceType é implicitamente o tipo de expressão em tempo de compilação ... A variável 'resource' está inacessível e invisível para a instrução incorporada.

T.O. a variável sDummy é invisível e inacessível à instrução incorporada do bloco using, e todas as operações nessa expressão são executadas com outra variável sDummy2.

Como resultado, o método Main gera no console “False False”, e não “False True”, como muitos dos que encontraram esse exemplo pela primeira vez acreditam. Nesse caso, lembre-se de que não há embalagem, mas uma variável oculta adicional é criada.

A conclusão geral é a seguinte: tipos de valores mutáveis ​​são maus e devem ser evitados.

Um exemplo semelhante é considerado aqui . Se o tópico for interessante, recomendamos uma espiada.

Gostaria de agradecer especialmente a SergeyT pelos comentários valiosos sobre este exemplo.



Exemplo 2


Os construtores e a sequência de suas chamadas são um dos principais tópicos de qualquer linguagem de programação orientada a objetos. Às vezes, essa sequência de chamadas pode surpreender e, pior ainda, "preencher" o programa no momento mais inesperado.

Portanto, considere a classe MyLogger:

 class MyLogger { static MyLogger innerInstance = new MyLogger(); static MyLogger() { Console.WriteLine("Static Logger Constructor"); } private MyLogger() { Console.WriteLine("Instance Logger Constructor"); } public static MyLogger Instance { get { return innerInstance; } } } 

Suponha que essa classe tenha alguma lógica comercial que precisamos oferecer suporte ao log (a funcionalidade não é tão importante no momento).

Vamos ver o que está em nossa classe MyLogger:

  1. Construtor estático especificado
  2. Existe um construtor privado sem parâmetros
  3. Variável estática fechada innerInstance definida
  4. E existe uma propriedade estática aberta da Instância para comunicação com o mundo externo

Para facilitar a análise deste exemplo, adicionamos uma saída simples do console aos construtores da classe.

Fora da classe (sem usar truques como reflexão), só podemos usar a propriedade Instance pública estática, que podemos chamar assim:

 class Program { public static void Main() { var logger = MyLogger.Instance; } } 

O que esse programa produzirá?
Todos sabemos que um construtor estático é chamado antes de acessar qualquer membro da classe (com exceção das constantes). Nesse caso, ele é iniciado apenas uma vez no domínio do aplicativo.

No nosso caso, recorremos ao membro da classe - a propriedade Instance, que deve fazer com que o construtor estático seja iniciado primeiro e, em seguida, o construtor da instância da classe será chamado. I.e. o programa produzirá:

Construtor de registrador estático
Construtor de Registradores de Instâncias


No entanto, depois de iniciar o programa, entramos no console:

Construtor de Registradores de Instâncias
Construtor de registrador estático


Como assim? O construtor de instância trabalhou antes do construtor estático?!?
Resposta: Sim!

E aqui está o porquê.

O padrão C # ECMA-334 declara o seguinte para classes estáticas:

17.4.5.1: “Se um construtor estático (§17.11) existe na classe, a execução dos inicializadores de campo estático ocorre imediatamente antes da execução desse construtor estático.
...
17.11: ... Se uma classe contiver algum campo estático com inicializadores, esses inicializadores serão executados em ordem textual imediatamente antes da execução do construtor estático

(O que, em uma tradução livre, significa: se houver um construtor estático na classe, a inicialização dos campos estáticos será iniciada imediatamente ANTES do início do construtor estático.
...
Se a classe contiver algum campo estático com inicializadores, esses inicializadores serão iniciados na ordem no texto do programa ANTES de o construtor estático ser executado.)

No nosso caso, o campo estático innerInstance é declarado junto com o inicializador, que é o construtor da instância da classe. De acordo com o padrão ECMA, o inicializador deve ser chamado ANTES de chamar o construtor estático. O que acontece em nosso programa: o construtor de instância, sendo o inicializador do campo estático, é chamado ANTES do construtor estático. Concordo, inesperadamente.

Observe que isso é verdadeiro apenas para inicializadores de campo estático. Em geral, um construtor estático é chamado ANTES de chamar o construtor da instância da classe.

Como, por exemplo, aqui:

 class MyLogger { static MyLogger() { Console.WriteLine("Static Logger Constructor"); } public MyLogger() { Console.WriteLine("Instance Logger Constructor"); } } class Program { public static void Main() { var logger = new MyLogger(); } } 

E espera-se que o programa produza no console:

Construtor de registrador estático
Construtor de Registradores de Instâncias


imagem

Exemplo 3


Os programadores geralmente precisam escrever funções auxiliares (utilitários, auxiliares etc.) para facilitar sua vida. Normalmente, essas funções são bastante simples e geralmente levam apenas algumas linhas de código. Mas você pode tropeçar mesmo do nada.

Suponha que precisamos implementar uma função que verifique o número quanto à singularidade (ou seja, que o número não seja divisível por 2 sem o restante).

Uma implementação pode ser assim:

 static bool isOddNumber(int i) { return (i % 2 == 1); } 

À primeira vista, está tudo bem e, por exemplo, para os números 5.7 e 11, esperamos obter True.

O que a função isOddNumber (-5) retornará?
-5 é um número ímpar, mas como resposta à nossa função obtemos Falso!
Vamos descobrir qual é o motivo.

De acordo com o MSDN , está escrito o seguinte sobre o restante do operador de% divisão:
"Para operandos inteiros, o resultado de a% b é o valor produzido por a - (a / b) * b"
No nosso caso, para a = -5, b = 2, obtemos:
Qual é a raiz quadrada de 2? (-5) / (-5) / 2)
Mas -1 nem sempre é igual a 1, o que explica nosso resultado Falso.

O operador% é sensível ao sinal de operandos. Portanto, para não receber essas “surpresas”, é melhor comparar o resultado com zero, o que não tem sinal:

 static bool isOddNumber(int i) { return (i % 2 != 0); } 

Ou obtenha uma função separada para verificar a paridade e implementar a lógica por meio dela:

 static bool isEvenNumber(int i) { return (i % 2 == 0); } static bool isOddNumber(int i) { return !isEvenNumber(i); } 


Exemplo 4


Todo mundo que programou em C # provavelmente se encontrou com o LINQ, o que é muito conveniente para trabalhar com coleções, criar consultas, filtrar e agregar dados ...

Nós não olharemos para os bastidores do LINQ. Talvez façamos isso outra vez.

Enquanto isso, considere um pequeno exemplo:

 int[] dataArray = new int[] { 0, 1, 2, 3, 4, 5 }; int summResult = 0; var selectedData = dataArray.Select( x => { summResult += x; return x; }); Console.WriteLine(summResult); 

O que esse código produzirá?
Aparecemos na tela o valor da variável summResult, que é igual ao valor inicial, ou seja, 0

Por que isso aconteceu?

E porque a definição de uma consulta LINQ e o lançamento dessa consulta são duas operações executadas separadamente. Assim, a definição de uma solicitação não significa seu lançamento / execução.

A variável summResult é usada dentro de um delegado anônimo no método Select: os elementos da matriz dataArray são classificados sequencialmente e adicionados à variável summResult.

Podemos supor que nosso código imprima a soma dos elementos da matriz dataArray. Mas o LINQ não funciona dessa maneira.

Considere a variável selectedData. A palavra-chave var é "açúcar sintático", que em muitos casos reduz o tamanho do código do programa e melhora sua legibilidade. E o tipo real da variável selectedData implementa a interface IEnumerable. I.e. nosso código fica assim:

  IEnumerable<int> selectedData = dataArray.Select( x => { summResult += x; return x; }); 

Aqui, definimos a consulta (Consulta), mas a consulta em si não é iniciada. De maneira semelhante, você pode trabalhar com o banco de dados especificando a consulta SQL como uma sequência, mas para obter o resultado, consulte o banco de dados e execute essa consulta explicitamente.

Ou seja, até agora, apenas definimos uma solicitação, mas não a lançamos. É por isso que o valor da variável summResult permanece inalterado. Uma consulta pode ser iniciada, por exemplo, usando os métodos ToArray, ToList ou ToDictionary:

 int[] dataArray = new int[] { 0, 1, 2, 3, 4, 5 }; int summResult = 0; //        selectedData IEnumerable<int> selectedData = dataArray.Select( x => { summResult += x; return x; }); //   selectedData selectedData.ToArray(); //    summResult Console.WriteLine(summResult); 

Esse código já exibirá o valor da variável summResult, igual à soma de todos os elementos da matriz dataArray, igual a 15.

Nós descobrimos isso. E então o que esse programa exibirá na tela?

 int[] dataArray = new int[] { 0, 1, 2, 3, 4, 5 }; //1 var summResult = dataArray.Sum() + dataArray.Skip(3).Take(2).Sum(); //2 var groupedData = dataArray.GroupBy(x => x).Select( //3 x => { summResult += x.Key; return x.Key; }); Console.WriteLine(summResult); //4 

A variável groupedData (linha 3) realmente implementa a interface IEnumerable e essencialmente define a solicitação para a fonte de dados dataArray. Isso significa que, para que um delegado anônimo funcione, que altera o valor da variável summResult, essa solicitação deve ser executada explicitamente. Mas não existe esse lançamento em nosso programa. Portanto, o valor da variável summResult será alterado apenas na linha 2 e não podemos considerar tudo o mais em nossos cálculos.

É fácil calcular o valor da variável summResult, que é, respectivamente, 15 + 7, ou seja, 22)

Exemplo 5


Digamos imediatamente - não consideramos esse exemplo em nossas palestras na Academia, mas, às vezes, discutimos isso durante os intervalos do café e não como uma piada.

Apesar de dificilmente ser indicativo do ponto de vista da determinação do nível do desenvolvedor, encontramos este exemplo em vários testes diferentes. Talvez seja usado para versatilidade, porque funciona da mesma forma em C e C ++, bem como em C # e Java.

Portanto, que haja uma linha de código:

 int i = (int)+(char)-(int)+(long)-1; 

Qual será o valor da variável i?
Resposta: 1

Você pode pensar que a aritmética numérica é usada aqui sobre os tamanhos de cada tipo em bytes, pois os sinais "+" e "-" são encontrados inesperadamente aqui para a conversão de tipos.

Em C #, o tipo inteiro é conhecido por ter 4 bytes, 8 caracteres, caractere 2.

É fácil pensar que nossa linha de código será equivalente à seguinte expressão aritmética:

 int i = (4)+(2)-(4)+(8)-1; 

No entanto, isso não é verdade. E, para confundir e direcionar por um falso raciocínio, o exemplo pode ser alterado, por exemplo, assim:

 int i = (int)+(char)-(int)+(long)-sizeof(int); 

Os sinais "+" e "-" são usados ​​neste exemplo não como operações aritméticas binárias, mas como operadores unários. Em seguida, nossa linha de código é apenas uma sequência de conversões explícitas de tipo combinadas com chamadas para operações unárias, que podem ser escritas da seguinte maneira:

  int i = (int)( // call explicit operator int(char), ie char to int +( // call unary operator + (char)( // call explicit operator char(int), ie int to char -( // call unary operator - (int)( // call explicit operator int(long), ie long to int +( // call unary operator + (long)( // call explicit operator long(int), ie int to long -1 ) ) ) ) ) ) ); 


imagem

Interessado em aprender na Veeam Academy?


Agora, há um conjunto intensivo de primavera em C # em São Petersburgo e convidamos todos a fazer testes on-line no site da Veeam Academy.

O curso começa em 18 de fevereiro de 2019, vai até meados de maio e, como sempre, será totalmente gratuito. A inscrição para quem deseja se submeter ao teste de entrada já está disponível no site da Academia: academy.veeam.ru

imagem

Source: https://habr.com/ru/post/pt439478/


All Articles