33

RAII が Stackoverflow で多くの注目を集めていることに気付きましたが、私のサークル (ほとんどが C++) では、RAII は非常に明白で、何がクラスかデストラクタかを尋ねるようなものです。

だから、私が毎日ハードコアな C++ プログラマーに囲まれていて、RAII が一般的に (C++ を含めて) あまり知られていないからなのか、それとも Stackoverflow に関するこのすべての質問が事実によるものなのか、非常に興味があります。私は今、C++ で育ったのではなく、他の言語では RAII を使用していない/知らないプログラマーと連絡を取り合っているということですか?

4

17 に答える 17

25

RAII があまり知られていない理由はたくさんあります。まず、名前は特に明白ではありません。RAII が何であるかを知らなかったら、名前から推測することはできませんでした。(リソースの取得は初期化ですか? これは、 RAII の真の特徴であるデストラクタまたはクリーンアップと何の関係があるのでしょうか?)

もう 1 つの理由は、決定論的なクリーンアップのない言語ではうまく機能しないことです。

C++ では、デストラクタがいつ呼び出されるか、デストラクタが呼び出される順序が正確にわかり、好きなことを行うように定義できます。

最近のほとんどの言語では、すべてがガベージ コレクションされるため、RAII の実装が難しくなっています。C# などに RAII 拡張機能を追加できない理由はありませんが、C++ ほど明白ではありません。しかし、他の人が言及したように、Perl やその他の言語は、ガベージ コレクションが行われているにもかかわらず、RAII をサポートしています。

とはいえ、C# やその他の言語で独自の RAII スタイルのラッパーを作成することは可能です。少し前にC#でやった。データベース接続が使用直後に確実に閉じられるように何かを作成する必要がありました。これは、C++ プログラマーなら誰でも RAII の明らかな候補と見なすタスクです。もちろんusing、db 接続を使用するときはいつでも -statements ですべてをラップできますが、それは面倒でエラーが発生しやすくなります。

私の解決策は、デリゲートを引数として受け取るヘルパー関数を作成し、呼び出されたときにデータベース接続を開き、using ステートメント内でそれをデリゲート関数の疑似コードに渡すことでした。

T RAIIWrapper<T>(Func<DbConnection, T> f){
  using (var db = new DbConnection()){
    return f(db);
  }
}

C++-RAII ほど良くも明白でもありませんが、ほぼ同じことを達成しました。DbConnection が必要なときはいつでも、後で閉じられることを保証するこのヘルパー関数を呼び出す必要があります。

于 2008-12-28T16:47:06.867 に答える
20

私は常に C++ RAII を使用していますが、長い間 Visual Basic 6 での開発も行っており、そこでは RAII は常に広く使用されている概念でした (ただし、そう呼ぶ人は聞いたことがありません)。

実際、多くの VB6 プログラムは RAII に大きく依存しています。私が繰り返し見た興味深い使用法の 1 つは、次の小さなクラスです。

' WaitCursor.cls '
Private m_OldCursor As MousePointerConstants

Public Sub Class_Inititialize()
    m_OldCursor = Screen.MousePointer
    Screen.MousePointer = vbHourGlass
End Sub

Public Sub Class_Terminate()
    Screen.MousePointer = m_OldCursor
End Sub

使用法:

Public Sub MyButton_Click()
    Dim WC As New WaitCursor

    ' … Time-consuming operation. '
End Sub

時間のかかる操作が終了すると、元のカーソルが自動的に復元されます。

于 2008-10-03T11:44:58.343 に答える
14

RAII はResource Acquisition Is Initializationの略です。これは、言語に依存するものではありません。このマントラがここにあるのは、C++ がそのように機能するためです。C++ では、コンストラクターが完了するまでオブジェクトは構築されません。オブジェクトが正常に構築されていない場合、デストラクタは呼び出されません。

実用的な言語に翻訳すると、コンストラクターは、その仕事を完全に完了できない場合を確実にカバーする必要があります。たとえば、構築中に例外が発生した場合、デストラクタは助けにならないため、コンストラクタはそれを適切に処理する必要があります。これは通常、コンストラクター内で例外をカバーするか、この手間を他のオブジェクトに転送することによって行われます。例えば:

class OhMy {
public:
    OhMy() { p_ = new int[42];  jump(); } 
    ~OhMy() { delete[] p_; }

private:
    int* p_;

    void jump();
};

jump()コンストラクターの呼び出しがスローされた場合、p_リークするため、問題が発生します。これを次のように修正できます。

class Few {
public:
    Few() : v_(42) { jump(); } 
    ~Few();

private:
    std::vector<int> v_;

    void jump();
};

人々がこれに気付いていない場合、それは次の 2 つのいずれかが原因です。

  • 彼らは C++ をよく知りません。この場合、次のクラスを記述する前にTCPPPLを再度開く必要があります。具体的には、この本の第 3 版のセクション 14.4.1 で、この手法について説明しています。
  • 彼らは C++ をまったく知りません。それはいいです。この慣用句は非常に C++ 的です。C++ を学ぶか、これをすべて忘れて生活を続けてください。できればC++を学びましょう。;)
于 2008-10-03T05:10:38.880 に答える
11

このスレッドで RAII (リソースの取得は初期化です) についてコメントしている人のために、動機付けの例を次に示します。

class StdioFile {
    FILE* file_;
    std::string mode_;

    static FILE* fcheck(FILE* stream) {
        if (!stream)
            throw std::runtime_error("Cannot open file");
        return stream;
    }

    FILE* fdup() const {
        int dupfd(dup(fileno(file_)));
        if (dupfd == -1)
            throw std::runtime_error("Cannot dup file descriptor");
        return fdopen(dupfd, mode_.c_str());
    }

public:
    StdioFile(char const* name, char const* mode)
        : file_(fcheck(fopen(name, mode))), mode_(mode)
    {
    }

    StdioFile(StdioFile const& rhs)
        : file_(fcheck(rhs.fdup())), mode_(rhs.mode_)
    {
    }

    ~StdioFile()
    {
        fclose(file_);
    }

    StdioFile& operator=(StdioFile const& rhs) {
        FILE* dupstr = fcheck(rhs.fdup());
        if (fclose(file_) == EOF) {
            fclose(dupstr); // XXX ignore failed close
            throw std::runtime_error("Cannot close stream");
        }
        file_ = dupstr;
        return *this;
    }

    int
    read(std::vector<char>& buffer)
    {
        int result(fread(&buffer[0], 1, buffer.size(), file_));
        if (ferror(file_))
            throw std::runtime_error(strerror(errno));
        return result;
    }

    int
    write(std::vector<char> const& buffer)
    {
        int result(fwrite(&buffer[0], 1, buffer.size(), file_));
        if (ferror(file_))
            throw std::runtime_error(strerror(errno));
        return result;
    }
};

int
main(int argc, char** argv)
{
    StdioFile file(argv[1], "r");
    std::vector<char> buffer(1024);
    while (int hasRead = file.read(buffer)) {
        // process hasRead bytes, then shift them off the buffer
    }
}

ここで、StdioFileインスタンスが作成されると、リソース (この場合はファイル ストリーム) が取得されます。破棄されると、リソースが解放されます。tryまたはfinallyブロックは必要ありません。読み取りによって例外が発生した場合はfclose、デストラクタにあるため、自動的に呼び出されます。

デストラクタは、関数が終了するときに、main通常または例外によって呼び出されることが保証されています。この場合、ファイル ストリームはクリーンアップされます。世界は再び安全です。:-D

于 2008-10-03T04:58:38.707 に答える
9

ライイ。

コンストラクタとデストラクタで始まりますが、それ以上のものです。
例外の存在下でリソースを安全に制御することがすべてです。

RAII が final やそのようなメカニズムよりも優れているのは、オブジェクトを正しく使用する責任がオブジェクトのユーザーからオブジェクトの設計者に移されるため、コードをより安全に使用できるようになることです。

これを読む

RAIIを使ってStdioFileを正しく使う例。

void someFunc()
{
    StdioFile    file("Plop","r");

    // use file
}
// File closed automatically even if this function exits via an exception.

finally と同じ機能を取得するには。

void someFunc()
{
      // Assuming Java Like syntax;
    StdioFile     file = new StdioFile("Plop","r");
    try
    {
       // use file
    }
    finally
    {
       // close file.
       file.close(); // 
       // Using the finaliser is not enough as we can not garantee when
       // it will be called.
    }
}

try{} finally{} ブロックを明示的に追加する必要があるため、このコーディング方法はエラーが発生しやすくなります (つまり、例外について考える必要があるのはオブジェクトのユーザーです)。RAII 例外安全性を使用することにより、オブジェクトの実装時に 1 回コード化する必要があります。

問題は、この C++ 固有のものです。
短い答え: いいえ。

より長い答え:
コンストラクター/デストラクタ/例外と、定義された有効期間を持つオブジェクトが必要です。

技術的には、例外は必要ありません。例外が存在する場合のリソースの制御が非常に簡単になるため、例外が潜在的に使用される可能性がある場合は、はるかに便利になります。
ただし、制御が関数を早期に終了し、すべてのコードを実行できないすべての状況で役立ちます (関数からの早期復帰など)。これが、C の複数のリターン ポイントが悪いコード臭であるのに対し、C++ の複数のリターン ポイントはコードの悪臭ではない理由です。コードの臭い [RAII を使用してクリーンアップできるため])。

C++ では、スタック変数またはスマート ポインターによって制御された有効期間が実現されます。しかし、寿命を厳密に管理できるのはこれだけではありません。たとえば、Perl オブジェクトはスタックベースではありませんが、参照カウントのために寿命が非常に制御されています。

于 2008-10-03T05:16:22.423 に答える
8

RAIIの問題は頭字語です。概念との明確な相関関係はありません。これはスタック割り当てと何の関係がありますか? それが結局のところです。C++ では、オブジェクトをスタックに割り当て、スタックが巻き戻されたときにそれらのデストラクタが呼び出されることを保証できます。それを踏まえると、RAII はそれをカプセル化する有意義な方法のように思えますか? いいえ、私は数週間前にここに来るまで RAII のことを聞いたことがありませんでした。RAII が何であるかを知らない C++ プログラマーを雇うことはないだろうという投稿を誰かが読んだときは、大笑いするしかありませんでした。確かに、この概念は有能なプロの C++ 開発者のほとんどすべてによく知られています。頭字語がよく考えられていないだけです。

于 2008-12-28T16:07:58.863 に答える
5

@ピエールの答えの修正:

Python の場合:

with open("foo.txt", "w") as f:
    f.write("abc")

f.close()例外が発生したかどうかにかかわらず、自動的に呼び出されます。

一般に、ドキュメントの contextlib.closingを使用して実行できます。

closing(thing): ブロックの完了時に Thing を閉じるコンテキスト マネージャーを返します。これは基本的に次と同等です。

from contextlib import contextmanager

@contextmanager
def closing(thing):
    try:
        yield thing
    finally:
        thing.close()

そして、次のようなコードを書くことができます:

from __future__ import with_statement # required for python version < 2.6
from contextlib import closing
import urllib

with closing(urllib.urlopen('http://www.python.org')) as page:
    for line in page:
        print line

明示的にページを閉じる必要はありません。エラーが発生した場合でも、with ブロックを抜けたときに page.close() が呼び出されます。

于 2008-10-03T17:54:54.633 に答える
3

Common Lisp には RAII があります。

(with-open-file (stream "file.ext" :direction :input)
    (do-something-with-stream stream))

参照: http://www.psg.com/~dlamkins/sl/chapter09.html

于 2009-02-27T20:36:26.890 に答える
2

まず第一に、あまり知られていないことにとても驚いています!RAII は、少なくとも C++ プログラマーにとっては当たり前のことだと思っていました。しかし、今では、人々が実際にそれについて尋ねる理由を理解できると思います. 私は囲まれています、そして私自身は C++ フリークに違いありません...

だから私の秘密..それは、私がそれを理解するまで、何年も前にマイヤーズ、サッター[編集:]、アンドレイを読んでいたことだと思います。

于 2008-10-03T05:02:31.207 に答える
1

RAII は、コード内で何が起こっても、コード ブロックの後にクリーンアップ手順が確実に実行されるようにするための C++ の方法です。コードは最後まで正しく実行されるか、例外が発生します。既に引用されている例は、処理後にファイルを自動的に閉じることです。 answer hereを参照してください。

他の言語では、それを達成するために他のメカニズムを使用します。

Java では、try { } finally {} コンストラクトがあります。

try {
  BufferedReader file = new BufferedReader(new FileReader("infilename"));
  // do something with file
}
finally {
    file.close();
}

Ruby では、自動ブロック引数があります。

File.open("foo.txt") do | file |
  # do something with file
end

Lisp ではunwind-protect、定義済みのwith-XXX

(with-open-file (file "foo.txt")
  ;; do something with file
)

スキームにはdynamic-wind、事前定義されたものがありますwith-XXXXX

(with-input-from-file "foo.txt"
  (lambda ()
    ;; do something 
)

Pythonでは、最後に試しました

try
  file = open("foo.txt")
  # do something with file
finally:
  file.close()

RAII としての C++ ソリューションは、必要なすべての種類のクリーンアップに対して 1 つのクラスを作成する必要があるという点で、かなり扱いにくいものです。これにより、多くの小さなばかげたクラスを作成する必要が生じる場合があります。

RAII の他の例は次のとおりです。

  • 取得後のミューテックスのロック解除
  • 開いた後にデータベース接続を閉じる
  • 割り当て後のメモリの解放
  • コードブロックの入口と出口のログ
  • ...
于 2008-10-03T07:24:34.283 に答える
1

RAII の問題は、C++ のスタックベースのオブジェクトに対して保証されている確定的なファイナライズが必要なことです。ガベージ コレクションに依存する C# や Java などの言語には、この保証がないため、何らかの形で「追加」する必要があります。C# では、これは IDisposable を実装することによって行われ、同じ使用パターンの多くが基本的に発生します。これは、「using」ステートメントの動機の 1 つであり、Disposal を保証し、非常によく知られ、使用されています。

基本的にイディオムはありますが、派手な名前はありません。

于 2008-10-03T05:34:01.413 に答える
0

私には、ハードコアで「仕様を読む」C++タイプの同僚がいます。彼らの多くはRAIIを知っていますが、私はそれがそのシーンの外で使われるのを実際に聞いたことがありません。

于 2008-10-11T18:00:59.800 に答える
0

デストラクタがいつ呼び出されるかを知ることと結びついていますよね?そのため、多くの GC で処理された言語でそれが与えられていないことを考えると、言語に完全に依存しているわけではありません。

于 2008-10-03T04:52:26.410 に答える
0

delete他の多くの言語 (たとえば、を持たない言語) では、プログラマーがオブジェクトの有効期間をまったく同じように制御できないため、リソースの決定論的な破棄を提供する他の手段が必要であると思います。たとえば、C# では with を使用usingするのIDisposableが一般的です。

于 2008-10-03T04:54:22.570 に答える
0

finallyRAII は C++ で人気があります。これは、複雑なスコープ ローカル変数を割り当てることができる数少ない (唯一の?) 言語の 1 つですが、句がないためです。C#、Java、Python、Ruby はすべて、finallyまたは同等のものを持っています。Cにはありませんfinallyが、変数がスコープから外れるとコードを実行できません。

于 2008-10-03T04:58:19.563 に答える
-1

CPython(Cで記述された公式のPython)は、(ガベージが収集されるときではなく)即時スコープベースの破棄で参照カウントオブジェクトを使用するため、RAIIをサポートします。残念ながら、Jython(JavaのPython)とPyPyは、この非常に便利なRAIIイディオムをサポートしておらず、多くのレガシーPythonコードを破壊します。したがって、ポータブルPythonの場合、Javaと同じように、すべての例外を手動で処理する必要があります。

于 2008-10-11T17:39:50.340 に答える
-2

RAII は C++ に固有です。C++ には、スタック割り当てオブジェクト、アンマネージ オブジェクトの有効期間、および例外処理の必要な組み合わせがあります。

于 2008-10-03T04:58:40.920 に答える