C# と VB.NET の質問掲示板

わんくま同盟

ASP.NET、C++/CLI、Java 何でもどうぞ

C# と VB.NET の入門サイト

ツリー一括表示

finallyで発生した例外の直前のthrowを捕捉したい /ded (24/08/14(Wed) 20:06) #103279
Re[1]: finallyで発生した例外の直前のthrowを捕捉したい /WebSurfer (24/08/14(Wed) 23:50) #103281
Re[1]: finallyで発生した例外の直前のthrowを捕捉したい /とくま (24/08/15(Thu) 08:14) #103282
Re[1]: finallyで発生した例外の直前のthrowを捕捉したい /WebSurfer (24/08/15(Thu) 10:27) #103284
Re[1]: finallyで発生した例外の直前のthrowを捕捉したい /WebSurfer (24/08/15(Thu) 11:49) #103285
  └ Re[2]: finallyで発生した例外の直前のthrowを捕捉したい /ded (24/08/19(Mon) 19:40) #103290
    └ Re[3]: finallyで発生した例外の直前のthrowを捕捉したい /ded (24/08/20(Tue) 10:28) #103291 解決済み


親記事 / ▼[ 103281 ] ▼[ 103282 ] ▼[ 103284 ] ▼[ 103285 ]
■103279 / 親階層)  finallyで発生した例外の直前のthrowを捕捉したい
□投稿者/ ded (7回)-(2024/08/14(Wed) 20:06:20)

分類:[C#] 

VS2022
Win10
C# 8 or 7.9?
Framework 4.8
-----
説明下手なのでソースで語る^^;

procメソッドのtry〜catch〜finallyで、finally句でも例外が発生した場合、
直前のcatchでthrowしているのが無効(?)される。
・出来ればApplicationExceptionのInnerException に NullReferenceExceptionを入れたい
・InnerException で無くても、とりあえずNullReferenceExceptionが発生した事は捕捉したい

よろしくお願いします

-----下記実行結果
System.ApplicationException: Proc:finally
   場所 ConsoleApp1.Program.proc() 場所 C:\Users\dorag\source\repos\ConsoleApp1\ConsoleApp1\Program.cs:行 48
   場所 ConsoleApp1.Program.Main(String[] args) 場所 C:\Users\dorag\source\repos\ConsoleApp1\ConsoleApp1\Program.cs:行 16
InnerException=null
System.ApplicationException: Proc:finally
   場所 ConsoleApp1.Program.proc() 場所 C:\Users\dorag\source\repos\ConsoleApp1\ConsoleApp1\Program.cs:行 48
   場所 ConsoleApp1.Program.Main(String[] args) 場所 C:\Users\dorag\source\repos\ConsoleApp1\ConsoleApp1\Program.cs:行 16
any key.


-----
namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            try
            {
                proc();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                if (ex.InnerException != null)
                    Console.WriteLine(ex.InnerException.ToString());
                else
                    Console.WriteLine($"InnerException=null");
                if (ex.GetBaseException() != null)
                {
                    Exception bex = ex.GetBaseException();
                    Console.WriteLine(bex.ToString());
                }
                else
                    Console.WriteLine($".GetBaseException()=null");
            }

            Console.WriteLine($"any key.");
            Console.Read();
        }

        static void proc()
        {
            try
            {
                throw new NullReferenceException($"proc:Test!!");
            }
            catch (Exception) { throw; }
            finally
            {
                // InnerException に NullReferenceExceptionを入れたい
                throw new ApplicationException($"Proc:finally");
            }
        }
    }
}

[ □ Tree ] 返信 編集キー/

▲[ 103279 ] / 返信無し
■103281 / 1階層)  Re[1]: finallyで発生した例外の直前のthrowを捕捉したい
□投稿者/ WebSurfer (2915回)-(2024/08/14(Wed) 23:50:19)
No103279 (ded さん) に返信

そもそも何がしたいのか分からないのでハズレかもしれませんが・・・

以下のようにして目的は果たせないですか?

using System;

namespace ConsoleApp9
{
    internal class Program
    {
        static string msg = "";

        static void Main(string[] args)
        {
            try
            {
                proc();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.WriteLine(msg);
            }            
        }

        static void proc()
        {
            try
            {
                throw new NullReferenceException("NullReferenceException");
            }
            finally
            {
                try 
                {
                    throw new ApplicationException("ApplicationException");
                }
                catch (Exception ex)
                {
                   msg  = ex.Message;
                }
            }
        }
    }
}

[ 親 103279 / □ Tree ] 返信 編集キー/

▲[ 103279 ] / 返信無し
■103282 / 1階層)  Re[1]: finallyで発生した例外の直前のthrowを捕捉したい
□投稿者/ とくま (13回)-(2024/08/15(Thu) 08:14:32)
2024/08/15(Thu) 09:52:39 編集(投稿者)
catch句で再スローする場合に、引数省略しないと、破棄した意味になっちゃうって
やつ?
https://light11.hatenadiary.com/entry/2020/06/10/215904

ちょっと違うな。編集中。
できるだけ元のPGを維持するとこうかな?作法として良いのか分らんが。
もう、自分でエラー処理を詳細まで分類して、全部作り直した方が良い気もするが。
多分、finally で throw する仕様の意図まで解説してもらわないと、本当に良いの
っていう回答者側の疑問は無くならないんだろうな。

        static void proc()
        {
            Exception exKeep = null;
            try
            {
                throw new NullReferenceException($"proc:Test!!");
            }
            catch  (Exception ex )
            { exKeep = ex; throw; }
            finally
            {
                // InnerException に NullReferenceExceptionを入れたい
                if (exKeep != null)
                {
                    throw new ApplicationException($"Proc:finally", exKeep);
                }
                throw new ApplicationException($"Proc:finally");
            }
        }

補足:本来の設計では、エラーはエラーハンドリングを最上位の呼び出し元のみ記述、
それ以外は正常処理なのでエラー情報などは使わないとするので、finally で何か
区別しなきゃいけないという状況自体がおかしい。。。となる。

ただし、何でもかんでも throw で呼び出し元へ返すクソPGがリリースされてて、
運用に乗ってる場合は下手に手出しできないので、現存のコードを極力変更しない
コーディングが求められる。その辺の背景までちゃんと説明してくれれば、
回答者の書き込みも180度方針が変わる場合もあるし、何より回答しやすい。

[ 親 103279 / □ Tree ] 返信 編集キー/

▲[ 103279 ] / 返信無し
■103284 / 1階層)  Re[1]: finallyで発生した例外の直前のthrowを捕捉したい
□投稿者/ WebSurfer (2916回)-(2024/08/15(Thu) 10:27:12)
No103279 (ded さん) に返信

.NET アプリの例外処置について Microsoft Blog に書いてあったことを要約して
紹介しておきます(Blog は今はリンク切れです)。

以下を読んで、今のやり方を考え直した方が良さそうな気がします。

(1) 予測可能で正しい業務フローに戻すことができる「業務エラー」(例:ユー
  ザーの入力間違い)と、予測できないもしくは予測はできても何の対応もで
  きない「例外」(例:DB サーバーダウン)を区別して対処。

(2) 「例外」はランタイムに拾わせてアプリケーションを停止させる。無かった
  ことにして、ユーザが作業を続けられるようにすると、強制的に停止させる
  より好ましからざる状況に陥るかも(ユーザーが大事なデータを壊したりとか)。

(3) よほどのことがない限り try-catch は書かない。

(4) キャッチせざるを得ない場合でも Execption はキャッチしない。範囲を絞る。
  例えば DB 関係の例外が予測されるなら SqlException に限定して catch し、
  Number プロパティなどでエラーの内容を調べて対処するとか。

(5) 間違って補足してしまった例外は throw する。(注:catch ブロックでキ
  ャッチした例外を throw するとスタックトレースが途切れるので単に
  throw と書く)

(6) ユーザーへの通知が必要なら、集約的例外処置を利用する。

それから、.NET 4 からは破損状態例外は catch できなくなっているそうですが、
「それでも Catch (Exception e) を使用するのはよくない」ということについて
は以下の記事を見てください。

破損状態例外を処理する
https://learn.microsoft.com/ja-jp/archive/msdn-magazine/2009/february/clr-inside-out-handling-corrupted-state-exceptions


以下の記事も参考になると思います。

例外の推奨事項
https://learn.microsoft.com/ja-jp/dotnet/standard/exceptions/best-practices-for-exceptions
[ 親 103279 / □ Tree ] 返信 編集キー/

▲[ 103279 ] / ▼[ 103290 ]
■103285 / 1階層)  Re[1]: finallyで発生した例外の直前のthrowを捕捉したい
□投稿者/ WebSurfer (2917回)-(2024/08/15(Thu) 11:49:54)
No103279 (ded さん) に返信

No103284 の (1) 〜 (5) 書いた件、サンプルコードがあった方が分かりやすいと
思いますので以下に紹介しておきます。

自分が持っている Microsoft の本に載っていたものです。

データベースに INSERT する際に PK 制約違反で発生する例外のみ catch して
「業務エラー」(2 重登録)としてユーザーに再入力を促し、その他は再 throw 
してランタイムに拾わせてアプリケーションを停止させるというものです。

public bool InsertAuthors()
{
    var connection = new SqlConnection("接続文字列");
    var sql = "INSERT INTO authors VALUES ('172-32-1176', 'White', ...)";
    var command = new SqlCommand(sql, connection);
    try
    {
        // 接続プールから接続を取得
        connection.Open();
        try
        {
            // SQL Server への INSERT 処理実行
            command.ExecuteNonQuery();
        }
        // このケースのように例外を catch せざるを得ない場合でも 
        // Execption はキャッチしない。範囲を SqlException に絞る
        catch(SqlException sqle)
        {
            // 2627 は PK 制約違反
            if (sqle.Number == 2627)
            {
                return false;
            }
            else
            {
                // PK 制約違反以外の例外は throw し、ランタイム
                // に拾わせてアプリケーションを停止させる
                throw;
            }
        }
    }
    finally
    {
        // 例外が発生してアプリが停止される状況に陥っても、上の 
        // connection.Open で取得した接続を、必ず接続プールに戻
        // すよう、finally 句で Close する
        connection.Close();
    }

    return true;
}

[ 親 103279 / □ Tree ] 返信 編集キー/

▲[ 103285 ] / ▼[ 103291 ]
■103290 / 2階層)  Re[2]: finallyで発生した例外の直前のthrowを捕捉したい
□投稿者/ ded (8回)-(2024/08/19(Mon) 19:40:51)
みなさん回答ありがとうございます。
#お盆休みで遅くなり申し訳ございません。

再現(質問)用のミニマムコードでしたので、情報不足で申し訳ございません。

まず回答を行います。

・そもそも何がしたいのか分からない
 →COM(Microsoft.Office.Interop.Excel or Word)の操作のため
  →xls/docが過去資産として残っている為、xlsx/docxと決め打ちできない=OpenXML系ライブラリを使用できない
   →さらに補足すると、バッチ処理の一部です。これが落ちると、他のバッチ処理まで止まるためです。

・catch句で再スローする場合に、引数省略しないと、破棄した意味になっちゃうってやつ?
 →その通りです。ExceptionDispatchInfoは知りませんでした。調べてみます。

・例外の推奨事項
 →「イベントが頻繁に発生しない場合、つまり、イベントが本当に例外的であり、予期しないファイルの終わりなどのエラーを示す場合は、例外処理を使用します。 例外処理を使用すると、通常の状況では、実行されるコードが少なくなります。」
  に該当するかと。通常は問題ないのですが、処理中にApplicationが落ちて、
  「System.Runtime.InteropServices.COMException (0x800706BA): RPC サーバーを利用できません。 (HRESULT からの例外:0x800706BA)」が稀に発生するためです

・なぜTry〜Catch〜Finallyなのか?
 →Finally句でRange等の一時オブジェクトの一括開放、BookやapplicationのCloseとログ出力処理(担当者へのメール通知)を行うため
  →Dispose自体はusingで行う
 →Finally句で例外が発生する原因は、Comラッパークラスを利用してusingで開放するのですが、
  アプリケーション側が何らかの原因で落ちると、Close/Exit等で例外が前述の例外(RPCエラー)が発生するのをどうにかしたいと悩んでいます。

・データベースに 〜 アプリケーションを停止させるというものです。
 →このサンプルに近いです。
  →COMException と、 ErrorCode でキャッチする様に工夫してみます。

-----現在のミニマムコード
namespace hoge
{
    internal class Program
    {
        static void Main(string[] args)
        {
            try
            {
                proc(); // 18行目
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                //if (ex.InnerException != null)
                //    Console.WriteLine(ex.InnerException.ToString());
                //else
                //    Console.WriteLine($"InnerException=null");
                //if (ex.GetBaseException() != null)
                //{
                //    Exception bex = ex.GetBaseException();
                //    Console.WriteLine(bex.ToString());
                //}
                //else
                //    Console.WriteLine($".GetBaseException()=null");
            }

            Console.WriteLine($"any key.");
            Console.Read();
        }

        static void proc()
        {
            Exception exKeep = null;
            try
            {
                throw new NullReferenceException($"proc:Test!!");   // 45行目
            }
            catch (Exception ex) { exKeep = ex; throw; }    // 47行目
            finally
            {
                // InnerException に NullReferenceExceptionを入れたい
                try
                {
                    // 実際はCOMException (0x800706BA)
                    unchecked
                    {
                        throw new COMException("Proc:finally", (int)0x800706BA);    // 56行目
                    }
                }
                catch (COMException e)
                {
                    unchecked
                    {
                        if (e.ErrorCode == (int)0x800706BA)
                            throw new ApplicationException($"Proc:finally", exKeep);    // 64行目
                        else
                            throw;
                    }
                }
            }
        }
    }

}

-----現在のミニマムコードの実行結果
System.ApplicationException: Proc:finally ---> System.NullReferenceException: proc:Test!!
   場所 hoge.Program.proc() 場所 C:\Users\71420505\source\repos\hoge\hoge\Program.cs:行 47
   場所 hoge.Program.Main(String[] args) 場所 C:\Users\71420505\source\repos\hoge\hoge\Program.cs:行 18
   --- 内部例外スタック トレースの終わり ---
   場所 hoge.Program.proc() 場所 C:\Users\71420505\source\repos\hoge\hoge\Program.cs:行 64
   場所 hoge.Program.Main(String[] args) 場所 C:\Users\71420505\source\repos\hoge\hoge\Program.cs:行 18
any key.

----期待値
・47行目→45行目のスタックトレースが知りたい
・64行目→56行目のスタックトレースが知りたい
※ExceptionDispatchInfoは現時点で未勉強

-----Comラッパークラス ※どっかのblog?から引用

/// <summary>COMのオブジェクトにIDisposableを実装させる</summary>
/// <typeparam name="T">型指定</typeparam>
public class ComWrapper<T> : IDisposable
{
    public T ComObject { get; }

    public ComWrapper(T comObject)
    {
        this.ComObject = comObject;
    }

    public ComWrapper()
    {
    }

    private bool disposedValue = false;

    protected virtual void Dispose(bool disposing)
    {

        if (!disposedValue)
        {
            if (disposing)
            {
                //nop
            }

            try
            {
                Marshal.ReleaseComObject(ComObject);
            }
            catch (Exception)
            {
                throw;
            }
            disposedValue = true;
        }
    }

    ~ComWrapper()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
    }
}


/// <summary>List()にリソース開放処理</summary>
/// <typeparam name="T">型指定</typeparam>
public class DispoableList<T> : List<T>, IDisposable
{

    /// <summary>開放処理</summary>
    public void Dispose()
    {

        // List()内要素にてリソース解放が必要なら実施
        ForEach(value =>
        {

            if (value is IDisposable disposable)
            {
                disposable.Dispose();
            }

        });

        // List()から全ての要素を削除
        Clear();

    }

}

----- 皆さんの回答を

    internal class Program
    {
        static void Main(string[] args)
        {
            try
            {
                proc();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                if (ex.InnerException != null)
                    Console.WriteLine(ex.InnerException.ToString());
                else
                    Console.WriteLine($"InnerException=null");
                if (ex.GetBaseException() != null)
                {
                    Exception bex = ex.GetBaseException();
                    Console.WriteLine(bex.ToString());
                }
                else
                    Console.WriteLine($".GetBaseException()=null");
            }

            Console.WriteLine($"any key.");
            Console.Read();
        }

        static void proc()
        {
            Exception exKeep = null;
            try
            {
                throw new NullReferenceException($"proc:Test!!");
            }
            catch (Exception ex) { exKeep = ex; throw; }
            finally
            {
                // InnerException に NullReferenceExceptionを入れたい
                if( e != null ) throw new ApplicationException($"Proc:finally", exKeep);
                else throw new ApplicationException($"Proc:finally");
            }
        }
    }



[ 親 103279 / □ Tree ] 返信 編集キー/

▲[ 103290 ] / 返信無し
■103291 / 3階層)  Re[3]: finallyで発生した例外の直前のthrowを捕捉したい
□投稿者/ ded (9回)-(2024/08/20(Tue) 10:28:48)
メインロジックに実装してみたところ、throwするのではなく、COMExceptionが「発生」するので前述の方法が使えず。
ExceptionDispatchInfoを調べてみたところ、下記で満足しました。

COMException が知りたいのではなく、COMException が発生する前の例外(ミニマムコードですとNullReferenceException)が発生したことを知りたいので、
ExceptionDispatchInfoを利用することにしました。

例外の推奨事項 − 例外をキャプチャして適切に再スローする
https://learn.microsoft.com/ja-jp/dotnet/standard/exceptions/best-practices-for-exceptions#capture-exceptions-to-rethrow-later
こんな記述もありなのですね。

みなさん、ありがとうございました。

-----
        static void proc()
        {
            Exception exKeep = null;
            try
            {
                throw new NullReferenceException($"proc:Test!!");   
            }
            catch (Exception ex) { exKeep = ex; throw; }    
            finally
            {
                // InnerException に NullReferenceExceptionを入れたい
                try
                {
                    // 実際はCOMException (0x800706BA)
                    unchecked
                    {
                        throw new COMException("Proc:finally", (int)0x800706BA);
                    }
                }
                catch (COMException e)
                {
                    unchecked
                    {
                        if (e.ErrorCode == (int)0x800706BA)
                        {
                            //throw new ApplicationException($"Proc:finally", exKeep); // Throwするのではなく発生するので、この方法を使えず...
                            if (exKeep != null)
                            {
                                //ExceptionDispatchInfo.Capture(exKeep).Throw(e); // .NET Core 1.1〜 .NET Std 2.1〜
                                ExceptionDispatchInfo.Capture(exKeep).Throw();
                            }
                            else
                                throw;
                        }
                        else
                            throw;
                    }
                }
            }
        }

解決済み
[ 親 103279 / □ Tree ] 返信 編集キー/


管理者用

- Child Tree -