Java / C#でRAIIを実装できないのはなぜですか?


29

質問:Java / C#でRAIIを実装できないのはなぜですか?

明確化:ガベージコレクターは決定論的ではないことを認識しています。そのため、現在の言語機能では、オブジェクトのDispose()メソッドをスコープの終了時に自動的に呼び出すことはできません。しかし、そのような決定論的な機能を追加できますか?

私の理解:

RAIIの実装は、次の2つの要件を満たす必要があると感じてい
ます。1.リソースの有効期間はスコープにバインドされている必要があります。
2.暗黙的。リソースの解放は、プログラマーによる明示的なステートメントなしで行われる必要があります。明示的なステートメントなしでメモリを解放するガベージコレクターに似ています。「暗黙性」は、クラスの使用ポイントでのみ発生する必要があります。もちろん、クラスライブラリの作成者は、デストラクタまたはDispose()メソッドを明示的に実装する必要があります。

Java / C#はポイント1を満たします。C#では、IDisposableを実装するリソースを「using」スコープにバインドできます。

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

これはポイント2を満たしません。プログラマは、オブジェクトを特別な「使用」スコープに明示的に結び付けなければなりません。プログラマーは、リソースをスコープに明示的に結び付けることを忘れる可能性があり(実際にそうします)、リークを作成します。

実際、「使用中」ブロックは、コンパイラーによって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がされ、これまでにかかわらず、それらがトリガーしているかの、実行します。スコープの終わりに暗黙的な破壊を追加しても、それは役に立ちません。
テラスティン

20
@Telastyn Huh?C#規格が今言っていることは、ドキュメントそのものの変更について議論しているため、関連性はありません。唯一の問題は、これが実際に実行できるかどうかであり、そのため、現在の保証の欠如に関する唯一の興味深い点は、この保証の欠如の理由です。usingの実行Dispose 保証されていることに注意してください(まあ、例外がスローされることなく突然死んでしまうプロセスを割り引くと、その時点ですべてのクリーンアップはおそらく無意味になります)。

4
複製のJavaの開発者が意識的にRAIIを放棄しましたか?、受け入れられた答えは完全に間違っていますが。簡単な答えは、Javaは値(スタック)セマンティクスではなく参照(ヒープ)セマンティクスを使用するため、決定論的なファイナライズはあまり役に立たない/可能性がないということです。C#に値のセマンティクス()がありますが、非常に特別な場合を除き、通常は使用されません。も参照してくださいstruct
BlueRaja-ダニーPflughoeft

2
まったく同じではなく、まったく同じです。
マニエロ

3
blogs.msdn.com/b/oldnewthing/archive/2010/08/10/10048150.aspxは、この質問に関連するページです。
マニエロ

回答:


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#ではそうではありません(既にstructsがありますが、それらのユーザー定義のコピーコンストラクタはありません)が、それでもこれらのRAIIオブジェクトはより特別なものになります。または、線形型の限定バージョン(Rustを参照)でも問題を解決できますが、パラメーターの受け渡しを含むエイリアシングを禁止します(Rustのような借用参照と借用チェッカーを採用してさらに複雑なものを導入したい場合を除きます)。

技術的にはできますが、言語の他のすべてとは非常に異なるもののカテゴリになります。これはほとんどの場合、悪いアイデアであり、実装者(すべての部門でより多くのエッジケース、より多くの時間/コスト)およびユーザー(学習するより多くの概念、バグの可能性)をもたらします。利便性を追加する価値はありません。


コピー/移動コンストラクターが必要な理由 ファイルは参照型のままです。その状況では、ポインターであるfが呼び出し元にコピーされ、リソースを
破棄する

1
@bigown Fileこの方法へのすべての参照を扱う場合、何も変更されDisposeず、呼び出されません。常にを呼び出す場合Dispose、使い捨てオブジェクトでは何もできません。または、廃棄する場合としない場合があるためのスキームを提案していますか?その場合は、詳細に説明してください。失敗する状況を説明します。

私はあなたが今言ったことを見ることができません(あなたが間違っていると言っているわけではありません)。オブジェクトには、参照ではなくリソースがあります。
マニエロ

私の理解では、例をリターンに変更すると、コンパイラはリソース取得の直前にトライを挿入し(例の3行目)、スコープの最後の直前にfinally-disposeブロックを挿入します(6行目)。ここで問題ありませんか?例に戻ります。コンパイラーは転送を確認し、ここでtry-finallyを挿入できませんでしたが、呼び出し元はFileオブジェクト(へのポインター)を受け取り、呼び出し元がこのオブジェクトを再度転送しないと仮定すると、コンパイラーはtry-finallyパターンを挿入します。つまり、転送されないすべてのIDisposableオブジェクトは、try-finallyパターンを適用する必要があります。
マニエロ

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
ビリーONeal

1
ふむ まあ、それはおそらく彼らがusing声明を思いついた理由です。
ロバートハーベイ

2
まさに。これは、C ++の初心者のバグの巨大なソースであり、Java / C#にも存在します。Java / C#は、破棄されようとしているリソースへの参照をリークする機能を排除しませんが、明示的およびオプションの両方にすることにより、プログラマに思い出させ、何をするかを意識的に選択させます。
アレクサンドルドゥビンスキー

1
@svick IWrapSomething処分するのは最高ですT。作成者Tは、を使用するかusingIDisposableそれ自体であるか、またはアドホックなリソースライフサイクルスキームを持っているかどうかを心配する必要があります。
アレクサンドルドゥビンスキー

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#では、2つのケースを区別できないため、コンパイラはオブジェクトをファイナライズするかどうかを判断できません。

あなたができることは、フィールドなどに入れることができない特別なローカル変数の種類を導入することです*。それは範囲外になると自動的に破棄されます。これはまさに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を追加しなかった理由を推測した場合、2つの異なるタイプのローカル変数を持つことは価値がないと考えたためです。しばしば。

* C ++ / CLI での&演算子に相当するものがないわけではありません%。メソッドが終了すると、フィールドは破棄されたオブジェクトを参照するという意味で、そうすることは「安全ではありません」。


1
structDのように型のデストラクタが許可されていれば、C#はRAIIを簡単に実行できます。
1月Hudec

6

usingブロックに悩まされるのがその明示性である場合、C#の仕様自体を変更するのではなく、おそらく、明示性を低くするための小さな一歩を踏み出すことができます。次のコードを検討してください。

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

local追加したキーワードをご覧ください。それがないすべてが同じように、少し、より多くのシンタックスシュガーを追加でusing呼び出すようにコンパイラに伝える、Dispose中にfinally変数のスコープの終わりでブロック。以上です。以下と完全に同等です:

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

ただし、明示的なスコープではなく、暗黙的なスコープを使用します。クラスをスコープバインドとして定義する必要がないため、他の提案よりも簡単です。よりクリーンで、より暗黙的な構文糖。

ここでは解決が難しいスコープに問題がある可能性がありますが、今はそれを見ることができません。


1
@ mike30しかし、それを型定義に移動すると、リストされている他の問題に正確につながります-ポインターを別のメソッドに渡すか、関数から返すとどうなりますか?このようにして、スコープは他の場所ではなくスコープ内で宣言されます。タイプはDisposableかもしれませんが、Disposeを呼び出すのはそれだけではありません。
アヴナーシャハルカシュタン

3
@ mike30:ああ。この構文が行うことは、中括弧を削除することと、拡張により、それらが提供するスコープ制御を削除することだけです。
ロバートハーベイ

1
@RobertHarveyまさに。よりクリーンでネストの少ないコードのために柔軟性を犠牲にします。@delnanの提案を受け入れてusingキーワードを再利用すると、特定のスコープが必要ない場合に、既存の動作を維持し、これも使用できます。using現在のスコープにブレースなしのデフォルトを設定します。
アヴナーシャハルカシュタン

1
言語設計の半実用的な演習で問題はありません。
アヴナーシャハル

1
@RobertHarvey。現在、C#で実装されていないものには偏見があるようです。C#1.0に満足していれば、ジェネリック、linq、using-block、ipmlicit型などはありません。この構文は暗黙性の問題を解決しませんが、現在のスコープにバインドするのに適した砂糖です。
mike30

1

ガベージコレクションされた言語でRAIIがどのように機能するかの例についてはwith、Pythonキーワードを確認してください。決定論的に破棄されたオブジェクトに依存する代わりに、指定されたレキシカルスコープにメソッド__enter__()__exit__()メソッドを関連付けましょう。一般的な例は次のとおりです。

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

C ++のRAIIスタイルと同様に、「通常の」出口、a break、即時return、例外のいずれであっても、そのブロックを終了するときにファイルは閉じられます。

open()呼び出しは通常のファイルを開く関数であることに注意してください。これを機能させるために、返されるファイルオブジェクトには2つのメソッドが含まれます。

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

これはPythonの一般的なイディオムです。リソースに関連付けられているオブジェクトには通常、これら2つのメソッドが含まれています。

ファイルオブジェクトは、__exit__()呼び出し後も割り当てられたままになる可能性があることに注意してください。重要なことは、ファイルオブジェクトが閉じられていることです。


7
withPythonのusingC#とほぼ同じです。したがって、この質問に関する限り、RAIIはそうではありません。

1
Pythonの「with」はスコープにバインドされたリソース管理ですが、スマートポインターの暗黙性が欠落しています。ポインターをスマートとして宣言する行為は「明示的」と見なすことができますが、コンパイラーがオブジェクト型の一部としてスマートネスを強制すると、「暗黙的」に傾くでしょう。
mike30

RAIIのポイントであるAFAICTは、リソースに対する厳密なスコーピングを確立しています。オブジェクトの割り当てを解除することだけに関心がある場合は、ガベージコレクション言語ではできません。一貫してリソースを解放することに興味がある場合は、これがその方法です(別のdefer言語はGo言語です)。
ハビエル

1
実際、JavaとC#は明示的な構造を強く支持していると言っても過言ではないと思います。それ以外の場合、インターフェイスと継承の使用に固有のすべての式に悩ま​​されるのはなぜですか?
ロバートハーベイ

1
@ delnan、Goには「暗黙の」インターフェースがあります。
ハビエル
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.