Lire ce post en Francais
Você nota algum problema no trecho de código abaixo? Dica: existe pelo menos um, relacionado ao uso de Value Types & performance.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public struct S | |
{ | |
public void M() | |
{ | |
System.Console.WriteLine($"M()"); | |
} | |
} | |
public class C | |
{ | |
private readonly static S s = new S(); | |
static void Main() | |
{ | |
s.M(); | |
s.M(); | |
s.M(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public struct S | |
{ | |
public void M() | |
{ | |
System.Console.WriteLine($"M() : {counter++}"); | |
} | |
private int counter; | |
} | |
public class C | |
{ | |
private readonly static S s = new S(); | |
static void Main() | |
{ | |
s.M(); | |
s.M(); | |
s.M(); | |
} | |
} |
M() : 0
M() : 1
M() : 2
Você provavelmente ficará surpreso ao ver:
M() : 0
M() : 0
M() : 0
Agora ficou evidente que algo está errado.
O problema com este programa é que o campo s foi declarado como readonly o que faz com que o compilador emita cópias defensivas antes da chamada de qualquer método do tipo (no nosso caso s.M()); isso significa que o método M() incrementará o campo counter em uma cópia da estrutura original (ao invés de s) o que explica o comportamento observado.
Note que na primeira versão do programa (em que a estrutura em questão não é mutável) não é fácil detectar que algo pode não estar correto. Mesmo na última versão do programa não é claro o porquê nunca observamos o incremento de s.counter.
Para entender o que está ocorrendo vamos olhar o código IL gerado para o método Main() (você pode usar o sharplab.io para experimentar com o código):
Agora ficou mais fácil observar as cópias extras que ocorrem nos offsets IL_0001 & IL_0006 e depois em IL_000f & IL_0014 e finalmente em IL_001D & IL_0022. Note que após copiar o conteúdo de s para a variável local localizada no slot 1 o endereço da mesma é carregado na pilha e a seguir o método M() é executado.
Infelizmente em C# é relativamente fácil cair em outras “armadilhas”; O exemplo abaixo apresenta mais dois casos: parâmetros in e variáveis local ref readonly:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//#define READONLY | |
#if !READONLY | |
public struct S | |
#else | |
public readonly struct S | |
#endif | |
{ | |
public void M() | |
{ | |
#if !READONLY | |
System.Console.WriteLine($"M() : {counter++}"); | |
#endif | |
} | |
#if !READONLY | |
private int counter; | |
#endif | |
} | |
public class C | |
{ | |
static void Main() | |
{ | |
S s = new S(); | |
Foo(in s); | |
} | |
static void Foo(in S s) | |
{ | |
s.M(); // this will cause the struct to be copied | |
S localS= default; | |
localS.M(); | |
ref readonly S l = ref GetS(ref localS); | |
l.M(); // this will cause the struct to be copied | |
} | |
static ref readonly S GetS(ref S s) | |
{ | |
return ref s; | |
} | |
} |
- < 7.3: A única maneira que conheço é remover o modificador readonly do campo (s no nosso exemplo)
- >= 7.3: Neste caso você pode declarar sua struct como readonly de forma a garantir que a mesma não é mutável; desta forma o compilador não precisará emitir cópias defensivas (caso o código tente modificar o estado da estrutura o compilador emitirá um erro).
No caso de comportamento incorreto acredito ser consenso que o desenvolvedor deve encontrar/corrigir estas cópias; já em cenários em que tais cópias afetam apenas a performance eu diria que o desenvolvedor deve analisar os prós/contras antes de investir tempo para encontrar/remover tais cópias (para um número de aplicativos o impacto em performance causado por estas cópias extras é desprezível). Dito isso, se você desenvolve usando o Visual Studio você pode instalar o ErrorProne.NET que irá facilitar em muito a localização de problemas deste tipo.
Finalmente, se você se interessa em entender a plataforma .Net mais a fundo recomendo acompanhar este blog
Have fun!
Adriano
No comments:
Post a Comment