参照
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
を付随してヒープに作られたオブジェクトは、
どこの変数からも参照がなくなったりして使われなくなると自動的に解放されるため、私たちは通常通りにコードを書くだけで良いのです。