質問: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を満たしていません。プログラマーは、オブジェクトを特別な「using」スコープに明示的に関連付ける必要があります。プログラマは、リソースをスコープに明示的に関連付けることを忘れることがあり、実際に実行すると、リークが発生します。
実際、「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.
私は暗黙のうちに「それに値する」と思います。ガベージコレクションの暗黙性が「それに値する」のと同じように。明示的なusingブロックは目にはさわやかですが、try-finally-dispose()よりも意味上の利点はありません。
そのような機能をJava/C#言語に実装することは非現実的ですか?古いコードを壊すことなく導入できますか?
このような言語拡張は、あなたが考えているよりもはるかに複雑で侵襲的です。追加することはできません
スタックバインド型の変数のライフタイムが終了する場合、参照するオブジェクトで
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
sがありますが、AFAIKのユーザー定義のコピーコンストラクターはありません)、それでもこれらのRAIIオブジェクトはより特別になります。または、線形型の限定バージョン(Rustを参照)でも問題を解決できます(ただし、Rustのような借用参照と借用チェッカーを採用することでさらに複雑にしたくない場合を除く)。
技術的には可能ですが、最終的にはveryが他の言語とは異なるカテゴリに分類されます。これはほとんど常に悪い考えであり、実装者(より多くのEdgeケース、すべての部門でより多くの時間/コスト)とユーザー(より多くの概念を学ぶこと、バグの可能性が高い)に影響を与えます。追加された便利さの価値はありません。
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
はすでに使用しています。
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では&
である%
演算子に相当するものがないわけではありません。そうすることは、メソッドが終了した後、フィールドが破棄されたオブジェクトを参照するという意味で「安全」ではありません。
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);
}
}
しかし、明示的なスコープではなく、暗黙的なスコープを使用します。クラスをスコープバインドとして定義する必要がないため、他の提案よりも簡単です。ただよりすっきりとした、より暗黙的な構文糖。
現時点では確認できませんが、ここでは解決が難しいスコープに問題がある可能性があります。見つけられる方はだれでも感謝します。
RAIIがガベージコレクションされた言語でどのように機能するかの例については、 with
Pythonのキーワード を確認してください。確定的に破壊されたオブジェクトに依存する代わりに、__enter__()
および__exit__()
メソッドを特定の字句スコープに関連付けます。一般的な例は次のとおりです。
_with open('output.txt', 'w') as f:
f.write('Hi there!')
_
C++のRAIIスタイルと同様に、「通常の」出口、break
、即時return
、または例外のいずれであっても、そのブロックを終了するとファイルは閉じられます。
open()
呼び出しは、通常のファイルを開く関数であることに注意してください。これを機能させるために、返されるファイルオブジェクトには2つのメソッドが含まれています。
_def __enter__(self):
return self
def __exit__(self):
self.close()
_
これはPythonの一般的なイディオムです。リソースに関連付けられているオブジェクトには通常、これらの2つのメソッドが含まれています。
__exit__()
の呼び出し後もファイルオブジェクトが割り当てられたままになる可能性があることに注意してください。重要なのは、ファイルオブジェクトが閉じられていることです。