IDisposable이 모든 수업에 퍼지는 것을 어떻게 방지합니까?


138

이 간단한 수업으로 시작하십시오 ...

다음과 같은 간단한 클래스 세트가 있다고 가정 해 봅시다.

class Bus
{
    Driver busDriver = new Driver();
}

class Driver
{
    Shoe[] shoes = { new Shoe(), new Shoe() };
}

class Shoe
{
    Shoelace lace = new Shoelace();
}

class Shoelace
{
    bool tied = false;
}

A BusDriver, Driver두 개 는 Shoe각각 Shoe을가 Shoelace집니다. 모두 매우 바보입니다.

신발 끈에 IDisposable 객체 추가

나중에 나는 일부 작업 Shoelace이 멀티 스레드 될 수 있다고 결정 하므로 EventWaitHandle스레드가 통신 할 수 있도록 추가합니다 . 그래서 Shoelace지금은 다음과 같습니다 :

class Shoelace
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;
    // ... other stuff ..
}

신발 끈에 IDisposable 구현

그러나 이제 Microsoft의 FxCop 은 다음과 같이 불평 할 것 입니다.

좋아, 내가 구현 IDisposableShoelace내 깔끔한 작은 클래스는이 끔찍한 엉망이된다 :

class Shoelace : IDisposable
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;
    private bool disposed = false;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    ~Shoelace()
    {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                if (waitHandle != null)
                {
                    waitHandle.Close();
                    waitHandle = null;
                }
            }
            // No unmanaged resources to release otherwise they'd go here.
        }
        disposed = true;
    }
}

또는 (해설자가 지적한 바와 같이) Shoelace자체에는 관리되지 않는 리소스가 없기 때문에 Dispose(bool)and Destructor가 없어도 더 간단한 dispose 구현을 사용할 수 있습니다 .

class Shoelace : IDisposable
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;

    public void Dispose()
    {
        if (waitHandle != null)
        {
            waitHandle.Close();
            waitHandle = null;
        }
        GC.SuppressFinalize(this);
    }
}

IDisposable이 퍼짐에 따라 공포에서 시청

맞습니다. 그러나 이제 FxCop은을 Shoe생성 한다고 불평 Shoelace하므로 너무 커야 Shoe합니다 IDisposable.

그리고 Driver생성은 Shoe그렇게 Driver해야합니다 IDisposable. 그리고 Bus생성은 Driver그렇게 Bus해야합니다 IDisposable등등합니다.

갑자기 내 작은 변경으로 Shoelace인해 많은 작업이 발생했으며 상사는 Bus변경을 위해 왜 체크 아웃해야하는지 궁금 합니다 Shoelace.

질문

이러한 확산을 방지 IDisposable하면서도 관리되지 않는 개체가 올바르게 처리되도록 하려면 어떻게해야 합니까?


9
예외적으로 좋은 질문은 대답이 사용을 최소화하고 높은 수준의 IDisposables를 사용하여 수명을 짧게 유지하려고 노력하지만 항상 가능하지는 않다는 것입니다. 답을 보지 마라.
annakata

댄, 구두끈에 IDisposable을 구현하는 두 가지 방법을 모두 보여주기 위해 질문을 업데이트했습니다.
GrahamS

나는 일반적으로 나를 보호하기 위해 다른 클래스의 구현 세부 사항에 의존하는 것을 경계합니다. 쉽게 예방할 수 있다면 위험하지 않습니다. 어쩌면 내가 오버 조심 해요 아니면 그냥 C 프로그래머로 너무 오래 보냈다, 그러나 나는 오히려 아일랜드 접근을 것 ", 확실히 확인하기 위해":
GrahamS

@Dan : 개체 자체가 null로 설정되지 않았는지 확인하기 위해 null 검사가 여전히 필요합니다.이 경우 waitHandle.Dispose ()에 대한 호출에서 NullReferenceException이 발생합니다.
Scott Dorman

1
어쨌든 "Shoelace에 IDisposable 구현"섹션에 나와있는 것처럼 Dispose (bool) 메서드를 실제로 사용해야합니다. 클래스에 IDisposable이 포함되어 있다고해서 반드시 종료자가 필요하지는 않습니다.
Scott Dorman

답변:


36

IDisposable의 확산을 실제로 "예방"할 수는 없습니다. 일부 클래스는와 같이 처리해야 AutoResetEvent하며 가장 효율적인 방법은 종료 자 Dispose()오버 헤드를 피하기 위해 메소드 에서 수행하는 것입니다 . 그러나이 방법은 어떻게 든 예제에서와 같이 IDisposable을 캡슐화하거나 포함하는 클래스에서이를 처리해야하므로 처분해야합니다. 등을 피하는 유일한 방법은 다음과 같습니다.

  • 가능하면 IDisposable 클래스 사용을 피하고, 단일 장소에서 이벤트를 잠 그거나 기다리거나, 고가의 자원을 단일 장소에 보관하십시오
  • 필요할 때만 작성하고 바로 다음에 배치하십시오 ( using패턴)

경우에 따라 IDisposable은 선택적 사례를 지원하므로 무시할 수 있습니다. 예를 들어, WaitHandle은 명명 된 Mutex를 지원하기 위해 IDisposable을 구현합니다. 이름을 사용하지 않으면 Dispose 메서드는 아무 작업도 수행하지 않습니다. MemoryStream은 또 다른 예이며, 시스템 리소스를 사용하지 않으며 Dispose 구현도 아무 것도 수행하지 않습니다. 관리되지 않는 리소스의 사용 여부에 대해 신중하게 생각하면 도움이 될 수 있습니다. 따라서 .net 라이브러리에 사용 가능한 소스를 검사하거나 디 컴파일러를 사용할 수 있습니다.


4
미래의 버전이 Dispose에서 실제로 중요한 작업을 수행 할 수 있기 때문에 현재 구현이 아무 것도하지 않기 때문에 무시할 수있는 조언은 누출을 추적하기가 어렵습니다.
Andy

1
"고가의 자원 유지", IDisposable은 반드시 고가 일 필요는 없으며, 실제로 짝수 대기 핸들을 사용하는이 예제는 "가벼운"정도입니다.
markmnl

20

정확성 측면에서, 부모 개체가 이제 일회용 개체 여야하고 자식 개체를 소유하는 경우 개체 관계를 통해 IDisposable의 확산을 막을 수 없습니다. 이 상황에서는 FxCop이 정확하며 부모는 ID를 지정할 수 있어야합니다.

객체 계층의 리프 클래스에 IDisposable을 추가하지 마십시오. 이것은 항상 쉬운 일이 아니지만 재미있는 운동입니다. 논리적 관점에서 ShoeLace를 일회용으로 사용할 필요는 없습니다. 여기에 WaitHandle을 추가하는 대신 사용되는 지점에서 ShoeLace와 WaitHandle 간의 연관을 추가 할 수도 있습니다. 가장 간단한 방법은 Dictionary 인스턴스를 통하는 것입니다.

WaitHandle이 실제로 사용되는 지점에서 맵을 통해 WaitHandle을 느슨한 연관으로 이동할 수 있으면이 체인을 끊을 수 있습니다.


2
그곳에서 연관을 맺는 것은 매우 이상합니다. 이 AutoResetEvent는 Shoelace 구현에 비공개이므로 공개적으로 노출하는 것이 제 생각에는 잘못된 것입니다.
GrahamS

@ GrahamS, 공개적으로 공개한다고 말하지 않습니다. 신발 끈이 묶인 지점으로 옮길 수 있다고 말하고 있습니다. 신발 끈을 묶는 것이 복잡한 작업 인 경우 신발 끈 등급 클래스 여야합니다.
JaredPar

1
몇 가지 코드 스 니펫 JaredPar로 답변을 확장 할 수 있습니까? 내 예제를 약간 늘릴 수도 있지만 Shoelace는 waitHandle.WaitOne ()에서 참을성있게 기다리는 Tyer 스레드를 만들고 시작한다고 상상하고 있습니다.
GrahamS

1
이것에 +1. 나는 신발 끈이 동시에 호출되지만 다른 것은 아닌 것이 이상하다고 생각합니다. 총계 루트가 무엇인지 생각하십시오. 이 경우 도메인에 익숙하지 않더라도 Bus, IMHO 여야합니다. 따라서이 경우 버스에는 대기 핸들과 버스에 대한 모든 작업이 포함되어야하며 모든 하위 항목이 동기화됩니다.
Johannes Gustafsson

16

IDisposable확산 을 방지하기 위해 단일 방법 내에서 일회용 물체의 사용을 캡슐화해야합니다. Shoelace다르게 디자인 해보십시오 .

class Shoelace { 
  bool tied = false; 

  public void Tie() {

    using (var waitHandle = new AutoResetEvent(false)) {

      // you can even pass the disposable to other methods
      OtherMethod(waitHandle);

      // or hold it in a field (but FxCop will complain that your class is not disposable),
      // as long as you take control of its lifecycle
      _waitHandle = waitHandle;
      OtherMethodThatUsesTheWaitHandleFromTheField();

    } 

  }
} 

대기 핸들의 범위는 Tie메소드 로 제한되며 클래스에는 일회용 필드가 없어도되므로 일회용 자체가 아니어도됩니다.

대기 핸들은의 내부 구현 세부 사항 Shoelace이므로 선언에 새 인터페이스를 추가하는 것과 같이 공용 인터페이스를 변경해서는 안됩니다. 일회용 필드가 더 이상 필요하지 않은 경우 IDisposable어떻게됩니까? 선언 을 제거 하시겠습니까? Shoelace 추상화 에 대해 생각 하면과 같은 인프라 종속성으로 인해 오염되지 않아야한다는 것을 금방 알 수 있습니다 IDisposable. IDisposable결정 론적 정리를 요구하는 자원을 추상화하는 클래스를 위해 예약되어야한다. 즉, 일회용이 추상화의 일부인 클래스의 경우 .


1
Shoelace의 구현 세부 사항이 공용 인터페이스를 오염시키지 않아야한다는 점에 전적으로 동의하지만 내 요점은 피하기 어려울 수 있다는 것입니다. 여기서 제안하는 것이 항상 가능하지는 않습니다. AutoResetEvent ()의 목적은 스레드간에 통신하는 것이므로 범위는 일반적으로 단일 메서드를 넘어 확장됩니다.
GrahamS

@ GrahamS : 그것이 내가 그런 식으로 디자인하려고한다고 말한 이유입니다. 일회용을 다른 방법으로 전달할 수도 있습니다. 클래스에 대한 외부 호출이 일회용 수명주기를 제어 할 때만 고장납니다. 이에 따라 답변을 업데이트하겠습니다.
Jordão

2
죄송합니다. 일회용품을 전달할 수는 있지만 여전히 작동하지 않습니다. 내 예제에서는 AutoResetEvent동일한 클래스 내에서 작동하는 다른 스레드간에 통신하는 데 사용되므로 멤버 변수 여야합니다. 범위를 메서드로 제한 할 수 없습니다. (예를 들어 하나의 스레드가을 차단하여 일부 작업을 기다리는 중이라고 생각합니다 waitHandle.WaitOne(). 주 스레드는 shoelace.Tie()메소드 를 호출합니다.이 메소드는 a를 수행 waitHandle.Set()하고 즉시 반환합니다).
GrahamS

3

기본적으로 컴포지션 또는 집계를 일회용 클래스와 혼합 할 때 발생합니다. 언급했듯이 첫 번째 방법은 신발 끈에서 waitHandle을 리팩터링하는 것입니다.

그러나 관리되지 않는 리소스가없는 경우 Disposable 패턴을 상당히 제거 할 수 있습니다. (여전히 이것에 대한 공식 참조를 찾고 있습니다.)

그러나 소멸자와 GC를 생략 할 수 있습니다. 그리고 아마도 가상 void Dispose (bool 처분)를 약간 정리합니다.


고마워 k 내가 신발 끈에서 WaitHandle이의 생성을했다 경우 누군가가 여전히 처분이 어딘가에 어떻게 것이라고 누군가의 신발 끈이 함께 완료 (그리고 신발 끈이 다른 클래스에 전달하지 않았 음) 것을 알고?
GrahamS

당신은 창조의 창조뿐만 아니라 대기 핸들의 '책임'을 옮겨야합니다. jaredPar의 답변에는 흥미로운 아이디어가 있습니다.
Henk Holterman

이 블로그는 IDisposable 구현을 다루고 있으며 특히 단일 클래스 블로그
PaulS

3

흥미롭게 Driver도 위와 같이 정의 된 경우 :

class Driver
{
    Shoe[] shoes = { new Shoe(), new Shoe() };
}

때 그런 Shoe되어 IDisposable,의 FxCop (v1.36)는 그 불평하지 않습니다 Driver도해야한다 IDisposable.

그러나 다음과 같이 정의 된 경우 :

class Driver
{
    Shoe leftShoe = new Shoe();
    Shoe rightShoe = new Shoe();
}

그러면 불평 할 것입니다.

첫 번째 버전에서는 Shoe인스턴스가 여전히 생성되고 Driver여전히 배치되어야하기 때문에 이것이 솔루션이 아니라 FxCop의 제한 사항이라고 생각합니다 .


2
Show가 IDisposable을 구현하면 필드 이니셜 라이저에서 IDisposable을 작성하는 것은 위험합니다. C #에서 안전하게 할 수있는 유일한 방법은 ThreadStatic 변수를 사용하는 것이기 때문에 FXCop이 그러한 초기화를 허용하지 않을 것이라는 점에 흥미가 있습니다. vb.net에서는 ThreadStatic 변수없이 IDisposable 개체를 안전하게 초기화 할 수 있습니다. 코드는 여전히 추악하지만 그렇게 끔찍한 것은 아닙니다. MS가 그러한 필드 이니셜 라이저를 안전하게 사용할 수있는 좋은 방법을 제공하기를 바랍니다.
supercat

1
@ supercat : 감사합니다. 위험 한지 설명 할 수 있습니까 ? Shoe생성자 중 하나에서 예외가 발생 shoes하면 어려운 상태로 남아있을 것이라고 생각합니다.
GrahamS

3
특정 유형의 IDisposable을 안전하게 버릴 수 있다는 것을 알지 못하면 작성된 모든 객체가 Dispose'd가되도록해야합니다. IDisposable이 객체의 필드 이니셜 라이저에서 생성되었지만 객체가 완전히 생성되기 전에 예외가 발생하면 객체와 모든 필드가 버려집니다. "시도"블록을 사용하여 구성이 실패했음을 감지하는 팩토리 메소드로 오브젝트의 구성이 랩핑되어 있어도, 팩토리가 처리가 필요한 오브젝트에 대한 참조를 얻기가 어렵습니다.
supercat

3
C #에서 처리하는 유일한 방법은 ThreadStatic 변수를 사용하여 현재 생성자가 throw되면 처리 해야하는 객체 목록을 유지하는 것입니다. 그런 다음 각 필드 이니셜 라이저가 각 오브젝트가 작성 될 때마다 등록하도록하고 팩토리가 성공적으로 완료되면 목록을 지우고 "완료"절을 사용하여 모든 항목이 지워지지 않은 경우 목록에서 제거하십시오. 그것은 가능한 접근 방식이지만 ThreadStatic 변수는 추악합니다. vb.net에서는 ThreadStatic 변수 없이도 비슷한 기술을 사용할 수 있습니다.
supercat

3

디자인을 단단히 결합하면 IDisposable이 확산되는 것을 막는 기술적 인 방법이 없다고 생각합니다. 그런 다음 디자인이 옳은지 궁금합니다.

귀하의 예에서, 신발에 신발 끈을 소유하고 운전자가 신발을 소유해야하는 것이 합리적이라고 생각합니다. 그러나 버스는 운전자를 소유해서는 안됩니다. 일반적으로 버스 운전사는 버스를 따라 폐차장으로 이동하지 않습니다.) 운전자와 신발의 경우, 운전자가 자신의 신발을 만드는 일은 거의 없습니다.

다른 디자인은 다음과 같습니다.

class Bus
{
   IDriver busDriver = null;
   public void SetDriver(IDriver d) { busDriver = d; }
}

class Driver : IDriver
{
   IShoePair shoes = null;
   public void PutShoesOn(IShoePair p) { shoes = p; }
}

class ShoePairWithDisposableLaces : IShoePair, IDisposable
{
   Shoelace lace = new Shoelace();
}

class Shoelace : IDisposable
{
   ...
}

새로운 디자인은 신발과 운전자의 구체적인 인스턴스를 인스턴스화하고 처리하기 위해 추가 클래스가 필요하기 때문에 불행히도 더 복잡하지만, 이러한 문제는 해결되는 문제에 내재되어 있습니다. 좋은 점은 신발 끈을 처분하기 위해 버스를 더 이상 일회용으로 사용할 필요가 없다는 것입니다.


2
그래도 원래 문제는 해결되지 않습니다. FxCop을 조용하게 유지할 수는 있지만 여전히 신발 끈을 폐기해야합니다.
AndrewS

당신이 한 모든 일은 다른 곳으로 문제를 옮기는 것이라고 생각합니다. ShoePairWithDisposableLaces는 이제 Shoelace ()를 소유하고 있습니다. 따라서 신발을 처분하는 사람도 IDisposable로 만들어야합니다. 이것을 처리하기 위해 IoC 컨테이너에 남겨두고 있습니까?
GrahamS

@AndrewS : 사실, 무언가는 여전히 운전자, 신발 및 끈을 처리해야하지만 버스로 처리를 시작해서는 안됩니다. @ GrahamS : 당신은 옳습니다. 다른 곳의 문제라고 생각하기 때문에 다른 곳으로 문제를 옮기고 있습니다. 일반적으로 신발을 처리하는 클래스 인스턴스화 신발이 있습니다. ShoePairWithDisposableLaces를 일회용으로 만들기 위해 코드를 편집했습니다.
Joh

@Joh : 감사합니다. 당신이 말했듯이, 다른 곳으로 옮기는 문제는 훨씬 더 복잡하다는 것입니다. ShoePairWithDisposableLaces가 다른 클래스에 의해 생성되면, 드라이버가 신발을 다 끝냈을 때 해당 클래스에 알림을 보내지 않아야합니다.
GrahamS

1
@Graham : 예. 일종의 알림 메커니즘이 필요합니다. 예를 들어, 참조 카운트에 기초한 폐기 정책이 사용될 수있다. 약간의 복잡성이 추가되었지만, 아시다시피, "무료 신발과 같은 것은 없습니다":)
Joh

1

Inversion of Control을 사용하는 것은 어떻습니까?

class Bus
{
    private Driver busDriver;

    public Bus(Driver busDriver)
    {
        this.busDriver = busDriver;
    }
}

class Driver
{
    private Shoe[] shoes;

    public Driver(Shoe[] shoes)
    {
        this.shoes = shoes;
    }
}

class Shoe
{
    private Shoelace lace;

    public Shoe(Shoelace lace)
    {
        this.lace = lace;
    }
}

class Shoelace
{
    bool tied;
    private AutoResetEvent waitHandle;

    public Shoelace(bool tied, AutoResetEvent waitHandle)
    {
        this.tied = tied;
        this.waitHandle = waitHandle;
    }
}

class Program
{
    static void Main(string[] args)
    {
        using (var leftShoeWaitHandle = new AutoResetEvent(false))
        using (var rightShoeWaitHandle = new AutoResetEvent(false))
        {
            var bus = new Bus(new Driver(new[] {new Shoe(new Shoelace(false, leftShoeWaitHandle)),new Shoe(new Shoelace(false, rightShoeWaitHandle))}));
        }
    }
}

이제 호출자 문제로 만들었고 구두끈은 필요할 때 사용할 수있는 대기 핸들에 의존 할 수 없습니다.
Andy

@andy "신발 끈은 필요할 때 사용할 수있는 대기 핸들에 의존 할 수 없습니다"는 무슨 뜻입니까? 생성자에 전달됩니다.
Darragh

Shoelace에 제공하기 전에 자동 재설정 이벤트 상태에 문제가있을 수 있으며 ARE 상태가 나쁜 상태 일 수 있습니다. Shoelace가 작업을 수행하는 동안 Sare가 잘못된 작업을 수행하는 동안 ARE의 상태에 문제가 생길 수 있습니다. 개인 회원 만 잠그는 것과 같은 이유입니다. "일반적으로 .. 코드 제어를 넘어서는 인스턴스 잠금 방지" docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
Andy

개인 회원 만 잠금에 동의합니다. 그러나 대기 핸들이 다른 것 같습니다. 실제로 동일한 인스턴스를 사용하여 스레드간에 통신해야하므로 객체에 객체로 전달하는 것이 더 유용합니다. 그럼에도 불구하고 IoC는 OP 문제에 유용한 솔루션을 제공한다고 생각합니다.
Darragh

OPs 예제에 개인 멤버로서 대기 핸들이 있다고 가정하면, 안전한 가정은 오브젝트가 독점 액세스를 기대한다는 것입니다. 인스턴스 외부에서 핸들을 볼 수있게 설정하면 위반됩니다. 일반적으로 IoC는 이와 같은 작업에는 적합하지만 스레딩에는 적합하지 않습니다.
Andy
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.