はじめに

こんにちは!エンジニアの藤井です。このドキュメントは、C# 使いの皆さんのための Ruby 初学用に書かれています。

Ruby でアレはどう書くんだろう?というときに逆引きできるようにしたほか、ほかの人が書いたコードがざっくり読めるような基礎知識も入れました。

どこからでもレファレンスとして読めますが、通しで読みたい方は、まずは前編からご覧ください。

少しでも皆さんのお役にたてたら幸いです。

制御構造

ブロック

  • do end や then end と { } は同じ。

条件分岐

  • if 文は C# と違い、条件はカッコでくくらなくてよい。 else if は elsif と書く。
  • if (!condition) は unless condition と書ける。
  • if、unless は後置できる。前の部分は1文のみ。
  • C# の switch-case-else 文は case-when-else と書く。C# と同じくフォールスルーしない。when句はケース等価演算子(===)を内部で使用しているので、Range 内にあるかどうかなど調べられて便利。
if pet == "dog" then # then は省略可
    print "bow"
elsif pet == "cat" then
    print "miew'
else
    print "hello "
end

# if not は unless と書けます
unless pet == "owl" then
    pet.sleep
end

# 後置できます
pet.bark if pet == “dog"
pet.sleep unless pet == “owl‘

# フォールスルーしません
case temperature
when 0...30
    "cold"
when 30...100
    "hot"
else
    "not water"
end

繰り返し

  • Enumerable モジュールがMix-in されているオブジェクトなら、each メソッドが使える。
  • while not は until と書く。while 、until は後置できる。
  • for 文も一応ある。
array= [0,2,4,8,16,32,64]

# Enumerable#each
array.each do |a|
  print a
end

# while を使った書き方
i = 0
while i < array.length do # do は省略可
  print array[i]
  i += 1
end

# 後置できます
sleep(60) while io_not_ready?

# while not は until と書きます
until f.eof? do # do は省略可
  print f.gets
end

# 後置できます
print(f.gets) until f.eof?

# for 文もあります
for i in [1, 2, 3]
  print i*2, "\n"
end

ループ制御

  • C# の break、continue は、break、next と書く。
  • ブロックの最初からやり直す redo という句もある。
until f.eof?
  s = f.gets

  # ループをブロックの最初からやり直す(条件を評価しない)
  redo if f.err

  # ループを条件の再評価からやり直す(条件を評価する)
  next if s.empty?

  print s

  # ループを抜ける
  break unless input.empty?
end

引数がブロックのメソッド

  • Enumerable#each などは、ブロックを引数にとるメソッドで、C# の delegate や ラムダ式に近い。ブロック引数はパイプ(|)で囲む。
sum = 0
( 1..10 }.each do |n|
    sum += n
end

# 上と同じ
sum = 0
( 1..10 }.each {|n| sum+= n }  

例外

  • C# の try-catch-finally は、 begin-rescue-ensure と書く。
  • C# の throw は、raise と書く。
  • retry は、ブロックの最初からやり直す。
  • else は、例外をキャッチしなかった時の処理を書く。
  • C# の型を指定しない catch は、 Ruby の rescue では StandardError のみをキャッチする。C# のようにすべての例外をキャッチするには、 Exception をキャッチするしかない。
  • a = foo.bar rescue nil のような書き方もできる。 foo.bar が例外をあげたら、nil を代入するという意味。
attempts = 0
begin
  file = File.open("test.txt")
  raise "ファイルは空です。" if file.eof?
  # 何か処理
rescue TemporaryError
  attempts +=1
  retry if attempts < 3 
rescue IOError => e
  puts "読み込み失敗: #{e.message}"
rescue => e
   puts e.message
else
  puts "無事に読み込めました"
ensure
  file.close if file # エラーの有無に関わらず必ず閉じる
end

モジュール

  • Ruby には namespace はないが、名前空間のような、クラスのような、インターフェイスのような、モジュールというものがある。
module MyLib

  module Hogable
    def self.hoge
    end

    def hoge
      puts "hoge"
    end
  end

  class Fuga
    include MyLib::Hogable # インスタンスメソッドとして取り込み
  end

end

クラスとモジュールの違い

  • モジュールはインスタンスを作れない。
  • モジュールは継承できない。

モジュールをクラスに組み込む

  • include: モジュールのメソッドを インスタンスメソッド として追加する。
  • extend: モジュールのメソッドを クラスメソッド(静的メソッド)として追加する。
  • prepend: include と似ているが、同名メソッドがあった場合にモジュール側を優先させる(メソッドの上書き・フックに使う)。

ファイル

  • モジュールとファイルは対応しない。
  • partial はいらない。いくらでもファイルは分けられる(Mix-in)。
  • require 'packagename' Gem(後述)など、パッケージを読み込む。
  • require_relative './lib/file' 相対パスでファイルを読み込む。
  • load 'myconfig' 実行されるたびに読み込みなおされるので、設定ファイルなど書き換えられることのあるファイルを読み込む。

同名の扱い

  • Rubyで同じ名前のモジュールが複数定義された場合、エラーにはならず、既存のモジュールに中身が「追加(マージ)」される。これを「オープンモジュール(またはオープンクラス)」と呼ぶ。
  • 同じ名前のメソッドを再定義した場合は、「後から書いたもの」が優先(上書き)される。
  • module として定義した名前に、後から class として同じ名前を使おうとした場合(またはその逆)、TypeError が発生する。

入れ子

  • モジュールは名前空間として機能するため、何重にも入れ子(ネスト)にすることができる。
  • クラスも、入れ子にできる。
  • 入れ子の呼び出し方法‌‌
    ::MyLib::Fuga または MyLib::Fuga

メソッド(関数)

def hoge(arg1, arg2 = true, *rest_args, keyword_arg: false, **keyword_args, &block_arg)
  fuga()
end

引数

次の順番で定義すること。

  • 通常の引数
  • デフォルト値つきの引数 (arg = true)
  • 可変長引数(*args) 引数を配列で受ける。
  • キーワード引数(arg:)
  • キーワード引数(**args) 引数をハッシュで受ける。1つのみ。
  • ブロック引数(&block) ブロックを引数にとる。eachメソッドなどが代表的。1つのみ。必ず最後に記載する。

引数の委譲

  • 引数の委譲(...) メソッド中で別のメソッドを呼び出すとき、すべての引数をそのメソッドにそのまま渡す。
    def hoge(...)
       fuga(...)
    end

ブロック引数

  • 明示せずにブロック引数を使える。‌‌
    def hoge‌‌
       yield if block_given‌‌
    end
  • ブロック引数が存在するかは、block_given を使う。
  • ブロック引数を実行するには yield を使う。C# の yield とはぜんぜん違うので注意。

呼び出し

  • func(x: 1) キーワード引数が使える。
  • def func(*args, **nil) キーワード引数を使えないようにしたメソッドも定義できる。

メソッドの定義

  • def self.hoge( ) クラスメソッド
  • def foo.hoge( ) 特異メソッド‌‌インスタンス foo 専用のメソッド。

戻り値

  • 戻り値は最後に評価された式。
  • return 戻り値 の形も使える。

メソッドの名前

  • スーパークラスのメソッドをundef できる。
  • 同名メソッドはあと勝ちなので、上書きできる(モンキーパッチ)。
  • alias size length メソッド名のエイリアスが作れる。
  • hoge? 末尾が?なのは返値が true か falseのメソッド。
  • hoge! 末尾が!なのは!がない同名メソッドより危険。破壊的メソッドなことが多い。

ラムダ式

  • C# の delegate に近い。
  • 次の記法で作れる。‌‌
    square = ->(x) { x * x }‌‌
    multiply = lambda { |x, y|  x * y }
  • ただ、ブロックを引数で渡せるので、そちらを使うことが多い。

Proc クラス

  • ブロックを表すクラス。ブロックを引数にしてインスタンスを作る。
  • Proc 内のローカル変数は、スコープ(ブロック)を抜けたあとも、値が維持される。環境をまるっともつ(クロージャ)。
  • カプセル化できるため、簡易クラスとして使える。
def create_counter
  count = 0 # このメソッド内のローカル変数
  
  # Procを返却する
  return Proc.new do
    count += 1 # 外側の変数 count を参照・更新している
    puts "現在のカウント: #{count}"
  end
end

# カウンターを作成
counter_a = create_counter
counter_b = create_counter

counter_a.call # => 現在のカウント: 1
counter_a.call # => 現在のカウント: 2
counter_a.call # => 現在のカウント: 3

# counter_b は別の環境を持っている
counter_b.call # => 現在のカウント: 1

カリー化

  • カリー化とは、複数の引数をとる関数を「引数を1つだけ受け取り、残りの引数を受け取る関数を返す」という形式に変換すること。つまり、f(a, b, c) を f(a)(b, c) に変換すること。
  • なにが嬉しいかというと、同じようなメソッドをたくさん作る、テンプレートのような方法がとれる。
def multiply(a, b)
  a * b
end

# メソッドをオブジェクト化してカリー化
double = method(:multiply).curry.(2)
triple = method(:multiply).curry.(3)

p triple.(10) # => 30

クラス

クラスひと回り

module MyLib

  # スーパークラス
  class Animal

    # プロパティ(シンタックスシュガー)
    attr_reader :age

    # コンストラクタ
    def initialize(age)
      @age = age
    end

    # クラスメソッド
    def self.from_pet_shop(pet_name)
      pet = new(0)

      # 特異クラスの定義
      class << pet
        attr_accessor :name
      end

      pet.name = pet_name
      pet
    end

    # インスタンスメソッド
    def sleep
      print “sleeps on the ground."
    end
  end

  # サブクラス
  class Human < Animal

    attr_reader :pet
    
    def initialize(age)
      super
      @pet = Animal.from_pet_shop(“Pochi")

      # 特異メソッドの定義
      def @pet.sleep
        print “sleeps on the bed."
      end
    end

  end
end

変数のスコープ

  • @hoge インスタンス変数。
  • @@hoge クラス変数。 C# の staticと違い、そのクラスとサブクラスで共有し、サブクラスで書き換えると値が変更される。
  • @hoge クラスインスタンス変数。‌‌宣言された場所がクラスorクラスメソッドだとクラスインスタンス変数になる。Ruby のクラスは、Class クラスのインスタンスのため、データを持つことができる。これをクラスインスタンス変数と言い、継承先のクラスで書き換えても、変更されない。
  • $hoge グローバル変数。

可視性

  • public 外部からアクセス可能。クラスの中ではデフォルトpublic。
  • private そのクラスとサブクラスのインスタンスからアクセス可能。サブクラスからもアクセスできるのが C# との違い。‌‌ただし、レシーバを指定して(オブジェクトのメソッドとして)呼び出すことはできない。つまり、他のインスタンスの private メソッドは呼び出せない。
  • protected  そのクラスとサブクラスのインスタンスからアクセス可能。レシーバを指定して呼び出せる。

特定インスタンスへの振る舞いの追加

  • 特異メソッド 特定インスタンス向けメソッド。インスタンス作成後に定義する。
  • 特異クラス 特定インスタンス向けクラス。インスタンス作成後に定義する。

インナークラス

  • インナークラスを作成できる。
  • C# のインナークラスは、外側のクラスの private メンバーにアクセスできる特権があるが、Ruby では、内側のクラスは外側のクラスのインスタンス変数やプライベートメソッドに直接アクセスすることはできない。

プロパティ

  • attr_accessor 読み書きできるプロパティ
  • attr_reader 読み取り専用プロパティ
  • attr_writer 書き込み専用プロパティ

アンマネージ操作

メモリやバイナリ操作に必須のアンマネージ操作も標準ライブラリでできる。

  • C#のMarshal クラスのように、ポインタ関連の操作をしたいときは、fiddle ライブラリを使う。
  • バイナリファイルの読み込みなら、Ruby の Marshal クラスを使う。

ライブラリ(Gem)

Ruby では、外部ライブラリのパッケージを Gem と呼ぶ。

  • Gem には、プログラム本体、テスト、ドキュメント、設定などが含まれる。
  • Gem のバージョン管理には、Bundler というツールを使う。

さいごに

さいごに、筆者の Ruby に対する思いをしたためようと思います。

第一印象

Ruby の勉強をしてみての最初の感想は、ちょこちょこっと書くにはクラス定義も楽だし、後置 if とか読みやすいし、ライブラリにモンキーパッチでガシガシ上書きできるし、わかって使う分にはパワフルで、しかもキレイに書ける言語だなあというものでした。

チーム開発で

ただ、大きなプログラムを読むうち、引数に型指定がないため、仮引数の名前が適当だと渡す値を間違うので、コード規約を相当きつくしないとチーム開発には向かないだろうな、と思うようになりました。構造体がないからって引数をハッシュ(Key-Value)にして引き回すとか、読みにくいにもほどがあります。拡張性はあるのでしょうが、カプセル化してくれないと、保守性が悪すぎます。

RDoc とかでコメントをちゃんと書けばよいのですが、それなら言語側でチェックしてほしいし、静的解析ツールとか、Ruby 3.0 からの型定義ファイルとかが欲しくなるというのは、やはりそもそも大規模開発に動的型付け言語は向かないということだ思います。

ライブラリを使ってみて

Ruby on Rails などライブラリや環境が強力ですが、ライブラリのドキュメントにも型が書いていないことが多く、結局ソースを読む羽目になるのですが、これまた読みにくい。みんな name というプロパティがあったら、何も考えず文字列を代入するのだろうか…?

動かして確認しようにもコンパイルしないからそのコードを通らないとエラーにならないし、カバレッジ100%のテストを書くしかない、なるほど、テスト推進言語ということで、納得。

結論として

自分の書いたメソッドも三日後には忘れる筆者には、Ruby は難しすぎました。すべてのクラスとメソッドの型を記憶できる頭のいい人向けの言語ということで、頑張れ .NET Framework!

おまけ

やはりオフィシャルが最高ということで、こちらをご紹介します。