[c#] 예외 안전을 위해 “범위 지정 동작”을 얻기위한 수단으로 IDisposable 및 “사용”을 사용하는 것이 악의적입니까?

내가 C ++에서 자주 사용했던 것은 생성자와 소멸자 를 통해 A클래스가 다른 클래스에 대한 상태 진입 및 종료 조건을 처리 하도록하여 해당 범위의 무언가가 예외를 던지면 B가 알려진 상태를 가질 수 있도록하는 것입니다. 범위가 종료되었습니다. 이것은 약어가가는 한 순수한 RAII는 아니지만 그럼에도 불구하고 확립 된 패턴입니다.BA

C #에서는 종종

class FrobbleManager
{
    ...

    private void FiddleTheFrobble()
    {
        this.Frobble.Unlock();
        Foo();                  // Can throw
        this.Frobble.Fiddle();  // Can throw
        Bar();                  // Can throw
        this.Frobble.Lock();
    }
}

이렇게해야하는

private void FiddleTheFrobble()
{
    this.Frobble.Unlock();

    try
    {
        Foo();                  // Can throw
        this.Frobble.Fiddle();  // Can throw
        Bar();                  // Can throw
    }
    finally
    {
        this.Frobble.Lock();
    }
}

반품 Frobble시 상태 를 보증하고 싶다면 FiddleTheFrobble. 코드는 더 좋을 것입니다.

private void FiddleTheFrobble()
{
    using (var janitor = new FrobbleJanitor(this.Frobble))
    {
        Foo();                  // Can throw
        this.Frobble.Fiddle();  // Can throw
        Bar();                  // Can throw
    }
}

여기서 FrobbleJanitor보이는 대략 좋아

class FrobbleJanitor : IDisposable
{
    private Frobble frobble;

    public FrobbleJanitor(Frobble frobble)
    {
        this.frobble = frobble;
        this.frobble.Unlock();
    }

    public void Dispose()
    {
        this.frobble.Lock();
    }
}

그리고 그것이 제가하고 싶은 방법입니다. 내가 무엇 때문에 지금 현실은, 잡는다 원하는 사용하는 것은 필요로 그를FrobbleJanitor 사용 하여 using . 나는 이것을 코드 검토 문제라고 생각할 수 있지만 무언가가 나를 괴롭힌다.

질문 : 위의 내용이 using및의 남용으로 간주 IDisposable됩니까?



답변

필연적으로 그렇게 생각하지는 않습니다. 기술적으로 식별 가능 되어 관리되지 않는 자원을 가지고 일에 사용되는 의미,하지만 그때 사용하는 지시어의 일반적인 패턴을 구현 단지 깔끔한 방법입니다try .. finally { dispose } .

순수 주의자는 ‘그렇다-그것은 학대 적이다’라고 주장 할 것이고, 순수 주의적 의미에서는 그렇다. 그러나 우리 대부분은 순수주의적인 관점에서 코딩하는 것이 아니라 반 예술적인 관점에서 코딩합니다. 이런 식으로 ‘사용’구조를 사용하는 것은 제 생각에 실제로 꽤 예술적입니다.

IDisposable 위에 다른 인터페이스를 붙여서 조금 더 멀리 밀어 넣어야합니다.이 인터페이스가 IDisposable을 의미하는 이유를 다른 개발자에게 설명해야합니다.

이 작업에 대한 다른 많은 대안이 있지만 궁극적으로 이것만큼 깔끔 할 것이라고 생각할 수 없으므로 시도하십시오!


답변

나는 이것이 using 문장의 남용이라고 생각합니다. 나는이 위치에서 내가 소수라는 것을 알고 있습니다.

나는 이것이 세 가지 이유로 남용이라고 생각합니다.

내가 기대하고 있기 때문에 먼저, 그하는 데 사용됩니다 “사용” 자원 사용 하고 당신이 그것을 완료하면 폐기를 . 프로그램 상태 변경 은 리소스를 사용 하지 않으며 다시 변경해도 폐기 되지 않습니다. 도 아무것도 . 따라서 상태를 변경하고 복원하기 위해 “사용”하는 것은 남용입니다. 코드는 일반 독자에게 오해의 소지가 있습니다.

둘째, “사용”은 필수가 아닌 예의 로 사용되기를 기대하기 때문 입니다. 이 때문에 당신이 그것을 완료하면 당신이 파일 처분 “을 사용하여”사용하는 이유는 아닙니다 필요 그렇게하지만 때문에 정중 – 다른 사람이 그래서 “완료를 말하고, 그 파일을 사용하여 대기 중일 수 있습니다 지금 “은 도덕적으로 올바른 일입니다. 나는 “사용”을 리팩토링하여 사용 된 리소스를 더 오래 보관하고 나중에 폐기 할 수 있어야하며 그렇게함으로써 다른 프로세스약간의 불편을 끼치게 . 프로그램 상태에 의미 론적 영향을 미치는 “사용”블록 그것은 필요가 아닌 편의성과 공손함을 위해 존재하는 것처럼 보이는 구조에서 프로그램 상태의 중요하고 필요한 변이를 숨기기 때문에 학대 적입니다.

셋째, 프로그램의 동작은 상태에 따라 결정됩니다. 주의 깊은 상태 조작의 필요성이 바로 우리가 처음에이 대화를하는 이유입니다. 원래 프로그램을 분석하는 방법을 고려해 보겠습니다.

이것을 제 사무실의 코드 리뷰에 가져 오 셨나요? 제가 물어볼 첫 번째 질문은 “예외가 발생하면 프 러블을 잠그는 것이 정말 맞습니까?”입니다. 이 일이 무슨 일이 일어나더라도 공격적으로 프 러블을 다시 잠그는 것이 프로그램에서 뻔뻔스럽게 분명합니다. 맞습니까? 예외가 발생했습니다. 프로그램이 알 수없는 상태입니다. 우리는 Foo, Fiddle 또는 Bar가 던 졌는지, 왜 던 졌는지 또는 정리되지 않은 다른 상태에 어떤 돌연변이를 수행했는지 알 수 없습니다. 그 끔찍한 상황 에서 다시 잠그는 것이 항상 옳은 일이라고 저 를 설득 할 수 있습니까 ?

그럴 수도 있고 아닐 수도 있습니다. 내 요점은 원래 작성된 코드로 코드를 사용하여 코드 검토자가 질문하는 것을 알고 있다는 것 입니다. “사용”을 사용하는 코드에서는 질문을해야할지 모르겠습니다. 나는 “using”블록이 리소스를 할당하고 약간 사용하고 완료되면 정중하게 처리한다고 가정합니다. “using”블록의 닫는 중괄호임의로 많은 경우 예외적으로 내 프로그램 상태를 변경 하는 것이 아닙니다. 프로그램 상태 일관성 조건을 위반했습니다.

의미 론적 효과를 갖기 위해 “using”블록을 사용하면이 프로그램이 조각화됩니다.

}

매우 의미가 있습니다. 제가 그 하나의 폐쇄 보조기를 볼 때 “저 보조기가 내 프로그램의 글로벌 상태에 광범위한 영향을 미치는 부작용이 있습니다”라고 즉시 생각하지 않습니다. 하지만 이렇게 “사용”을 남용하면 갑자기 그렇게됩니다.

두 번째로 원래 코드를 보았는지 물어볼 것입니다. “잠금 해제 후 시도가 입력되기 전에 예외가 발생하면 어떻게됩니까?” 최적화되지 않은 어셈블리를 실행하는 경우 컴파일러가 시도 전에 no-op 명령을 삽입했을 수 있으며 no-op에서 스레드 중단 예외가 발생할 수 있습니다. 이것은 드물지만 실제로는 특히 웹 서버에서 발생합니다. 이 경우 잠금 해제가 발생하지만 시도 전에 예외가 발생했기 때문에 잠금이 발생하지 않습니다. 이 코드가이 문제에 취약 할 수 있으며 실제로 작성해야합니다.

bool needsLock = false;
try
{
    // must be carefully written so that needsLock is set
    // if and only if the unlock happened:

    this.Frobble.AtomicUnlock(ref needsLock);
    blah blah blah
}
finally
{
    if (needsLock) this.Frobble.Lock();
}

다시 말하지만, 아마도 그렇지 않을 수도 있지만 질문을해야한다는 것을 알고 있습니다. “using”버전에서는 동일한 문제에 취약합니다. Frobble이 잠긴 후 사용과 관련된 try-protected 영역이 입력되기 전에 스레드 중단 예외가 발생할 수 있습니다. 그러나 “사용”버전에서는 이것이 “그래서 무엇입니까?”라고 가정합니다. 상태. 그런 일이 발생하면 안타까운 일이지만 “사용”은 매우 중요한 프로그램 상태를 변경하는 것이 아니라 예의를 갖추기위한 것이라고 생각합니다. 끔찍한 스레드 중단 예외가 정확히 잘못된 시간에 발생하면 가비지 수집기가 종료자를 실행하여 결국 해당 리소스를 정리할 것이라고 가정합니다.


답변

깨끗하고 범위가 지정된 코드를 원한다면 람다를 사용할 수도 있습니다.

myFribble.SafeExecute(() =>
    {
        myFribble.DangerDanger();
        myFribble.LiveOnTheEdge();
    });

를 Where .SafeExecute(Action fribbleAction)방법은 랩 trycatchfinally블록.


답변

는 C # 언어 디자인 팀에 있던 에릭 Gunnerson은 준 이 답변을 꽤 많이 같은 질문에 :

Doug가 묻습니다.

re : 제한 시간이있는 잠금 문 …

여러 방법에서 일반적인 패턴 을 다루기 위해 이전에이 트릭을 수행했습니다 . 일반적으로 잠금 획득 이지만 다른 일부가 있습니다 . 문제는 객체가 ” 범위가 끝날 때까지 콜백 “만큼 일회용이 아니기 때문에 항상 해킹처럼 느껴진다는 것 입니다.

더그,

using 문을 결정했을 때 정확히이 시나리오에 사용할 수 있도록 개체를 처리하는 것보다 “사용”이라는 이름을 지정하기로 결정했습니다.


답변

미끄러운 경사입니다. IDisposable에는 종료자가 백업 한 계약이 있습니다. 귀하의 경우 종료자는 쓸모가 없습니다. 클라이언트에게 using 문을 사용하도록 강요 할 수 없으며 사용하도록 권장 할뿐입니다. 다음과 같은 방법으로 강제 할 수 있습니다.

void UseMeUnlocked(Action callback) {
  Unlock();
  try {
    callback();
  }
  finally {
    Lock();
  }
}

그러나 그것은 람다 없이는 조금 어색 해지는 경향이 있습니다. 즉, 나는 당신처럼 IDisposable을 사용했습니다.

그러나 귀하의 게시물에는 이것을 안티 패턴에 위험하게 가깝게 만드는 세부 사항이 있습니다. 이러한 메서드가 예외를 throw 할 수 있다고 언급했습니다. 이것은 호출자가 무시할 수있는 것이 아닙니다. 그는 그것에 대해 세 가지를 할 수 있습니다.

  • 아무것도하지 마십시오. 예외는 복구 할 수 없습니다. 정상적인 경우입니다. Unlock 호출은 중요하지 않습니다.
  • 예외 포착 및 처리
  • 그의 코드에서 상태를 복원하고 예외가 호출 체인을 통과하도록합니다.

후자의 두 가지는 호출자가 명시 적으로 try 블록을 작성해야합니다. 이제 using 문이 방해가됩니다. 그것은 당신의 수업이 상태를 관리하고 있으며 추가 작업을 할 필요가 없다고 믿게 만드는 혼수 상태에 빠지게 할 수 있습니다. 거의 정확하지 않습니다.


답변

실제 사례는 ASP.net MVC의 BeginForm입니다. 기본적으로 다음과 같이 작성할 수 있습니다.

Html.BeginForm(...);
Html.TextBox(...);
Html.EndForm();

또는

using(Html.BeginForm(...)){
    Html.TextBox(...);
}

Html.EndForm은 Dispose를 호출하고 Dispose는 </form>태그 만 출력합니다 . 이것에 대한 좋은 점은 {} 괄호가 가시적 인 “범위”를 생성하여 양식 내에 무엇이 있는지, 무엇이 아닌지 쉽게 볼 수 있다는 것입니다.

나는 그것을 과도하게 사용하지 않을 것이다. 그러나 본질적으로 IDisposable은 단지 “당신이 그것을 끝냈을 때이 함수를 호출해야한다”라고 말하는 방법 일 뿐이다. MvcForm은이를 사용하여 폼이 닫혔는지 확인하고 Stream은이를 사용하여 스트림이 닫혔는지 확인하고 개체가 잠금 해제되었는지 확인하는 데 사용할 수 있습니다.

개인적으로 다음 두 가지 규칙이 참인 경우에만 사용하지만 임의로 설정합니다.

  • Dispose는 항상 실행해야하는 함수 여야하므로 Null-Check를 제외하고 조건이 없어야합니다.
  • Dispose () 후에는 개체를 다시 사용할 수 없어야합니다. 재사용 가능한 객체를 원한다면 폐기하는 대신 열기 / 닫기 메소드를 제공하고 싶습니다. 그래서 폐기 된 객체를 사용하려고 할 때 InvalidOperationException을 던졌습니다.

결국, 그것은 모두 기대에 관한 것입니다. 개체가 IDisposable을 구현하면 정리가 필요하다고 가정하므로이를 호출합니다. 보통 “종료”기능이있는 것보다 낫다고 생각합니다.

즉,이 라인이 마음에 들지 않습니다.

this.Frobble.Fiddle();

FrobbleJanitor가 이제 Frobble을 “소유”하고 있으므로, 대신 Janitor에서 Frobble에게 Fiddle을 호출하는 것이 낫지 않을까요?


답변

우리는 코드베이스에서이 패턴을 많이 사용하고 있으며 이전에 모든 곳에서 본 적이 있습니다. 여기서도 논의되었을 것입니다. 일반적으로이 작업을 수행하는 데 어떤 문제가 있는지 알 수 없으며 유용한 패턴을 제공하며 실제 피해를주지 않습니다.