web-dev-qa-db-ja.com

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

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

さて、私はこの例でそれがちょっとばかげていることを知っています。他にもできることがたくさんありますが、それらのほとんどは実際には優れています(変数をまったく使用できなかったでしょう)。

しかし、これを行うのがそれほど馬鹿ではない場合があるかどうか、私は興味を持ちました。他に考えられる唯一のことは、counttempのような一般的な変数名を再利用したい場合ですが、優れた命名規則と短いメソッドにより、それが役に立たない可能性は低くなります。

変数スコープを減らすためにブロックonlyを使用することが理にかなっているケースはありますか?

39
Lord Farquaad

まず、基礎となる力学に話します:

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++処理に似ています。コンパイラーはせいぜいローカル変数スロットを再利用できますが、(C/C++とは異なり)タイプが同じ場合のみです。

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

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

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

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

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

35
Erik Eidt

私の意見では、ブロックを独自の方法に引き出す方が明確です。このブロックを残した場合、そもそもなぜブロックをそこに置くのかを明確にするコメントを期待しています。この時点で、initializeKeyManager()へのメソッド呼び出しがあり、パスワード変数がメソッドのスコープに制限されたままになり、コードブロックで意図が示されているように感じるだけです。

27
Casey Langford

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

switchステートメントで、これが役立つと思われるケースが1つあります。 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;
}
17
Idan Arye
  • 一般的なケース:

変数のスコープを減らすためだけにブロックを使用することが理にかなっているケースはありますか?

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

私が問題を感じるケースは、コードのその部分が少数の引数以上のものを使用する場合です。それぞれが多くのパラメーターを受け取る(または複数の値を返すことをシミュレートするための回避策が必要になる)小さなメソッドで終わる可能性がある状況があります。その特定の問題の解決策は、使用するさまざまな変数を保持し、比較的短いメソッドで心に留めておくすべてのステップを実装する別のクラスを作成することです。これは、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);
}
_

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

さらに、コードのチャンクを個別のメソッドに分離するというアドバイスは、一般的に適切です。 3行だけの場合はほとんど価値がありません(別のメソッドを作成すると読みやすさが低下する場合があります)。

6
Bruno

私の控えめな意見では、異なるタスクを実行する散発的な関数を除いて、一般的にそれを実行するように誘惑されないので、それは一般に量産コードではほとんど避けるべきものです。私は物事をテストするために使用されるいくつかのスクラップコードでそれを行う傾向がありますが、私は各関数が何をすべきかについて事前にいくつかの考えを置いた生産コードでそれを行う誘惑を見つけることができません。その地方の状態に関して限られた範囲。

質問をしなかった関数で意味のある方法でスコープを減らすために、このように匿名ブロックが使用される例(条件、トランザクションのtryブロックなどを含まない)を見たことはありませんそれが、匿名ブロックからの本物のSEの観点から実際に実際に利益を得た場合、スコープを削減した単純な関数にさらに分割できない理由の説明です。私たちがこれに手を伸ばそうとしているのは、通常、緩やかに関連する、または非常に関連のないことを実行している折衷的なコードです。

例として、countという名前の変数を再利用するためにこれを行おうとしている場合、2つの異なるものを数えることを示唆しています。変数名がcountのように短くなる場合、それを関数のコンテキストに関連付けることが理にかなっています。これは、1つのタイプのことを数えるだけの可能性があります。次に、関数の名前やドキュメントを即座に確認し、countを参照して、すべてのコードを分析しなくても、関数が何をしているかというコンテキストでそれが何を意味するかを即座に知ることができます。匿名のスコープ/ブロックを他の方法よりも魅力的にする方法で同じ変数名を再利用する2つの異なるものを数える関数に適切な引数が見つからないことがよくあります。これは、すべての関数が1つだけを数える必要があることを示唆しているわけではありません。同じ変数名を再利用して2つ以上のものを数え、無名ブロックを使用してスコープを制限する関数へのエンジニアリングの利点はほとんどない実用的なと私は言っています個々のカウント。関数が単純で明確な場合、2つの異なる名前のcount変数があり、最初の変数が理想的に必要とされるよりも数行多いスコープの可視性を持つことは、世界の終わりではありません。そのような関数は一般に、そのローカル変数の最小スコープをさらに減らすために、そのような匿名ブロックを欠いているエラーの原因ではありません。

余分なメソッドの提案ではありません

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

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

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

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

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

何もしなければ、いたるところに匿名ブロックを利用することは、エキゾチックであり、慣用的ではなく、エキゾチックなコードは、数年後、自分ではないとしても他の人に嫌われるリスクがあります。

スコープの縮小の実用的有用性

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

2
user204677

変数のスコープを減らすためだけにブロックを使用することが理にかなっているケースはありますか?

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

Caseyが述べたように、ブロックは「コードの匂い」であり、ブロックを関数に抽出する方がよい場合があることを示しています。しかし、そうではないかもしれません。

John Carmarkは、2007年に インラインコードに関するメモ を書きました。彼の結論の要約

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

したがって、考える、目的を持って選択し、(オプション)選択した動機を説明する証拠を残します。

2
VoiceOfUnreason

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

私は主に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つの変数があり、1つを処理してから別の変数を処理し、それらを別々に保持したい場合です。私はこのコードを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が完了し、何もアクセスしないように指示しています。

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