Java / C #에서 RAII를 구현할 수없는 이유는 무엇입니까?


29

질문 : Java / C #에서 RAII를 구현할 수없는 이유는 무엇입니까?

설명 : 가비지 수집기가 결정적이지 않다는 것을 알고 있습니다. 따라서 현재 언어 기능을 사용하면 범위 종료시 객체의 Dispose () 메서드를 자동으로 호출 할 수 없습니다. 그러나 그러한 결정적 기능을 추가 할 수 있습니까?

내 이해 :

RAII 구현은 두 가지 요구 사항을 충족해야한다고 생각합니다.
1. 리소스 수명은 범위에 속해야합니다.
2. 암시 적. 프로그래머가 명시 적으로 진술하지 않고 자원을 확보해야합니다. 명시 적 진술없이 메모리를 비우는 가비지 수집기와 유사합니다. "내 재성"은 클래스 사용 시점에서만 발생하면됩니다. 클래스 라이브러리 생성자는 물론 소멸자 또는 Dispose () 메서드를 명시 적으로 구현해야합니다.

Java / C #은 포인트 1을 만족합니다. C #에서 IDisposable을 구현하는 리소스는 "사용"범위에 바인딩 될 수 있습니다.

void test()
{
    using(Resource r = new Resource())
    {
        r.foo();
    }//resource released on scope exit
}

이것은 포인트 2를 만족시키지 않습니다. 프로그래머는 명시 적으로 개체를 특수한 "사용"범위에 묶어야합니다. 프로그래머는 명시 적으로 리소스를 스코프에 연결하여 누출을 생성하는 것을 잊어 버릴 수 있습니다.

실제로 "using"블록은 컴파일러에 의해 try-finally-dispose () 코드로 변환됩니다. try-finally-dispose () 패턴의 명시 적 특성과 동일합니다. 암시 적 방출이 없으면 범위에 대한 후크는 구문 설탕입니다.

void test()
{
    //Programmer forgot (or was not aware of the need) to explicitly
    //bind Resource to a scope.
    Resource r = new Resource(); 
    r.foo();
}//resource leaked!!!

스마트 포인터를 통해 스택에 후크되는 특수 객체를 허용하는 Java / C #에서 언어 기능을 만드는 것이 좋습니다. 이 기능을 사용하면 클래스를 스코프 바운드로 플래그 지정할 수 있으므로 항상 스택에 대한 후크로 생성됩니다. 다른 유형의 스마트 포인터에 대한 옵션이있을 수 있습니다.

class Resource - ScopeBound
{
    /* class details */

    void Dispose()
    {
        //free resource
    }
}

void test()
{
    //class Resource was flagged as ScopeBound so the tie to the stack is implicit.
    Resource r = new Resource(); //r is a smart-pointer
    r.foo();
}//resource released on scope exit.

내포가 "가치"라고 생각합니다. 가비지 수집의 암시가 "가치있는"것처럼. 명시 적 사용 블록은 눈에 상쾌하지만 try-finally-dispose ()에 비해 의미 론적 이점을 제공하지 않습니다.

그러한 기능을 Java / C # 언어로 구현하는 것은 실용적이지 않습니까? 오래된 코드를 손상시키지 않고 소개 할 수 있습니까?


3
비현실적이지 않습니다 . 불가능 합니다. 소멸자을 보장하지 않습니다 C #을 표준 / DisposeS가되어 지금 에 관계없이 트리거하는 방법, 실행합니다. 범위 끝에 암시 적 파괴를 추가해도 도움이되지 않습니다.
Telastyn

20
@Telastyn Huh? C # 표준이 말한 것은 문서 변경에 대해 논의하고 있기 때문에 관련성이 없습니다. 유일한 문제는 이것이 실용적인지 여부이며, 현재 보증 부족에 대한 유일한 흥미로운 점은이 보증 부족의 이유 입니다. 에 대한주의 using의 실행 Dispose 됩니다 보장 (예외없이 죽어 갑자기 과정을 할인, 잘하는 모든 정리가 아마도 논쟁이되는 포인트, 슬로우).

4
의 중복 자바의 개발자가 의식적으로 RAII를 포기 했습니까? 허용되는 답변이 완전히 올바르지 는 않습니다. 짧은 대답은 Java 가 가치 (스택) 의미가 아닌 참조 (힙) 의미를 사용 하므로 결정적 마무리는 유용하지 않습니다. C #은 않습니다 가치 의미를 (가 ), 그러나 그들은 일반적으로 아주 특별한 경우를 제외하고는 피할 수있다. 도 참조하십시오 . struct
BlueRaja-대니 Pflughoeft

2
정확히 중복되지는 않습니다.
Maniero

3
blogs.msdn.com/b/oldnewthing/archive/2010/08/10/10048150.aspx 는이 질문과 관련된 페이지입니다.
Maniero

답변:


17

그러한 언어 확장은 생각보다 훨씬 복잡하고 침습적입니다. 당신은 단지 추가 할 수 없습니다

스택 바운드 유형의 변수 수명이 끝나면 Dispose참조하는 객체를 호출하십시오 .

언어 사양의 관련 섹션으로 이동하여 수행하십시오. new Resource().doSomething()좀 더 일반적인 표현으로 해결할 수있는 임시 값 ( ) 의 문제를 무시할 것입니다. 이것은 가장 심각한 문제는 아닙니다. 예를 들어,이 코드는 깨졌을 것입니다 (그리고 이런 종류의 일은 일반적으로 불가능할 것입니다).

File openSavegame(string id) {
    string path = ... id ...;
    File f = new File(path);
    // do something, perhaps logging
    return f;
} // f goes out of scope, caller receives a closed file

이제 사용자 정의 복사 생성자가 필요하거나 생성자를 이동하여 어디서나 호출 할 수 있습니다. 이것은 성능에 영향을 줄뿐만 아니라 이러한 것들을 효과적으로 유형의 가치로 만드는 반면, 거의 모든 다른 객체는 참조 유형입니다. Java의 경우, 이것은 객체 작동 방식과 근본적인 편차입니다. C #에서는 struct그렇지 않지만 (이미 AFAIK를위한 사용자 정의 사본 생성자가 없지만)이 RAII 객체를 더 특별하게 만듭니다. 또는 제한된 형식의 선형 유형 (Rust 참조)도 매개 변수 전달을 포함하여 앨리어싱을 금지하는 비용으로 문제를 해결할 수 있습니다 (Rust와 같은 차용 참조 및 차용 검사기를 채택하여 훨씬 더 복잡한 것을 도입하려는 경우는 제외).

기술적으로 할 수는 있지만 언어의 다른 모든 것과는 매우 다른 범주의 범주로 끝납니다 . 구현 자 (가장 많은 경우, 모든 부서에서 더 많은 시간 / 비용)와 사용자 (배워야 할 더 많은 개념, 더 많은 버그 가능성)에 영향을주는 것은 거의 항상 나쁜 생각입니다. 추가 편의 가치가 없습니다.


복사 / 이동 생성자가 필요한 이유는 무엇입니까? 파일은 여전히 ​​참조 유형입니다. 포인터 인 f는 호출자에게 복사되고 자원을 처리 할 책임이있다 (컴파일러는 암시 적으로 호출자에게 try-finally-dispose 패턴을 암시 할 것이다)
Maniero

1
@bigown File이 방법으로 모든 참조를 처리하면 아무것도 바뀌지 Dispose않으며 호출되지 않습니다. 항상을 호출 Dispose하면 일회용 객체로 아무것도 할 수 없습니다. 아니면 때로는 처분하고 때로는 그렇지 않은 계획을 제안하고 있습니까? 그렇다면 자세히 설명해 주시면 실패한 상황을 알려 드리겠습니다.

나는 당신이 지금 말한 것을 보지 못합니다 (나는 당신이 틀렸다고 말하는 것이 아닙니다). 개체에는 참조가 아닌 리소스가 있습니다.
Maniero

예제를 단지 반환으로 바꾸는 나의 이해는 컴파일러가 리소스 획득 직전에 시도를 삽입하고 (귀하의 3 행) 최종 범위 블록 (6 행) 직전에 마지막으로 처리 블록을 삽입한다는 것입니다. 문제 없어요 동의하십니까? 예를 들어 보자. 컴파일러는 전송을 보았습니다. 여기에 마지막으로 삽입 할 수는 없지만 호출자는 File 객체를 받고 호출자 가이 객체를 다시 전송하지 않는다고 가정하면 컴파일러는 try-finally 패턴을 삽입합니다. 다시 말해서, 전송되지 않은 모든 IDisposable 객체는 최종 패턴을 적용해야합니다.
Maniero

1
@bigown 즉, Dispose참조가 이스케이프되면 호출하지 않습니까? 탈출 분석은 오래되고 어려운 문제이므로 언어를 더 이상 변경하지 않으면 항상 작동하지는 않습니다. 참조가 다른 (가상) 메소드 ( something.EatFile(f);) 로 전달 될 때 f.Dispose범위 끝에서 호출 해야 합니까? 그렇다면 f나중에 사용하기 위해 저장 하는 발신자를 차단 합니다. 그렇지 않은 경우 호출자가 저장 하지 않으면 리소스가 누출 f됩니다. 이것을 제거하는 유일하고 간단한 방법은 선형 유형 시스템이며, (나중에 이미 답변했듯이) 다른 많은 합병증을 소개합니다.

26

Java 또는 C #에서 이와 같은 것을 구현하는 데 가장 큰 어려움은 리소스 전송의 작동 방식을 정의하는 것입니다. 범위를 넘어 자원 수명을 연장 할 수있는 방법이 필요합니다. 치다:

class IWrapAResource
{
    private readonly Resource resource;
    public IWrapAResource()
    {
        // Where Resource is scope bound
        Resource builder = new Resource(args, args, args);

        this.resource = builder;
    } // Uh oh, resource is destroyed
} // Crap, there's no scope for IWrapAResource we can bind to!

더 나쁜 것은 이것이 다음 구현 자에게는 분명하지 않을 수 있다는 것입니다 IWrapAResource.

class IWrapSomething<T>
{
    private readonly T resource; // What happens if T is Resource?
    public IWrapSomething(T input)
    {
        this.resource = input;
    }
}

C #과 같은 것 using 진술 것은 아마도 계산 자원을 참조하거나 C 또는 C ++과 같은 모든 곳에서 가치 의미를 강요하지 않고 RAII 의미를 가지게 될 것입니다. Java 및 C #은 가비지 수집기에서 관리하는 리소스를 암시 적으로 공유하므로 프로그래머가 할 수있는 최소한의 방법은 리소스가 바인딩되는 범위를 선택하는 것 using입니다.


범위를 벗어난 후 변수를 참조 할 필요가 없다고 가정 하고 (실제로 그러한 필요는 없어야 함), 나는 종결자를 작성하여 객체를 스스로 처분 할 수 있다고 주장합니다. . 종료자는 객체가 가비지 수집되기 직전에 호출됩니다. 참조 msdn.microsoft.com/en-us/library/0s71x931.aspx
로버트 하비에게

8
@Robert : 올바르게 작성된 프로그램은 종료자를 실행한다고 가정 할 수 없습니다. blogs.msdn.com/b/oldnewthing/archive/2010/08/09/10047586.aspx
Billy ONeal

1
흠. 글쎄, 아마도 그들이 using진술을 생각 해낸 이유 일 것입니다 .
Robert Harvey

2
정확하게. 이것은 C ++의 초보자 버그의 큰 소스이며 Java / C #에도 있습니다. Java / C #은 파괴 될 리소스에 대한 참조를 유출하는 기능을 제거하지는 않지만 명시 적이거나 선택적으로 만들면 프로그래머에게 생각 나게하고 무엇을해야할지 의식적으로 선택할 수 있습니다.
Aleksandr Dubinsky

1
@svick IWrapSomething폐기 할 책임이 없습니다 T. 만든 사람 은 자체를 사용하거나 자체 자원 수명주기 체계를 T사용하는지 여부에 대해 걱정해야합니다 . usingIDisposable
Aleksandr Dubinsky

13

RAII가 C #과 같은 언어로는 작동하지 않지만 C ++에서는 작동하는 이유는 C ++에서는 객체가 실제로 임시인지 (스택에 할당하여) 또는 오래 지속되는지 여부를 결정할 수 있기 때문입니다 ( new포인터를 사용 하고 사용하여 힙에 할당 ).

따라서 C ++에서는 다음과 같이 할 수 있습니다.

void f()
{
    Foo f1;
    Foo* f2 = new Foo();
    Foo::someStaticField = f2;

    // f1 is destroyed here, the object pointed to by f2 isn't
}

C #에서는 두 경우를 구별 할 수 없으므로 컴파일러는 객체를 마무리할지 여부를 모를 것입니다.

당신이 할 수있는 일은 일종의 특수 지역 변수 종류를 소개하는 것입니다. 필드에 넣을 수 없으며 * 범위를 벗어날 때 자동으로 처리됩니다. 정확히 C ++ / CLI가하는 일입니다. C ++ / CLI에서 다음과 같은 코드를 작성합니다.

void f()
{
    Foo f1;
    Foo^ f2 = gcnew Foo();
    Foo::someStaticField = f2;

    // f1 is disposed here, the object pointed to by f2 isn't
}

이것은 기본적으로 다음 C #과 동일한 IL로 컴파일됩니다.

void f()
{
    using (Foo f1 = new Foo())
    {
        Foo f2 = new Foo();
        Foo.someStaticField = f2;
    }
    // f1 is disposed here, the object pointed to by f2 isn't
}

결론적으로, C #의 설계자가 RAII를 추가하지 않은 이유를 추측한다면, 두 가지 유형의 로컬 변수를 갖는 것이 가치가 없다고 생각했기 때문입니다. 주로 GC가있는 언어에서는 결정 론적 마무리가 유용하지 않기 때문입니다. 자주.

* C ++ / CLI &연산자 와 동등한 연산자 가없는 것은 아닙니다 %. 그렇게하는 것은 방법이 끝난 후에 필드가 배치 된 객체를 참조한다는 점에서 "안전하지 않은"것입니다.


1
C struct와 같이 D와 같은 유형의 소멸자를 허용하면 CII는 사소하게 RAII를 수행 할 수 있습니다.
Jan Hudec

6

using블록으로 당신을 괴롭히는 것이 그들의 명백 함 이라면 , 아마도 C # 스펙 자체를 변경하는 것보다 덜 명시 적으로 작은 단계를 취할 수 있습니다. 이 코드를 고려하십시오.

public void ReadFile ()
{
  string filename = "myFile.dat";
  local Stream file = File.Open(filename);
  file.Read(blah blah blah);
}

local추가 한 키워드가 보입니까? 그것은이하는 모든처럼, 조금 더 문법 설탕을 추가 할 것입니다 using전화 컴파일러를 말하고, DisposeA의 finally변수의 범위의 끝 부분에 블록. 그게 다야 다음과 완전히 같습니다.

public void ReadFile ()
{
  string filename = "myFile.dat";
  using (Stream file = File.Open(filename))
  {
      file.Read(blah blah blah);
  }
}

그러나 명시 적 범위가 아니라 암시 적 범위를 갖습니다. 클래스를 범위 제한으로 정의 할 필요가 없으므로 다른 제안보다 간단합니다. 더 깨끗하고 암시적인 구문 설탕.

해결하기 어려운 범위에 문제가있을 수 있지만 지금은 볼 수 없지만 찾을 수있는 사람에게 감사드립니다.


1
@ mike30이지만 유형 정의로 이동하면 다른 사람들이 나열된 문제를 정확하게 유발합니다. 포인터를 다른 메소드에 전달하거나 함수에서 반환하면 어떻게됩니까? 이렇게하면 범위가 다른 곳이 아닌 범위에서 선언됩니다. 유형은 Disposable 일 수 있지만 Dispose를 호출하는 것은 아닙니다.
Avner Shahar-Kashtan

3
@ mike30 : Meh. 이 모든 구문은 괄호를 제거하고 확장 기능으로 제공되는 범위 제어를 제거합니다.
Robert Harvey

1
@RobertHarvey 정확하게. 더 깔끔하고 덜 중첩 된 코드를 위해 약간의 유연성을 희생합니다. @delnan의 제안을 받아 using키워드를 재사용 하면 특정 범위가 필요하지 않은 경우 기존 동작을 유지하고 사용할 수도 있습니다. 중괄호없이 using기본값을 현재 범위로 설정하십시오.
Avner Shahar-Kashtan

1
언어 디자인에 대한 반 실습 연습에는 문제가 없습니다.
Avner Shahar-Kashtan

1
@RobertHarvey. 현재 C #으로 구현되지 않은 것에 대한 편견이있는 것 같습니다. C # 1.0에 만족하면 generics, linq, using-blocks, ipmlicit 유형 등이 없습니다. 이 구문은 내 재성 문제를 해결하지는 않지만 현재 범위에 바인딩하는 것이 좋습니다.
mike30

1

RAII가 가비지 수집 언어에서 작동하는 방법에 대한 예는 withPython 에서 키워드를 확인하십시오 . 결정적으로 파괴 된 객체에 의존하는 대신 주어진 어휘 범위에 메소드 __enter__()__exit__()메소드를 연관시킬 수 있습니다 . 일반적인 예는 다음과 같습니다.

with open('output.txt', 'w') as f:
    f.write('Hi there!')

C ++의 RAII 스타일과 마찬가지로 파일은 '정상'종료인지, break즉각적인 지 return또는 예외인지에 관계없이 해당 블록을 종료 할 때 닫힙니다 .

점을 유의 open()통화가 보통의 파일 열기 기능입니다. 이 작업을 수행하기 위해 리턴 된 파일 오브젝트에는 두 가지 메소드가 포함됩니다.

def __enter__(self):
  return self
def __exit__(self):
  self.close()

이것은 파이썬의 일반적인 관용구입니다. 리소스와 관련된 객체에는 일반적으로이 두 가지 방법이 포함됩니다.

파일 객체는 __exit__()호출 후에도 할당 된 상태를 유지할 수 있지만 중요한 것은 파일 객체 가 닫혀 있다는 것입니다.


7
with파이썬 using에서 C #에서와 거의 동일 하며이 질문에 관한 한 RAII는 아닙니다.

1
파이썬의 "with"는 스코프 바운드 리소스 관리이지만 스마트 포인터의 암시성이 없습니다. 포인터를 스마트로 선언하는 행위는 "명시 적"으로 간주 될 수 있지만, 컴파일러가 객체 유형의 일부로 스마트 성을 강화한 경우 "암시 적"으로 기울어집니다.
mike30

RAAI의 핵심 인 AFAICT는 자원에 대한 엄격한 범위를 설정하고 있습니다. 객체를 할당 해제하여 수행하는 데 관심이 있다면 가비지 수집 언어는 할 수 없습니다. 지속적으로 리소스를 공개하는 데 관심이있는 경우이를 수행하는 방법입니다 (또 다른 defer언어는 Go 언어 임).
Javier

1
실제로 Java와 C #이 명시 적 구성을 강력하게 선호한다고 말하는 것이 공정하다고 생각합니다. 그렇지 않으면 왜 인터페이스와 상속 사용에 내재 된 모든 의식이 필요합니까?
Robert Harvey

1
@delnan, Go에는 '암시 적'인터페이스가 있습니다.
Javier
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.