programing

던지지 않도록 IDisposable.Dispose ()를 구현해야합니까?

procenter 2021. 1. 15. 19:47
반응형

던지지 않도록 IDisposable.Dispose ()를 구현해야합니까?


C ++ (소멸자)의 동등한 메커니즘의 경우 일반적으로 예외를 throw하지 않아야한다는 조언이 있습니다. 이것은 주로 그렇게함으로써 프로세스를 종료 할 수 있기 때문에 매우 드물게 좋은 전략입니다.

.NET의 동등한 시나리오에서 ...

  1. 첫 번째 예외가 발생합니다.
  2. finally 블록은 첫 번째 예외의 결과로 실행됩니다.
  3. finally 블록은 Dispose () 메서드를 호출합니다.
  4. Dispose () 메서드에서 두 번째 예외가 발생합니다.

... 프로세스가 즉시 종료되지 않습니다. 그러나 .NET이 첫 번째 예외를 두 번째 예외로 잘못 대체하기 때문에 정보가 손실됩니다. 따라서 호출 스택의 어딘가에있는 catch 블록은 첫 번째 예외를 볼 수 없습니다. 그러나 일반적으로 일이 잘못되기 시작한 이유에 대한 더 나은 단서를 제공하기 때문에 일반적으로 첫 번째 예외에 더 관심이 있습니다.

.NET에는 예외가 보류중인 동안 코드가 실행 중인지 여부를 감지하는 메커니즘이 없기 때문에 IDisposable을 구현할 수있는 방법은 실제로 두 가지 뿐인 것 같습니다.

  • 항상 Dispose () 내에서 발생하는 모든 예외를 삼키십시오. OutOfMemoryException, ExecutionEngineException 등을 삼키게 될 수도 있기 때문에 좋지 않습니다. 이미 보류중인 다른 예외없이 발생하면 프로세스를 해체 할 수 있습니다.
  • 모든 예외가 Dispose () 밖으로 전파되도록합니다. 문제의 근본 원인에 대한 정보를 잃을 수 있으므로 좋지 않습니다. 위를 참조하십시오.

그렇다면 두 가지 악 중 어느 것이 더 적습니까? 더 좋은 방법이 있습니까?

편집 : 명확히하기 위해 Dispose ()에서 예외를 적극적으로 던지는 것에 대해 이야기하고 있지 않습니다. 예를 들어 Dispose ()에서 호출 한 메서드에 의해 발생한 예외가 Dispose ()에서 전파되는지 여부에 대해 이야기하고 있습니다.

using System;
using System.Net.Sockets;

public sealed class NntpClient : IDisposable
{
    private TcpClient tcpClient;

    public NntpClient(string hostname, int port)
    {
        this.tcpClient = new TcpClient(hostname, port);
    }

    public void Dispose()
    {
        // Should we implement like this or leave away the try-catch?
        try
        {
            this.tcpClient.Close(); // Let's assume that this might throw
        }
        catch
        {
        }
    }
}

나는 삼키는 것이이 시나리오에서 두 가지 악 중 더 적은 것이라고 주장 할 것이다. 원래의 Exception 경고 를 높이는 것이 더 낫기 때문이다., 그렇지 않다면 , 아마도 깨끗하게 처리하는 데 실패하는 것은 그 자체로 매우 치명적일 것이다 (아마도 TransactionScope처리 할 수 없다면 , 롤백 실패를 나타낼 수 있음).

래퍼 / 확장 방법 아이디어를 포함하여 이에 대한 더 많은 생각을 보려면 여기참조 하십시오 .

using(var foo = GetDodgyDisposableObject().Wrap()) {
   foo.BaseObject.SomeMethod();
   foo.BaseObject.SomeOtherMethod(); // etc
} // now exits properly even if Dispose() throws

물론, 원래 예외와 두 번째 ( Dispose()) 예외 를 모두 사용하여 복합 예외를 다시 던지는 이상한 일을 할 수도 있습니다 . 그러나 생각 : 여러 using블록을 가질 수 있습니다 . 빠르게 관리 할 수 ​​없게됩니다. 실제로 원래 예외는 흥미로운 것입니다.


프레임 워크 디자인 지침 (2 에드) (§9.4.1)로이있다 :

피하기 포함하는 프로세스가 손상된 중요한 상황 (누출 일관성 공유 상태 등)을 제외하고 폐기 (BOOL) 내에서 예외 발생.

논평 [편집] :

  • 엄격한 규칙이 아닌 지침이 있습니다. 그리고 이것은 "하지 말 것"지침이 아니라 "방지"입니다. 주석에서 언급했듯이 프레임 워크는이 (및 기타) 지침을 제자리에서 위반합니다. 요령은 지침을 어길 때를 아는 것입니다. 그것은 여러면에서 Journeyman과 Master의 차이입니다.
  • 정리의 일부가 실패 할 수있는 경우 호출자가 처리 할 수 ​​있도록 예외를 throw하는 Close 메서드를 제공해야합니다.
  • dispose 패턴을 따르는 경우 (유형에 관리되지 않는 리소스가 직접 포함되어 Dispose(bool)있어야 함)는 종료 자에서 호출 될 수 있으며, 종료 자에서 던지는 것은 나쁜 생각이며 다른 개체가 종료되는 것을 차단합니다.

견해 : Dispose에서 벗어나는 예외는 지침에서와 같이 현재 프로세스에서 더 이상 신뢰할 수있는 기능을 사용할 수 없을만큼 충분히 재앙 적이어야합니다.


Dispose목적을 수행하고 개체를 처리하도록 설계되어야합니다. 이 작업 은 안전하며 대부분의 경우 예외를 발생시키지 않습니다 . 에서 예외를 던지는 Dispose것을 본다면 그 안에서 너무 많은 일을하고 있는지 두 번 생각해야 할 것입니다. 그 외에도 Dispose다른 모든 방법처럼 취급해야 한다고 생각 합니다. 할 수 있으면 처리하고, 할 수 없으면 거품을 내야합니다.

편집 : 지정된 예제의 경우 내 코드 가 예외를 일으키지 않도록 코드를 작성 하지만 정리하면 TcpClient예외가 발생할 수 있습니다. 이는 내 의견 으로 전파 하거나 더 일반적인 것으로 처리하고 다시 던져야합니다. 모든 방법과 마찬가지로 예외) :

public void Dispose() { 
   if (tcpClient != null)
     tcpClient.Close();
}

그러나 다른 메소드와 마찬가지로 tcpClient.Close()무시해야하거나 (중요하지 않음) 다른 예외 객체로 표시해야하는 예외가 발생할 수 있다는 것을 알고 있다면 이를 포착 할 수 있습니다.


리소스 해제는 "안전한"작업이어야합니다. 리소스를 해제 할 수없는 상태에서 어떻게 복구 할 수 있습니까? 따라서 Dispose에서 예외를 던지는 것은 의미가 없습니다.

그러나 Dispose 내부에서 프로그램 상태가 손상되었음을 발견하면 예외를 던진 다음 삼키는 것이 더 낫습니다. 지금 부수고 실행을 계속하고 잘못된 결과를 생성하는 것이 좋습니다.


Microsoft가 Dispose에 Exception 매개 변수를 제공하지 않았기 때문에 처리 자체에서 예외가 발생하는 경우 InnerException으로 래핑 될 의도가 있습니다. 확실히 그러한 매개 변수를 효과적으로 사용하려면 C #이 지원하지 않는 예외 필터 블록을 사용해야하지만 그러한 매개 변수의 존재가 C # 디자이너가 그러한 기능을 제공하도록 동기를 부여했을 수 있습니까? 내가보고 싶은 좋은 변형 중 하나는 마지막 블록에 예외 "매개 변수"를 추가하는 것입니다. 예 :

  finally Exception ex : // In C #
  마지막으로 Ex as Exception 'In VB

이는 'Ex'가 'Try'가 완료되면 null / Nothing이되고 그렇지 않은 경우 throw 된 예외를 보유한다는 점을 제외하고는 일반 finally 블록처럼 동작합니다. 안타깝게도 기존 코드에서 이러한 기능을 사용할 수있는 방법이 없습니다.


로깅을 사용하여 첫 번째 예외에 대한 세부 정보를 캡처 한 다음 두 번째 예외가 발생하도록 허용합니다.


Dispose메서드 에서 예외를 전파하거나 삼키는 다양한 전략이 있으며 , 이는 처리되지 않은 예외가 주 논리에서도 throw되었는지 여부에 따라 결정됩니다. 가장 좋은 해결책은 특정 요구 사항에 따라 결정을 발신자에게 맡기는 것입니다. 나는 이것을 제공하는 일반적인 확장 방법을 구현했습니다.

  • 예외 using전파 의 기본 의미Dispose
  • 항상 Dispose예외를 삼키는 Marc Gravell의 제안
  • Dispose그렇지 않으면 손실 될 수있는 메인 로직의 예외가있을 때 예외를 삼키는 maxyfc의 대안
  • Daniel Chambers의 여러 예외를AggregateException
  • 항상 모든 예외를 AggregateException(like Task.Waitdoes) 로 래핑하는 유사한 접근 방식

이것은 내 확장 방법입니다.

/// <summary>
/// Provides extension methods for the <see cref="IDisposable"/> interface.
/// </summary>
public static class DisposableExtensions
{
    /// <summary>
    /// Executes the specified action delegate using the disposable resource,
    /// then disposes of the said resource by calling its <see cref="IDisposable.Dispose()"/> method.
    /// </summary>
    /// <typeparam name="TDisposable">The type of the disposable resource to use.</typeparam>
    /// <param name="disposable">The disposable resource to use.</param>
    /// <param name="action">The action to execute using the disposable resource.</param>
    /// <param name="strategy">
    /// The strategy for propagating or swallowing exceptions thrown by the <see cref="IDisposable.Dispose"/> method.
    /// </param>
    /// <exception cref="ArgumentNullException"><paramref name="disposable"/> or <paramref name="action"/> is <see langword="null"/>.</exception>
    public static void Using<TDisposable>(this TDisposable disposable, Action<TDisposable> action, DisposeExceptionStrategy strategy)
        where TDisposable : IDisposable
    {
        ArgumentValidate.NotNull(disposable, nameof(disposable));
        ArgumentValidate.NotNull(action, nameof(action));
        ArgumentValidate.IsEnumDefined(strategy, nameof(strategy));

        Exception mainException = null;

        try
        {
            action(disposable);
        }
        catch (Exception exception)
        {
            mainException = exception;
            throw;
        }
        finally
        {
            try
            {
                disposable.Dispose();
            }
            catch (Exception disposeException)
            {
                switch (strategy)
                {
                    case DisposeExceptionStrategy.Propagate:
                        throw;

                    case DisposeExceptionStrategy.Swallow:
                        break;   // swallow exception

                    case DisposeExceptionStrategy.Subjugate:
                        if (mainException == null)
                            throw;
                        break;    // otherwise swallow exception

                    case DisposeExceptionStrategy.AggregateMultiple:
                        if (mainException != null)
                            throw new AggregateException(mainException, disposeException);
                        throw;

                    case DisposeExceptionStrategy.AggregateAlways:
                        if (mainException != null)
                            throw new AggregateException(mainException, disposeException);
                        throw new AggregateException(disposeException);
                }
            }

            if (mainException != null && strategy == DisposeExceptionStrategy.AggregateAlways)
                throw new AggregateException(mainException);
        }
    }
}

다음은 구현 된 전략입니다.

/// <summary>
/// Identifies the strategy for propagating or swallowing exceptions thrown by the <see cref="IDisposable.Dispose"/> method
/// of an <see cref="IDisposable"/> instance, in conjunction with exceptions thrown by the main logic.
/// </summary>
/// <remarks>
/// This enumeration is intended to be used from the <see cref="DisposableExtensions.Using"/> extension method.
/// </remarks>
public enum DisposeExceptionStrategy
{
    /// <summary>
    /// Propagates any exceptions thrown by the <see cref="IDisposable.Dispose"/> method.
    /// If another exception was already thrown by the main logic, it will be hidden and lost.
    /// This behaviour is consistent with the standard semantics of the <see langword="using"/> keyword.
    /// </summary>
    /// <remarks>
    /// <para>
    /// According to Section 8.10 of the C# Language Specification (version 5.0):
    /// </para>
    /// <blockquote>
    /// If an exception is thrown during execution of a <see langword="finally"/> block,
    /// and is not caught within the same <see langword="finally"/> block, 
    /// the exception is propagated to the next enclosing <see langword="try"/> statement. 
    /// If another exception was in the process of being propagated, that exception is lost. 
    /// </blockquote>
    /// </remarks>
    Propagate,

    /// <summary>
    /// Always swallows any exceptions thrown by the <see cref="IDisposable.Dispose"/> method,
    /// regardless of whether another exception was already thrown by the main logic or not.
    /// </summary>
    /// <remarks>
    /// This strategy is presented by Marc Gravell in
    /// <see href="http://blog.marcgravell.com/2008/11/dontdontuse-using.html">don't(don't(use using))</see>.
    /// </remarks>
    Swallow,

    /// <summary>
    /// Swallows any exceptions thrown by the <see cref="IDisposable.Dispose"/> method
    /// if and only if another exception was already thrown by the main logic.
    /// </summary>
    /// <remarks>
    /// This strategy is suggested in the first example of the Stack Overflow question
    /// <see href="https://stackoverflow.com/q/1654487/1149773">Swallowing exception thrown in catch/finally block</see>.
    /// </remarks>
    Subjugate,

    /// <summary>
    /// Wraps multiple exceptions, when thrown by both the main logic and the <see cref="IDisposable.Dispose"/> method,
    /// into an <see cref="AggregateException"/>. If just one exception occurred (in either of the two),
    /// the original exception is propagated.
    /// </summary>
    /// <remarks>
    /// This strategy is implemented by Daniel Chambers in
    /// <see href="http://www.digitallycreated.net/Blog/51/c%23-using-blocks-can-swallow-exceptions">C# Using Blocks can Swallow Exceptions</see>
    /// </remarks>
    AggregateMultiple,

    /// <summary>
    /// Always wraps any exceptions thrown by the main logic and/or the <see cref="IDisposable.Dispose"/> method
    /// into an <see cref="AggregateException"/>, even if just one exception occurred.
    /// </summary>
    /// <remarks>
    /// This strategy is similar to behaviour of the <see cref="Task.Wait()"/> method of the <see cref="Task"/> class 
    /// and the <see cref="Task{TResult}.Result"/> property of the <see cref="Task{TResult}"/> class:
    /// <blockquote>
    /// Even if only one exception is thrown, it is still wrapped in an <see cref="AggregateException"/> exception.
    /// </blockquote>
    /// </remarks>
    AggregateAlways,
}

샘플 사용 :

new FileStream(Path.GetTempFileName(), FileMode.Create)
    .Using(strategy: DisposeExceptionStrategy.Subjugate, action: fileStream =>
    {
        // Access fileStream here
        fileStream.WriteByte(42);
        throw new InvalidOperationException();
    });   
    // Any Dispose() exceptions will be swallowed due to the above InvalidOperationException

업데이트 : 값을 반환하거나 비동기적인 대리자를 지원해야하는 경우 다음과 같은 오버로드를 사용할 수 있습니다.

/// <summary>
/// Provides extension methods for the <see cref="IDisposable"/> interface.
/// </summary>
public static class DisposableExtensions
{
    /// <summary>
    /// Executes the specified action delegate using the disposable resource,
    /// then disposes of the said resource by calling its <see cref="IDisposable.Dispose()"/> method.
    /// </summary>
    /// <typeparam name="TDisposable">The type of the disposable resource to use.</typeparam>
    /// <param name="disposable">The disposable resource to use.</param>
    /// <param name="strategy">
    /// The strategy for propagating or swallowing exceptions thrown by the <see cref="IDisposable.Dispose"/> method.
    /// </param>
    /// <param name="action">The action delegate to execute using the disposable resource.</param>
    public static void Using<TDisposable>(this TDisposable disposable, DisposeExceptionStrategy strategy, Action<TDisposable> action)
        where TDisposable : IDisposable
    {
        ArgumentValidate.NotNull(disposable, nameof(disposable));
        ArgumentValidate.NotNull(action, nameof(action));
        ArgumentValidate.IsEnumDefined(strategy, nameof(strategy));

        disposable.Using(strategy, disposableInner =>
        {
            action(disposableInner);
            return true;   // dummy return value
        });
    }

    /// <summary>
    /// Executes the specified function delegate using the disposable resource,
    /// then disposes of the said resource by calling its <see cref="IDisposable.Dispose()"/> method.
    /// </summary>
    /// <typeparam name="TDisposable">The type of the disposable resource to use.</typeparam>
    /// <typeparam name="TResult">The type of the return value of the function delegate.</typeparam>
    /// <param name="disposable">The disposable resource to use.</param>
    /// <param name="strategy">
    /// The strategy for propagating or swallowing exceptions thrown by the <see cref="IDisposable.Dispose"/> method.
    /// </param>
    /// <param name="func">The function delegate to execute using the disposable resource.</param>
    /// <returns>The return value of the function delegate.</returns>
    public static TResult Using<TDisposable, TResult>(this TDisposable disposable, DisposeExceptionStrategy strategy, Func<TDisposable, TResult> func)
        where TDisposable : IDisposable
    {
        ArgumentValidate.NotNull(disposable, nameof(disposable));
        ArgumentValidate.NotNull(func, nameof(func));
        ArgumentValidate.IsEnumDefined(strategy, nameof(strategy));

#pragma warning disable 1998
        var dummyTask = disposable.UsingAsync(strategy, async (disposableInner) => func(disposableInner));
#pragma warning restore 1998

        return dummyTask.GetAwaiter().GetResult();
    }

    /// <summary>
    /// Executes the specified asynchronous delegate using the disposable resource,
    /// then disposes of the said resource by calling its <see cref="IDisposable.Dispose()"/> method.
    /// </summary>
    /// <typeparam name="TDisposable">The type of the disposable resource to use.</typeparam>
    /// <param name="disposable">The disposable resource to use.</param>
    /// <param name="strategy">
    /// The strategy for propagating or swallowing exceptions thrown by the <see cref="IDisposable.Dispose"/> method.
    /// </param>
    /// <param name="asyncFunc">The asynchronous delegate to execute using the disposable resource.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public static Task UsingAsync<TDisposable>(this TDisposable disposable, DisposeExceptionStrategy strategy, Func<TDisposable, Task> asyncFunc)
        where TDisposable : IDisposable
    {
        ArgumentValidate.NotNull(disposable, nameof(disposable));
        ArgumentValidate.NotNull(asyncFunc, nameof(asyncFunc));
        ArgumentValidate.IsEnumDefined(strategy, nameof(strategy));

        return disposable.UsingAsync(strategy, async (disposableInner) =>
        {
            await asyncFunc(disposableInner);
            return true;   // dummy return value
        });
    }

    /// <summary>
    /// Executes the specified asynchronous function delegate using the disposable resource,
    /// then disposes of the said resource by calling its <see cref="IDisposable.Dispose()"/> method.
    /// </summary>
    /// <typeparam name="TDisposable">The type of the disposable resource to use.</typeparam>
    /// <typeparam name="TResult">The type of the return value of the asynchronous function delegate.</typeparam>
    /// <param name="disposable">The disposable resource to use.</param>
    /// <param name="strategy">
    /// The strategy for propagating or swallowing exceptions thrown by the <see cref="IDisposable.Dispose"/> method.
    /// </param>
    /// <param name="asyncFunc">The asynchronous function delegate to execute using the disposable resource.</param>
    /// <returns>
    /// A task that represents the asynchronous operation. 
    /// The task result contains the return value of the asynchronous function delegate.
    /// </returns>
    public static async Task<TResult> UsingAsync<TDisposable, TResult>(this TDisposable disposable, DisposeExceptionStrategy strategy, Func<TDisposable, Task<TResult>> asyncFunc)
        where TDisposable : IDisposable
    {
        ArgumentValidate.NotNull(disposable, nameof(disposable));
        ArgumentValidate.NotNull(asyncFunc, nameof(asyncFunc));
        ArgumentValidate.IsEnumDefined(strategy, nameof(strategy));

        Exception mainException = null;

        try
        {
            return await asyncFunc(disposable);
        }
        catch (Exception exception)
        {
            mainException = exception;
            throw;
        }
        finally
        {
            try
            {
                disposable.Dispose();
            }
            catch (Exception disposeException)
            {
                switch (strategy)
                {
                    case DisposeExceptionStrategy.Propagate:
                        throw;

                    case DisposeExceptionStrategy.Swallow:
                        break;   // swallow exception

                    case DisposeExceptionStrategy.Subjugate:
                        if (mainException == null)
                            throw;
                        break;    // otherwise swallow exception

                    case DisposeExceptionStrategy.AggregateMultiple:
                        if (mainException != null)
                            throw new AggregateException(mainException, disposeException);
                        throw;

                    case DisposeExceptionStrategy.AggregateAlways:
                        if (mainException != null)
                            throw new AggregateException(mainException, disposeException);
                        throw new AggregateException(disposeException);
                }
            }

            if (mainException != null && strategy == DisposeExceptionStrategy.AggregateAlways)
                throw new AggregateException(mainException);
        }
    }
}

다음은 using또는의 내용에 의해 발생하는 예외를 상당히 깔끔하게 잡는 방법 Dispose입니다.

원래 코드 :

using (var foo = new DisposableFoo())
{
    codeInUsing();
}

다음은 던지 codeInUsing()거나 foo.Dispose()던지거나 둘 다 던지면 던지는 코드이며 첫 번째 예외를 볼 수 있습니다 (때로는 InnerExeption으로 래핑 됨).

var foo = new DisposableFoo();
Helpers.DoActionThenDisposePreservingActionException(
    () =>
    {
        codeInUsing();
    },
    foo);

위대하지는 않지만 나쁘지는 않습니다.

이를 구현하는 코드는 다음과 같습니다. 디버거가 연결되지 않은 경우 에만 설명 된대로 작동 하도록 설정했습니다 . 디버거가 연결될 때 첫 번째 예외에서 올바른 위치에서 중단 될 것이 더 걱정되기 때문입니다. 필요에 따라 수정할 수 있습니다.

public static void DoActionThenDisposePreservingActionException(Action action, IDisposable disposable)
{
    bool exceptionThrown = true;
    Exception exceptionWhenNoDebuggerAttached = null;
    bool debuggerIsAttached = Debugger.IsAttached;
    ConditionalCatch(
        () =>
        {
            action();
            exceptionThrown = false;
        },
        (e) =>
        {
            exceptionWhenNoDebuggerAttached = e;
            throw new Exception("Catching exception from action(), see InnerException", exceptionWhenNoDebuggerAttached);
        },
        () =>
        {
            Exception disposeExceptionWhenExceptionAlreadyThrown = null;
            ConditionalCatch(
                () =>
                {
                    disposable.Dispose();
                },
                (e) =>
                {
                    disposeExceptionWhenExceptionAlreadyThrown = e;
                    throw new Exception("Caught exception in Dispose() while unwinding for exception from action(), see InnerException for action() exception",
                        exceptionWhenNoDebuggerAttached);
                },
                null,
                exceptionThrown && !debuggerIsAttached);
        },
        !debuggerIsAttached);
}

public static void ConditionalCatch(Action tryAction, Action<Exception> conditionalCatchAction, Action finallyAction, bool doCatch)
{
    if (!doCatch)
    {
        try
        {
            tryAction();
        }
        finally
        {
            if (finallyAction != null)
            {
                finallyAction();
            }
        }
    }
    else
    {
        try
        {
            tryAction();
        }
        catch (Exception e)
        {
            if (conditionalCatchAction != null)
            {
                conditionalCatchAction(e);
            }
        }
        finally
        {
            if (finallyAction != null)
            {
                finallyAction();
            }
        }
    }
}

참조 URL : https://stackoverflow.com/questions/577607/should-you-implement-idisposable-dispose-so-that-it-never-throws

반응형