Hisakeyのブログ

エンジニアが色々呟くブログです。

RubyとGoの「値渡し」の違いを実際に試してみた

はじめに

Rubyの参照の値渡しについては、一度ブログに書いていますが、改めて振り返ろうと思い書き始めました

背景・動機

基本的には、Rubyだけをかいてきたのですが、転職したタイミングでGoを使うようになりました

挙動や書き方がRubyと全然違うなーと思い、せっかくならRubyとGoの比較を書きたいなと思い、RubyとGoの値渡しについて書くことにしました

実例・やってみたこと

Rubyの振り返り

まず結論をだすと、

  • オブジェクトの中身を変更すると、呼び出し元にも影響します
  • 引数そのものを別のオブジェクトへ再代入しても、呼び出し元には影響しません
# object_mutation.rb
class User
  attr_accessor :name

  def initialize(name)
    @name = name
  end
end

def change_name(user)
  puts "Before change_name: user.object_id = #{user.object_id}"

  user.name = 'Bob'

  puts "After change_name: user.object_id = #{user.object_id}"
end

user = User.new('Alice')
puts "main: user.object_id = #{user.object_id}"

change_name(user)

puts user.name

# main: user.object_id = 16
# Before change_name: user.object_id = 16
# After change_name: user.object_id = 16
# Bob

object_mutation.rbの挙動

  1. user = User.new("Alice") によって User オブジェクトが生成されます。(object_id = 16)
  2. change_name(user) では、object_id = 16 の User オブジェクトの name を "Alice" から "Bob" に変更します。
  3. puts user.name を実行すると、object_id = 16 の User オブジェクトの name は "Bob" になっているため、Bob が出力されます。
# reassignment.rb
class User
  attr_accessor :name

  def initialize(name)
    @name = name
  end
end

def change_name(user)
  puts "Before change_name: user.object_id = #{user.object_id}"

  user = User.new("Bob")

  puts "After change_name: user.object_id = #{user.object_id}"
end

user = User.new('Alice')
puts "main: user.object_id = #{user.object_id}"

change_name(user)

puts user.name

# main: user.object_id = 16
# Before change_name: user.object_id = 16
# After change_name: user.object_id = 24
# Alice

reassignment.rbの挙動

  1. user = User.new("Alice")で生成されたuserオブジェクト(object_id = 16)
  2. change_name(user)では、user = User.new("Bob")で新たにuserオブジェクト(object_id = 24)を生成します
  3. change_name(user)では、引数で渡されたuserオブジェクト(object_id = 16)に対してなにも変更をせず、userオブジェクトを再代入している
  4. user = User.new("Alice")で生成されたuserオブジェクト(object_id = 16)のname(Alice)が出力されます

  5. user = User.new("Alice") によって User オブジェクトが生成されます。(object_id = 16)

  6. change_name(user) を呼び出します。メソッド内で user = User.new("Bob") を実行し、新しい User オブジェクト(object_id = 24)が生成されます。
  7. メソッド内の変数 user は object_id = 24 を指すようになりますが、呼び出し元の user は引き続き object_id = 16 を指しています。 そのため、呼び出し元のオブジェクトは変更されません。
  8. puts user.name を実行すると、呼び出し元の user は object_id = 16 の User オブジェクトを指したままなので、Alice が出力されます。

Goの挙動

ポインターの概念があるので、Rubyとは少しかわります

Ruby同じファイル名で挙動は同じとなるように書いてみました

// pointer_reassignment.go
package main

import "fmt"

type User struct {
    Name string
}

func changeName(user *User) {
    fmt.Printf("Before: changeName: %p\n", user)
    user.Name = "Bob"
    fmt.Printf("After: changeName: %p\n", user)
}

func main() {
    user := User{Name: "Alice"}
    fmt.Printf("main: %p\n", &user)

    changeName(&user)

    fmt.Println(user.Name) // Bob
}

// main: 0x223fd4f3e020
// Before: changeName: 0x223fd4f3e020
// After: changeName: 0x223fd4f3e020
// Bob

pointer_reassignment.goの挙動

  1. user := User{Name: "Alice"} によって User 構造体が生成されます。
    • この構造体のアドレスを 0x223fd4f3e020 とします
    • changeName(&user) を呼び出します。
  2. &user によって、構造体のアドレス 0x223fd4f3e020 が引数として渡されます。
    • 関数内の user は、同じアドレスを保持するポインタ
    • 関数内で user.Name = "Bob" を実行します。
  3. アドレス 0x223fd4f3e020 が指す User 構造体の Name が "Alice" から "Bob" に変更されます。
    • アドレスは変わらず、中身だけが変更されます。
  4. fmt.Println(user.Name) を実行します。
    • main の user もアドレス 0x223fd4f3e020 の User 構造体を参照しているため、Bob が出力されます。
// pointer_reassignment.go
package main

import "fmt"

type User struct {
    Name string
}

func changeName(user *User) {
    user = &User{Name: "Bob"}
}

func main() {
    user := User{Name: "Alice"}

    changeName(&user)

    fmt.Println(user.Name) // Alice
}

// main: 0x21043b26020
// Before: changeName: 0x21043b26020
// After: changeName: 0x21043b26030
// Alice

pointer_reassignment.goの挙動

  1. user := User{Name: "Alice"} によって User 構造体が生成されます。(アドレス: 0x21043b26020)
  2. changeName(&user) を呼び出します。
    • &user によって、アドレス 0x21043b26020 が引数として渡されます。
    • Before: changeName でも同じアドレス 0x21043b26020 が表示されます。
  3. user = &User{Name: "Bob"} を実行します。
    • 新しい User 構造体が生成されます。(0x21043b26030)
    • 関数内の user は 0x21043b26030 を指すようになります。
    • 呼び出し元の user は引き続き 0x21043b26020 を指したままです。
  4. fmt.Println(user.Name) を実行します。
    • main の user はアドレス 0x21043b26020 の User 構造体を参照しているため、Alice が出力されます。
// value_mutation.go
package main

import "fmt"

type User struct {
    Name string
}

func changeName(user User) {
    fmt.Printf("Before: changeName: %p\n", &user)

    user.Name = "Bob"

    fmt.Printf("After: changeName: %p\n", &user)
}

func main() {
    user := User{Name: "Alice"}

    fmt.Printf("main: %p\n", &user)

    changeName(user)

    fmt.Println(user.Name)
}


// main: 0x72e5b70ee070
// Before: changeName: 0x72e5b70ee080
// After: changeName: 0x72e5b70ee080
// Alice

value_mutation.goの挙動

こちらはRubyでは、見かけない挙動となるかもしれません。

  1. user := User{Name: "Alice"} によって User 構造体が生成されます。(アドレス: 0x72e5b70ee070)
  2. changeName(user) を呼び出します。
    • User 構造体がコピーされます。
    • 関数内の user は別の構造体となり、アドレスは 0x72e5b70ee080 になります。
  3. 関数内で user.Name = "Bob" を実行します。
    • アドレス 0x72e5b70ee080 の構造体だけが変更されます。
    • main の user(0x72e5b70ee070)には影響しません。
  4. fmt.Println(user.Name) を実行します。
    • main の user の Name は "Alice" のままなので、Alice が出力されます。

学び・気づき

Rubyは「参照の値渡し」

最初は、メソッド内の user と呼び出し元の user は同じ変数だと思っていましたが、実際は、同じobject_idをもつ別々な変数でした

そのため、user.name = "Bob" のようにオブジェクトの中身を変更すると呼び出し元にも反映されますが、user = User.new("Bob") のように再代入すると、メソッド内の user だけが新しいオブジェクトを指すようになります。

この挙動から、「Rubyは参照渡し」ではなく「参照の値渡し」と呼ばれる理由がなんとなく理解できました

Goは「値渡し」

Goは「構造体」と「ポインタ」で挙動が違うものだと思っていました。

しかし、サンプルコードを書いてアドレスを確認してみると、どちらも引数用の変数が新しく作られるという点は同じでした

違っていたのは、引数に渡されるもの

  • 構造体を渡した場合は、構造体そのものがコピー
  • ポインタを渡した場合は、ポインタがコピー

そのため、構造体を渡した場合は呼び出し元には影響しませんが、ポインタを渡した場合は同じ構造体を指しているため、中身の変更が呼び出し元にも反映されます

③ RubyとGoは思ったより考え方が近い?

最初は全く違う仕組みだと思っていました

しかし比較してみると、

  • Rubyは「参照という値」をコピー
  • Goは「ポインタという値」をコピー

という共通点があります

そして、

オブジェクト(構造体)の中身を変更すると呼び出し元にも反映される 変数(ポインタ)を再代入すると呼び出し元には影響しない

という点も共通しています

「Rubyは参照の値渡し」「Goはポインタも値渡し」という言葉だけでは分かりにくかったものが、サンプルコードで比較したことで理解が深まりました

まとめ

Rubyの「参照の値渡し」という言葉は知っていましたが、ちゃんと説明することはできませんでいた

object_id を確認しながら挙動を追ってみたことで、メソッド呼び出し時には引数用の変数が新しく作られ、最初は呼び出し元と同じオブジェクトを指していることが理解できました。そのため、オブジェクトの中身を変更すると呼び出し元にも反映されますが、再代入するとメソッド内だけが新しいオブジェクトをさすことがわかりました

また、Goでも同じようにサンプルコードを書いてアドレスを確認したことで、「構造体」と「ポインタ」で仕組みが異なるのではなく、どちらも値渡しであることが分かりました。違いは、構造体を渡すと構造体がコピーされ、ポインタを渡すとポインタがコピーされる点です

こんなことを記事しましたが、難しいのですぐに忘れていきそうですねw 忘れたときは今回の事を思い出したいなと思っています(きっと何回も今回の記事を見ることになるでしょうw)

最後までお付き合いいただきありがとうございました