#RAIIとは
Resource Acquisition Is Initialization
リソースの確保は初期化時に
という意味であるが、コンストラクタ、デストラクタのある言語では具体的に
コンストラクタでリソースを確保し、デストラクタで開放
という事になる
##RAIIしていない例
エラーチェックとか無視してるけど
#include <string>
#include <stdio.h>
class Hoge{
private:
FILE *fp;
public:
void Open(const char *file){
fp = fopen(file, "r");
}
void Close(){
fclose(fp);
}
};
int main()
{
auto hoge = new Hoge();
// x ここはリソースが確保されていないのでReadWriteするとエラー
hoge->Open("xxx.txt");
// o ここはリソースが確保されている
hoge->Close(); // x Close忘れるとリソースがリークする
// x Close後にアクセスするとエラー
delete hoge;
}
色々とバグが潜む隙があるので、RAIIを行うには下記のようにします
(本当は unique_ptrなどを使うが、今回はC++については 細かく扱わない)
#include <string>
#include <stdio.h>
class Hoge{
private:
FILE *fp;
Hoge();
public:
Hoge(const char *file){
fp = fopen(file, "r");
}
virtual ~Hoge(){
fclose(fp);
}
};
int main()
{
// auto hhh = new Hoge(); // o デフォルトコンストラクタは禁止
auto hoge = new Hoge("xxx.txt");
// o ここはリソースが確保されている
delete hoge; // o デストラクタで確実に開放される
}
これで、クラスの生存期間とリソースの確保期間が一致し、不具合の潜む箇所が減りました
#Goでは?
Goにはコンストラクタもデストラクタもありません
##New関数
Goでは NewHoge という関数をコンストラクタとする習わしがあります
type MyFile struct {
fp *os.File
filename string
}
func NewMyFile(filename string) *MyFile{
fp, _ := os.Open(filename)
return &MyFile{filename: filename, fp: fp}
}
func (s *MyFile) Read(len int) {
// Read処理
}
func (s *MyFile) Close() {
s.fp.Close()
}
func main() {
{
mf = NewMyFile("hoge.txt") // o コンストラクタでリソース取得
mf.Read(10) // o リソース確保されている
mf.Close() // x Close忘れるとリークする
// x Closeしているのでここでアクセスしたらエラー
}
}
何個かの問題が解決したように思えますが、実は全然解決していない
例えばメイン関数でNewを使わず直接構造体生成された場合
func main() {
{
mf = &MyFile{} // x 直接生成された
// x リソースが確保されていない
mf.Open("xxx.txt") // o リソース確保されている
mf.Read(10) // o リソース確保されている
mf.Close() // x Close忘れるとリークする
// x Closeしているのでここでアクセスしたらエラー
}
}
と、Newを使わずに構造体を直接生成されるとアウトだ
##Private構造体にする
構造体をPrivate(頭文字を小文字)にすると、別のパッケージからは直接生成不可能になる
type myFile struct {
fp *os.File
filename string
}
func NewMyFile(filename string) *myFile{
fp, _ = os.Open(filename)
return &myFile{filename: filename, fp: fp}
}
func (s *myFile) Read(len int) {
// Read処理
}
func (s *myFile) Close() {
s.fp.Close()
}
func main() {
{
mf = MyFile.NewMyFile("hoge.txt") // o コンストラクタでリソース取得
mf.Read(10) // o リソース確保されている
mf.Close() // x Close忘れるとリークする
// x Closeしているのでここでアクセスしたらエラー
}
{
// mf = &MyFile.myFile{} // o Private構造体は直接生成できない
}
}
##コールバックを使い開放をする
Private構造体にする前のものに改造を行う
Newxxx関数に、リソース確保したら呼び出すコールバック関数を渡す
これでNewxxx内部でリソースの確保と開放が制御できる
ここでは deferを使う
deferは、その関数の終了時に実行される関数を指定できる
type MyFile struct {
fp *os.File
filename string
}
func NewMyFile(filename string, cb func(*MyFile) error) error {
fp, err := os.Open(filename)
s := &MyFile{filename: filename, fp: fp}
if err != nil {
return err
}
defer s.fp.close()
err = cb(s)
return err
}
func (s *MyFile) Read(len int) {
// Read処理
}
func (s *MyFile) close() {
s.fp.Close()
}
func main() {
_ = NewMyFile("hoge.txt", func(mf *MyFile) error {
mf.Read(10)
return nil
})
}
##private構造体とコールバックを組み合わせる
main関数から *myFile が参照できずコンパイルエラー
func main() {
_ = NewMyFile("hoge.txt", func(mf *myFile) error { // ここがエラー
mf.Read(10)
return nil
})
}
##結論、Interfaceを使う
上記は *myFile がコールバック型で参照できないので、Interfaceにする
type ImyFile interface {
Read(len int)
}
type myFile struct {
fp *os.File
filename string
}
func NewMyFile(filename string, cb func(ImyFile) error) error {
fp, err := os.Open(filename)
s := &myFile{filename: filename, fp: fp}
if err != nil {
return err
}
defer s.fp.Close()
err = cb(s)
return err
}
func (s *myFile) Read(len int) {
// Read処理
}
func main() {
_ = MyFile.NewMyFile("hoge.txt", func(mf MyFile.ImyFile) error { // o 初期化時にリソース確保
mf.Read(10)
return err
// o コールバック抜けた後、New関数を抜け、deferにてCloseが呼ばれるので開放忘れなし
})
// mf := &MyFile.ImyFile{} // o Interfaceは生成不可能
// mf := &MyFile.myFile{} // o Privateなので構造体を直接生成不可能
}
冗長なコードになったけど、これでGoでも RAIIを守れたんじゃないかな?