Ruby on Railsチュートリアル第4章
第4章(Rails風味のRuby)
文字列、オブジェクトとメソッド
この章では文字列、オブジェクト、メソッドについての基本的な内容が記載されているのでまとめておきます。
文字列
- ダブルコートまたはシングルコートで囲ったものは「文字列リテラル(リテラル文字)」と呼ばれる。
#{ }
で{ }内の式展開が可能となる。この場合、ダブルコート「" "」を使用する。シングルコートだと式展開されない。- rubyには改行やタブは特殊文字であり、\n、\t で表される。これもシングルコートだと特殊文字化されない。
- 特殊文字を改行やタブでは無く、そのまま文字列として表現したい場合はエスケープ(\を追加)する。
オブジェクトとメソッド
- Rubyではあらゆるものをオブジェクトとして扱う。
- オブジェクトに渡されるメッセージを「メソッド」と呼ぶ。
- 条件分岐のメソッドには、if-elseやelsifが使用される。
- Rubyのメソッドには暗黙の戻り値(メソッド内で最後に評価された式の値)がある。
カスタムヘルパー
まず、ヘルパーとはRailsにあらかじめ処理をメソッド化して扱えるようにしたもの。form_with
、link_to
、image_tag
など。カスタムヘルパーはその名の通りユーザーカスタムのヘルパー。
ヘルパーは、サイトの全てで使用するようなものはapp/helpers/application_helper.rbへ記述する。。
特定のコントローラだけが使うヘルパーであれば、それに対応するヘルパーファイルを置く。例えばStaticPagesコントローラ用ヘルパーは、app/helpers/static_pages_helper.rbとする。
ここでは、全てのページで使用するページのタイトルを返すfull_title
ヘルパーを定義。
module ApplicationHelper def full_title(page_title = '') base_title = 'Ruby on Rails Tutorial Sample App' if page_title.empty? base_title else "#{page_title} | #{base_title}" end end end
データ構造
配列やブロック、ハッシュといったデータ構造について簡易的にまとめておきます。
配列
- 配列の要素のアクセスする方法はいくつかあり、[ ]で要素の番号をしているする方法や、
.first
や.last
のような指定も可能。.last
は[-1]とも表記可能。 - 配列に要素を追加する場合は、
push
メソッドもしくは<<
演算子を使用する。 - Rubyの配列は他の多くの言語と異なり、同一の配列の中に異種の方が共存できる。(例えば整数と文字列)
0..9
で表されるような「範囲」は、(0..9).to_aのような括弧付けで「配列化」が可能。
ブロック
- ブロックは「メソッドに渡されるコードの塊部分」のこと。
- ブロックは
{}
で囲う、もしくはdo..end
で囲む。そのため以下の2つは同義。
>> (1..5).each { |i| puts 2 * i } #{}で囲う記法 >> (1..5).each do |i| #do..end記法 ?> puts 2 * i >> end
- Rubyの慣習的には、1行で完結するものは
{}
で記述して、複数行に渡るものはdo..end
で記述する。 - ブロック内の記述には"symbol-to-proc"で省略するのが一般的で、
{ |char| char.downcase }
は、(&:downcase)
とシンボルを使用した記述になる。 - testメソッドにはdo..endの記法が使用される。
- eachメソッドはブロックを配列の各要素に対して実行する。しかし、加工するだけで新しい配列を返す訳ではない。
- mapメソッドはブロックを配列の各要素に対して実行し、更にその戻り値から新しい配列を作成する。
ハッシュとシンボル
- ハッシュは"キー(key)"と"値(value)"のペアを波括弧{ }で囲んで表現する手法。※ブロックとは全くの別物。
- ハッシュはデータの集合を表現するため配列のようであるが、配列は「並び順が重要でありインデックスによって要素にアクセス出来る」ことに対し、ハッシュは「並び順は保証されずキーによって値にアクセス出来る」点で異なる。
- ハッシュのキーには文字列では無くシンボルを使用することが一般的。
- ハッシュのシンボル記法では、
:name =>'Michael Hartl'
とするよりも、後置コロンでname: 'Michael Hartl'
とする方がシンプルで好まれる。但し、前置コロンの:nameはシンボルとして独立しているが、後置コロンのname:は引数を伴わないと意味が成り立たない。 - Rubyではネストされたハッシュが多用される。
- ハッシュに対するeachメソッドの処理では、ブロックの第1引数がkeyで、第2引数がvalueとして扱われる。
Rubyにおけるクラス
クラスの基本事項
- メソッドがクラス自身に対して呼び出された場合のメソッドは「クラスメソッド」。
- クラスから生成されたオブジェクトは「インスタンス」。
- インスタンスに対して呼び出されたメソッドは「インスタンスメソッド」。
- クラスは継承によって親クラスの性質を受け継ぐ。
- Rubyにデフォルトで組み込まれているクラス(String、Hashなど)にも、メソッドを自由に追加出来る。但し、真に正当な理由が無い限りはよくない。例えば、blank?メソッドはObjectクラスに設定されている。
attr_accessor
について
Railsチュートリアルでattr_accessor
は、いきなり出てきてかなり混乱するのでまとめておきます。
インスタンス変数(@userのような)はクラス内であればどこからでもアクセス出来る変数であるが、クラスの外部からはアクセス出来ない。
そのため、参照用のメソッドを作っておくことで外部からもアクセス可能となるが、その記述が面倒なため、attr_accessorという1行だけで済むようになる。
attr_accessorで宣言した属性(attribute)とそれに対応するアクセサー(accessor)が作成され、更に取り出すメソッド(getter)と代入するメソッド(setter)も定義される。
第4章ー演習ー
4.2.1
①city変数に適当な市区町村名を、prefecture変数に適当な都道府県名を代入してください。
(解答)
irb(main):001:0> city = '渋谷' => "渋谷" irb(main):002:0> prefucture = '東京' => "東京"
②先ほど作った変数と式展開を使って、「東京都 新宿区」のような住所の文字列を作ってみましょう。出力にはputsを使ってください。
(解答)
irb(main):003:0> puts "#{prefucture}都 #{city}区" 東京都 渋谷区 => nil
③上記の文字列の間にある半角スペースをタブに置き換えてみてください。(ヒント: 改行文字と同じで、タブも特殊文字です。)
(解答)
irb(main):004:0> puts "#{prefucture}都\t#{city}区" 東京都 渋谷区 => nil
④タブに置き換えた文字列を、ダブルクォートからシングルクォートに置き換えてみるとどうなるでしょうか?
(解答) シングルコートだと式展開やエスケープされないため、そのまま出力される。
irb(main):005:0> puts '#{prefucture}都\t#{city}区' #{prefucture}都\t#{city}区 => nil
4.2.2
①"racecar" の文字列の長さはいくつですか? lengthメソッドを使って調べてみてください。
(解答) 7文字
irb(main):006:0> 'racecar'.length => 7
②reverseメソッドを使って、"racecar"の文字列を逆から読むとどうなるか調べてみてください。
(解答) racecar
irb(main):007:0> 'racecar'.reverse => "racecar"
③変数sに "racecar" を代入してください。その後、比較演算子(==)を使って変数sとs.reverseの値が同じであるかどうか、調べてみてください。
(解答) 同じ
irb(main):008:0> s = 'racecar' => "racecar" irb(main):009:0> s == s.reverse => true
④リスト 4.9を実行すると、どんな結果になるでしょうか? 変数sに "onomatopoeia" という文字列を代入するとどうなるでしょうか?(ヒント: 上矢印、またはCtrl-Pコマンドを使って以前に使ったコマンドを再利用すれば、コマンドを全部入力せずに済むので便利です。)
(解答) s == s.reverse
が成立する場合は、"It's a palindrome"が出力される。
irb(main):010:0> puts "It's a palindrome!" if s == s.reverse It's a palindrome! => nil irb(main):011:0> s = 'onomatopoeia' => "onomatopoeia" irb(main):012:0> puts "It's a palindrome!" if s == s.reverse => nil
4.2.3
①リスト 4.10の(コードを書き込む)の部分を適切なコードに置き換え、回文かどうかをチェックするメソッドを定義してみてください。(ヒント: リスト 4.9の比較方法を参考にしてください。)
(解答)s == s.reverse
を書く。
irb(main):013:1* def parindrome_tester(s) irb(main):014:2* if s == s.reverse irb(main):015:2* puts "It's a palindrome!" irb(main):016:2* else irb(main):017:2* puts "It's not a palindrome!" irb(main):018:1* end irb(main):019:0> end => :parindrome_tester
②上で定義したメソッドを使って “racecar” と “onomatopoeia” が回文かどうかを確かめてみてください。1つ目は回文である、2つ目は回文でない、という結果になれば成功です。
(解答)
irb(main):022:0> parindrome_tester('racecar') It's a palindrome! => nil irb(main):023:0> parindrome_tester('onomatopoeia') It's not a palindrome! => nil
③palindrome_tester("racecar")に対してnil?メソッドを呼び出し、戻り値がnilであるかどうかを確認してみてください(つまりnil?を呼び出した結果がtrueであることを確認してください)。このメソッドチェーンは、nil?メソッドがリスト 4.10の戻り値を受け取り、その結果を返しているという意味になります。
(解答) putsの戻り値はnilのため、.nil?
はtrueとなる。
irb(main):024:0> parindrome_tester('racecar').nil? It's a palindrome! => true
4.3.1
①文字列「A man, a plan, a canal, Panama」を ", " で分割して配列にし、変数aに代入してみてください。
(解答) そのまま配列化してみた。
irb(main):009:0> a = ["A man", "a plan", "a canal", "Panama"] => ["A man", "a plan", "a canal", "Panama"]
たぶん正解はこうではなく、split
を使用するのだろうけどスペースの入れ方が不明だったので調べてみたところ、splitに引数を指定すると特定の文字列で分割出来る。
irb(main):016:0> a = "A man,a plan,a canal,Panama".split(",") => ["A man", "a plan", "a canal", "Panama"]
②今度は、変数aの要素を連結した結果(文字列)を、変数sに代入してみてください。
(解答)
irb(main):017:0> s = a.join => "A mana plana canalPanama"
③変数sを半角スペースで分割した後、もう一度連結して文字列にしてください。(ヒント: メソッドチェーンを使うと1行でもできます。)リスト 4.10で使った回文をチェックするメソッドを使って、(現状ではまだ)変数sが回文ではないことを確認してください。downcaseメソッドを使って、s.downcaseは回文であることを確認してください。
(解答) downcaseにして、全て小文字としたら回文となった。
irb(main):020:1* def palindrome_tester(s) irb(main):021:2* if s == s.reverse irb(main):022:2* puts "It's a palindrome!" irb(main):023:2* else irb(main):024:2* puts "It's not a palindrome." irb(main):025:1* end irb(main):026:0> end => :palindrome_tester irb(main):027:0> palindrome_tester(s.split(' ').join) It's not a palindrome. => nil irb(main):028:0> palindrome_tester(s.split(' ').join.downcase) It's a palindrome! => nil
④aからzまでの範囲オブジェクトを作成し、7番目の要素を取り出してみてください。同様にして、後ろから7番目の要素を取り出してみてください。(ヒント: 範囲オブジェクトを配列に変換するのを忘れないでください。)
(解答)前から7番目の要素は配列でいう[6]なので"g"、後ろから7番目は[-7]なので"t"。
irb(main):030:0> alphabet = ('a'..'z').to_a => ["a", ... => "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"] irb(main):032:0> alphabet[6] => "g" irb(main):033:0> alphabet[-7] => "t"
4.3.2
①範囲オブジェクト0..16を使って、各要素の2乗を出力してください。
(解答)
irb(main):040:0> (0..16).each{|i| puts i ** 2} 0 1 4 9 16 25 36 49 64 81 100 121 144 169 196 225 256 => 0..16
②yeller(大声で叫ぶ)というメソッドを定義してください。このメソッドは、文字列の要素で構成された配列を受け取り、各要素を連結した後、大文字にして結果を返します。例えばyeller(['o', 'l', 'd'])と実行したとき、"OLD"という結果が返ってくれば成功です。(ヒント: mapとupcaseとjoinメソッドを使ってみましょう。)
(解答)
irb(main):041:1* def yeller(s) irb(main):042:1* s.map(&:upcase).join irb(main):043:0> end => :yeller irb(main):044:0> yeller(['o', 'l', 'd']) => "OLD"
③shuffled_subdomainというメソッドを定義してください。このメソッドは、完全にシャッフルされたアルファベット8文字を文字列として返します。(ヒント: サブドメインを作るときに使ったRubyコードをメソッド化したものです。)
(解答)
irb(main):051:1* def shuffled_subdomain irb(main):052:1* ('a'..'z').to_a.shuffle[0..7].join irb(main):053:0> end => :shuffled_subdomain irb(main):054:0> shuffled_subdomain => "pmecotsx"
④リスト 4.12の「?」の部分を、それぞれ適切なメソッドに置き換えてみてください。(ヒント:split、shuffle、joinメソッドを組み合わせると、メソッドに渡された文字列(引数)をシャッフルさせることができます。)
(解答)
irb(main):066:1* def string_shuffle(s) irb(main):067:1* s.split('').shuffle.join irb(main):068:0> end => :string_shuffle irb(main):069:0> string_shuffle('foobar') => "orbofa"
4.3.3
①キーが'one'、'two'、'three'となっていて、それぞれの値が'uno'、'dos'、'tres'となっているハッシュを作ってみてください。その後、ハッシュの各要素をみて、それぞれのキーと値を"'#{key}'はスペイン語で'#{value}'"といった形で出力してみてください。
(解答)
irb(main):071:0> h = {one: 'uno', two:'dos', three:'tres'} => {:one=>"uno", :two=>"dos", :three=>"tres"} irb(main):072:1* h.each do|key,value| irb(main):073:1* puts "#{key}'はスペイン語で'#{value}" irb(main):074:0> end one'はスペイン語で'uno two'はスペイン語で'dos three'はスペイン語で'tres => {:one=>"uno", :two=>"dos", :three=>"tres"}
②person1、person2、person3という3つのハッシュを作成し、それぞれのハッシュに:firstと:lastキーを追加し、適当な値(名前など)を入力してください。その後、次のようなparamsというハッシュのハッシュを作ってみてください。1)キーparams[:father]の値にperson1を代入、2)キーparams[:mother]の値にperson2を代入、3)キーparams[:child]の値にperson3を代入。最後に、ハッシュのハッシュを調べていき、正しい値になっているか確かめてみてください。(例えばparams[:father][:first]がperson1[:first]と一致しているか確かめてみてください)
(解答) 正しい値になった。
irb(main):084:0> person1 => {:first=>"Taro", :last=>"Yamada"} irb(main):085:0> person2 => {:first=>"Ichiro", :last=>"Sato"} irb(main):086:0> person3 => {:first=>"Jiro", :last=>"Tanaka"} irb(main):081:0> params = {father: person1, mother: person2, child: person3} => {:father=>{:first=>"Taro", :last=>"Yamada"}, ... irb(main):082:0> params[:father][:first] => "Taro" irb(main):083:0> params[:father][:first] == person1[:first] => true
③userというハッシュを定義してみてください。このハッシュは3つのキー:name、:email、:password_digestを持っていて、それぞれの値にあなたの名前、あなたのメールアドレス、そして16文字からなるランダムな文字列が代入されています。
(解答) ランダム文字列は演習4.3.2と同じ。
irb(main):087:0> user = {name: 'Taro Yamada', email: 't.yamada@example.com', password_ digest: nil} => {:name=>"Taro Yamada", :email=>"t.yamada@example.com", :password_digest=>nil} irb(main):088:0> user[:password_digest] = ('a'..'z').to_a.shuffle[0..15].join => "zoyaniwhdelprusv" irb(main):089:0> user => {:name=>"Taro Yamada", :email=>"t.yamada@example.com", :password_digest=>"zoyaniwhdelprusv"}
④「Ruby API」や「るりまサーチ」を使って、Hashクラスのmergeメソッドについて調べてみてください。次のコードを実行せずに、どのような結果が返ってくるか推測できますか? 推測できたら、実際にコードを実行して推測があっていたか確認してみましょう。
(解答) bの値がmergeのハッシュの値に結合される。
irb(main):090:0> { "a" => 100, "b" => 200 }.merge({ "b" => 300 }) => {"a"=>100, "b"=>300}
発展演習①RubyのHashオブジェクトのキーには、「1」や「3」といった数字や、「'email'」や「'name'」といった文字列、そして「:email」や「:name」といったシンボルなどが使えますが、特にシンボルが多く使われています。その理由を考えてみましょう21 。
(解答) 参考サイトとchatGPTに聞いてみた内容。
* シンボルは一度生成したら不変であり、同じキーを持つハッシュが存在する場合にキーとしてのシンボルを共有出来るためメモリ効率が良くなる。
* 文字列と違ってハッシュは整数値として扱われるため高速動作する。
* コロンを付けるだけでシンボル化出来、一目でわかりやすくコードの可読性も上がる。
発展演習②先ほど考えたハッシュのキーにおける「数字 vs. 文字列 vs. シンボル」の比較を、WebブラウザからRubyのコードを実行できるirb.wasmを使って、下記のコードを実行して測定してみましょう。
(解答) ベンチマークがシンボル: 0.000279、文字列: 0.000051、整数: 0.000040で、シンボルが5-7倍高速。
irb(main):002:0> require 'benchmark' symbol = { :foo => "value" } string = { "foo" => "value" } integer = { 1 => "value" } Benchmark.benchmark do |x| x.report("Symbol") { symbol[:foo] } x.report("String") { string["foo"] } x.report("Integer") { integer[1] } end Symbol 0.000279 0.000000 0.000279 ( 0.000140) String 0.000051 0.000000 0.000051 ( 0.000030) Integer 0.000040 0.000000 0.000040 ( 0.000020) => [#<Benchmark::Tms:0x2de3b5d4 @cstime=0.0, @cutime=0.0, @label="Symbol", @real=0.0001399517059326172, @stime=0.0, @total=0.00027899999999903, @utime=0.00027899999999903>, #<Benchmark::Tms:0x2de3af6c @cstime=0.0, @cutime=0.0, @label="String", @real=3.0040740966796875e-05, @stime=0.0, @total=5.099999999913507e-05, @utime=5.099999999913507e-05>, #<Benchmark::Tms:0x2de3a990 @cstime=0.0, @cutime=0.0, @label="Integer", @real=2.0265579223632812e-05, @stime=0.0, @total=3.999999999848569e-05, @utime=3.999999999848569e-05>]
4.4.1
①1から10の範囲オブジェクトを生成するリテラルコンストラクタは何でしたか?(復習です)
(解答) Range
irb(main):091:0> (1..10).class => Range
②今度はRangeクラスとnewメソッドを使って、1から10の範囲オブジェクトを作ってみてください。(ヒント: newメソッドに2つの引数を渡す必要があります。)
(解答)
irb(main):092:0> r = Range.new(1, 10) => 1..10
③比較演算子==を使って、上記2つの課題で作ったそれぞれのオブジェクトが同じであることを確認してみてください。
(解答)
irb(main):093:0> (1..10) == r => true
4.4.2
①Rangeクラスの継承階層を調べてみてください。同様にして、HashとSymbolクラスの継承階層も調べてみてください。
(解答)
irb(main):094:0> Range.class => Class irb(main):095:0> Hash.class => Class irb(main):096:0> Symbol.class => Class irb(main):097:0> Range.class.superclass => Module irb(main):098:0> Hash.class.superclass => Module irb(main):099:0> Range.class.superclass.superclass => Object
②リスト 4.15にあるself.reverseのselfを省略し、reverseと書いてもうまく動くことを確認してみてください。
(解答) 確認のみなので省略
4.4.3
①palindrome?メソッドを使って、“racecar”が回文であり、“onomatopoeia”が回文でないことを確認してみてください。南インドの言葉「Malayalam」は回文でしょうか? (ヒント: downcaseメソッドで小文字にすることをお忘れなく。)
(解答)
irb(main):100:0> palindrome_tester('racecar') It's a palindrome! => nil irb(main):101:0> palindrome_tester('onomatopoeia') It's not a palindrome. => nil irb(main):102:0> palindrome_tester('Malayalam'.downcase) It's a palindrome! => nil
②リスト 4.16を参考に、Stringクラスにshuffleメソッドを追加してみてください。(ヒント: リスト 4.12も参考になります。)
(解答)
irb(main):103:1* class String irb(main):104:2* def shuffle irb(main):105:2* self.split('').shuffle.join irb(main):106:1* end irb(main):107:0> end => :shuffle irb(main):108:0> 'foobar'.shuffle => "booarf"
③リスト 4.16のコードにおいて、self.を削除してもうまく動くことを確認してください。
(解答) うまくいく。
irb(main):109:1* class String irb(main):110:2* def shuffle irb(main):111:2* split('').shuffle.join irb(main):112:1* end irb(main):113:0> end => :shuffle irb(main):114:0> 'foobar'.shuffle => "fooarb"
4.4.4
①第2章で作ったToyアプリケーションのディレクトリでRailsコンソールを開き、User.newと実行することでuserオブジェクトが生成できることを確認してみましょう。
②生成したuserオブジェクトのクラスの継承階層を調べてみてください
(解答)飛ばします。
4.4.5
①Userクラスで定義されているname属性を修正して、first_name属性とlast_name属性に分割してみましょう。また、それらの属性を使って "Michael Hartl" といった文字列を返すfull_nameメソッドを定義してみてください。最後に、formatted_emailメソッドの@nameの部分を、full_nameに置き換えてみましょう(元々の結果と同じになっていれば成功です)
(解答)
変更したUserクラスメソッド。
class User attr_accessor :first_name, :last_name, :email def initialize(attributes = {}) @first_name = attributes[:first_name] @last_name = attributes[:last_name] @email = attributes[:email] end def formatted_email "#{full_name} <#{@email}>" end def full_name "#{@last_name} #{first_name}" end end
コンソールの結果
irb(main):001:0> require './example_user' => true irb(main):002:0> example = User.new => #<User:0x00007fb370591bb0 @email=nil, @first_name=nil, @last_name=nil> irb(main):003:0> example.first_name = 'Taro' => "Taro" irb(main):004:0> example.last_name = 'Yamada' => "Yamada" irb(main):005:0> example.email = 't.yamada@example.com' => "t.yamada@example.com" irb(main):006:0> example.formatted_email => "Yamada Taro <t.yamada@example.com>"
②"Hartl, Michael" といったフォーマット(苗字と名前がカンマ+半角スペースで区切られている文字列)で返すalphabetical_nameメソッドを定義してみましょう。
(解答)
定義したメソッド
def alphabetical_name "#{@last_name}, #{@first_name}" end
コンソールの結果
irb(main):005:0> example.alphabetical_name => "Yamada, Taro"
③full_name.splitとalphabetical_name.split(', ').reverseの結果を比較し、同じ結果になるかどうか確認してみましょう。
(解答)どっちかのメソッドのlastとfirstが逆だったためfalseになった。修正は飛ばします。
irb(main):006:0> example.full_name.split == example.alphabetical_name.split(', ').reve rse => false irb(main):007:0> example.full_name.split => ["Yamada", "Taro"] irb(main):008:0> example.alphabetical_name.split(', ').reverse => ["Taro", "Yamada"]
第4章ーまとめー
- Rubyでは全てをオブジェクトとして扱う。
- ブロックの
each
メソッドとmap
メソッドは、戻り値から新しい配列を返すかどうかで動作が異なる。新しい配列を作るのはmap
メソッド。 - 配列は並び順がありインデックスによって値にアクセス出来る。
- ハッシュは並び順は無く、キーによって値にアクセス出来る。
- ハッシュのキーとしてはシンボルが好まれる。シンボルは文字列や整数に比べ利点がある。
- Rubyはクラスの継承によって親クラスの機能を受け継ぐ。
attr_accessor
で簡単に外部からアクセス出来るインスタンス変数を定義できる。
第4章終わり🐻