Uso de sentencias compuestas (bloques "{" ... "}") para imponer la localidad variable [duplicar]

40

Introducción

Muchos lenguajes de programación "tipo C" usan sentencias compuestas (bloques de código especificados con "{" y "}") para definir un ámbito de variables.

Aquí hay un ejemplo simple.

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);
}

Esto es bueno porque limita el alcance del value a donde se usa. Es difícil para los programadores usar value de manera que no deben hacerlo porque solo pueden acceder a él desde dentro de su alcance.

Casi todos ustedes son conscientes de esto y están de acuerdo en que es una buena práctica declarar las variables en el bloque que se utilizan para limitar su alcance.

Pero a pesar de que es una convención establecida para declarar variables en su alcance más pequeño posible, no es muy común usar una declaración compuesta desnuda (es una declaración compuesta que no está conectada a un if , for , while instrucción).

Intercambiando los valores de dos variables

Los programadores a menudo escriben el código así:

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

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

¿No sería mejor escribir el código de esta manera?

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

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

Parece bastante feo, pero creo que esta es una buena manera de imponer la localidad variable y hacer que el código sea más seguro de usar.

Esto no solo se aplica a los temporales

A menudo veo patrones similares donde una variable se usa una vez en una función

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 qué no lo escribimos así?

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.
    ...
}

Resumen de la pregunta

Es difícil dar buenos ejemplos. Estoy seguro de que hay mejores maneras de escribir el código en mis ejemplos, pero de eso no se trata esta pregunta.

Mi pregunta es por qué no utilizamos más declaraciones compuestas "desnudas" para limitar el alcance de las variables.

¿Qué piensas acerca del uso de una declaración compuesta como esta?

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

para limitar el alcance de tmp ?

¿Es una buena práctica? ¿Es una mala práctica? Explica tus pensamientos.

    
pregunta wefwefa3 10.11.2015 - 15:13

9 respuestas

-1

El siguiente código java muestra lo que creo que es uno de los mejores ejemplos de cómo los bloques simples pueden ser útiles.

Como puede ver, el método compareTo() tiene tres comparaciones que hacer, y los resultados de los dos primeros deben almacenarse temporalmente en un local. El local es solo una "diferencia" en ambos casos, pero reutilizar la misma variable local es una mala idea y, de hecho, en un IDE decente, puede configurar la reutilización de la variable local para provocar una advertencia.

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 );
    }
}

Observe cómo, en este caso particular, no puede descargar el trabajo a una función separada, como sugiere la respuesta de Kilian Foth. Entonces, en casos como este, los bloques desnudos son siempre mi preferencia. Pero incluso en los casos en los que de hecho puede mover el código a una función separada, prefiero a) mantener las cosas en un lugar para minimizar el desplazamiento necesario para dar sentido a una parte del código, yb) no hinchar mi código Con muchas funciones. Definitivamente una buena práctica.

(Nota al margen: una de las razones por las que el estilo egipcio de corchetes realmente apesta es que no funciona con bloques desnudos).

(Otra nota al margen que recordé ahora, un mes más tarde: el código anterior está en Java, pero en realidad adquirí el hábito de mis días de C ++, donde el corchete de cierre hace que se invocen los destructores. Esta es la El fragmento RAII que rachet freak también menciona en su respuesta, y no es solo algo bueno, es bastante indispensable.

    
respondido por el Mike Nakis 11.11.2015 - 15:35
74

De hecho, es una buena práctica mantener el alcance de su variable pequeño. Sin embargo, la introducción de bloques anónimos en métodos grandes solo resuelve la mitad del problema: ¡el alcance de las variables se reduce, pero el método (ligeramente) crece!

La solución es obvia: lo que querías hacer en un bloque anónimo, deberías hacerlo en un método. El método obtiene su propio bloque y su propio alcance automáticamente, y con un nombre significativo también obtendrá una mejor documentación de él.

    
respondido por el Kilian Foth 10.11.2015 - 15:19
22

A menudo, si encuentra lugares para crear dicho alcance, es una oportunidad para extraer una función.

En un idioma con paso por referencia, en su lugar, llamará swap(x,y) .

Para escribir el archivo que sería recomendable utilizar el bloque para asegurar que RAII cerrará el archivo y liberará los recursos lo antes posible.

    
respondido por el ratchet freak 10.11.2015 - 15:21
12

Supongo que no sabes, aún, de ¿Lenguajes orientados a la expresión ?

En los lenguajes orientados a la expresión, (casi) todo es una expresión. Esto significa, por ejemplo, que un bloque puede ser una expresión como en 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);
}

puede que te preocupe que el compilador compute de forma redundante x * x y decide memorizar el 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);
}

Sin embargo, ahora x2 claramente sobrevive a su utilidad. Bueno, en Rust, un bloque es una expresión que devuelve el valor de su última expresión (no de su última instrucción, así que evite un ; de cierre):

// 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);
}

Por lo tanto, diría que los idiomas más nuevos han reconocido la importancia de limitar el alcance de las variables (hace que las cosas estén más limpias), y están ofreciendo cada vez más facilidades para hacerlo.

Incluso los expertos notables de C ++ como Herb Sutter están recomendando bloques anónimos de este tipo, "pirateando" lambdas para inicializar variables constantes (porque inmutable es genial):

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

Enhorabuena, tiene un aislamiento de alcance de algunas variables triviales en una función grande y compleja.

Desafortunadamente, tienes una función grande y compleja. Lo correcto es, en lugar de crear un ámbito dentro de la función para la variable, extraerla a su propia función. Esto fomenta la reutilización del código y permite que el alcance enclosing se pase como parámetros a la función y no sean variables pseudo-globales para ese alcance anónimo.

Esto significa que todo lo que esté fuera del alcance del bloque sin nombre todavía está dentro del alcance en el bloque sin nombre. Está efectivamente programando con funciones globales y sin nombre que se ejecutan en serie sin que sea posible reutilizar el código.

Además, considere que en muchos tiempos de ejecución, la declaración de la variable all dentro de los ámbitos anónimos se declara y asigna en la parte superior del método.

Veamos algunos C # ( enlace ):

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);
    }
}

Y cuando empiezas a excavar en la IL para obtener el código (con dotnetfiddle, debajo de 'tidy up' también hay 'View IL'), justo allí en la parte superior muestra lo que se ha asignado 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)

Esto asigna el espacio para dos cadenas y dos enteros en la inicialización del método Main, a pesar de que solo un int32 está dentro del alcance en un momento dado. Puede ver un análisis más profundo de esto en un tema tangencial de inicialización de variables en Inicialización de bucles y variables de Foreach .

Veamos un código Java en su lugar. Esto debería 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 esto con javac, y luego invoque javap -v Main.class y obtendrá Desensamblador de archivos de clase Java .

Justo en la parte superior, le indica cuántas ranuras necesita para las variables locales ( salida del comando javap va a la explicación de las partes de la misma un poco más):

  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

Necesita cuatro ranuras. Aunque dos de estas variables locales nunca están en el ámbito al mismo tiempo, still asigna espacio para cuatro variables locales.

Por otro lado, si le da al compilador la opción, extraer métodos cortos (como un 'swap') hará un mejor trabajo para incorporar el método y hacer un uso más óptimo de las variables.

    
respondido por el user40980 10.11.2015 - 15:50
3

Sí, los bloques desnudos pueden limitar los ámbitos variables más de lo que normalmente los limitaría. Sin embargo:

  1. La única ganancia es un final anterior de la vida útil de las variables; Usted puede limitar el comienzo de su vida útil de forma sencilla y discreta moviendo la declaración hacia abajo al lugar apropiado. Esta es una práctica común, por lo que ya estamos a mitad de camino de donde puedes conseguir con bloques desnudos, y esta mitad viene gratis.

  2. Por lo general, cada variable tiene su propio lugar donde deja de ser útil; Al igual que cada variable tiene su propio lugar donde debe ser declarada. Por lo tanto, si quisiera limitar todos los ámbitos de variables tanto como fuera posible, en muchos casos terminaría con un bloque para cada variable.

  3. Un bloque desnudo introduce estructura adicional a la función, lo que dificulta su operación. Junto con el punto 2, esto puede hacer que una función con ámbitos totalmente restringidos sea casi ilegible.

Entonces, supongo, la conclusión es que los bloques desnudos simplemente no dan resultado en la gran mayoría de los casos. Hay casos en los que un bloque desnudo puede tener sentido porque necesita controlar dónde se llama a un destructor, o porque tiene varias variables con un final de uso casi idéntico. Sin embargo, especialmente en el último caso, definitivamente debería pensar en factorizar el bloqueo en su propia función. Las situaciones que se resuelven mejor con un bloque simple son extremadamente raras.

    
respondido por el cmaster 10.11.2015 - 22:39
3

Sí, los bloques desnudos rara vez se ven, y creo que eso se debe a que rara vez se necesitan.

Un lugar donde los uso yo mismo es en las instrucciones de cambio:

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

Tiendo a hacer esto siempre que declaro variables dentro de las ramas de un switch, porque mantiene las variables fuera del alcance de las ramas subsiguientes.

    
respondido por el Michael Kay 11.11.2015 - 01:13
3

Buenas prácticas. Período. Parada completa. Haces explícito a los futuros lectores que la variable no se usa en ningún otro lugar, impuesta por las reglas del idioma.

Esto es cortesía provisional para los futuros mantenedores, porque les dices algo que sabes. Estos hechos podrían tomar decenas de minutos para determinar en una función suficientemente larga o complicada.

Los segundos que pasará documentando este hecho poderoso al agregar un par de llaves le valdrán minutos para cada mantenedor que no tenga que determinar este hecho.

El futuro mantenedor puede ser usted mismo, años más tarde, ( porque usted es el único que conoce este código ) con otras cosas en su mente ( porque el proyecto ha terminado hace mucho tiempo. y has sido reasignado desde entonces ), y bajo presión. ( porque estamos haciendo esto amablemente para el cliente, ya saben, han sido socios desde hace mucho tiempo, ya saben, y tuvimos que suavizar nuestra relación comercial con ellos porque el último proyecto que entregamos no fue así. a las expectativas, ya sabes, y oh, es solo un pequeño cambio y no debería llevar tanto tiempo para un experto como tú, y no tenemos presupuesto, así que no hagamos "exceso de calidad" )

Te odiarías a ti mismo por no hacerlo.

    
respondido por el Laurent LA RIZZA 10.11.2015 - 17:08
2

Los bloques / ámbitos adicionales agregan verborrea adicional. Si bien la idea conlleva cierta apelación inicial, pronto se encontrará con situaciones en las que, de todos modos, no es factible porque tiene variables temporales con un alcance superpuesto que no pueden ser reflejadas adecuadamente por una estructura de bloques.

Por lo tanto, como no se puede hacer que la estructura de bloques refleje de manera consistente la vida útil variable de todos modos, tan pronto como las cosas se vuelven un poco más complejas, los casos simples en los que funciona parecen un ejercicio de inutilidad eventual.

Para las estructuras de datos con destructores que bloquean recursos importantes durante su vida útil, existe la posibilidad de llamar al destructor explícitamente. A diferencia de usar una estructura de bloques, esto no exige un orden de destrucción particular para las variables introducidas en diferentes momentos.

Por supuesto, los bloques tienen su uso cuando están vinculados con unidades lógicas: particularmente cuando se usa la programación de macros, el alcance de cualquier variable que no se nombre explícitamente como argumento de macro para la salida se restringe mejor al cuerpo de la macro para no causar Sorprende al usar una macro varias veces.

Pero como marcadores de vida útil para variables en ejecución secuencial, los bloques tienden a ser excesivos.

    
respondido por el user203568 10.11.2015 - 17:01

Lea otras preguntas en las etiquetas