例外を正しく使用する方法

例外を使用してプログラムのフローを制御することは、 長年の トピックです。 このトピックを要約し、例外の正しい使用と誤った使用の例を挙げたいと思います。



ifsの代わりの例外:なぜですか?



ほとんどの場合、書くよりも頻繁にコードを読みます。 ほとんどのプログラミング手法は、コードの理解を単純化することを目的としています。コードが単純であるほど、含まれるバグが少なくなり、サポートが容易になります。



例外を使用してプログラムの進行を制御すると、プログラマの意図が隠されるため、これは悪い習慣と見なされます。



public void ProcessItem(Item item) { if (_knownItems.Contains(item)) { // Do something throw new SuccessException(); } else { throw new FailureException(); } }
      
      





ProcessItemメソッドは、コードの理解を複雑にします。 その署名を見ただけでは、その実装の可能な結果は何であるかを言うことは不可能です。 このようなコードは、 最も驚き少ない原則に違反しています。 肯定的な結果の場合でも例外をスローします。



この特定の場合、解決策は明らかです。例外をスローすることをブール値の戻り値に置き換える必要があります。 より複雑な例を見てみましょう。



着信データの検証の例外



おそらく、例外のコンテキストで最も一般的な方法は、無効な入力を受け取った場合に例外を使用することです。



 public class EmployeeController : Controller { [HttpPost] public ActionResult CreateEmployee(string name, int departmentId) { try { ValidateName(name); Department department = GetDepartment(departmentId); // Rest of the method } catch (ValidationException ex) { // Return view with error } } private void ValidateName(string name) { if (string.IsNullOrWhiteSpace(name)) throw new ValidationException(“Name cannot be empty”); if (name.Length > 100) throw new ValidationException(“Name length cannot exceed 100 characters”); } private Department GetDepartment(int departmentId) { using (EmployeeContext context = new EmployeeContext()) { Department department = context.Departments .SingleOrDefault(x => x.Id == departmentId); if (department == null) throw new ValidationException(“Department with such Id does not exist”); return department; } } }
      
      





明らかに、このアプローチにはいくつかの利点があります。任意のメソッドからCreateEmployeeメソッドのcatchブロックに直接「戻る」ことができます。



次の例を見てみましょう。



 public static Employee FindAndProcessEmployee(IList<Employee> employees, string taskName) { Employee found = null; foreach (Employee employee in employees) { foreach (Task task in employee.Tasks) { if (task.Name == taskName) { found = employee; goto M1; } } } // Some code M1: found.IsProcessed = true; return found; }
      
      





これら2つのサンプルの共通点は何ですか? どちらも現在の実行スレッドを中断し、コード内の特定のポイントにすばやくジャンプできます。 このコードの唯一の問題は、読みやすさが著しく低下することです。 どちらのアプローチもコードの理解を難しくしているため、プログラムの実行フローを制御するための例外の使用は、gotoを使用して頻繁に均等化されます。



例外を使用する場合、それらがどこでキャッチされるかを正確に理解することは困難です。 同じメソッドのtry / catchブロックに例外をスローするコードをラップするか、try / catchブロックをスタックのいくつかのレベルに配置できます。 これが意図的なものであるかどうかを確実に知ることはできません。



 public Employee CreateEmployee(string name, int departmentId) { //          try/catch ? ValidateName(name); // Rest of the method }
      
      





見つける唯一の方法は、スタック全体を分析することです。 検証に使用される例外により、コードがはるかに読みにくくなります。 開発者の意図を明確に示していません。 そのようなコードを見て、何が間違っているのか、どう対処するのかを言うのは不可能です。



もっと良い方法はありますか? もちろん:



 [HttpPost] public ActionResult CreateEmployee(string name, int departmentId) { if (!IsNameValid(name)) { // Return view with error } if (!IsDepartmentValid(departmentId)) { // Return view with another error } Employee employee = new Employee(name, departmentId); // Rest of the method }
      
      





すべてのチェックを明示的に指定すると、意図がより明確になります。 このバージョンのメソッドはシンプルで明白です。



例外の例外



それでは、いつ例外を使用するのでしょうか? 例外の主な目標は驚きです! -アプリケーションの例外的な状況を示します。 例外的な状況とは、何をすべきかわからない状況で、現在の操作を停止することが最善の方法です(場合によっては、エラーの詳細を事前にログに記録してください)。



例外的な状況の例には、データベースへの接続の問題、必要な構成ファイルの不足などが含まれます。 検証エラーは例外はありません。 定義により、着信データをチェックするメソッドは、それらが正しくないと予想します。



例外の正しい使用の別の例は、コードコントラクトの検証です。 あなたは、クラスの作成者として、このクラスの顧客がその契約に従うことを期待しています。 メソッドコントラクトが尊重されない状況は例外的であり、例外をスローするに値します。



他のライブラリによってスローされた例外を処理する方法は?



状況が例外的かどうかは、状況によって異なります。 サードパーティライブラリの開発者は、データベースへの接続の問題に対処する方法を知らない場合があります。 彼は自分のライブラリがどのようなコンテキストで使用されるかを知りません。



同様の問題が発生した場合、ライブラリ開発者はそれで何もできないため、例外をスローすることが適切な解決策になります。 Entity FrameworkまたはNHibernateを例に取ることができます。彼らはデータベースが常に利用可能であることを期待し、そうでない場合は例外をスローします。



一方、ライブラリを使用する開発者 、データベースが時々オフラインになり、この事実に基づいてアプリケーションを開発することを期待できます。 データベースに障害が発生した場合、クライアントアプリケーションは同じ操作を繰り返したり、後で操作を繰り返すことを提案するメッセージをユーザーに表示しようとする場合があります。



したがって、状況は、基礎となるコードの観点からは例外的であり、クライアントコードの観点からは予想される場合があります。 この場合、そのようなライブラリによってスローされた例外をどのように処理しますか?



同様の例外は、それらをスローするコードのできるだけ近くでキャッチする必要があります 。 そうでない場合、コードにはgotoを使用したサンプルコードと同じ欠陥があります。コールスタック全体を分析しないと、この例外が処理される場所を理解することはできません。



 public void CreateCustomer(string name) { Customer customer = new Customer(name); bool result = SaveCustomer(customer); if (!result) { MessageBox.Show(“Error connecting to the database. Please try again later.”); } } private bool SaveCustomer(Customer customer) { try { using (MyContext context = new MyContext()) { context.Customers.Add(customer); context.SaveChanges(); } return true; } catch (DbUpdateException ex) { return false; } }
      
      





上記の例でわかるように、SaveCustomerメソッドはデータベースの問題を予期し、これに関連するすべてのエラーを意図的にキャッチします。 ブールフラグを返します。これは、コードの上流で処理されます。



SaveCustomerメソッドには、クライアントの保存プロセス中に問題が発生する可能性があること、これらの問題が予想されること、戻り値をチェックしてすべてが正常であることを示す明確な署名があります。



この場合に適用される広く知られている慣行に注目する価値があります。汎用ハンドラーでそのようなコードをラップする必要はありません。 ジェネリックハンドラーは、例外が予想されると主張しますが、これは本質的に真実ではありません。



例外が本当に予想される場合は、ごく限られた数の例外に対してのみ実行します。例外を処理できることは確かです。 汎用ハンドラーを配置すると、予期しない例外が飲み込まれ、アプリケーションが一貫性のない状態になります。



汎用ハンドラーを適用できる唯一の状況は、それらを保護するために、アプリケーションスタックの最上位に配置して、以下のコードでキャッチされないすべての例外をキャッチすることです。 このような例外を処理しようとしないでください;できるのは、アプリケーションを閉じる(ステートフルアプリケーションの場合)か、現在の操作を終了する(ステートレスアプリケーションの場合)だけです。



例外とフェイルファーストの原則





このようなコードに出くわす頻度はどれくらいですか?



 public bool CreateCustomer(int managerId, string addressString, string departmentName) { try { Manager manager = GetManager(managerId); Address address = CreateAddress(addressString); Department department = GetDepartment(departmentName); CreateCustomerCore(manager, address, department); return true; } catch (Exception ex) { _logger.Log(ex); return false; } }
      
      





これは、汎用例外ハンドラーの誤った使用例です。 上記のコードは、メソッドの本体から発生するすべての例外が、カスタムの作成プロセスにおけるエラーの兆候であることを意味しています。 このコードの問題は何ですか?



上記の「goto」セマンティクスとの類似性に加えて、問題はcatchブロックに来る例外が既知の例外ではない可能性があることです。 例外は、予想されるArgumentExceptionまたはContractViolationExceptionのいずれかです。 後者の場合、このような例外を処理する方法を知っているふりをして、バグを隠します。



その後、予期しないクラッシュからアプリケーションを保護したい開発者も同様のアプローチを採用しています。 実際、このようなアプローチは問題をマスクするだけであり、問​​題をキャッチするプロセスをより複雑にします。



予期しない例外に対処する最善の方法は、現在の操作を完全に停止し、アプリケーションが一貫性のない状態を伝播しないようにすることです。



おわりに





元の記事へのリンク: C#でのフロー制御の例外



All Articles