web-dev-qa-db-ja.com

複合ステートメント( "{" ... "}"ブロック)を使用して変数の局所性を強制する

前書き

多くの「Cライク」プログラミング言語は、複合ステートメント(「{」と「}」で指定されたコードブロック)を使用して変数のスコープを定義します。

これは簡単な例です。

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

valueのスコープが使用される場所に制限されるため、これは良いことです。プログラマは、スコープ内からしかアクセスできないため、意図しない方法でvalueを使用することは困難です。

私はほとんどすべての人がこれを認識しており、スコープを制限するために使用されるブロックで変数を宣言することは良い習慣であることに同意します。

しかし、可能な限り最小のスコープで変数を宣言することは確立された規則ですが、ネイキッドコンパウンドステートメント(ifforwhileステートメント)。

2つの変数の値を交換する

プログラマーは次のようなコードを書くことがよくあります。

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

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

次のようなコードを書く方が良いでしょうか?

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

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

見苦しいように見えますが、これは変数の局所性を強制し、コードをより安全に使用するための良い方法だと思います。

これは一時的なものだけに当てはまるわけではありません

変数が関数内で一度使用される同様のパターンをよく見ます

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

こんな風に書いてみませんか?

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

質問のまとめ

良い例を思いつくのは難しい。私の例では、コードを書くためのより良い方法があると確信していますが、それはこの質問についてではありません。

私の質問は、なぜ「ネイキッド」な複合ステートメントを使用して変数のスコープを制限しないのかということです。

このような複合ステートメントを使用することについてどう思いますか

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

tmpのスコープを制限するには?

いい練習ですか?それは悪い習慣ですか?あなたの考えを説明してください。

41
wefwefa3

次のJavaコードは、ネイキッドブロックがどのように役立つかを示す最良の例の1つであると私が信じるものを示しています。

ご覧のとおり、compareTo()メソッドには3つの比較があり、最初の2つの結果はローカルに一時的に保存する必要があります。ローカルはどちらの場合も単なる「違い」ですが、同じローカル変数を再利用することはお勧めできません。実際、まともなIDEでローカル変数の再利用を設定できます警告を引き起こします。

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

この特定のケースでは、Kilian Fothの回答が示唆しているように、cannotが作業を別の関数にオフロードする方法に注意してください。したがって、このような場合、裸のブロックが常に私の好みです。しかし、実際にコードを別の関数に移動できる場合でも、私はa)コードを理解するために必要なスクロールを最小限に抑えるために1か所に置いておき、b)コードを膨らませないことを好みますたくさんの機能を備えています。間違いなく良い習慣です。

(補足:エジプトの波括弧のスタイルが本当に悪い理由の1つは、裸のブロックでは機能しないことです。)

(もう1か月後、今覚えていることですが、上記のコードはJavaで記述されていますが、実際には、C++の日々の習慣から、中括弧を閉じるとデストラクタが呼び出されます。これは、ラシェフリークも彼の答えで言及しています、そしてそれはただ良いことではなく、それはかなり不可欠です)

1
Mike Nakis

変数のスコープを小さく保つことは確かに良い習慣です。ただし、無名ブロックを大きなメソッドに導入しても、問題の半分しか解決できません。変数のスコープは小さくなりますが、メソッドは(わずかに)大きくなります!

解決策は明白です。無名ブロックで実行したいことは、メソッドで実行する必要があります。このメソッドは、独自のブロックと独自のスコープを自動的に取得します。意味のある名前を付けることで、より適切なドキュメントも取得できます。

73
Kilian Foth

多くの場合、そのようなスコープを作成する場所を見つけると、関数を抽出する機会になります。

参照渡しの言語では、代わりにswap(x,y)を呼び出します。

ブロックを使用してRAIIがファイルを閉じ、リソースをできるだけ早く解放することをお勧めするファイルを書き込む場合。

22
ratchet freak

Expression-Oriented 言語をまだ知らないのではないでしょうか?

式指向言語では、(ほとんど)すべてが式です。これは、たとえば、blockが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);
}

ただし、コンパイラがx * xを冗長に計算するのではないかと心配し、結果を記憶することにしました。

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

しかし、今ではx2は明らかにその有用性よりも長持ちします。さて、Rustの場合、ブロックは最後の式の値を返す式なので、最後のステートメントの値ではないので、;を閉じないでください):

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

したがって、新しい言語は変数のスコープを制限することの重要性を認識し(物事をよりきれいにする)、そのための機能をますます提供していると私は言うでしょう。

Herb Sutterなどの著名なC++の専門家でさえ、この種の匿名ブロックを推奨し、ラムダを「ハッキング」して定数変数を初期化します(不変がすばらしいため)。

int32_t const y = [&]{
    int32_t const x2 = x * x;
    return x * x2 + x2 + x;
}(); // do not forget to actually invoke the lambda with ()
13
Matthieu M.

おめでとうございます。大規模で複雑な関数内のいくつかの自明な変数のスコープ分離を取得しました。

残念ながら、あなたは大きくて複雑な機能を持っています。適切なことは、変数の関数内にスコープを作成する代わりに、それを独自の関数に抽出することです。これにより、コードの再利用が促進され、enclosingスコープをパラメーターとして関数に渡すことができ、その匿名スコープの疑似グローバル変数にすることはできません。

これは、名前のないブロックのスコープ外にあるすべてのものは、名前のないブロックのスコープ内にあることを意味します。グローバルを使用して効率的にプログラミングし、名前のない関数がコードを再利用する方法なしに逐次実行されます。


さらに、多くのランタイムでは、匿名スコープ内のall変数宣言が宣言され、メソッドのトップ。

C#を見てみましょう( https://dotnetfiddle.net/QKtaG4 ):

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

そして、コードのILを掘り始めると(dotnetfiddleを使用して、 'tidy up'の下に 'View IL'もあります)、そのすぐ上に、このメソッドに割り当てられているものが表示されます。

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

これにより、メソッドMainの初期化時に、2つの文字列と2つの整数にスペースが割り当てられます。 Foreachループと変数の初期化 で、変数の初期化に関する接線のトピックでこれについての詳細な分析を見ることができます。

代わりにいくつかのJava=コードを見てみましょう。これはかなりおなじみのはずです。

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

これをjavacでコンパイルしてからjavap -v Main.classを呼び出すと、 Javaクラスファイル逆アセンブラ が得られます。

上部のすぐそこに、ローカル変数に必要なスロットの数が表示されます( javapコマンドの出力 は、その部分の説明に少し入っています)。

  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

4つのスロットが必要です。これらのローカル変数のうち2つが同時にスコープ内に存在することはありませんが、stillは4つのローカル変数に領域を割り当てます。

一方、コンパイラーにオプションを与えた場合、短いメソッド(「スワップ」など)を抽出すると、メソッドをインライン化して変数をより最適に使用できるようになります。

8
user40980

グッドプラクティス。ピリオド。フルストップ。変数が他の場所で使用されておらず、言語の規則によって強制されていることを将来の読者に明示します。

これは将来のメンテナにとっては暫定的な礼儀正しさです。これらの事実は、十分に長い関数または複雑な関数であると判断するのに数十分かかることがあります。

中括弧のペアを追加することによってこの強力な事実を文書化するのに費やす秒数は、この事実を判断する必要がない各メンテナーにとって数分を支払うことになります。

将来のメンテナは、数年後、あなた自身になることができます(あなたがこのコードを知っているのはあなただけだからです)他のことを頭に入れて(プロジェクトが長らく終了し、それ以来、割り当てが変更されたため)、プレッシャーにさらされています。 (顧客のためにこれを丁寧に行っているので、彼らは長年のパートナーでしたね。そして、私たちは彼らとの商業的関係をスムーズにする必要がありました。私たちが提供したのは、期待したほどではありませんでした。ご存知のとおり、それは小さな変更であり、あなたのような専門家にとってそれほど長くはかからないはずです。予算がないため、「過剰」にしないでください

あなたはそれをしないと自分を憎むでしょう。

3

はい、ネイキッドブロックは、通常の制限よりもさらに変数スコープを制限する場合があります。しかしながら:

  1. 唯一の利点は、変数の有効期間の初期のendです。宣言を適切な場所に移動することで、そのライフタイムの始まりを簡単かつ完全に控えめに制限できます。これは一般的な慣習であるため、裸のブロックで取得できる場所の半分まではすでにあり、この半分の道は無料で提供されます。

  2. 通常、各変数には、有用でなくなる独自の場所があります。ちょうど各変数が宣言されるべき独自の場所を持っているように。したがって、すべての変数のスコープをできるだけ制限したい場合は、多くの場合、すべての変数のブロックが作成されます。

  3. ネイキッドブロックは、関数に追加の構造を導入し、その操作を把握しにくくします。ポイント2と合わせて、これは完全に制限されたスコープを持つ関数を、ほとんど読み取り不可能にレンダリングする可能性があります。

つまり、結論として、大部分の場合、ネイキッドブロックは効果を上げないということです。デストラクタが呼び出される場所を制御する必要があるため、またはほぼ同じ使用終了の変数がいくつかあるために、ネイキッドブロックが有効な場合があります。ただし、特に後者の場合は、ブロックを独自の関数に分解することを検討する必要があります。ネイキッドブロックによって最適に解決される状況は非常にまれです。

はい、裸のブロックがめったに見られないので、私はそれらが非常にまれに必要とされるためだと思います。

私が自分で使用する場所の1つは、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;
}

変数を後続のブランチのスコープ外に保つため、スイッチのブランチ内で変数を宣言するときはいつでもこれを行う傾向があります。

3
Michael Kay

追加のブロック/スコープは、追加の言い回しを追加します。このアイデアにはいくつかの最初の魅力がありますが、ブロック構造によって適切に反映できないoverlappingスコープを持つ一時変数があるため、とにかく実現不可能になる状況にすぐに出くわします。

ですから、少し複雑になるとすぐにブロック構造に変数のライフタイムを一貫して反映させることができないので、機能する単純なケースを逆に曲げると、最終的には無益になります。

存続期間中に大量のリソースをロックするデストラクタを持つデータ構造の場合、デストラクタを明示的に呼び出す可能性があります。ブロック構造を使用するのとは対照的に、これは、異なる時点で導入された変数の特定の破棄順序を要求しません。

もちろん、ブロックは論理ユニットに関連付けられている場合に使用されます。特にマクロプログラミングを使用する場合、出力用のマクロ引数として明示的に指定されていない変数のスコープは、使用時に驚きを引き起こさないように、マクロ本体自体に制限するのが最適です。マクロを数回。

しかし、逐次実行における変数のライフタイムマーカーとして、ブロックは過剰になりがちです。

2
user203568