Escopos Locais Explícitos – Qualquer Benefício Verdadeiro?

Eu estava limpando algum código e removi uma declaração if que não era mais necessária. No entanto, percebi que esqueci de remover os suportes. Isto, obviamente, é válido e acabou de criar um novo escopo local. Agora isso me fez pensar. Em todos os meus anos de desenvolvimento em C #, nunca encontrei uma razão para usá-los. Na verdade, eu meio que esqueci que poderia fazer isso.

Existe algum benefício real para definir um escopo local? Eu entendo que posso definir variables ​​em um escopo e, em seguida, definir os mesmos novamente em um escopo não relacionado (para, foreach, etc.) como abaixo:

 void SomeMethod() { { int i = 20; } int i = 50; //Invalid due to i already being used above. } void SomeMethod2() { { int i = 20; } { int i = 50; //Valid due to scopes being unrelated. } { string i = "ABCDEF"; } } 

Qual é o verdadeiro ponto de definir um escopo local? Pode realmente haver algum tipo de ganho de desempenho (ou potencialmente uma perda)? Eu sei que você poderia fazer isso em C ++ e era parte de ajudar você a gerenciar memory, mas como isso é .NET, realmente haveria um benefício? Isso é apenas um bi-produto da linguagem que nos permite definir escopos randoms mesmo que não haja nenhum benefício real?

    Em C #, é puramente syntax transformar um grupo de instruções em uma única instrução. Obrigatório para qualquer palavra-chave que espera que uma única instrução seja seguida, como if, for, using, etc. Alguns casos:

    • a palavra-chave case dentro de um switch é especial, já que não exige que seja uma única instrução. A palavra-chave break ou goto termina. O que explica por que você pode usar chaves para interferir em uma declaração de variável.
    • as palavras-chave try e catch são especiais, elas requerem chaves mesmo que apenas uma única instrução seja seguida. Bastante incomum, mas provavelmente inspirado ao forçar o programador a pensar sobre o escopo das declarações dentro dos blocos, um bloco catch não pode se referir a variables ​​dentro do bloco try devido ao modo como o tratamento de exceções funciona.

    Limitar o escopo das variables ​​locais é uma causa perdida. É um grande negócio em C ++ porque a chave final é o lugar onde o compilador injetará as chamadas do destrutor para variables ​​dentro do bloco de escopo. Isso é ab / usado o tempo todo para o padrão RAII, nada muito bonito em ter pontuação em um programa tem efeitos colaterais tão drásticos.

    A equipe de C # não tem muita escolha sobre isso, a vida útil das variables ​​locais é estritamente controlada pelo jitter. Que é alheio a quaisquer construções de agrupamento dentro de um método, ele só sabe sobre IL. Que não tem construções de agrupamento além de try / except / finally. O escopo de qualquer variável local, não importa onde ela foi escrita, é o corpo do método. Algo que você pode ver quando você executa ildasm.exe em código C # compilado, você verá as variables ​​locais içadas para o topo do corpo do método. Que em parte também explica porque o compilador C # não permite que você declare outra variável local em outro bloco de escopo com o mesmo nome.

    O jitter tem regras interessantes sobre a vida útil das variables ​​locais, elas são inteiramente dominadas por como o coletor de lixo funciona. Quando ele junta um método, ele não apenas gera o código de máquina para o método, mas também cria uma tabela que descreve o escopo real de cada variável local, o endereço de código no qual ele é inicializado e o endereço de código onde não é mais usado . O coletor de lixo usa essa tabela para decidir se uma referência a um object é válida, com base no endereço de execução ativo.

    O que torna muito eficiente na coleta de objects. Um pouco eficiente demais, por vezes, e problemático quando você interopera com código nativo, você pode precisar do método mágico GC.KeepAlive () para prolongar a vida útil. Um método muito notável, não gera nenhum código. Seu único uso é obter o jitter para alterar a tabela e inserir um endereço maior para a variável tempo de vida.

    Assim como as funções, esses “blocos” são basicamente apenas para isolar regiões de código (principalmente) não relacionado e suas variables ​​locais dentro de uma function.

    Você pode usá-lo se precisar de alguma variável temporária apenas para passar entre duas chamadas de function, por exemplo

     int Foo(int a) { // ... { int temp; SomeFuncWithOutParam(a, out temp); NowUseThatTempJustOnce(temp); } MistakenlyTryToUse(temp); // Doesn't compile! // ... } 

    Pode-se argumentar, no entanto, que se você precisar desse tipo de escopo léxico, os bloqueios internos deveriam ser funções por conta própria.

    Quanto ao desempenho, etc. Eu duvido que isso importe. O compilador analisa a function como um todo e coleta todas as variables ​​locais (mesmo aquelas declaradas em linha), ao determinar o tamanho do quadro de pilha. Então basicamente todas as variables ​​locais são agrupadas de qualquer maneira. É uma coisa puramente léxica para lhe dar um pouco mais de restrição sobre o uso de suas variables.

    Há um local onde os escopos locais podem ser úteis: dentro de instruções de case de uma instrução switch . Todos os casos compartilham por padrão o mesmo escopo da instrução switch . Declarar uma variável temporária local com o mesmo nome dentro de várias declarações case não é permitido, e você pode acabar declarando a variável apenas na primeira instrução case ou mesmo fora da declaração do switch . Você pode resolver esse problema dando a cada instrução de caso um escopo local e declarar a variável temporária dentro do escopo. No entanto, não torne seus casos complexos demais, esse problema pode ser uma indicação de que é melhor chamar um método separado para lidar com a declaração de case .

    A vantagem é principalmente que torna a definição da linguagem mais simples.

    A definição pode agora simplesmente afirmar que if, while, for, etc. devem ser seguidos por uma única instrução. Um conjunto de instruções entre parênteses é simplesmente mais um tipo possível de declaração.

    Não há ganho real ao proibir blocos de instruções, conforme usado em seu exemplo. Às vezes eles são úteis para evitar conflitos de nome, mas você poderia resolver seu problema sem. Também introduziria desnecessariamente uma diferença nas regras de syntax em comparação com linguagens como C, C ++ e Java.

    Caso você queira saber, isso também não altera a vida útil do object dos objects referenciados.

    Não haverá um ganho de desempenho, pelo menos no modo de liberação: o GC pode coletar um object se souber – ou pelo menos pensar – que não será mais usado, independentemente do escopo. Isso pode atrapalhar você com a interoperabilidade não gerenciada: http://blogs.msdn.com/b/oldnewthing/archive/2010/08/13/10049634.aspx .

    Dito isso, eu uso esse “recurso” de tempos em tempos para garantir que o código mais adiante no método não possa usar algum object temporário. Na maioria dos casos, isso provavelmente poderia ser feito dividindo-se em methods adicionais, mas ocasionalmente isso fica desnecessariamente pesado, ou é, por algum motivo, impossível (por exemplo, em um construtor onde você está definindo membros de somente leitura). E de vez em quando eu uso para poder reutilizar nomes de variables, mas isso geralmente é acoplado com o motivo do “object temporário”.