던지지 않도록 IDisposable.Dispose ()를 구현해야합니까?
C ++ (소멸자)의 동등한 메커니즘의 경우 일반적으로 예외를 throw하지 않아야한다는 조언이 있습니다. 이것은 주로 그렇게함으로써 프로세스를 종료 할 수 있기 때문에 매우 드물게 좋은 전략입니다.
.NET의 동등한 시나리오에서 ...
- 첫 번째 예외가 발생합니다.
- finally 블록은 첫 번째 예외의 결과로 실행됩니다.
- finally 블록은 Dispose () 메서드를 호출합니다.
- 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
(likeTask.Wait
does) 로 래핑하는 유사한 접근 방식
이것은 내 확장 방법입니다.
/// <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();
}
}
}
}
'programing' 카테고리의 다른 글
데이터 테이블 온더 플라이 크기 조정 (0) | 2021.01.16 |
---|---|
TypeScript의 'instanceof'에서 " 'Foo'는 유형 만 참조하지만 여기서는 값으로 사용되고 있습니다."라는 오류가 표시되는 이유는 무엇입니까? (0) | 2021.01.15 |
constexpr은 람다 함수 / 식에서 지원됩니까? (0) | 2021.01.15 |
MySQL vs. JSON-왜? (0) | 2021.01.15 |
"from __future__ import braces"코드는 어디에 있습니까? (0) | 2021.01.15 |