Faz sentido criar blocos apenas para reduzir o escopo de uma variável?

38

Estou escrevendo um programa em Java, onde em um momento preciso carregar uma senha para o meu keystore. Apenas por diversão, tentei manter minha senha em Java o mais curta possível, fazendo o seguinte:

//Some code
....

KeyManagerFactory keyManager = KeyManagerFactory.getInstance("SunX509");
Keystore keyStore = KeyStore.getInstance("JKS");

{
    char[] password = getPassword();
    keyStore.load(new FileInputStream(keyStoreLocation), password);
    keyManager.init(keyStore, password);
}

...
//Some more code

Agora, eu sei que nesse caso isso é meio idiota. Há várias outras coisas que eu poderia ter feito, a maioria delas realmente melhor (eu não poderia usar uma variável).

No entanto, fiquei curioso para saber se houve um caso em que isso não foi tão burro. A única outra coisa em que consigo pensar é se você deseja reutilizar nomes de variáveis ​​comuns como count, ou temp, mas boas convenções de nomenclatura e métodos curtos tornam improvável que isso seja útil.

Existe um caso em que usar apenas blocos para reduzir o escopo variável faz sentido?

Lord Farquaad
fonte
13
@gnat .... o que? Nem sei como editar minha pergunta para ser menos parecida com esse alvo idiota. Que parte disso levou você a acreditar que essas perguntas são iguais?
Lord Farquaad 29/11
22
A resposta usual para isso é que seu bloco anônimo deve ser um método com um nome significativo, que fornece documentação automática e o escopo reduzido desejado. De imediato, não consigo pensar em uma situação em que extrair um método não seja melhor do que inserir um bloco nu.
Kilian Foth
4
Há algumas perguntas semelhantes que fazem o C ++: É uma prática recomendada criar blocos de código? e Código de estrutura com colchetes . Talvez um deles contenha uma resposta para essa pergunta, embora C ++ e Java sejam muito diferentes nesse aspecto.
amon
2
Possível duplicata de É prática recomendada criar blocos de código?
Panzercrisis
4
@gnat Por que essa pergunta está aberta? É uma das coisas mais amplas que eu vi. É uma tentativa de uma pergunta canônica?
jpmc26

Respostas:

34

Primeiro, falando com a mecânica subjacente:

No escopo C ++ == tempo de vida, os destruidores b / c são chamados na saída do escopo. Além disso, uma distinção importante em C / C ++ podemos declarar objetos locais. No tempo de execução do C / C ++, o compilador geralmente aloca um quadro de pilha para o método que for tão grande quanto necessário, com antecedência, em vez de alocar mais espaço de pilha na entrada de cada escopo (que declara variáveis). Portanto, o compilador está recolhendo ou achatando os escopos.

O compilador C / C ++ pode reutilizar o espaço de armazenamento da pilha para os locais que não entram em conflito durante a vida útil do uso (geralmente usa a análise do código real para determinar isso em vez do escopo, b / c que é mais preciso que o escopo!).

Menciono C / C ++ b / c A sintaxe do Java, ou seja, chaves e escopo, é pelo menos em parte derivada dessa família. E também porque o C ++ surgiu nos comentários da pergunta.

Por outro lado, não é possível ter objetos locais em Java: todos os objetos são objetos de heap e todos os locais / formais são variáveis ​​de referência ou tipos primitivos (btw, o mesmo é verdadeiro para estática).

Além disso, no escopo e no tempo de vida de Java não são exatamente iguais: estar dentro / fora do escopo é um conceito principalmente de tempo de compilação que inclui acessibilidade e conflitos de nome; nada realmente acontece em Java na saída do escopo em relação à limpeza de variáveis. A coleta de lixo de Java determina (o ponto final da) vida útil dos objetos.

Além disso, o mecanismo de bytecode de Java (a saída do compilador Java) tende a promover variáveis ​​de usuário declaradas em escopos limitados para o nível superior do método, porque não há recurso de escopo no nível de bytecode (isso é semelhante ao tratamento com C / C ++ do pilha). Na melhor das hipóteses, o compilador poderia reutilizar slots de variáveis ​​locais, mas (ao contrário do C / C ++), apenas se o tipo for o mesmo.

(Porém, para garantir que o compilador JIT subjacente no tempo de execução possa reutilizar o mesmo espaço de armazenamento de pilha para dois locais de tipos diferentes (e com fendas diferentes), com análise suficiente do bytecode).

Falando com a vantagem do programador, eu tenderia a concordar com os outros que um método (privado) é uma construção melhor, mesmo se usado apenas uma vez.

Ainda assim, não há nada realmente errado em usá-lo para desconfigurar nomes.

Fiz isso em raras circunstâncias, quando criar métodos individuais é um fardo. Por exemplo, ao escrever um intérprete usando uma switchdeclaração grande em um loop, eu poderia (dependendo dos fatores) introduzir um bloco separado para cada um casepara manter os casos individuais mais separados um do outro, em vez de tornar cada caso um método diferente (cada um que é invocado apenas uma vez).

(Observe que, como código em blocos, os casos individuais têm acesso às instruções "break;" e "continue;" relacionadas ao loop envolvente, enquanto os métodos exigiriam o retorno de booleanos e o uso de condicionais do chamador para obter acesso a esse fluxo de controle afirmações.)

Erik Eidt
fonte
Resposta muito interessante (+1)! No entanto, um complemento para a parte C ++. Se a senha for uma string em um bloco, o objeto string alocará espaço em algum lugar do armazenamento gratuito para a parte de tamanho variável. Ao sair do bloco, a string será destruída, resultando na desalocação da parte variável. Mas como não está na pilha, nada garante que será substituído em breve. Então gerenciar melhor a confidencialidade, substituindo cada um o seu carácter antes da string é destruído ;-)
Christophe
1
@ErikEidt Eu estava tentando chamar a atenção para o problema de falar em "C / C ++" como se fosse realmente uma "coisa", mas não é: "No tempo de execução do C / C ++, o compilador geralmente aloca um quadro de pilha para o método que for tão grande quanto necessário ", mas C não possui métodos. Tem funções. Eles são diferentes, especialmente em C ++.
tchrist
7
" Por outro lado, não é possível ter objetos locais em Java: todos os objetos são objetos de heap e todos os locais / formais são variáveis ​​de referência ou tipos primitivos (btw, o mesmo é verdadeiro para a estática). " - Observe que isso é não é inteiramente verdade, especialmente não para variáveis ​​locais do método. A análise de escape pode alocar objetos para a pilha.
Boris the Spider
1
Na melhor das hipóteses, o compilador poderia reutilizar slots de variáveis ​​locais, mas (ao contrário do C / C ++), apenas se o seu tipo for o mesmo. Está simplesmente errado. No nível do bytecode, as variáveis ​​locais podem ser atribuídas e reatribuídas com o que você quiser, a única restrição é que o uso subsequente seja compatível com o tipo de item atribuído mais recente. Reutilizar slots de variáveis ​​locais é a norma; por exemplo, com { int x = 42; String s="blah"; } { long y = 0; }, a longvariável reutilizará os slots de ambas as variáveis xe scom todos os compiladores comumente usados ​​( javace ecj).
Holger
1
@ Boris the Spider: essa é uma afirmação coloquial frequentemente repetida que realmente não corresponde ao que está acontecendo. A Análise de escape pode ativar a escalarização, que é a decomposição dos campos do objeto em variáveis, que são então sujeitas a otimizações adicionais. Alguns deles podem ser eliminados como não utilizados, outros podem ser dobrados com as variáveis ​​de origem usadas para inicializá-los, outros são mapeados para registros da CPU. O resultado raramente corresponde ao que as pessoas podem imaginar ao dizer "alocado na pilha".
Holger
27

Na minha opinião, seria mais claro colocar o bloco em seu próprio método. Se você deixou esse bloco, espero ver um comentário esclarecendo por que você está colocando o bloco lá em primeiro lugar. Nesse ponto, parece mais limpo ter a chamada de método initializeKeyManager()que mantém a variável de senha limitada ao escopo do método e mostra sua intenção com o bloco de código.

Casey Langford
fonte
2
Acho que sua resposta é contornada por um caso-chave, é um passo intermediário durante um refator. Pode ser útil limitar o escopo e saber que seu código ainda funciona pouco antes da extração de um método.
RubberDuck
16
@RubberDuck verdade, mas nesse caso o resto de nós nunca deve vê-lo, para que não importe o que pensamos. O que um codificador adulto e um compilador autorizam na privacidade de seu próprio cubículo não é preocupação de mais ninguém.
Candied_orange #
@candied_orange temo eu entendi não :( Cuidados para elaborar?
doubleOrt
@doubleOrt Eu quis dizer que as etapas intermediárias de refatoração são reais e necessárias, mas certamente não precisam ser consagradas no controle de origem, onde todos podem olhar para ela. Só terminar de refatoração antes check-in.
candied_orange
@candied_orange Oh, pensei que você estava discutindo e não estendendo o argumento de RubberDuck.
doubleOrt
17

Eles podem ser úteis no Rust, com regras rígidas de empréstimo. E geralmente, em linguagens funcionais, onde esse bloco retorna um valor. Mas em linguagens como Java? Embora a ideia pareça elegante, na prática raramente é usada, portanto a confusão de vê-la diminui a clareza potencial de limitar o escopo das variáveis.

Há um caso em que acho útil - em uma switchdeclaração! Às vezes, você deseja declarar uma variável em case:

switch (op) {
    case ADD:
        int result = a + b;
        System.out.printf("%s + %s = %s\n", a, b, result);
        break;
    case SUB:
        int result = a - b;
        System.out.printf("%s - %s = %s\n", a, b, result);
        break;
}

Isso, no entanto, falhará:

error: variable result is already defined in method main(String[])
            int result = a - b;

switch declarações são estranhas - são declarações de controle, mas, devido à sua inovação, não introduzem escopos.

Então - você pode apenas usar blocos para criar os escopos:

switch (op) {
    case ADD: {
        int result = a + b;
        System.out.printf("%s + %s = %s\n", a, b, result);
    } break;
    case SUB: {
        int result = a - b;
        System.out.printf("%s - %s = %s\n", a, b, result);
    } break;
}
Idan Arye
fonte
6
  • Caso Geral:

Existe um caso em que usar apenas blocos para reduzir o escopo variável faz sentido?

É geralmente considerado uma boa prática manter os métodos curtos. Reduzir o escopo de algumas variáveis ​​pode ser um sinal de que seu método é bastante longo, suficiente para que você pense que o escopo das variáveis ​​precisa ser reduzido. Nesse caso, provavelmente vale a pena criar um método separado para essa parte do código.

Os casos que eu consideraria problemáticos são quando essa parte do código usa mais do que alguns argumentos. Há situações em que você pode acabar com métodos pequenos, cada um com vários parâmetros (ou precisa de uma solução alternativa para simular o retorno de vários valores). A solução para esse problema específico é criar outra classe que mantenha as várias variáveis ​​usadas e implementa todas as etapas que você tem em mente em seus métodos relativamente curtos: esse é um caso de uso típico para usar um padrão Builder .

Dito isto, todas essas diretrizes de codificação podem ser aplicadas vagamente às vezes. Às vezes, existem casos estranhos, nos quais o código pode ser mais legível se você o mantiver em um método um pouco mais longo, em vez de dividi-lo em pequenos sub-métodos (ou padrões de construtor). Normalmente, isso funciona para problemas de tamanho médio, bastante lineares e onde muita divisão realmente atrapalha a legibilidade (especialmente se você precisar pular entre muitos métodos para entender o que o código faz).

Pode fazer sentido, mas é muito raro.

  • Seu caso de uso:

Aqui está o seu código:

KeyManagerFactory keyManager = KeyManagerFactory.getInstance("SunX509");
Keystore keyStore = KeyStore.getInstance("JKS");

{
    char[] password = getPassword();
    keyStore.load(new FileInputStream(keyStoreLocation), password);
    keyManager.init(keyStore, password);
}

Você está criando um FileInputStream, mas nunca o fecha.

Uma maneira de resolver isso é usar uma instrução try-with-resources . Ele escopo InputStream, e você também pode usar este bloco para reduzir o escopo de outras variáveis, se desejar (dentro do razoável, uma vez que essas linhas estão relacionadas):

KeyManagerFactory keyManager = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
Keystore keyStore = KeyStore.getInstance("JKS");

try (InputStream fis = new FileInputStream(keyStoreLocation)) {
    char[] password = getPassword();
    keyStore.load(fis, password);
    keyManager.init(keyStore, password);
}

(It'a também muitas vezes melhor usar getDefaultAlgorithm()em vez de SunX509, por sinal.)

Além disso, embora o conselho para separar pedaços de código em métodos separados seja geralmente bom. Raramente vale a pena se for por apenas 3 linhas (se houver, é um caso em que a criação de um método separado dificulta a legibilidade).

Bruno
fonte
"Reduzir o escopo de algumas variáveis ​​pode ser um sinal de que seu método é bastante longo" - Pode ser um sinal, mas também pode e o IMHO deve ser usado para documentar por quanto tempo o valor de uma variável local é usado. De fato, para idiomas que não suportam escopos aninhados (por exemplo, Python), eu apreciaria se um comentário de código correspondente declarasse o "fim da vida" de uma variável. Se a mesma variável for reutilizada posteriormente, seria fácil descobrir se ela deve (ou deveria) ter sido inicializada com um novo valor em algum lugar antes (mas após o comentário "fim da vida útil").
Jonny Dee
2

Na minha humilde opinião, geralmente é algo a ser evitado em grande parte pelo código de produção, porque geralmente você não é tentado a fazê-lo, exceto em funções esporádicas que executam tarefas díspares. Costumo fazê-lo em algum código de sucata usado para testar as coisas, mas não sinto tentação de fazê-lo no código de produção, onde pensei antes sobre o que cada função deve fazer, pois a função naturalmente terá uma escopo limitado em relação ao seu estado local.

Eu realmente nunca vi exemplos de blocos anônimos sendo usados ​​assim (sem envolver condicionais, um trybloco para uma transação, etc.) para reduzir o escopo de maneira significativa em uma função que não implorava a pergunta de por que não podia ser dividido ainda mais em funções mais simples, com escopos reduzidos, se realmente se beneficiasse praticamente do ponto de vista genuíno do SE dos blocos anônimos. Geralmente, o código eclético está fazendo um monte de coisas pouco relacionadas ou muito relacionadas, nas quais estamos mais tentados a buscar isso.

Como exemplo, se você estiver tentando fazer isso para reutilizar uma variável denominada count, isso sugere que você está contando duas coisas díspares. Se o nome da variável for tão curto quanto count, então faz sentido associá-lo ao contexto da função, que poderia estar contando apenas um tipo de coisa. Em seguida, você pode olhar instantaneamente o nome e / ou a documentação da função, ver counte saber instantaneamente o que isso significa no contexto do que a função está fazendo sem analisar todo o código. Não costumo encontrar um bom argumento para uma função contar duas coisas diferentes, reutilizando o mesmo nome de variável de maneira a tornar escopos / blocos anônimos tão atraentes em comparação com as alternativas. Isso não sugere que todas as funções contem apenas uma coisa. EU'benefício de engenharia para uma função reutilizando o mesmo nome de variável para contar duas ou mais coisas e usando blocos anônimos para limitar o escopo de cada contagem individual. Se a função é simples e clara, não é o fim do mundo ter duas variáveis ​​de contagem com nomes diferentes, sendo que a primeira possui possivelmente mais linhas de visibilidade do escopo do que o ideal. Tais funções geralmente não são a fonte de erros que carecem de blocos anônimos para reduzir ainda mais o escopo mínimo de suas variáveis ​​locais.

Não é uma sugestão para métodos supérfluos

Isso não é para sugerir que você crie métodos à força, apenas para reduzir o escopo. É indiscutivelmente tão ruim ou pior, e o que estou sugerindo não deve exigir uma necessidade de métodos "auxiliares" particulares, mais do que uma necessidade de escopos anônimos. Isso é pensar demais no código como está agora e como reduzir o escopo das variáveis ​​do que pensar em como resolver conceitualmente o problema no nível da interface, de maneira que produza uma visibilidade curta e limpa dos estados da função local naturalmente sem aninhamento profundo de blocos e 6+ níveis de indentação. Concordo com Bruno que você pode dificultar a legibilidade do código colocando três linhas de código em uma função, mas isso começa com a suposição de que você está evoluindo as funções criadas com base na implementação existente, em vez de projetar as funções sem se envolver nas implementações. Se você fizer isso da última maneira, eu encontrei pouca necessidade de blocos anônimos que não servem para nada além de reduzir o escopo da variável em um determinado método, a menos que você esteja tentando zelosamente reduzir o escopo de uma variável em apenas algumas linhas de código inofensivo onde a introdução exótica desses blocos anônimos indiscutivelmente contribui com a sobrecarga intelectual que eles removem.

Tentando reduzir ainda mais os escopos mínimos

Se a redução de escopos de variáveis ​​locais para o mínimo absoluto valeu a pena, deve haver uma ampla aceitação de código como este:

ImageIO.write(new Robot("borg").createScreenCapture(new Rectangle(Toolkit.getDefaultToolkit().getScreenSize())), "png", new File(Db.getUserId(User.handle()).toString()));

... como isso causa a visibilidade mínima do estado, nem mesmo criando variáveis ​​para se referir a elas em primeiro lugar. Eu não quero parecer dogmático, mas acho que a solução pragmática é evitar bloqueios anônimos quando possível, assim como evitar a linha monstruosa de código acima e se eles parecem absolutamente necessários em um contexto de produção do perspectiva de correção e manutenção de invariantes dentro de uma função, definitivamente penso em como você organiza seu código em funções e cria suas interfaces vale a pena reexaminar. Naturalmente, se seu método tiver 400 linhas e o escopo de uma variável for visível para 300 linhas de código a mais do que o necessário, isso pode ser um problema de engenharia genuíno, mas não é necessariamente um problema a ser resolvido com blocos anônimos.

Se nada mais, a utilização de blocos anônimos em todo o lugar é exótica, não idiomática, e o código exótico corre o risco de ser odiado por outras pessoas, se não por você, anos depois.

A utilidade prática de reduzir o escopo

A utilidade final de reduzir o escopo das variáveis ​​é permitir que você obtenha o gerenciamento de estado correto e mantenha-o correto, além de raciocinar facilmente sobre o que qualquer parte de uma base de código faz - para poder manter invariantes conceituais. Se o gerenciamento de estado local de uma única função é tão complexo que você precisa reduzir vigorosamente o escopo com um bloco anônimo de código que não deve ser finalizado e bom, novamente, isso é um sinal para mim de que a própria função precisa ser reexaminada . Se você tiver dificuldade em raciocinar sobre o gerenciamento de estado das variáveis ​​em um escopo de função local, imagine a dificuldade de raciocinar sobre as variáveis ​​privadas acessíveis a todos os métodos de uma classe inteira. Não podemos usar blocos anônimos para reduzir sua visibilidade. Para mim, ajuda começar com a aceitação de que as variáveis ​​tenderão a ter um escopo um pouco mais amplo do que o ideal, em muitos idiomas, desde que não fique fora de controle a ponto de você ter dificuldade em manter invariantes. Não é algo para resolver tanto com bloqueios anônimos, como eu o vejo como aceitável do ponto de vista pragmático.


fonte
Como uma advertência final, como mencionei, ainda utilizo blocos anônimos de tempos em tempos, geralmente nos principais métodos de entrada para códigos de sucata. Alguém o mencionou como uma ferramenta de refatoração e acho que está bem, já que o resultado é intermediário e pretende ser refatorado ainda mais, não como código finalizado. Não estou descartando completamente a utilidade deles, mas se você estiver tentando escrever um código amplamente aceito, bem testado e correto, dificilmente vejo a necessidade de blocos anônimos. Ficar obcecado em usá-los pode mudar o foco da criação de funções mais claras com blocos naturalmente pequenos.
2

Existe um caso em que usar apenas blocos para reduzir o escopo variável faz sentido?

Eu acho que motivação é motivo suficiente para introduzir um bloqueio, sim.

Como Casey observou, o bloco é um "cheiro de código" que indica que é melhor extrair o bloco em uma função. Mas você não pode.

John Carmark escreveu um memorando sobre código embutido em 2007. Seu resumo final

  • Se uma função é chamada apenas de um único local, considere incluí-la.
  • Se uma função for chamada de vários locais, verifique se é possível organizar o trabalho para ser realizado em um único local, talvez com sinalizadores, e coloque-o em linha.
  • Se houver várias versões de uma função, considere criar uma única função com mais parâmetros, possivelmente padrão.
  • Se o trabalho estiver quase puramente funcional, com poucas referências ao estado global, tente torná-lo completamente funcional.

Então pense , faça uma escolha com propósito e (opcional) deixe evidências descrevendo sua motivação para a escolha que você fez.

VoiceOfUnreason
fonte
1

Minha opinião pode ser controversa, mas sim, acho que certamente há situações em que eu usaria esse tipo de estilo. No entanto, "apenas para reduzir o escopo da variável" não é um argumento válido, porque é isso que você já está fazendo. E fazer algo apenas para fazer isso não é uma razão válida. Observe também que essa explicação não tem sentido se sua equipe já tiver resolvido se esse tipo de sintaxe é convencional ou não.

Eu sou principalmente um programador de C #, mas ouvi dizer que, em Java, retornar uma senha char[]para permitir reduzir o tempo em que a senha permanecerá na memória. Neste exemplo, isso não ajudará, porque o coletor de lixo pode coletar a matriz a partir do momento em que não está em uso, portanto, não importa se a senha sair do escopo. Não estou discutindo se é viável limpar a matriz depois que você terminar com ela, mas, neste caso, se você quiser fazê-lo, o escopo da variável faz sentido:

{
    char[] password = getPassword();
    try{
        keyStore.load(new FileInputStream(keyStoreLocation), password);
        keyManager.init(keyStore, password);
    }finally{
        Arrays.fill(password, '\0');
    }
}

Isso é realmente semelhante à instrução try-with-resources , porque escopo o recurso e executa uma finalização após a conclusão. Novamente, observe que não estou argumentando para lidar com senhas dessa maneira, apenas que, se você decidir fazer isso, esse estilo é razoável.

A razão para isso é que a variável não é mais válida. Você o criou, usou e invalidou seu estado para não conter informações significativas. Não faz sentido usar a variável após esse bloco, portanto, é razoável fazer o escopo.

Outro exemplo em que consigo pensar é quando você tem duas variáveis ​​com nome e significado semelhantes, mas trabalha com uma e depois com outra e deseja mantê-las separadas. Eu escrevi esse código em c #:

{
    MethodBuilder m_ToString = tb.DefineMethod("ToString", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, typeofString, Type.EmptyTypes);
    var il = m_ToString.GetILGenerator();
    il.Emit(OpCodes.Ldstr, templateType.ToString()+":"+staticType.ToString());
    il.Emit(OpCodes.Ret);
    tb.DefineMethodOverride(m_ToString, m_Object_ToString);
}
{
    PropertyBuilder p_Class = tb.DefineProperty("Class", PropertyAttributes.None, typeofType, Type.EmptyTypes);
    MethodBuilder m_get_Class = tb.DefineMethod("get_Class", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, typeofType, Type.EmptyTypes);
    var il = m_get_Class.GetILGenerator();
    il.Emit(OpCodes.Ldtoken, staticType);
    il.Emit(OpCodes.Call, m_Type_GetTypeFromHandle);
    il.Emit(OpCodes.Ret);
    p_Class.SetGetMethod(m_get_Class);
    tb.DefineMethodOverride(m_get_Class, m_IPattern_get_Class);
}

Você pode argumentar que eu posso simplesmente declarar ILGenerator il;na parte superior do método, mas também não quero reutilizar a variável para objetos diferentes (meio que abordagem funcional). Nesse caso, os blocos facilitam a separação dos trabalhos executados, tanto sintaticamente quanto visualmente. Também diz que, após o bloqueio, eu terminei ile nada deveria acessá-lo.

Um argumento contra este exemplo está usando métodos. Talvez sim, mas neste caso, o código não é tão longo, e separá-lo em diferentes métodos também precisaria passar todas as variáveis ​​usadas no código.

IllidanS4 quer Monica de volta
fonte