Usando instruções compostas (blocos “{”… “}”) para impor a localidade variável [duplicada]

40

Introdução

Muitas linguagens de programação "C-like" usam instruções compostas (blocos de código especificados com "{" e "}") para definir um escopo de variáveis.

Aqui está um exemplo simples.

for (int i = 0; i < 100; ++i) {
    int value = function(i); // Here 'value' is local to this block
    printf("function(%d) == %d\n", i, value);
}

Isso é bom porque limita o escopo do value para onde é usado. É difícil para os programadores usar value de maneiras que eles não devem, porque eles só podem acessá-lo dentro de seu escopo.

Eu quase todos vocês estão cientes disso e concordam que é uma boa prática declarar variáveis no bloco que elas são usadas para limitar seu escopo.

Mas, embora seja uma convenção estabelecida declarar variáveis em seu menor escopo possível, não é muito comum usar uma instrução composta nua (que é uma instrução composta que não está conectada a uma if , for , declaração while ).

Trocando os valores de duas variáveis

Programadores costumam escrever o código assim:

int x = ???
int y = ???

// Swap 'x' and 'y'
int tmp = x;
x = y;
y = tmp;

Não seria melhor escrever o código assim:

int x = ???
int y = ???

// Swap 'x' and 'y'
{
    int tmp = x;
    x = y;
    y = tmp;
}

Parece muito feio, mas acho que essa é uma boa maneira de impor a localidade variável e tornar o código mais seguro de usar.

Isso não se aplica apenas a temporários

Muitas vezes vejo padrões semelhantes nos quais uma variável é usada uma vez em uma função

Object function(ParameterType arg) {
    Object obj = new Object(obj);
    File file = File.open("output.txt", "w+");
    file.write(obj.toString());

    // 'obj' is used more here but 'file' is never used again.
    ...
}

Por que não escrevemos assim?

RET_TYPE function(PARAM_TYPE arg) {
    Object obj = new Object(obj);
    {
       File file = File.open("output.txt", "w+");
       file.write(obj.toString());
    }

    // 'obj' is used more here but 'file' is never used again.
    ...
}

Resumo da pergunta

É difícil encontrar bons exemplos. Tenho certeza de que há melhores formas de escrever o código em meus exemplos, mas não é sobre isso que se trata.

Minha pergunta é por que nós não usamos mais declarações compostas "nuas" para limitar o escopo das variáveis.

O que você acha de usar uma declaração composta como essa

{
    int tmp = x;
    x = y;
    y = z;
}

para limitar o escopo de tmp ?

É uma boa prática? Isso é uma prática ruim? Explique seus pensamentos.

    
por wefwefa3 10.11.2015 / 15:13
fonte

9 respostas

-1

O código java a seguir mostra o que acredito ser um dos melhores exemplos de como blocos nus podem ser úteis.

Como você pode ver, o método compareTo() tem três comparações para fazer, e os resultados dos dois primeiros precisam ser temporariamente armazenados em um local. O local é apenas uma 'diferença' em ambos os casos, mas reutilizar a mesma variável local é uma má idéia e, de fato, em um IDE decente, você pode configurar a reutilização de variáveis locais para causar um aviso.

class MemberPosition implements Comparable<MemberPosition>
{
    final int derivationDepth;
    final int lineNumber;
    final int columnNumber;

    MemberPosition( int derivationDepth, int lineNumber, int columnNumber )
    {
        this.derivationDepth = derivationDepth;
        this.lineNumber = lineNumber;
        this.columnNumber = columnNumber;
    }

    @Override
    public int compareTo( MemberPosition o )
    {
        /* first, compare by derivation depth, so that all ancestor methods will be executed before all descendant methods. */
        {
            int d = Integer.compare( derivationDepth, o.derivationDepth );
            if( d != 0 )
                return d;
        }

        /* then, compare by line number, so that methods will be executed in the order in which they appear in the source file. */
        {
            int d = Integer.compare( lineNumber, o.lineNumber );
            if( d != 0 )
                return d;
        }

        /* finally, compare by column number.  You know, just in case you have multiple test methods on the same line.  Whatever. */
        return Integer.compare( columnNumber, o.columnNumber );
    }
}

Note como neste caso em particular você não pode descarregar o trabalho para uma função separada, como a resposta de Kilian Foth sugere. Então, em casos assim, blocos nus são sempre minha preferência. Mas mesmo nos casos em que você pode de fato mover o código para uma função separada, eu prefiro a) manter as coisas em um lugar para minimizar a rolagem necessária para entender um pedaço de código, eb) não inchar meu código com muitas funções. Definitivamente, uma boa prática.

(Nota lateral: uma das razões pelas quais o estilo de chave inglesa realmente suga é que ele não funciona com blocos nus.)

(Outra nota que lembrei agora, um mês depois: o código acima está em Java, mas na verdade peguei o hábito dos meus dias de C ++, onde o colchete de fechamento faz com que os destruidores sejam invocados. Esse é o RAII bit que rachet aberração também menciona em sua resposta, e não é apenas uma coisa boa, é praticamente indispensável.)

    
por 11.11.2015 / 15:35
fonte
74

Na verdade, é uma boa prática manter o escopo da sua variável pequeno. No entanto, a introdução de blocos anônimos em grandes métodos apenas resolve metade do problema: o escopo das variáveis diminui, mas o método (levemente) cresce!

A solução é óbvia: o que você queria fazer em um bloco anônimo, você deveria estar fazendo em um método. O método obtém seu próprio bloco e seu próprio escopo automaticamente, e com um nome significativo você também obtém uma documentação melhor.

    
por 10.11.2015 / 15:19
fonte
22

Muitas vezes, se você encontrar lugares para criar um escopo, é uma oportunidade de extrair uma função.

Em um idioma com passagem por referência, você chamaria swap(x,y) .

Para escrever o arquivo, seria aconselhável usar o bloco para garantir que o RAII feche o arquivo e libere os recursos o mais rápido possível.

    
por 10.11.2015 / 15:21
fonte
12

Eu presumo que você ainda não conhece idiomas orientados à expressão

Em linguagens orientadas a expressões, (quase) tudo é uma expressão. Isso significa, por exemplo, que um bloco pode ser uma expressão como em Rust:

// A typical function displaying x^3 + x^2 + x
fn typical_print_x3_x2_x(x: i32) {
    let y = x * x * x + x * x + x;
    println!("{}", y);
}

você pode estar preocupado que o compilador irá computar x * x de forma redundante, e decidiu memorizar o resultado:

// A memoizing function displaying x^3 + x^2 + x
fn memoizing_print_x3_x2_x(x: i32) {
    let x2 = x * x;
    let y = x * x2 + x2 + x;
    println!("{}", y);
}

No entanto, agora x2 claramente supera sua utilidade. Bem, em Rust, um bloco é uma expressão que retorna o valor de sua última expressão (não de sua última declaração, portanto evite um fechamento ; ):

// An expression-oriented function displaying x^3 + x^2 + x
fn expr_print_x3_x2_x(x: i32) {
    let y = {
        let x2 = x * x;
        x * x2 + x2 + x // <- missing semi-colon, this is an expression
    };
    println!("{}", y);
}

Assim, eu diria que as linguagens mais recentes reconheceram a importância de limitar o escopo das variáveis (torna as coisas mais limpas) e estão oferecendo cada vez mais facilidades para fazê-lo.

Até mesmo especialistas em C ++ notáveis, como Herb Sutter, estão recomendando blocos anônimos desse tipo, "hackeando" lambdas para inicializar variáveis constantes (porque imutável é ótimo):

int32_t const y = [&]{
    int32_t const x2 = x * x;
    return x * x2 + x2 + x;
}(); // do not forget to actually invoke the lambda with ()
    
por 10.11.2015 / 16:42
fonte
8

Parabéns, você conseguiu isolar o escopo de algumas variáveis triviais em uma função grande e complexa.

Infelizmente, você tem uma função grande e complexa. A melhor coisa a fazer é, em vez de criar um escopo dentro da função para a variável, extraí-la para sua própria função. Isso encoraja a reutilização do código e permite que o escopo fechado seja passado como parâmetros para a função e não sejam variáveis pseudo-globais para esse escopo anônimo.

Isso significa que tudo fora do escopo do bloco sem nome ainda está no escopo no bloco sem nome. Você está efetivamente programando com globals e funções sem nome executadas serialmente sem qualquer maneira de ter reutilização de código.

Além disso, considere que, em muitos tempos de execução, a declaração de variável all nos escopos anônimos é declarada e alocada na parte superior do método.

Vamos ver alguns C # ( link ):

using System;

public class Program
{
    public static void Main()
    {
        string h = "hello";
        string w = "world";
        {
            int i = 42;
            Console.WriteLine(i);
        }
        {
            int j = 4;
            Console.WriteLine(j);
        }
        Console.WriteLine(h + w);
    }
}

E quando você começa a cavar o IL para o código (com dotnetfiddle, sob 'tidy up' há também 'View IL'), ali mesmo no topo, mostra o que foi alocado para este método:

.class public auto ansi beforefieldinit Program
    extends [mscorlib]System.Object
{
    .method public hidebysig static void Main() cli managed
    {
      //
      .maxstack 2
      .locals init (string V_0,
          string V_1,
          int32 V_2,
          int32 V_3)

Isso aloca o espaço para duas seqüências de caracteres e dois inteiros na inicialização do método principal, mesmo que apenas um int32 esteja no escopo em um determinado momento. Você pode ver uma análise mais aprofundada disso em um tópico tangencial de inicialização de variáveis em Inicialização de loop e variável Foreach .

Vamos ver alguns códigos Java. Isso deve parecer bastante familiar.

public class Main {
    public static void main(String[] args) {
        String h = "hello";
        String w = "world";
        {
            int i = 42;
            System.out.println(i);
        }
        {
            int j = 4;
            System.out.println(j);
        }
        System.out.println(h + w);
    }
}

Compile isso com javac e invoque javap -v Main.class para obter as Desmontador do arquivo de classe Java .

Bem ali no topo, ele informa quantos slots ele precisa para variáveis locais ( saída do comando javap entra no explicação das partes de um pouco mais):

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=4, args_size=1

Precisa de quatro slots. Mesmo que duas dessas variáveis locais nunca estejam no escopo ao mesmo tempo, ainda aloca espaço para quatro variáveis locais.

Por outro lado, se você der ao compilador a opção, a extração de métodos curtos (como um 'swap') fará um trabalho melhor de embutir o método e fazer um uso mais otimizado das variáveis.

    
por 10.11.2015 / 15:50
fonte
3

Sim, os blocos nus podem limitar os escopos de variáveis mais do que você normalmente os limitaria. No entanto:

  1. O único ganho é um final anterior da vida útil das variáveis; você pode facilmente, e inteiramente sem obstrução, o começo de sua vida movendo a declaração para o lugar apropriado. Esta é uma prática comum, por isso já estamos a meio caminho de onde você pode ficar com blocos nus, e este meio caminho vem de graça.

  2. Normalmente, cada variável tem seu próprio lugar onde deixa de ser útil; assim como cada variável tem seu próprio lugar onde deve ser declarado. Então, se você quisesse limitar todos os escopos de variáveis o máximo possível, você acabaria com um bloco para cada variável em muitos casos.

  3. Um bloco nu introduz estrutura adicional na função, tornando sua operação mais difícil de entender. Juntamente com o ponto 2, isso pode tornar uma função com escopos totalmente restritos praticamente ilegível.

Então, eu acho, a linha inferior é que os blocos despidos simplesmente não compensam na grande maioria dos casos. Existem casos em que um bloco nu pode fazer sentido porque você precisa controlar onde um destruidor é chamado ou porque você tem várias variáveis com final de uso quase idêntico. No entanto, especialmente no último caso, você deve pensar em fatorar o bloqueio em sua própria função. As situações que são melhor resolvidas por um bloco nu são extremamente raras.

    
por 10.11.2015 / 22:39
fonte
3

Sim, os blocos nus raramente são vistos, e acho que é porque eles raramente são necessários.

Um lugar em que eu mesmo os uso é em instruções switch:

case 'a': {
  int x = getAmbientTemperature();
  int y = getBackgroundIllumination();
  setVasculosity(x * y);
  break;
}
case 'b': {
  int x = getUltrification();
  int y = getMendacity();
  setVasculosity(x + y);
  break;
}

Eu costumo fazer isso sempre que estou declarando variáveis dentro dos ramos de um switch, porque ele mantém as variáveis fora do escopo das ramificações subseqüentes.

    
por 11.11.2015 / 01:13
fonte
3

Boa prática. Período. Parada completa. Você torna explícito para os futuros leitores que a variável não é usada em nenhum outro lugar, imposta pelas regras do idioma.

Esta é uma cortesia provisória para os futuros mantenedores, porque você lhes diz algo que você sabe. Esses fatos podem levar dezenas de minutos para determinar em uma função suficientemente longa ou complicada.

Os segundos que você gasta documentando esse fato poderoso adicionando um par de chaves compensarão os minutos para cada mantenedor que não precisará determinar esse fato.

O futuro mantenedor pode ser você mesmo, anos depois, ( porque você é o único que conhece esse código ) com outras coisas em mente ( porque o projeto já passou e você foi realocado desde ) e sob pressão. ( porque estamos fazendo isso graciosamente para o cliente, você sabe, eles têm sido parceiros de longa data, você sabe, e nós tivemos que suavizar nosso relacionamento comercial com eles porque o último projeto que entregamos não foi assim às expectativas, você sabe, e oh, é apenas uma pequena mudança e não deve demorar tanto tempo para um especialista como você, e não temos orçamento, então não faça "overquality" )

Você se odiaria por não fazer isso.

    
por 10.11.2015 / 17:08
fonte
2

Blocos / escopos adicionais adicionam verbosidade adicional. Embora a ideia tenha algum apelo inicial, você logo se deparará com situações em que ela se torna inviável, porque há variáveis temporárias com escopo sobrepostas que não podem ser refletidas adequadamente por uma estrutura de bloco.

Portanto, como você não pode fazer com que a estrutura de blocos reflita de maneira consistente os tempos de vida variáveis, assim que as coisas ficarem um pouco mais complexas, recorrer aos casos simples em que ela funciona parece ser um exercício de futilidade.

Para estruturas de dados com destruidores que armazenam recursos significativos durante sua vida útil, existe a possibilidade de chamar o destruidor explicitamente. Ao contrário de usar uma estrutura de blocos, isso não exige uma ordem particular de destruição para variáveis introduzidas em diferentes pontos do tempo.

É claro que os blocos têm seus usos quando estão vinculados a unidades lógicas: particularmente ao usar programação de macro, o escopo de qualquer variável não explicitamente nomeada como argumento de macro destinado a saída é mais restrito ao próprio corpo da macro para não causar surpresas ao usar uma macro várias vezes.

Mas como marcadores vitalícios para variáveis em execução sequencial, os blocos tendem a ser exagerados.

    
por 10.11.2015 / 17:01
fonte