複数の返品ステートメント
以前は、メソッドの出口が 1 つになるように努力していたと聞いたことがあります。私はこれが時代遅れのアプローチであることを理解しており、特に注目に値するとは考えていませんでした。しかし最近、私はまだその考えに固執している何人かの開発者と接触し (前回はここにいました)、考えさせられました.
そこで初めて、実際に腰を下ろして 2 つのアプローチを比較しました。
概要
投稿の最初の部分では、複数の return ステートメントに対する引数と反対の引数を繰り返します。また、これらの議論を評価する際にクリーンなコードが果たす重要な役割も特定します。 2 番目の部分では、早期復帰によってメリットが得られる状況を分類します。
「return ステートメントが複数あるメソッド」について常に書くわけではないので、そのようにメソッドを構造化するアプローチをパターンと呼びます。これは少しやり過ぎかもしれませんが、より簡潔であることは間違いありません。
ディスカッション
メソッドを常に最後の行まで実行するか、結果を返す場所にするか、または複数の return ステートメントを使用して「早期に返す」ことができるかについて議論しています。
もちろん、これは新しい議論ではありません。たとえば、Wikipedia、Hacker Chick、StackOverflow などを参照してください。
構造化プログラミング
単一の return ステートメントが望ましいという考えは、1960 年代に開発された構造化プログラミングのパラダイムに由来します。サブルーチンに関しては、単一の入り口と単一の出口点を持つことを促進します。最新のプログラミング言語は前者を保証していますが、後者はいくつかの理由でやや時代遅れです.
単一の出口点が解決した主な問題は、メモリまたはリソースのリークでした。これらは、return ステートメントがメソッド内のどこかで、その最後にあるいくつかのクリーンアップ コードの実行を妨げたときに発生しました。現在、その多くは言語ランタイム (ガベージ コレクションなど) によって処理され、明示的なクリーンアップ ブロックは try-catch-finally で記述できます。そのため、議論は主に可読性を中心に展開されます。
読みやすさ
単一の return ステートメントに固執すると、ネストが増加し、追加の変数が必要になる可能性があります (たとえば、ループを中断するため)。一方、複数のポイントからメソッドが返されると、その制御フローに関して混乱が生じ、保守性が低下する可能性があります。コードの全体的な品質に関して、これら 2 つの側面の動作が大きく異なることに注意することが重要です。
クリーンなコーディング ガイドラインに準拠する方法を考えてみましょう。それは短く、明確な名前と意図を明らかにする構造を備えています。より多くのネストとより多くの変数を導入することによる可読性の相対的な損失は非常に顕著であり、きれいな構造を混乱させる可能性があります.しかし、メソッドはその簡潔さと形式により簡単に理解できるため、return ステートメントを見落とす大きなリスクはありません。そのため、複数存在する場合でも、制御フローは明らかです。
これを、より長い方法、おそらく複雑または最適化されたアルゴリズムの一部と比較してください。今、状況は逆転しています。メソッドには、すでにいくつかの変数と、おそらくいくつかのレベルのネストが含まれています。より多くを導入しても、読みやすさの相対的なコストはほとんどありません。しかし、いくつかのリターンのうちの 1 つを見落として、制御フローを誤解するリスクは非常に現実的です。
したがって、メソッドが短くて読みやすいかどうかという問題になります。そうであれば、複数の return ステートメントを使用すると、通常は改善されます。そうでない場合は、単一の return ステートメントが望ましいです。
その他の要因
ただし、読みやすさだけが要因ではないかもしれません。
この議論のもう 1 つの側面は、ロギングです。戻り値をログに記録したいが、アスペクト指向プログラミングに頼らない場合は、メソッドの出口ポイントにログ ステートメントを手動で挿入する必要があります。複数の return ステートメントでこれを行うのは面倒で、1 つを忘れるのは簡単です。
同様に、メソッドから戻る前に結果の特定のプロパティをアサートする場合は、単一の出口点を優先する場合があります。
返品明細書が複数ある状況
メソッドが複数の return ステートメントから利益を得ることができる状況には、いくつかの種類があります。ここでそれらを分類しようとしましたが、完全なリストがあるとは主張していません。 (別の状況が繰り返される場合は、コメントを残してください。それを含めます。)
すべての状況には、コード サンプルが付属しています。これらは要点を伝えるために短縮されており、いくつかの方法で改善できることに注意してください。
CC-BY 2.0 の下で JDHancock により発行
ガード条項
ガード句はメソッドの先頭に立ちます。それらはその引数をチェックし、特定の特殊なケースではすぐに結果を返します。
null または空のコレクションに対する保護条項
private Set<T> intersection(Collection<T> first, Collection<T> second) { // intersection with an empty collection is empty if (isNullOrEmpty(first) || isNullOrEmpty(second)) return new HashSet<>(); return first.stream() .filter(second::contains) .collect(Collectors.toSet()); }
最初に特殊なケースを除外することには、いくつかの利点があります。
- 特別なケースと通常のケースの処理が明確に分離され、読みやすさが向上します
- 読みやすさを維持する追加チェックのデフォルトの場所を提供します
- 通常のケースの実装でエラーが発生しにくくなります
- これらの特殊なケースではパフォーマンスが向上する可能性があります (ただし、これが関連することはめったにありません)
基本的に、このパターンが適用可能なすべてのメソッドは、このパターンを使用することで恩恵を受けます。
ガード節の注目に値する支持者は Martin Fowler ですが、分岐の端での彼の例を検討します (以下を参照)。
分岐
一部のメソッドの責任は、多くの場合特殊なサブルーチンの 1 つに分岐する必要があります。通常、これらのサブルーチンを独自のメソッドとして実装するのが最善です。元のメソッドには、いくつかの条件を評価して正しいルーチンを呼び出すという唯一の責任が残されます。
特殊なメソッドへの委任
public Offer makeOffer(Customer customer) { boolean isSucker = isSucker(customer); boolean canAffordLawSuit = customer.canAfford( legalDepartment.estimateLawSuitCost()); if (isSucker) { if (canAffordLawSuit) return getBigBucksButStayLegal(customer); else return takeToTheCleaners(customer); } else { if (canAffordLawSuit) return getRid(customer); else return getSomeMoney(customer); } }
(すべての else
を省略できることはわかっています。 -行。いつの日か、このような場合に私がそうしない理由を説明する投稿を書くかもしれません.)
複数の return ステートメントを使用すると、結果変数と単一の return に比べていくつかの利点があります。
final
の場合、これは単一の戻り値でも実現できます) そしてそのクラスは不変です。ただし、後者は読者には明らかではありません)break
がないため、即時 return ステートメントはケースごとに行を節約します。 定型文を減らし、読みやすさを向上させる必要がありますこのパターンは、分岐以外にほとんど何もしないメソッドにのみ適用する必要があります。ブランチがすべての可能性をカバーすることが特に重要です。これは、分岐ステートメントの下にコードがないことを意味します。存在する場合、メソッドを介したすべてのパスについて推論するには、はるかに多くの労力が必要になります。メソッドがこれらの条件を満たす場合、メソッドは小さくてまとまりがあり、理解しやすくなります。
カスケード チェック
メソッドの動作が主に複数のチェックで構成され、各チェックの結果によってそれ以上のチェックが不要になる場合があります。その場合は、できるだけ早く (おそらく各チェック後) 戻ることをお勧めします。
アンカーの親を探している間のチェックのカスケード
private Element getAnchorAncestor(Node node) { // if there is no node, there can be no anchor, // so return null if (node == null) return null; // only elements can be anchors, // so if the node is no element, recurse to its parent boolean nodeIsNoElement = !(node instanceof Element); if (nodeIsNoElement) return getAnchorAncestor(node.getParentNode()); // since the node is an element, it might be an anchor Element element = (Element) node; boolean isAnchor = element.getTagName().equalsIgnoreCase("a"); if (isAnchor) return element; // if the element is no anchor, recurse to its parent return getAnchorAncestor(element.getParentNode()); }
この他の例は、equals
の通常の実装です。 または compareTo
ジャワで。また、通常、各チェックがメソッドの結果を決定するカスケード チェックで構成されます。存在する場合、値はすぐに返されます。それ以外の場合、メソッドは次のチェックを続行します。
単一の return ステートメントと比較すると、このパターンでは、より深いインデントを防ぐためにフープをジャンプする必要はありません。また、新しいチェックを追加して、チェック アンド リターン ブロックの前にコメントを配置することも簡単になります。
分岐と同様に、複数の return ステートメントは、短くて他にほとんど何もしないメソッドにのみ適用する必要があります。カスケード チェックは、(入力の検証を除いて) 中心的な、またはできれば唯一のコンテンツである必要があります。チェックまたは戻り値の計算に 2 行または 3 行以上が必要な場合は、別のメソッドにリファクタリングする必要があります。
検索中
データ構造があるところには、特別な条件を持つ項目があります。それらを検索するメソッドは、よく似ています。そのようなメソッドが探していた項目に遭遇した場合、多くの場合、すぐに返すのが最も簡単です。
見つかった要素をすぐに返す
private <T> T findFirstIncreaseElement(Iterable<T> items, Comparator<? super T> comparator) { T lastItem = null; for (T currentItem : items) { boolean increase = increase(lastItem, currentItem, comparator); lastItem = currentItem; if (increase) { return currentItem; } } return null; }
単一の return ステートメントと比較すると、ループから抜け出す方法を探す手間が省けます。これには次の利点があります。
- ループを中断する追加のブール変数はありません
- ループには追加の条件がないため (特に for ループでは) 見過ごされやすく、バグを助長します
- 最後の 2 つのポイントをまとめることで、ループがずっと理解しやすくなります
- ほとんどの場合、メソッド全体にまたがる結果用の追加の変数はありません
複数の return ステートメントを使用するほとんどのパターンと同様に、これにもクリーンなコードが必要です。メソッドは小さく、検索以外の責任を持たない必要があります。重要なチェックと結果の計算には、独自のメソッドが必要です。
リフレクション
複数の return ステートメントに対する賛否両論と、クリーンなコードが果たす重要な役割を見てきました。分類は、メソッドが早期に戻ることでメリットが得られる繰り返しの状況を特定するのに役立ちます。