参照
Nimでは2種類のポインタと呼ばれる物を扱う事ができます。
TIP
ポインタとは、ある値が保存されている場所を格納する変数のことであり、しばしば出てくるポインタ変数の略称のようなものです。
ちなみに、ある値が保存されている場所の事をアドレスと言います。
Nimでポインタを扱うには変数をvarで定義しなければなりません。
しかし、これはアドレスが欲しい変数のみの制約であり、変数のアドレスを格納する変数(ポインタ)はletでも大丈夫です。
refとptrの2種類のポインタ型があり、refはGCが追跡するヒープ内のオブジェクトを指し、使われなくなったら自動的にリソースを解放します。
ptrはGCの管理外のヒープ内のオブジェクトを指すので、私達プログラマーがリソースを使い終わったら解放する必要があり、アンセーフです。
基本的にはrefを使い、メモリとポインタをよく知っている人たちはptrを使ってもいいでしょう。
ここではrefを解説し、ptrやGCについてはまた別の章で解説します。
ヒープの話もptrを説明する章で少し詳しく説明するので、そちらも合わせて見てみてください。
とりあえず今は2種類のポインタがある、ということだけ覚えておきましょう。
refと型
まずは定義例を見てみましょう。
var refStr: ref string
この変数は、stringのrefポインタ変数を宣言しています。
しかし、まだ値を入れることは出来ません。
どういう事か実際に見てみましょう。
echo repr refStrとすることでポインタの指し示す場所や値を見ることができます。
var refStr: ref string
echo repr refStr
nilと表示されましたか?それはバグではなく、正常な物です。
先程ポインタは値の場所を指し示す物だと言いました。
つまり、ここでは場所を記憶する変数を宣言しただけで、実際の値が入る場所の確保が出来ていないのです。
なので、値を入れる前に、new(string)でstringの値が入る場所を確保してあげる必要があります。
refStr = new(string)
# 引数が1つなので、このように書くことも可能
# refStr = new string
これでstringの値を確保し、refStrはその場所を指し示す物となりました。
先程と同じくecho repr refStrでもう一度見てみましょう。
var refStr: ref string
refStr = new string
echo repr refStr
おそらくref 0x英数字の並び --> nilというような物が表示されているはずです。
ここでのnilはstringの初期値なのでまだ文字列が何も無い事を意味します。
ref 0x英数字の並びは、実行するたびに変化します。これは場所の確保をする時に同じ場所を確保することが出来ない場合があるからです。
ここでは仮にref 0x12345678とすることにします。この英数字の並びは一般的にアドレスと呼ばれます。
では場所の確保も出来た所で、早速値を入れてみましょう。
値を入れるには、ポインタが指し示す場所を参照し、値を代入します。
このポインタが指し示す場所を参照することを間接参照や逆参照と呼びます。
値を間接参照するには配列などで使う[]をポインタ変数の後に付けて参照します。
refStr[] = "test"
これでrefStrが指し示す場所に"test"という文字列が入りました。
echoで表示することができます。
echo refStr[]
# echo repr refStr
# ref 0x12345678 -> ref 0x0abcdefg"test"
# 文字列の一番先頭の文字のアドレスを指し示している
ここまでで一般的なrefの説明は終わりですが、普通の変数と何が違うのか分かりますか?
以下のコードを見てみましょう。
var
str1 = "test"
str2 = str1
str2 = "aaaa"
echo str1
echo str2
何の変哲も無いコードです。str1をstr2に入れた後、str2に"aaaa"という文字列を代入しました。
実行するとtestとaaaaが表示されるはずです。
では次のコードを見てみましょう。
var
str1 = new string
str2 = str1
str1[] = "test"
str2[] = "aaaa"
echo str1[]
echo str2[]
実行すると何が表示されましたか?おそらく二回aaaaが表示されるはずです。
何故でしょうか? 私達はstr1には確かにtestを代入しているはずです。
ここで普通の変数との違いが出てきます。
str1とstr2はどちらもstringの値を指し示す変数です。
つまりstr2 = str1はどちらも同じstringの値の場所を指し示す物となります。
なので、str2の中身を変更すると同じ場所を指し示すstr1の値も同じ物になります。
ここではstr2の指し示す場所の値をaaaaに変更したため、同じ場所を指し示すstr1の中身もaaaaになっているのです。
この挙動が何で役に立つかというと、例えば関数の引数に使う例があります。
通常、関数の引数に渡す値は、コピーが発生します。
Nimでは通常参照渡しとなり、コピーが発生しません。しかし、渡された値を編集した時点でコピーされてしまします。
仮に巨大なサイズのオブジェクトがあってそれを関数内部で編集する関数があったとしましょう。
関数に渡して編集した時点で値がコピーされるので巨大なサイズのオブジェクトがもう一つできる事になります。
これが数回程度ならいいかもしれませんが、数千回、数万回となってくるとどうでしょうか?
ここで巨大なサイズのオブジェクトを指し示すポインタ変数を用意して、その変数を関数に渡すとどうでしょうか?
ここで発生するコピーはアドレスの数値のみです。中身は間接参照して取得することが出来ますよね。
つまり、値をコピーしたくないので、関数に値を渡す代わりにref Tを渡せば値のコピーがなくなり、処理速度が向上する、ということになります。
ただし、渡したref Tが指し示す値を直接変更したりする操作がある場合、
関数の外で他にも同じ場所を指し示すポインタ変数がある場合などで注意が必要です。
refとobject型
refをobjectで扱う事は通常のrefの使用とほぼ同じです。
objectはtypeステートメントで宣言しますが、ここでrefを使うのとnew objectをするのとでは少し違います。
まずは以下のコードを見てみましょう。
それぞれ関数内部でオブジェクトを作成し、それを返り値としています。
type
Obj = object
name: string
ObjRef = ref Obj
ObjRef2 = ref object
name: string
proc oCreate1(): Obj =
var o = Obj(name: "john")
echo repr o
return o
proc oCreate2(): ObjRef =
var o = ObjRef(name: "sam")
echo repr o
return o
proc oCreate3(): ObjRef2 =
var o = ObjRef2(name: "2vg")
echo repr o
return o
proc oCreate4(): ref Obj =
var o = new Obj
o.name = "mofu"
echo repr o
return o
var
o1 = oCreate1()
o2 = oCreate2()
o3 = oCreate3()
o4 = oCreate4()
echo "\n--------\n"
echo repr o1
echo repr o2
echo repr o3
echo repr o4
実行すると気づいた事があると思います。
oCreate1で作成されたオブジェクトは関数内のアドレスと関数の返り値でもらったオブジェクトは別々のアドレスになっていると思います。
しかし、oCreat2からoCreat4は関数内のアドレスと同じです。
違いが分かりますか?
oCreat1で作られるObjは関数内のスタックに作成されるのに対し、ObjRef、 ObjRef2、ref Objはどれもヒープ内に作成されます。
そのため、通常のobjectは関数を超えてアクセスできませんが、refが付随すると関数を超えてオブジェクトにアクセスすることができるのです。
refを付随してヒープに作られたオブジェクトは、
どこの変数からも参照がなくなったりして使われなくなると自動的に解放されるため、私たちは通常通りにコードを書くだけで良いのです。