다른 개체에 전달 될 때 IDisposable 개체에 대해 Dispose를 호출해야하는 사람은 누구입니까?
Dispose()
다른 개체의 메서드 또는 생성자에 전달 된 일회용 개체를 호출해야하는 사람에 대한 지침이나 모범 사례 가 있습니까?
제가 의미하는 바에 대한 몇 가지 예가 있습니다.
IDisposable 객체는 메서드에 전달됩니다 (완료되면 폐기해야합니까?).
public void DoStuff(IDisposable disposableObj)
{
// Do something with disposableObj
CalculateSomething(disposableObj)
disposableObj.Dispose();
}
IDisposable 개체는 메서드에 전달되고 참조가 유지됩니다 (삭제시이를 처리해야합니까 MyClass
?).
public class MyClass : IDisposable
{
private IDisposable _disposableObj = null;
public void DoStuff(IDisposable disposableObj)
{
_disposableObj = disposableObj;
}
public void Dispose()
{
_disposableObj.Dispose();
}
}
나는 현재 첫 번째 예제에서 호출자 가 DoStuff()
아마도 객체를 생성했을 때 객체를 처분해야한다고 생각하고 있습니다. 그러나 두 번째 예 MyClass
에서는 객체에 대한 참조를 유지하므로 객체를 폐기해야하는 것처럼 느껴집니다 . 이것의 문제는 호출하는 클래스가 MyClass
참조를 유지 했는지 알지 못할 수 있으므로 MyClass
사용이 완료 되기 전에 객체를 폐기하기로 결정할 수 있다는 것입니다. 이런 종류의 시나리오에 대한 표준 규칙이 있습니까? 있는 경우 일회용 개체가 생성자에 전달 될 때 차이가 있습니까?
일반적으로 객체를 생성 (또는 소유권을 획득) 한 경우 해당 객체를 폐기하는 것은 귀하의 책임입니다. 즉, 메서드 또는 생성자에서 매개 변수로 일회용 개체를받는 경우 일반적으로 해당 개체를 처리해서는 안됩니다.
참고 .NET Framework의 일부 클래스는 것을 어떻게 그들이 매개 변수로받은 처분 개체가. 예를 들어 a를 폐기 StreamReader
하면 기본 Stream
.
추신 : 새로운 답변 을 게시했습니다 (를 호출해야하는 간단한 규칙 세트
Dispose
와IDisposable
객체 를 처리하는 API를 설계하는 방법 포함 ). 현재 답변에는 귀중한 아이디어가 포함되어 있지만 주요 제안이 실제로 작동하지 않는 경우가 많다고 생각IDisposable
합니다. "거친 입자"개체에서 개체를 숨기는 것은 종종IDisposable
자신 이 될 필요가 있음을 의미합니다 . 그래서 하나는 시작된 곳에서 끝나고 문제는 남아 있습니다.
Dispose()
다른 개체의 메서드 또는 생성자에 전달 된 일회용 개체를 호출해야하는 사람에 대한 지침이나 모범 사례 가 있습니까?
짧은 대답:
그래, 내가 알고있는 가장이이 주제에 대한 많은 조언을, 그리고 것은 에릭 에반스 의 '개념 집계 에서 도메인 기반 디자인 . (간단히 말해, 적용되는 핵심 아이디어 IDisposable
는 이것이다. IDisposable
외부에서 보이지 않고 구성 요소 소비자에게 전달되지 않도록 굵은 구성 요소로 캡슐화합니다 .)
또한 IDisposable
개체 의 작성자가 개체를 처리해야한다는 생각은 너무 제한적이며 실제로 작동하지 않는 경우가 많습니다.
내 대답의 나머지 부분은 동일한 순서로 두 가지 점에 대해 자세히 설명합니다. 동일한 주제와 관련된 추가 자료에 대한 몇 가지 포인터로 답변을 마무리하겠습니다.
더 긴 답변 —이 질문의 전체적인 내용은 다음과 같습니다.
이 주제에 대한 조언은 일반적으로 IDisposable
. 사람들이 객체 수명과 소유권에 대해 이야기 할 때마다 그들은 매우 동일한 문제를 언급하고 있습니다 (그러나보다 일반적인 용어로).
이 주제가 .NET 에코 시스템에서 거의 발생하지 않는 이유는 무엇입니까? .NET의 런타임 환경 (CLR)은 자동 가비지 수집을 수행하기 때문에 모든 작업을 수행합니다. 더 이상 개체가 필요하지 않은 경우 개체를 잊어 버리면 가비지 수집기가 결국 해당 메모리를 회수합니다.
그렇다면 왜 그 질문에 IDisposable
사물이 떠오르는가? 왜냐하면 IDisposable
(종종 희소하거나 비용이 많이 드는) 리소스의 수명에 대한 명시적이고 결정적인 제어에 관한 것입니다. IDisposable
객체는 더 이상 필요하지 않은 즉시 해제되어야하며 가비지 수집기의 불확실한 보장 ( " 결국 메모리를 회수 할 것입니다." 당신이 사용했습니다! ") 단순히 충분하지 않습니다.
객체 수명 및 소유권에 대한 광범위한 용어로 다시 표현 된 질문 :
어떤 객체
O
A (일회용) 개체의 수명 종료에 대한 책임을 져야한다D
또한 객체에 전달되는,X,Y,Z
?
몇 가지 가정을 설정해 보겠습니다.
객체를 호출
D.Dispose()
하면 기본적으로 수명이 종료됩니다.IDisposable
D
논리적으로 객체의 수명은 한 번만 종료 될 수 있습니다. (
IDisposable
이것은 명시 적으로Dispose
.에 대한 다중 호출을 허용 하는 프로토콜 에 반대되는 순간에 대해서는 신경 쓰지 마십시오 .)따라서 단순성을 위해 정확히 하나의 객체
O
가D
.O
주인에게 전화합시다 .
이제 문제의 핵심에 도달했습니다. C # 언어 나 VB.NET은 개체 간의 소유권 관계를 적용하는 메커니즘을 제공하지 않습니다. 따라서 이것은 디자인 문제로 바뀝니다. O,X,Y,Z
다른 개체에 대한 참조를받는 모든 개체 는 D
.NET에 대한 소유권을 가진 사람을 정확히 규제하는 규칙을 따라야합니다 D
.
Aggregates로 문제를 단순화하십시오!
이 주제에 대해 내가 찾은 최고의 조언은 Eric Evans 의 2004 년 책인 Domain-Driven Design에서 나왔습니다 . 책에서 인용하겠습니다.
데이터베이스에서 Person 객체를 삭제했다고 가정 해 보겠습니다. 그 사람과 함께 이름, 생년월일, 직업 설명을 씁니다. 하지만 주소는 어떻습니까? 같은 주소에 다른 사람이있을 수 있습니다. 주소를 삭제하면 해당 Person 개체는 삭제 된 개체에 대한 참조를 갖게됩니다. 그대로두면 데이터베이스에 정크 주소가 누적됩니다. 자동 가비지 수집은 정크 주소를 제거 할 수 있지만 데이터베이스 시스템에서 사용 가능한 경우에도 해당 기술 수정은 기본 모델링 문제를 무시합니다. (125 쪽)
이것이 귀하의 문제와 어떤 관련이 있는지 보십니까? 이 예제의 주소는 일회용 개체와 동일하며 질문은 동일합니다. 누가 삭제해야합니까? 누가 "소유"합니까?
Evans는 이 디자인 문제에 대한 해결책으로 Aggregates 를 제안 합니다. 다시 책에서 :
집계는 데이터 변경을 위해 하나의 단위로 취급하는 관련 개체의 클러스터입니다. 각 집계에는 루트와 경계가 있습니다. 경계는 Aggregate 내부에있는 것을 정의합니다. 루트는 집계에 포함 된 하나의 특정 엔티티입니다. 루트는 경계 내의 객체가 서로에 대한 참조를 보유 할 수 있지만 외부 객체가 참조를 보유 할 수있는 집계의 유일한 구성원입니다. (126-127 쪽)
여기서 핵심 메시지는 IDisposable
객체 의 전달 을 엄격하게 제한된 다른 객체 세트 ( "집계")로 제한해야한다는 것 입니다. 집계 경계 외부의 개체는 IDisposable
. 이렇게하면 모든 개체의 가장 큰 부분, 즉 집계 외부에있는 Dispose
개체가 개체 가 될 수 있는지 더 이상 걱정할 필요가 없으므로 작업이 크게 단순화 됩니다. 당신이해야 할 일은 경계 안의 객체 가 누가 그것을 처리하는지 모두 알고 있는지 확인 하는 것입니다. 일반적으로 함께 구현하고 총계 경계를 합리적으로 "단단하게"유지하기 위해주의를 기울이기 때문에 이것은 해결하기에 충분히 쉬운 문제 여야합니다.
IDisposable
개체 의 작성자 도이를 처리해야한다는 제안은 어떻습니까?
이 가이드 라인은 합리적으로 들리며 매력적인 대칭이 있지만 그 자체로는 실제로 작동하지 않는 경우가 많습니다. 틀림없이 그것은 "는에 대한 참조를 통과하지 마십시오, 말과 같은 의미 IDisposable
하기 때문에 당신이 그렇게로서 곧, 당신은 수신 개체가 있음을 위험으로, 다른 개체에 개체" 가정 당신이 모르고 소유권 및 처분하는 그것.
하자 분명 엄지 손가락의 규칙을 위반는 .NET 기본 클래스 라이브러리 (BCL)의 두 눈에 잘 띄는 인터페이스 유형 봐 : IEnumerable<T>
와 IObservable<T>
. 둘 다 본질적으로 IDisposable
객체 를 반환하는 팩토리입니다 .
IEnumerator<T> IEnumerable<T>.GetEnumerator()
(에서IEnumerator<T>
상속 됨을 기억하십시오IDisposable
.)IDisposable IObservable<T>.Subscribe(IObserver<T> observer)
두 경우 모두 호출자 는 반환 된 개체를 처리해야합니다. 논란의 여지가 있지만, 우리의 가이드 라인은 단순히 객체 팩토리의 경우 이해가되지 않습니다 ... 아마도 우리가 요청자 (직접 생성자가 아닌 )가 IDisposable
그것을 릴리스 하도록 요구 하지 않는 한 .
덧붙여,이 예제는 위에서 설명한 집계 솔루션의 한계를 보여줍니다 모두 IEnumerable<T>
와 것은 IObservable<T>
지금까지 집계의 일부가 본질적으로 너무 일반적인 방법입니다. 집계는 일반적으로 매우 도메인별로 다릅니다.
추가 리소스 및 아이디어 :
UML에서 객체 간의 "has a"관계는 집계 (빈 다이아몬드) 또는 구성 (채워진 다이아몬드)의 두 가지 방법으로 모델링 할 수 있습니다. 구성은 포함 / 참조 된 개체의 수명이 컨테이너 / 참조 자의 수명으로 종료된다는 점에서 집계와 다릅니다. 귀하의 원래 질문은 집계 ( "양도 가능한 소유권")를 암시하는 반면, 저는 주로 구성 ( "고정 소유권")을 사용하는 솔루션을 지향했습니다. "개체 구성"에 대한 Wikipedia 기사를 참조하십시오 .
Autofac (a .NET 애플리케이션 컨텍스트 컨테이너)이 두 가지 문제를 해결한다 : 하나, 통신 소위 이용한 관계 타입 ,
Owned<T>
오버 소유권을 획득IDisposable
; 또는 Autofac에서 수명 범위라고하는 작업 단위의 개념을 통해.후자와 관련하여 Autofac의 창시자 인 Nicholas Blumhardt는 "IDisposable 및 소유권"섹션을 포함하는 "An Autofac Lifetime Primer"를 작성했습니다 . 전체 기사는 .NET의 소유권 및 수명 문제에 대한 훌륭한 논문입니다. Autofac에 관심이없는 사람들에게도 읽어 보는 것이 좋습니다.
C ++에서 RAII (Resource Acquisition Is Initialization) 관용구 (일반) 및 스마트 포인터 유형 (특히)은 프로그래머가 개체 수명 및 소유권 문제를 올바르게 파악하는 데 도움이됩니다. 불행히도 .NET에는 결정 론적 객체 파괴에 대한 C ++의 우아한 지원이 없기 때문에 .NET으로 전송할 수 없습니다.
Stack Overflow에 대한 질문에 대한 이 답변 을 참조하십시오 . "이종 구현 요구 사항을 어떻게 처리해야합니까?" , (내가 올바르게 이해한다면) 내 Aggregate-based answer와 유사한 생각을 따릅니다
IDisposable
.
일반적으로 Disposable 개체를 처리하면 더 이상 평생 소유권이 문제가되는 이상적인 관리 코드 세계에 있지 않습니다. 결과적으로, 어떤 개체가 논리적으로 "소유"하거나 일회용 개체의 수명을 책임 지는지 고려해야합니다.
일반적으로 메서드에 방금 전달 된 일회용 개체의 경우 한 개체가 다른 개체의 소유권을 가져 와서 처리하는 경우가 매우 드물기 때문에 메서드가 개체를 처리해서는 안됩니다. 같은 방법. 이러한 경우 발신자는 폐기 할 책임이 있습니다.
회원 데이터에 대해 이야기 할 때 "예, 항상 폐기합니다"또는 "아니요, 폐기하지 않습니다"라는 자동 응답은 없습니다. 그보다는 각각의 특정한 경우에있는 물체에 대해 생각하고 스스로에게 "이 물체가 일회용 물체의 수명을 담당 하는가?"라고 자문 해 볼 필요가 있습니다.
경험에 따르면 일회용품을 생성하는 개체가이를 소유하므로 나중에 폐기 할 책임이 있습니다. 소유권 이전이있는 경우에는 적용되지 않습니다. 예를 들면 :
public class Foo
{
public MyClass BuildClass()
{
var dispObj = new DisposableObj();
var retVal = new MyClass(dispObj);
return retVal;
}
}
Foo
은 (는) 생성 dispObj
에 대한 책임이 분명 하지만의 인스턴스에 소유권을 전달합니다 MyClass
.
이것은 이전 답변에 대한 후속 조치입니다 . 내가 다른 게시물을 게시하는 이유를 알아 보려면 초기 발언을 참조하십시오.
내 이전 답변은 한 가지를 맞았습니다. 각각 IDisposable
은 Dispose
정확히 한 번만 -ing 할 책임이있는 독점 "소유자"를 가져야합니다 . 관리 IDisposable
목적은 다음 관리되지 않는 코드 시나리오에서 메모리를 관리하는 매우 비교된다.
.NET의 이전 기술인 COM (Component Object Model) 은 개체 간의 메모리 관리 책임에 다음 프로토콜을 사용했습니다 .
- "인 파라미터는 호출자가 할당하고 해제해야합니다.
- "Out-parameter는 호출자가 할당해야하며 호출자 […]에 의해 해제됩니다.
- "in-out-parameters는 초기에 호출자에 의해 할당 된 다음, 필요한 경우 호출 된 매개 변수에 의해 해제되고 재 할당됩니다. out 매개 변수의 경우와 마찬가지로 호출자는 최종 반환 값을 해제 할 책임이 있습니다."
(오류 사례에 대한 추가 규칙이 있습니다. 자세한 내용은 위에 링크 된 페이지를 참조하세요.)
에 대해 이러한 지침을 적용한다면 IDisposable
다음과 같은 내용을 설명 할 수 있습니다.
IDisposable
소유권 에 관한 규칙 :
IDisposable
일반 매개 변수를 통해 메소드에가 전달 되면 소유권이 이전되지 않습니다. 호출 된 메서드는를 사용할 수IDisposable
있지만 사용해서는 안됩니다Dispose
(소유권을 전달 하지 않아야 합니다. 아래 규칙 4 참조).- 매개 변수 또는 반환 값을
IDisposable
통해 메서드에서가 반환 되면out
메서드에서 호출자에게 소유권이 전송됩니다. 발신자는이를 처리Dispose
해야합니다 (또는IDisposable
동일한 방식 으로 소유권을 전달 ). - 이 때
IDisposable
비아 방법에 주어진ref
파라미터,이를 통해 다음 소유권이 메소드로 전달된다. 메서드는을IDisposable
로컬 변수 또는 개체 필드에 복사 한 다음ref
매개 변수를 로 설정해야 합니다null
.
위와 같은 중요한 규칙 중 하나는 다음과 같습니다.
- 소유권이없는 경우 양도해서는 안됩니다. 즉,
IDisposable
일반 매개 변수를 통해 객체를 받은 경우 동일한 객체를ref IDisposable
매개 변수에 넣거나 반환 값 또는out
매개 변수 를 통해 노출 하지 마십시오 .
예:
sealed class LineReader : IDisposable
{
public static LineReader Create(Stream stream)
{
return new LineReader(stream, ownsStream: false);
}
public static LineReader Create<TStream>(ref TStream stream) where TStream : Stream
{
try { return new LineReader(stream, ownsStream: true); }
finally { stream = null; }
}
private LineReader(Stream stream, bool ownsStream)
{
this.stream = stream;
this.ownsStream = ownsStream;
}
private Stream stream; // note: must not be exposed via property, because of rule (2)
private bool ownsStream;
public void Dispose()
{
if (ownsStream)
{
stream?.Dispose();
}
}
public bool TryReadLine(out string line)
{
throw new NotImplementedException(); // read one text line from `stream`
}
}
This class has two static factory methods and thereby lets its client choose whether it wants to keep or pass on ownership:
One accepts a
Stream
object via a regular parameter. This signals to the caller that ownership will not be taken over. Thus the caller needs toDispose
:using (var stream = File.OpenRead("Foo.txt")) using (var reader = LineReader.Create(stream)) { string line; while (reader.TryReadLine(out line)) { Console.WriteLine(line); } }
One that accepts a
Stream
object via aref
parameter. This signals to the caller that ownership will be transferred, so the caller does not need toDispose
:var stream = File.OpenRead("Foo.txt"); using (var reader = LineReader.Create(ref stream)) { string line; while (reader.TryReadLine(out line)) { Console.WriteLine(line); } }
Interestingly, if
stream
were declared as ausing
variable:using (var stream = …)
, compilation would fail becauseusing
variables cannot be passed asref
parameters, so the C# compiler helps enforce our rules in this specific case.
Finally, note that File.OpenRead
is an example for a method that returns a IDisposable
object (namely, a Stream
) via the return value, so ownership over the returned stream is transferred to the caller.
Disadvantage:
The main disadvantage to this pattern is that AFAIK, noone uses it (yet). So if you interact with any API that doesn't follow the above rules (for example, the .NET Framework Base Class Library) you still need to read the documentation to find out who has to call Dispose
on IDisposable
objects.
One thing I decided to do before I knew much about .NET programming, but it still seems a good idea, is have a constructor that accepts an IDisposable
also accept a Boolean which says whether ownership of the object is going to be transferred as well. For objects which can exist entirely within the scope of using
statements, this generally won't be too important (since the outer object will be disposed within the scope of the Inner object's Using block, there's no need for the outer object to dispose the inner one; indeed, it may be necessary that it not do so). Such semantics can become essential, however, when the outer object will be passed as an interface or base class to code which doesn't know of the inner object's existence. In that case, the inner object is supposed to live until the outer object is destroyed, and thing that knows the inner object is supposed to die when the outer object does is the outer object itself, so the outer object has to be able to destroy the inner one.
Since then, I've had a couple of additional ideas, but haven't tried them. I'd be curious what other people think:
- A reference-counting wrapper for an
IDisposable
object. I haven't really figured out the most natural pattern for doing this, but if an object uses reference counting with Interlocked increment/decrement, and if (1) all code that manipulates the object uses it correctly, and (2) no cyclic references are created using the object, I would expect that it should be possible to have a sharedIDisposable
object which gets destroyed when the last usage goes bye-bye. Probably what should happen would be that the public class should be a wrapper for a private reference-counted class, and it should support a constructor or factory method which will create a new wrapper for the same base instance (bumping the instance's reference count by one). Or, if the class needs to be cleaned up even when wrappers are abandoned, and if the class has some periodic polling routine, the class could keep a list ofWeakReference
s to its wrappers and check to ensure that at least some of them still exist. IDisposable
객체 의 생성자 가 객체가 처음 삭제 될 때 호출 할 대리자를 수락하도록합니다 (객체는 정확히 한 번 삭제되도록 isDisposed 플래그를IDisposable
사용해야Interlocked.Exchange
합니다). 그런 다음 해당 델리게이트는 중첩 된 객체를 처리 할 수 있습니다 (아마도 다른 사람이 객체를 보유하고 있는지 확인하여).
둘 중 하나가 좋은 패턴처럼 보입니까?
'developer tip' 카테고리의 다른 글
캐치 블록으로 돌아 오시겠습니까? (0) | 2020.12.25 |
---|---|
Java에서 RESTful API를 만드는 방법을 배우는 데 가장 좋은 소스는 무엇입니까? (0) | 2020.12.25 |
C ++에서 typename T를 문자열로 변환하는 방법 (0) | 2020.12.25 |
드라이버 실행 파일은 webdriver.ie.driver 시스템 속성으로 설정해야합니다. (0) | 2020.12.25 |
Android Open and Save files to / from Google Drive SDK (0) | 2020.12.25 |