😀

GolangとRubyにおけるループ変数とメモリアドレスの関係

2024/04/18に公開

プログラミング言語においてループは非常に一般的な構造で、データの集合に対して繰り返し操作を行う際に使用されます。
しかし、異なるプログラミング言語では、ループ内の変数のスコープやメモリアドレスの扱い方が異なるため、予期しないバグが発生することがあります。
ここでは、特にGolangとRubyのループ変数の挙動の違いに焦点を当てます。

Golangのループ変数

Golangでは、forループを使用する際、ループ変数(例えばiやvalue)はループの各イテレーションで同じメモリアドレスを再利用します。この挙動は、ループ変数のアドレスをポインタとして他の変数に保存し、ループ外で使用する場合に問題を引き起こす可能性があります。以下の例を見てみましょう。

期待通りに動かない例

var pointers []*int
for i := 0; i < 3; i++ {
    pointers = append(pointers, &i)
}

for _, ptr := range pointers {
    fmt.Println(*ptr)  // 出力は期待する 0, 1, 2 ではなく、すべてが 3 になる
}

このコードは、すべてのポインタがiの最終値を指すため、予期しない出力が得られます。この問題を回避するためには、各イテレーションで新しい変数を宣言し、そのアドレスを使用する必要があります。

期待通りに動く例

var pointers []*int
for i := 0; i < 3; i++ {
    iCopy := i  // i の値のコピーを作成
    pointers = append(pointers, &iCopy)  // コピーのアドレスをリストに追加
}

for _, ptr := range pointers {
    fmt.Println(*ptr)  // 期待通りの出力: 0, 1, 2
}

Rubyのループ変数

一方、Rubyでは各ループイテレーションで新しいスコープが作成されるため、ブロック内の変数はそれぞれ独立しています。しかし、Rubyもオブジェクト参照を基に動作するため、ミュータブルなオブジェクト(配列やハッシュなど)の操作では注意が必要です。以下のRubyコードを考えてみましょう。

期待通りに動かない例

オブジェクトをループ外で宣言し、その値を変更する場合、全ての参照が最終的な変更を反映します。

items = []
item = {}
3.times do |i|
  item[:id] = i
  items << item
end

items.each { |item| puts item[:id] }  # すべての出力が 2 になる

このコードでは、itemは同じオブジェクトの参照を三回配列に追加しているため、すべての要素が最後に設定された値を持ちます。各ループの item は同じメモリアドレスにあります。

期待通りに動く例

items = []
3.times do |i|
  items << { id: i }
end

items.each { |item| puts item[:id] }  # 期待通り 0, 1, 2 が出力される

この場合、各イテレーションで新しいハッシュが生成されるため、問題は発生しません。メモリアドレスも独立しています。

まとめ

GolangとRubyではループ変数のスコープとメモリアドレスの扱い方が根本的に異なります。Golangではポインタの挙動に注意が必要であり、Rubyではオブジェクトの参照とミュータビリティに注意を払う必要があります。各言語の特性を理解し、それに合わせたコーディングスタイルを採用することで、バグを回避しやすくなります。

(Java,Rust も追加予定)

Discussion