Uso di istruzioni composte ("{" ... "}" blocchi) per forzare la località variabile [duplicato]

40

Introduzione

Molti linguaggi di programmazione "C-like" utilizzano istruzioni composte (blocchi di codice specificati con "{" e "}") per definire un ambito di variabili.

Ecco un semplice esempio.

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

Questo è utile perché limita l'ambito di value a dove viene utilizzato. È difficile per i programmatori utilizzare value in modi a cui non sono destinati perché possono accedervi solo dal suo ambito.

Sono quasi tutti a conoscenza di questo e sono d'accordo sul fatto che è buona norma dichiarare le variabili nel blocco in cui vengono utilizzate per limitare il loro ambito.

Ma anche se è una convenzione stabilita per dichiarare le variabili nel loro ambito più piccolo possibile, non è molto comune usare un'istruzione composta nuda (cioè un'istruzione composta che non è connessa a if , for , while istruzione).

Scambio dei valori di due variabili

I programmatori scrivono spesso il codice in questo modo:

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

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

Non sarebbe meglio scrivere il codice in questo modo:

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

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

Sembra abbastanza brutto, ma penso che questo sia un buon modo per rafforzare la località variabile e rendere il codice più sicuro da usare.

Questo non si applica solo ai provvisori

Vedo spesso modelli simili in cui una variabile viene utilizzata una sola volta in una funzione

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

Perché non lo scriviamo così?

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

Riepilogo della domanda

È difficile trovare buoni esempi. Sono sicuro che ci sono migliori modi per scrivere il codice nei miei esempi, ma non è quello di cui tratta questa domanda.

La mia domanda è perché non usiamo dichiarazioni composte "nude" più per limitare l'ambito delle variabili.

Cosa pensi dell'uso di una dichiarazione composta come questa

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

per limitare l'ambito di tmp ?

È una buona pratica? È una cattiva pratica? Spiega i tuoi pensieri.

    
posta wefwefa3 10.11.2015 - 15:13
fonte

9 risposte

-1

Il seguente codice java mostra quello che ritengo essere uno dei migliori esempi di come i blocchi nudi possano essere utili.

Come puoi vedere, il metodo compareTo() ha tre confronti da fare e i risultati dei primi due devono essere temporaneamente memorizzati in un locale. Il local è solo una "differenza" in entrambi i casi, ma riutilizzare la stessa variabile locale è una cattiva idea, e in effetti su un IDE decente puoi configurare il riutilizzo delle variabili locali per causare un avvertimento.

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

Nota come in questo caso specifico non puoi scaricare il lavoro in una funzione separata, come suggerisce la risposta di Kilian Foth. Quindi, in casi come questo, i blocchi nudi sono sempre la mia preferenza. Ma anche nei casi in cui è possibile spostare il codice in una funzione separata, preferisco a) tenere le cose in un posto per ridurre al minimo lo scorrimento necessario per dare un senso a un pezzo di codice e b) non per gonfiare il mio codice con molte funzioni. Sicuramente una buona pratica.

(Nota a margine: uno dei motivi per cui lo stile di parentesi graffa egiziano fa veramente schifo è che non funziona con i blocchi nudi.)

(Un'altra nota a margine che ho ricordato ora, un mese dopo: il codice sopra è in Java, ma in realtà ho preso l'abitudine dai miei giorni di C ++, dove la parentesi graffa di chiusura fa sì che vengano chiamati i distruttori. La RAII ha morso anche quella freak rachet nella sua risposta, e non è solo una buona cosa, è praticamente indispensabile.)

    
risposta data 11.11.2015 - 15:35
fonte
74

È davvero una buona pratica mantenere l'ambito della tua variabile piccolo. Tuttavia, l'introduzione di blocchi anonimi in grandi metodi risolve solo la metà del problema: l'ambito delle variabili si restringe, ma il metodo (leggermente) cresce!

La soluzione è ovvia: cosa volevi fare in un blocco anonimo, dovresti farlo in un metodo. Il metodo ottiene automaticamente il proprio blocco e il proprio ambito, e con un nome significativo si ottiene anche una migliore documentazione.

    
risposta data 10.11.2015 - 15:19
fonte
22

Spesso, se trovi luoghi per creare un tale ambito, è un'opportunità per estrarre una funzione.

In una lingua con pass-per-riferimento, dovresti chiamare swap(x,y) .

Per scrivere il file che sarebbe consigliabile utilizzare il blocco per garantire che RAII chiuda il file e libera le risorse il prima possibile.

    
risposta data 10.11.2015 - 15:21
fonte
12

Suppongo che tu non sappia ancora di linguaggi orientati alle espressioni ?

Nei linguaggi orientati alle espressioni, (quasi) tutto è un'espressione. Ciò significa, ad esempio, che un blocco può essere un'espressione come in 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);
}

potresti essere preoccupato che il compilatore calcoli in modo ridondante x * x , e decida di memorizzare il risultato:

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

Tuttavia, ora x2 sopravvive nettamente alla sua utilità. Bene, in Rust un blocco è un'espressione che restituisce il valore della sua ultima espressione (non della sua ultima istruzione, quindi evita una chiusura di ; ):

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

Quindi direi che le lingue più recenti hanno riconosciuto l'importanza di limitare la portata delle variabili (rende le cose più pulite) e offrono sempre più servizi per farlo.

Anche noti esperti C ++ come Herb Sutter raccomandano blocchi anonimi di questo tipo, "hacking" lambdas per inizializzare le variabili costanti (perché immutabile è grande):

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

Congratulazioni, hai l'isolamento dello scope di alcune variabili banali in una funzione ampia e complessa.

Sfortunatamente, hai una funzione ampia e complessa. La cosa giusta da fare è invece di creare un ambito all'interno della funzione per la variabile, per estrarlo nella sua funzione. Ciò incoraggia il riutilizzo del codice e consente di passare l'ambito che include come parametri per la funzione e non essere variabili pseudo-globali per tale ambito anonimo.

Ciò significa che tutto ciò che è esterno all'ambito del blocco senza nome è ancora incluso nel blocco senza nome. Si sta programmando efficacemente con globali e le funzioni senza nome vengono eseguite in serie senza alcuno scopo di riutilizzare il codice.

Inoltre, considera che in molti runtime la dichiarazione di variabili tutte all'interno degli ambiti anonimi è dichiarata e allocata nella parte superiore del metodo.

Vediamo alcuni 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 inizi a scavare nell'IL per il codice (con dotnetfiddle, sotto 'tidy up' c'è anche 'Visualizza IL'), proprio lì in alto mostra ciò che è stato allocato per questo metodo:

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

Questo alloca lo spazio per due stringhe e due interi all'inizializzazione del metodo Main, anche se solo un int32 è in ambito in un dato momento. Puoi vedere un'analisi più approfondita di questo argomento su un argomento tangenziale di inizializzazione delle variabili all'indirizzo Ciclo Foreach e inizializzazione delle variabili .

Invece, guardiamo un po 'di codice Java. Questo dovrebbe sembrare piuttosto familiare.

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

Compilalo con javac, quindi invoca javap -v Main.class e ottieni Disassemblatore di file di classi Java .

Proprio lì in alto, ti dice quanti slot ha bisogno per le variabili locali ( l'output del comando javap entra nel spiegazione delle parti di esso un po 'di più):

  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

Ha bisogno di quattro slot. Anche se due di queste variabili locali non sono mai nello scope contemporaneamente, ancora alloca lo spazio per quattro variabili locali.

D'altra parte, se si fornisce al compilatore l'opzione, l'estrazione di metodi brevi (come un "swap") farà un lavoro migliore per integrare il metodo e rendere l'uso più ottimale delle variabili.

    
risposta data 10.11.2015 - 15:50
fonte
3

Sì, i blocchi nudi potrebbero limitare gli ambiti variabili oltre a quelli normalmente limitati. Tuttavia:

  1. L'unico guadagno è un precedente fine delle variabili lifetime; puoi facilmente, e completamente senza limiti, limitare l'inizio della sua vita spostando la dichiarazione nel luogo appropriato. Questa è una pratica comune, quindi siamo già a metà strada da dove puoi ottenere con i blocchi nudi, e questo a metà strada è gratis.

  2. Tipicamente, ogni variabile ha il suo posto dove cessa di essere utile; proprio come ogni variabile ha il suo posto dove dovrebbe essere dichiarata. Quindi, se si volesse limitare il più possibile gli ambiti di tutte le variabili, si otterrebbe in molti casi un blocco per ogni singola variabile.

  3. Un blocco nudo introduce struttura aggiuntiva alla funzione, rendendo più difficile la comprensione del suo funzionamento. Insieme al punto 2, questo può rendere una funzione con ambiti completamente ristretti piuttosto vicino illeggibile.

Quindi, immagino, la linea di fondo è che i blocchi nudi semplicemente non pagano nella stragrande maggioranza dei casi. Ci sono casi in cui un blocco nudo può avere senso perché è necessario controllare dove viene chiamato un distruttore o perché si hanno diverse variabili con una destinazione d'uso quasi identica. Eppure, specialmente nel caso successivo, dovresti sicuramente pensare di considerare il blocco nella sua funzione. Le situazioni che sono meglio risolte da un blocco nudo sono estremamente rare.

    
risposta data 10.11.2015 - 22:39
fonte
3

Sì, i blocchi nudi sono visti molto raramente, e penso che sia perché sono raramente necessari.

Un posto in cui li uso personalmente è nelle istruzioni 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;
}

Tendo a farlo ogni volta che dichiaro le variabili all'interno dei rami di un interruttore, perché mantiene le variabili fuori dall'ambito dei rami successivi.

    
risposta data 11.11.2015 - 01:13
fonte
3

Buona pratica. Periodo. Punto fermo. Ai lettori del futuro rendi esplicito che la variabile non viene utilizzata da nessun'altra parte, applicata dalle regole della lingua.

Questa è una cortesia provvisoria per i futuri manutentori, perché dici loro qualcosa che sai. Questi fatti potrebbero richiedere dozzine di minuti per determinare una funzione sufficientemente lunga o complessa.

I secondi che trascorrerai documentando questo potente fatto aggiungendo un paio di parentesi pagheranno minuti per ogni manutentore che non dovrà determinare questo fatto.

Il futuro manutentore può essere te stesso, anni dopo ( perché sei l'unico a conoscere questo codice ) con altre cose a cui pensare ( perché il progetto è stato a lungo finito e sei stato riassegnato sin da ), e sotto pressione. ( perché lo stiamo facendo con gentilezza per il cliente, sai, sono stati partner da molto tempo, lo sai, e abbiamo dovuto smussare il nostro rapporto commerciale con loro perché l'ultimo progetto che abbiamo realizzato non era quello alle aspettative, lo sai, e oh, è solo un piccolo cambiamento e non dovrebbe richiedere così tanto tempo ad un esperto come te, e non abbiamo budget quindi non fare "overquality" )

Ti odierai per non averlo fatto.

    
risposta data 10.11.2015 - 17:08
fonte
2

Ulteriori blocchi / ambiti aggiungono verbosità aggiuntiva. Sebbene l'idea abbia un certo fascino iniziale, ti imbatterai presto in situazioni in cui diventa irrealizzabile perché hai variabili temporanee con un ambito che si sovrappone che non può essere riflesso correttamente da una struttura a blocchi.

Quindi, dato che non è possibile rendere la struttura dei blocchi coerentemente riflettente le durate variabili comunque non appena le cose diventano leggermente più complesse, piegarsi all'indietro per i casi semplici in cui funziona sembra un esercizio di futilità finale.

Per le strutture di dati con distruttori che bloccano risorse significative durante la loro vita, c'è la possibilità di chiamare il distruttore in modo esplicito. A differenza dell'utilizzo di una struttura a blocchi, questo non impone un particolare ordine di distruzione per le variabili introdotte in diversi punti del tempo.

Ovviamente i blocchi hanno i loro usi quando sono collegati con unità logiche: in particolare quando si utilizza la programmazione macro, l'ambito di ogni variabile non esplicitamente chiamata come argomento macro destinato all'output è meglio limitato al corpo della macro stessa per non causare sorprende quando si utilizza una macro più volte.

Ma come indicatori di durata per le variabili nell'esecuzione sequenziale, i blocchi tendono ad essere eccessivi.

    
risposta data 10.11.2015 - 17:01
fonte

Leggi altre domande sui tag