変数のスコープを減らすためだけにブロックを作成するのは理にかなっていますか?


38

私はJavaでプログラムを書いていますが、ある時点でキーストアのパスワードをロードする必要があります。ただの楽しみのために、私はこれを行うことによって、Javaでパスワードをできるだけ短くしようとしました。

//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

今、私はこのインスタンスでそれがちょっと馬鹿げていることを知っています。他にもできることがたくさんありますが、それらのほとんどは実際には改善されています(変数をまったく使用できませんでした)。

しかし、私はこれを行うことがそれほど愚かではない場合があるかどうか興味がありました。私が考えることができる他の唯一のことはcount、またはのような一般的な変数名を再利用したい場合ですtempが、良い命名規則と短いメソッドはそれが有用になる可能性が低いことです。

可変スコープを縮小するためだけにブロックを使用することが理にかなっている場合はありますか?


13
@gnat ....何?私の質問を編集して、そのだまされたターゲットにあまり似ていないようにする方法すら知りません。これらの質問が同じであると信じるようになった理由は何ですか?
主ファルカード

22
これに対する通常の回答は、匿名ブロックは代わりに重要な名前を持つメソッドである必要があるということです。これにより、自動ドキュメンテーション必要なスコープの縮小が得られます。率直に言って、メソッドを抽出することが裸のブロックを挿入するよりも良くない状況は考えられません。
キリアンフォス

4
C ++についてこれを尋ねる同様の質問がいくつかあります。コードのブロックを作成するのは悪い習慣ですか?そして、中括弧でコードを構造化します。C ++とJavaはこの点で大きく異なりますが、そのうちの1つにこの質問に対する答えが含まれている可能性があります。
アモン


4
@gnatなぜその質問が開かれているのですか?それは私が今まで見た中で最も広いものの一つです。それは標準的な質問の試みですか?
jpmc26

回答:


34

最初に、基礎となるメカニクスに話します:

C ++スコープでは、==ライフタイムb / cデストラクターがスコープの出口で呼び出されます。さらに、C / C ++の重要な違いは、ローカルオブジェクトを宣言できることです。C / C ++のランタイムでは、コンパイラは通常、各スコープ(変数を宣言する)へのエントリにより多くのスタックスペースを割り当てるのではなく、メソッドに必要な大きさのスタックフレームを事前に割り当てます。そのため、コンパイラはスコープを縮小またはフラット化しています。

C / C ++コンパイラは、使用期間が競合しないローカルのスタックストレージスペースを再利用する場合があります(通常、スコープよりも正確なb / cではなく、実際のコードの分析を使用してこれを決定します!)。

C / C ++ b / c Javaの構文、つまり中括弧とスコープは、少なくとも一部はそのファミリから派生したものだと言います。また、C ++が質問のコメントで登場したためです。

対照的に、Javaでローカルオブジェクトを持つことはできません。すべてのオブジェクトはヒープオブジェクトであり、すべてのローカル/フォーマルは参照変数またはプリミティブ型のいずれかです(ところで、静的変数についても同様です)。

さらに、Javaのスコープとライフタイムは厳密に同一視されていません。スコープ内/スコープ外は、ほとんどの場合、アクセシビリティと名前の競合に関するコンパイル時の概念です。変数のクリーンアップに関して、Javaでスコープの終了時に実際に何も起こりません。Javaのガベージコレクションは、オブジェクトの寿命(の終点)を決定します。

また、Javaのバイトコードメカニズム(Javaコンパイラの出力)は、限られたスコープ内で宣言されたユーザー変数をメソッドの最上位レベルに昇格させる傾向があります。これは、バイトコードレベルにスコープ機能がないためです(これは、スタック)。コンパイラは、せいぜいローカル変数スロットを再利用できますが、(C / C ++とは異なり)タイプが同じ場合のみです。

(ただし、ランタイムの基礎となるJITコンパイラーは、バイトコードを十分に分析して、2つの異なる型(および異なるスロット)ローカルで同じスタックストレージスペースを再利用できることを確認します。)

プログラマーの利点について言えば、1回しか使用しなくても、(プライベート)メソッドの方が優れた構成であるという点で他の人に同意する傾向があります。

それでも、名前の矛盾を解消するためにそれを使用しても、実際に問題はありません。

個々のメソッドを作成するのが負担になるとき、私はまれにこれを行いました。たとえばswitch、ループ内で大きなステートメントを使用してインタープリターを作成する場合、(要因に応じて)個別のブロックを導入caseして、個々のケースを異なるメソッド(各一度だけ呼び出されます)。

(ブロック内のコードとして、個々のケースは、囲むループに関する「break;」および「continue;」ステートメントにアクセスできますが、メソッドとしては、これらの制御フローにアクセスするためにブール値と呼び出し元の条件の使用を返す必要がありますステートメント。)


非常に興味深い回答(+1)!ただし、C ++の部分を1つ補完します。パスワードがブロック内の文字列である場合、文字列オブジェクトは、可変サイズ部分のために空きストアのどこかにスペースを割り当てます。ブロックを離れると、文字列が破棄され、変数部分の割り当てが解除されます。ただし、スタック上にないため、すぐに上書きされることを保証するものはありません。文字列が破壊される前に各文字を上書きすることにより、機密性をより適切に管理します;
クリストフ

1
@ErikEidt私はそれが実際には「もの」であるかのように、「C / C ++」といえばの問題に注意を引くためにしようとしていたが、そうではありません:「C / Cのランタイムで++コンパイラは、一般的にスタックフレームが割り当てられます必要とされる可能性のある大きさのメソッドに対して」と書かれていますが、Cにはメソッドがまったくありません。機能があります。これらは、特にC ++では異なります。
-tchrist

7
対照的に、Javaでローカルオブジェクトを使用することはできません。すべてのオブジェクトはヒープオブジェクトであり、すべてのローカル/フォーマルは参照変数またはプリミティブ型です(ところで、静的変数についても同じです)。」特にメソッドのローカル変数についてはそうではありません。エスケープ分析では、オブジェクトをスタックに割り当てることができます。
スパイダーボリス

1
せいぜいコンパイラはローカル変数スロットを再利用できますが、(C / C ++とは異なり)タイプが同じ場合のみです。」は単に間違っています。バイトコードレベルでは、ローカル変数を任意の方法で割り当ておよび再割り当てできます。唯一の制約は、後続の使用が最後に割り当てられたアイテムのタイプと互換性があることです。ローカル変数のスロットを再利用するなどして、ノルムで{ int x = 42; String s="blah"; } { long y = 0; }long変数のスロット再利用されます両方の変数を、xそしてsすべての一般的に使用されるコンパイラ(とjavacecj)。
ホルガー

1
@Boris the Spider:それは頻繁に繰り返される口語的発言であり、実際に起こっていることとは一致しません。エスケープ分析は、オブジェクトのフィールドを変数に分解するスカラー化を有効にします。スカラー化は、さらに最適化されます。それらの一部は未使用として削除される場合もあれば、初期化に使用されたソース変数で折り畳まれたり、CPUレジスタにマップされる場合もあります。結果は、「スタックに割り当てられている」と言ったときに人々が想像するものとほとんど一致しません。
ホルガー

27

私の意見では、ブロックを独自のメソッドに引き出す方がより明確だろう。このブロックを残しておいた場合、最初にブロックを配置する理由を明確にするコメントを見たいと思います。その時点でinitializeKeyManager()、パスワード変数をメソッドのスコープに限定して保持し、コードブロックで意図を示すメソッド呼び出しを行う方がわかりやすくなります。


2
あなたの答えは重要なユースケースによって回避されると思います。それはリファクタリングの中間ステップです。スコープを制限し、メソッドを抽出する直前にコードがまだ機能していることを知っておくと便利です。
ラバーダック

16
@RubberDuck trueですが、その場合、他の人は決してそれを見るべきではないので、私たちがどう思うかは関係ありません。大人のコーダーと同意するコンパイラーが自分のキュービクルのプライバシーで何に気付くかは、他の誰の関心事でもありません。
candied_orange

@candied_orange私はそれを手に入れられないのではないかと心配しています:(詳しく説明してください?
-doubleOrt

@doubleOrt私は、中間リファクタリング手順が現実的かつ必要であることを意味しましたが、誰もが見ることができるソース管理に固執する必要はありません。チェックインする前にリファクタリングを完了してください
。– candied_orange

@candied_orangeああ、あなたは論争していて、RubberDuckの議論を拡張していないと思った。
doubleOrt

17

Rustでは、厳密な借用ルールがあるので便利です。そして一般的に、関数型言語では、そのブロックが値を返します。しかし、Javaのような言語では?アイデアはエレガントに見えますが、実際にはめったに使用されないので、それを見ることによる混乱は、変数の範囲を制限する潜在的な明確さを小さくします。

しかし、これが便利だと思うケースが1つありますswitch。時々変数を宣言したい場合があります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;
}

ただし、これは失敗します。

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

switch ステートメントは奇妙です-それらは制御ステートメントですが、フォールスルーのためにスコープを導入しません。

したがって、ブロックを使用して自分でスコープを作成できます。

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

6
  • 一般的なケース:

可変スコープを縮小するためだけにブロックを使用することが理にかなっている場合はありますか?

一般的に、メソッドを短くすることをお勧めします。一部の変数のスコープを縮小することは、メソッドが非常に長く、変数のスコープを縮小する必要があると考えるほど十分に長いことを示しています。この場合、おそらくコードのその部分に別のメソッドを作成する価値があります。

私が問題があると思うのは、コードのその部分が少数の引数を使用している場合です。それぞれが多くのパラメーターを使用する小さなメソッドになる可能性がある状況があります(または、複数の値を返すことをシミュレートするための回避策が必要です)。その特定の問題の解決策は、使用するさまざまな変数を保持する別のクラスを作成し、比較的短いメソッドで念頭に置いているすべてのステップを実装することです:これは、Builderパターンを使用するための典型的なユースケースです。

ただし、これらのコーディングガイドラインはすべて、大まかに適用できる場合があります。奇妙な場合があります。コードを小さなサブメソッド(またはビルダーパターン)に分割するのではなく、少し長いメソッドで保持すると読みやすくなる場合があります。通常、これは中規模で非常に線形の問題、およびあまりにも多くの分割が実際に読みやすさを妨げる場合に機能します(特に、コードの機能を理解するためにあまりにも多くのメソッド間をジャンプする必要がある場合)。

理にかなっていますが、非常にまれです。

  • ユースケース:

コードは次のとおりです。

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

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

を作成してFileInputStreamいますが、決して閉じません。

これを解決する1つの方法は、try-with-resourcesステートメントを使用することです。スコープを設定しInputStream、必要に応じてこのブロックを使用して他の変数のスコープを縮小することもできます(これらの行は関連しているため)。

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

(ちなみに、のgetDefaultAlgorithm()代わりに使用することもよくありますSunX509。)

また、コードのチャンクを個別のメソッドに分離することは一般的に適切です。わずか3行の場合は、それだけの価値はありません(もしあれば、別のメソッドを作成すると読みにくくなる場合があります)。


「一部の変数のスコープを縮小することは、メソッドが非常に長いことの兆候になる可能性があります」- 兆候である可能性がありますが、ローカル変数の値が使用される期間を文書化するに、IMHO 使用する必要があります。実際、ネストされたスコープをサポートしていない言語(Pythonなど)の場合、対応するコードコメントで変数の「サポート終了」が示されていれば幸いです。同じ変数を後で再利用する場合、それがどこか前(ただし「終末期」コメントの後)に新しい値で初期化されている必要がある(または初期化されるべきである)かどうかは簡単にわかります。
ジョニーディー

2

私の謙虚な意見では、一般に、実稼働コードではほとんど避けるべきものです。なぜなら、異なるタスクを実行する散発的な関数を除いて、一般的にそれをやろうとしないからです。私は物事をテストするために使用されるいくつかのスクラップコードでそれを行う傾向がありますが、関数が自然に非常にローカル状態に関する制限されたスコープ。

tryなぜできないのかという質問をせずに、意味のある方法でスコープを減らすために、このように使用される匿名ブロックの例(条件付き、トランザクションのブロックなどを含まない)を実際に見たことがありません匿名ブロックからの純粋なSEの見地から実際に実際に利益が得られる場合は、スコープを減らしてより単純な関数にさらに分割します。これは通常、折for的なコードであり、大まかに関連する、または非常に関連のないことを行っているため、私たちはこの目的に到達したいと考えています。

例として、という名前の変数を再利用するためにこれを行おうとしcountている場合、2つの異なるものをカウントしていることを示唆しています。変数名がのように短くなる場合、countそれを関数のコンテキストに関連付けることは理にかなっています。次に、countすべてのコードを分析せずに、関数の名前やドキュメントを即座に調べ、参照して、関数が何をしているのかという文脈でそれが何を意味するかを即座に知ることができます。匿名のスコープ/ブロックを代替手段と比べて魅力的にする方法で同じ変数名を再利用する2つの異なるものを数える関数の良い引数を見つけることはあまりありません。これは、すべての関数が1つだけをカウントする必要があることを示唆しているわけではありません。私'同じ変数名を再利用して複数のものをカウントし、匿名ブロックを使用して個々のカウントの範囲を制限する関数のエンジニアリング上の利点。関数がシンプルで明確な場合、2つの異なる名前のカウント変数を使用することは世界の終わりではありません。最初の変数には、理想的に必要とされるよりも数行のスコープ可視性があります。通常、このような関数は、ローカル変数の最小スコープをさらに縮小するために、このような匿名ブロックを持たないエラーの原因ではありません。

余分なメソッドの提案ではない

これは、スコープを縮小するためだけにメソッドを強制的に作成することを示唆するものではありません。それは間違いなく同じくらい悪いか悪いことであり、私が提案していることは、匿名スコープの必要以上に厄介なプライベートな「ヘルパー」メソッドの必要を要求すべきではありません。ブロックを深くネストすることなく、自然にローカル関数の状態をきれいに短時間で可視化する方法で、インターフェイスレベルで問題を概念的に解決する方法を考えるよりも、現在のコードと変数の範囲を減らす方法について考えすぎていますおよび6レベル以上のインデント。3行のコードを強制的に関数に押し込むことでコードの可読性を妨げる可能性があることに同意しますが、それは既存の実装に基づいて作成する関数を進化させるという前提から始まります。実装に絡まることなく機能を設計するよりも。後者の方法で行う場合、無害なコードの数行だけで変数のスコープを縮小しようと熱心に試みない限り、特定のメソッド内の変数スコープを縮小する以外の目的に役立つ匿名ブロックはほとんど必要ありませんこれらの匿名ブロックのエキゾチックな導入は、間違いなく、除去するのと同じくらいの知的オーバーヘッドに貢献します。

最小スコープをさらに削減しようとしています

ローカル変数のスコープを絶対的な最小値に減らすことが価値がある場合、次のようなコードが広く受け入れられるはずです。

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

...そもそもそれらを参照する変数を作成しなくても、状態の可視性が最小限に抑えられるためです。私は独断的になりたくありませんが、実際の解決策は、上記の巨大なコード行を回避するのと同じように、可能な限り匿名ブロックを回避することであり、関数内の正確性と不変式の維持の観点から、コードを関数に編成し、インターフェイスを設計する方法を再検討する価値があると確信しています。当然、メソッドの長さが400行で、変数のスコープが必要以上に300行のコードで見える場合、それは真のエンジニアリングの問題である可能性がありますが、匿名ブロックで解決することは必ずしも問題ではありません。

他に何もなければ、あちこちで匿名ブロックを利用するのはエキゾチックであり、イディオムではなく、エキゾチックなコードは数年後に他人(自分ではないにしても)に嫌われるリスクを伴います。

範囲の縮小の実用的な有用性

変数の範囲を縮小することの究極の有用性は、状態管理を正しくして正しい状態に保ち、コードベースの特定の部分が何をするかを簡単に推論できるようにすることです-概念的な不変条件を維持できるようにするためです。単一の関数のローカル状態管理が非常に複雑であるため、コード内の匿名ブロックを使用してスコープを強制的に縮小する必要があります。このブロックは、最終化されたものではありません。 。ローカル関数スコープ内の変数の状態管理について推論するのが難しい場合、クラス全体のすべてのメソッドにアクセスできるプライベート変数について推論することの難しさを想像してください。匿名ブロックを使用してそれらの可視性を減らすことはできません。私にとって、変数は、多くの言語で理想的に必要な範囲よりもわずかに広い範囲を持つ傾向があるという受け入れから始めるのが役立ちます。ただし、不変式を維持するのが困難になるほど手に負えないという条件があります。匿名のブロックで解決することは、実用的な観点から受け入れられるとは思えません。


最後の注意事項として、私が述べたように、私はまだスクラップブロックのメインエントリポイントメソッドで匿名ブロックを時々使用します。他の誰かがそれをリファクタリングツールとして言及しましたが、結果は中間であり、ファイナライズされたコードではなく、さらにリファクタリングされることを意図しているので、それでいいと思います。私はそれらの有用性を完全に割り引いているわけではありませんが、広く受け入れられ、十分にテストされ、正しいコードを記述しようとしている場合、匿名ブロックの必要性はほとんどわかりません。それらの使用に夢中になると、自然に小さなブロックでより明確な機能を設計することから焦点を移すことができます。

2

可変スコープを縮小するためだけにブロックを使用することが理にかなっている場合はありますか?

動機はブロックを導入する十分な理由だと思います、はい。

Caseyが指摘したように、ブロックは「コードのにおい」であり、ブロックを関数に抽出した方が良いかもしれません。しかし、できません。

ジョンカーマークは、2007年にインラインコードに関するメモを書きました。

  • 関数が単一の場所からのみ呼び出される場合は、インライン化を検討してください。
  • 関数が複数の場所から呼び出された場合、おそらくフラグを使用して作業を単一の場所で行い、それをインラインに配置できるかどうかを確認します。
  • 関数のバージョンが複数ある場合は、デフォルトのパラメータを追加して単一の関数を作成することを検討してください。
  • 作業が純粋に機能的であり、グローバルステートへの参照がほとんどない場合は、完全に機能するようにしてください。

そう考えて、目的を持って選択をして、あなたが行った選択のためのあなたの動機を説明(オプション)休暇証拠。


1

私の意見は議論の余地があるかもしれませんが、はい、確かに、この種のスタイルを使用する状況があると思います。ただし、「変数のスコープを縮小するだけ」は有効な引数ではありません。これは既に行っていることです。そして、それをするためだけに何かをするのは正当な理由ではありません。また、この種の構文が従来のものであるかどうかをチームが既に解決している場合、この説明には意味がないことに注意してください。

私は主にC#プログラマですが、Javaではパスワードをそのまま返すchar[]ので、パスワードがメモリ内に留まる時間を短縮できます。この例では、ガベージコレクターは使用されていない時点から配列を収集することが許可されているため、役に立たないため、パスワードがスコープを離れても問題になりません。私はそれを使い終わった後に配列をクリアすることが実行可能かどうかを議論していませんが、この場合、あなたがそれをしたいなら、変数のスコープは意味があります:

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

これは、try-with-resourcesステートメント実際に似ています。これは、リソースのスコープを決定し、完了後にリソースに対してファイナライズを実行するためです。繰り返しますが、私はこの方法でパスワードを処理することを主張していないことに注意してください。あなたがそうすることを決めた場合、このスタイルは合理的です。

これは、変数が無効になったためです。意味のある情報が含まれないように、作成し、使用し、状態を無効にしました。このブロックの後に変数を使用することは意味がないため、スコープを設定するのが妥当です。

私が考えることができるもう1つの例は、名前と意味が似ている2つの変数があり、一方を使用してからもう一方を使用し、それらを分離したい場合です。私はこのコードを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);
}

ILGenerator il;メソッドの先頭で単純に宣言できると主張することができますが、異なるオブジェクトに変数を再利用したくはありません(ちょっと機能的なアプローチ)。この場合、ブロックにより、実行されるジョブを構文的にも視覚的にも簡単に分離できます。また、ブロックの後、処理が完了しil、何もアクセスすべきではないことを示しています。

この例に対する議論は、メソッドの使用です。たぶんそうかもしれませんが、この場合、コードはそれほど長くありません。また、コードを別のメソッドに分離するには、コードで使用されるすべての変数を渡す必要があります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.