Rubyプログラム実行環境による挙動の差異

  • 同じターミナル上でもIRBとRailsアプリをインストールしているフォルダでのみ実行できるrails consoleとでは一見よく似ているがだいぶ違う
  • rails console上では変数名やメソッド補完が利くが、IRBではファイル名補完しか利かない
    • ex.)同じように"user.rb"をrequireuser = User.newしても変数名やメソッド一覧補完が可能なのはrails console側だけ
  • 上記の点だけでも、IRBはどこからでも呼び出せるがrails consoleの方が使いやすいように思われる

Tips

文字列連結の<<演算子(メソッド)及びその別名concatメソッドの挙動に注意

サンプルプログラムにて文字列の連結処理で予想外の挙動が発生した。
名前表示メソッドのローカル変数nにより成形された戻り値がそのままファーストネームの値を上書きしてしまう(given_name = "太郎(19)"のようにnと同じ値に書き換えられる)という挙動
調べたところ、どうやら<<演算子の仕様によるものと思われる
(ただし公式リファレンスにも記述は無い)

以下、Ruby日本語リファレンスより引用

<<演算子(メソッド)は、文字列strの末尾に別の文字列other_strを加えます。
+演算子とは違い、レシーバ自身の文字列を変更します。戻り値はレシーバ自身です。

Rubyリファレンス << (String)

(実例)

class Person  
   attr_reader :given_name, :family_name, :age  

   def initialize(given_name, family_name, age )  
     @given_name = given_name  
     @family_name = family_name  
     @age = age  
   end  

   def name(full: true, with_age: true)  
     n = if full  
           # nには文字列が代入される  
           "#{given_name} #{family_name}"  
         else  
           # n = given_nameという参照渡しが実行される(同一オブジェクトID=同じ値を見ている状態)  
           given_name  
         end  
     n << "(#{age})" if with_age  
     n  
   end  
end  

上記をrequireし、インスタンス作成の上でperson1.name(full: false)とすると表示結果自体は期待通りgiven_name(age)の形式で値が返るが、その後オブジェクトの中身を見てみるとインスタンス変数@given_nameの値も同じ成形後文字列に上書きされてしまう。

full: trueの時には発生せずelseブロックを抜けた際のみ発生する。

これは、内部的にはfull: trueの際はn = 文字列 << 文字列という処理であるのに対し、falseの場合はn = given_nameが厳密には文字列の代入ではなく参照渡しとして働くが、<<演算子の仕様によりn (= given_name) << "(#{age})"と文字列連結されたローカル変数nが別のオブジェクトIDに変わらない為に元のインスタンス変数@given_nameも変更後の値を参照してしまうものと思われる。

※通常はn = given_nameのように代入(参照渡し)された場合、一方の中身が書き換えられた時点で別のオブジェクトIDに変わる為このような事態は発生しない

これを期待通りの動作にしたい場合<<演算子(メソッド)を+に置き換えることが考えられるが、上記の公式サイトの記述から逆に+演算子(メソッド)による文字列連結は<<と違いレシーバ自身の文字列を変更しないということでもある

故にn + "連結文字列"とした上で戻り値をnとすると文字列が連結される前のnが返されてしまう仕様らしい。

従って戻り値としての1行(return) nの記述を削除し、メソッド内の最終評価式をn + "連結文字列"とするか、明示的にその前にreturnを付け加えるかすることで期待通りの動作をする模様。

(ただし前にreturnを付けた場合に後置ifが問題なく動作するのかは未確認。要検証)

class Person  
   attr_reader :given_name, :family_name, :age  

   def initialize(given_name, family_name, age )  
     @given_name = given_name  
     @family_name = family_name  
     @age = age  
   end  

   def name(full: true, with_age: true)  
     n = if full  
           "#{given_name} #{family_name}"  
         else  
           given_name  
         end  
     n + "(#{age})" if with_age  
   end  
end