iPhoneアプリ開発初心者がSwift + CoreDataでシンプルなTODOアプリをつくってみた
(2015/11/7 追記)Swift2.0 に対応しつつ、リネームやリファクタリングなど修正を加えたものをGitHubにあげております。この記事内で説明している通りに実装しても動作しないと思うので、もしよろしければ GitHub のコードも参考にしてみてください。
僕は普段、Rails とか JavaScript とか触っているようなへっぽこ Web エンジニアでして、iOS や Android アプリの開発経験はありません。
HogeClass *hoge = [[HogeClass alloc] init]; // (゚Д゚≡゚Д゚)マジ?
て感じでした。
でも最近は Objective なんたらの 100 億倍書きやすそうな Swift という言語が出ていたりするので、楽しそうだしちょっと手を出してみるかと。
そこでまず iOS 開発勉強用のサンプルとして Swift でシンプルな TODO アプリをつくってみたので、今回はその時の実装メモを書きたいと思います。
iOS 開発とか1ナノもわからない状態からスタートしたので、同じような境遇の人のお役に立てたら幸いです。
事前準備
Swift どころか、Objective なんたらも C もまともにやったこと無い人間だったので、さすがにいきなり開発はツラいと。
そこでまずは鉄板のドットインストールさんで事前に Swift やら iOS の開発がどんな感じかをざっくり学びました。(プレミアム会員なりましたよ)
- 【旧版】iPhoneアプリ開発入門 (全13回) - プログラミングならドットインストール
- 【旧版】iOSレイアウト入門 (全12回) - プログラミングならドットインストール
- 【旧版】Swift入門 (全24回) - プログラミングならドットインストール
僕は「ストーリーボードって何ですの?」レベルだったのでかなり参考になりました。
その後、Swift の言語仕様を学びたい欲が高まってきたので、以下の本を読み始めました。
個人的に Swift はいろんな言語のいいとこ取りしてるような感じで面白い言語だなぁという印象でした。
あとハマるのはツラいので最低限のデバッグ方法をなんとなく調査。
こちらやこちらを見る感じだとブレークポイント仕込んで、po ◯◯
ってやれば値をいじくれそうな気配がします。
JS の DEBUG を Chrome でやるような感じでなんとなくできそうです。po!!
そんな感じで本を読みながら、ブレークポイントを仕込みながら、楽しく TODO アプリ開発スタートです。
作成したアプリ
今回作成したものはこんな感じです。
コードはこちらに公開しております。
TODO を新規に登録できて、それを編集できて、スワイプしたら DELETE ボタン出てきて削除できて、みたいな。
いい感じのチュートリアルを見つけたのでそちらをモロパクリした次第です(以下、チュートリアルはこの記事のことを指すものとします)。英語です。
CoreData tutorial in Swift 5 using NSFetchedResultsController – Ravi Shankar
基本的に上記チュートリアルを参考にしながら、改造していくスタイルで開発しました(チュートリアルに載っているコード、たまに動かなかったので気をつけてくださいw)
あとチュートリアルと僕の作成したもので命名がちょいちょい違うところあると思いますが、特に深い理由はないのでお気になさらず。
ちなみに CoreData なるものを使っていて割とそこがツラかったりしましたが、まあ何とかなると思いますw
プロジェクト作成
Create a new File -> New -> Project でテンプレートを選択します。
今回は Master-Detail Application ではなく、Single View Application で一から作っていきます。
あと Use Core Data のチェックを忘れずに。
UI 開発
チュートリアルに図が豊富に載っているし、ドットインストールでストーリーボードの使い方も勉強したので、UI まわりはほとんど詰まることなく進めることができました。
強いて言うなら最初、プロジェクト作成後に自動で作られる ViewController を消して、新たに TableViewController を追加するところ。 僕は viewController のルートを表すストーリーボードの → マークが消えて「どこいったぁーーーー」と荒れ狂ったのですが、そういう人は新しく追加した tableViewController を選択した状態で、
attributes inspector -> is initial view controller
にチェックを入れると幸せになれると思います。
とりあえず TODO 一覧画面、詳細画面を作成して、+ボタンで遷移できるところまで作ったとします。また、一覧、詳細の viewController はそれぞれTodosViewController
, TodoDetailViewController
とします。
CoreData の準備
続いて CoreData の準備にとりかかります。
といっても1ナノも使い方がわからないので、まずはざっとCore Data プログラミングガイドやこちらの記事に目を通します。
とりあえず
- CoreData は OR マッピングフレームワークであり、永続ストアには主に SQLite を使用する
- 今回特に使いそうなのは NSManagedObject、NSManagedObjectContext、NSEntityDescription、NSFetchRequest、NSFetchedResultsController あたり
ということがわかりました。
そして今回使いそうなクラスについて
- NSManagedObject
モデルクラス。CoreData で永続化するオブジェクトは必ずこのクラス、またはこのクラスのサブクラスのオブジェクトでなければならない。
- NSManagedObjectContext
データ操作を行うためのコンテキスト(作業領域)。NSManagedObject
に対する操作はすべてこのコンテキストを介して行われる。CRUD 操作、Undo Redo、Validation などを担当。
- NSEntityDescription
エンティティ記述という、エンティティの定義を管理するクラス。エンティティとは CoreData で永続化の対象となるデータのこと。エンティティとNSManagedObject
オブジェクトの関係は DB でいうところのテーブルとレコードの関係みたいな感じですかね。
- NSFetchRequest
データの検索条件を管理するクラス。ここで指定した条件が SQL に変換されてデータ検索に使われる。
- NSFetchedResultsController
NSManagedObject
オブジェクトを監視するコントローラークラス。NSManagedObject
オブジェクトが挿入、変更、削除された時にNSFetchedResultsControllerDelegate
オブジェクトに通知する。
ふむ。
プロジェクト作成時に Use Core Data をチェックした時点で CoreData を使うための設定はある程度やってくれているみたいですね。
では上記を踏まえ、チュートリアルに沿って進めていきます。
まずは xcdatamodeld ファイルを更新します。チュートリアルではエンティティを Tasks としていますが、僕は Todos として、Attributes は String 型の content を持つものとしました。
Todos.swift はとりあえずこんな感じになります。
import Foundation import CoreData class Todos: NSManagedObject { @NSManaged var content: String }
ちなみにここで@NSManaged
というものがついていますが、もともと Objective なんたらでは NSManagedObject のサブクラスのプロパティは@dynamic
という属性をつけていたらしく、「ここはゲッターセッターの自動生成は無しにして、NSManagedObject
の実装に任せましょう」みたいな意味だったらしいです。(参考)
これが Swift ではNSManagedObject
のサブクラスにのみ利用可能な@NSManaged
という属性が使われるようになったようです。
では、いよいよ TODO を登録するところを実装していきます。
TODO の登録
TODO を登録できるようにするためにTodoDetailViewController
を修正していきます。
まずNSManagedObjectContext
オブジェクトを用意します。
let managedObjectContext = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext
iOS では 1 アプリにつき必ず 1 つの UIApplication
が存在しており、そのシングルトンオブジェクトはUIApplication.sharedApplication
で取得できます。(参考)
そいつからAppDelegate
インスタンスのmanagedObjectContext
を取得します。
そしてテキストフィールドに入力したテキストを保存できるようにメソッドを追加していきます。
func createTodo() { let entity = NSEntityDescription.entityForName("Todos", inManagedObjectContext: managedObjectContext!) let todo = Todos(entity: entity!, insertIntoManagedObjectContext: managedObjectContext!) todo.content = self.textField.text managedObjectContext!.save(nil) } func dismissViewController() { self.navigationController?.popViewControllerAnimated(true) } @IBAction func save(sender: AnyObject) { self.createTodo() self.dismissViewController() }
おそらくお作法なのだろうと、真似して保存するところまで書いてみます。(エラーハンドリング等はとりあえず無視)
まずエンティティを取得して、それを使ってNSManagedObject
オブジェクトを取得。そいつにテキストフィールドの値を入れてNSManagedObjectContext
オブジェクトにて保存すると。
@IBAction func save(sender: AnyObject)
で Save ボタンを押したときの処理が書けるので、TODO の保存と詳細画面を閉じる処理を追加します。
登録した TODO の取得
次にTodosViewController
の修正です。
こちらの viewController でも CoreData をインポートし、NSFetchedResultsControllerDelegate
プロトコルに適合させていきます。またデータ操作のためのNSManagedObjectContext
オブジェクトも必要です。
import CoreData class TodosViewController: UITableViewController, NSFetchedResultsControllerDelegate { let managedObjectContext = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext var fetchedResultController: NSFetchedResultsController = NSFetchedResultsController()
これだけだとfetchedResultController
は無力なので、getFetchedController
メソッドを定義して、viewDidLoad
の中でゴニョゴニョやります。(viewDidLoad
がいつ呼ばれるか等、iOS のイベントに関してはこちらを参考にさせて頂きました)
override func viewDidLoad() { super.viewDidLoad() fetchedResultController = self.getFetchedResultController() fetchedResultController.delegate = self fetchedResultController.performFetch(nil) } func getFetchedResultController() -> NSFetchedResultsController { fetchedResultController = NSFetchedResultsController(fetchRequest: self.todoFetchRequest(), managedObjectContext: managedObjectContext!, sectionNameKeyPath: nil, cacheName: nil) return fetchedResultController } func todoFetchRequest() -> NSFetchRequest { let fetchRequest = NSFetchRequest(entityName: "Todos") let sortDescriptor = NSSortDescriptor(key: "content", ascending: true) fetchRequest.sortDescriptors = [sortDescriptor] return fetchRequest }
まずtodoFetchRequest
にて検索条件を作成します。そしてそいつをセットしてNSFetchedResultsController
オブジェクトを作成。performFetch
で検索条件をもとにデータ取得といった感じでしょうか。
あーキツくなってきたぞw
登録した TODO の一覧表示
次、一覧表示しますよ。まず、ストーリーボードの TodosViewController の Cell を選択した状態で、identifier を Cell としてください。
そんでからチュートリアルのコードをパクります。どん。
//こいつは必要なさそうな? override func numberOfSectionsInTableView(tableView: UITableView) -> Int { let numberOfSections = fetchedResultController.sections?.count return numberOfSections! } override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { let numberOfRowsInsection = fetchedResultController.sections?[section].numberOfObjects return numberOfRowsInsection! } override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { var cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell let todo = fetchedResultController.objectAtIndexPath(indexPath) as Todos cell.textLabel?.text = todo.content return cell } func controllerDidChangeContent(controller: NSFetchedResultsController!) { tableView.reloadData() }
こちらの記事を参考に tableView まわりのメソッドを整理していきます。
- numberOfSectionsInTableView
テーブル全体のセクションの数を返すように実装する。
- tableView(tableView: UITableView, numberOfRowsInSection section: Int)
セクション無しのテーブルを作成する場合はこのメソッドでセルの数を返すように実装する。テーブルにセクションを付ける場合はnumberOfRowsInSection
で指定されたセクションの項目数を返すように実装する。
- tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath)
cellForRowAtIndexPath
で指定したインデックスパスのセル(UITableViewCell)を作成し、そのインスタンスを返すように実装する。
ただ今回、別にセクション分けされた tableView を作っているわけではないのでnumberOFSectionsInTableView
は要らない気がしました。実際消しても何も問題起きないので僕は削除しました。
あと、tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath)
の以下の部分。
let todo = fetchedResultController.objectAtIndexPath(indexPath) as Todos
ここで Swift には NameSpace があるので、このダウンキャストはモジュール名.クラス名じゃないと使えないと。
てなわけで xcdatamodeld ファイルを再度開き、Data Model Inspector から上記に従ってクラス名を変更します。僕はiOSTodos
というプロジェクト名だったのでiOSTodos.Todos
です。
ここまでで新規登録と登録した TODO の一覧表示はできるかと。あーーキツイ!!
登録した TODO の削除
スワイプしたら DELETE ボタンが出てきて、クリックしたら削除できるあれですね。
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { let managedObject: NSManagedObject = fetchedResultController.objectAtIndexPath(indexPath) as NSManagedObject managedObjectContext?.deleteObject(managedObject) managedObjectContext?.save(nil) }
こんな感じになりました。コンテキストを介して、対象のNSManagedObject
オブジェクトを削除して保存。新規登録と同じ感じですね。
登録した TODO の編集
TODO を編集できるようにします。一覧画面からセルを選ぶと詳細画面に遷移して、編集、保存を行えるようにします。
まずストーリーボードの TodosViewController の Cell から TodoDetailViewController に向かってセグエを作成します。あとでセグエを特定するために identifier が必要になるので、identifier は edit とします。
次にTodosViewController
の修正をします。
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { if segue.identifier == "edit" { let cell = sender as UITableViewCell let indexPath = tableView.indexPathForCell(cell) let todoDetailController: TodoDetailViewController = segue.destinationViewController as TodoDetailViewController let todo: Todos = fetchedResultController.objectAtIndexPath(indexPath!) as Todos todoDetailController.todo = todo } }
prepareForSegue
は画面遷移が始まった時に呼ばれるメソッドですね。
クリックされた Cell をもとにindexPath
を取得して TODO を特定します。そして、segue.destinationViewController
で遷移先の viewController が取得できるのでそいつに TODO を渡しています。
TodoDetailViewController
のほうでtodo
を定義しないといけません。
TodoDetailViewController
の修正です。
var todo: Todos? override func viewDidLoad() { super.viewDidLoad() if todo != nil { self.textField.text = todo?.content } }
一覧画面から TODO が渡ってきた場合、テキストフィールドにそいつのcontent
をセットしています。
そして編集メソッド。
func editTodo() { todo?.content = self.textField.text managedObjectContext!.save(nil) }
入力したテキストフィールドの値で上書き保存します。
Save ボタンをおした時の挙動が新規登録、編集の2パターンになったので修正します。
@IBAction func save(sender: AnyObject) { if todo != nil { self.editTodo() } else { self.createTodo() } self.dismissViewController() }
これで新規登録、一覧表示、削除、更新、ひと通りの処理ができるようになりました。キツかった。。
テキストフィールドで Return した時にキーボードが閉じるようにする
現状ではテキストを入力し終わってドヤ顔で Return してもキーボードが閉じず、恥ずかしい思いをしてしまいます。
TodoDetailViewController
を修正していきます。
class TodoDetailViewController: UIViewController, UITextFieldDelegate { 略 func textFieldShouldReturn(textField: UITextField!) -> Bool { self.view.endEditing(true) return false }
UITextFieldDelegate
プロトコルに適合して、Return した時に呼ばれるtextFieldShouldReturn
を実装しておきます。これでキーボードが閉じるはず。
まとめ
以上、チュートリアルを参考に TODO アプリを作ってみました。作る過程で様々な本や記事に触れ、少しだけですが iPhone アプリ開発のことを知ることができました。やはり今までやったことない開発は楽しいし、Swift という言語自体にもかなり惹かれているので今後も勉強を続けていきたいなと思いました。ただどうしても現状 Objective なんたらの知識が求められる場面が多いのでそこはツラいですねw 諦めてそちらも勉強していきます。。
ちなみに他にもテキスト空の状態で登録できないようにしたり、ちょいちょい改良はしてみたのですが、長くなるので後はこちらで見ていただければと思います。
また本記事や公開しているコードにおいて、初心者ゆえの間違いも多々あるかもしれません。僕は非常に傷つきやすいので、もし発見された方は優しくコメントして頂けると幸いです。
参考
- 【旧版】iPhoneアプリ開発入門 (全13回) - プログラミングならドットインストール
- 【旧版】iOSレイアウト入門 (全12回) - プログラミングならドットインストール
- 【旧版】Swift入門 (全24回) - プログラミングならドットインストール
- http://mitaplus.sakura.ne.jp/wordpress/?p=335
- [iOS] デバッグ中に変数の値を変更する | DevelopersIO
- CoreData tutorial in Swift 5 using NSFetchedResultsController – Ravi Shankar
- Core Data プログラミングガイド
- サルでもわかる Core Data 入門【概念編】 - A Day In The Life
- SwiftでCoreData - Qiita
- iOS アプリの構造がどのようになっているか紐解いてみる - A Day In The Life
- iOS のイベント駆動をライフサイクルイベントとユーザアクションイベントにわけて理解する - A Day In The Life
- objectivec-iphone.com - objectivec iphone リソースおよび情報
- Obejctive-C 3分クッキング - A Day In The Life
Chromeのコンソール上でXPathのテストをする
最近、仕事で Web スクレイピングの機能を実装する機会があって XPath を使う必要に駆られました。そこで Chrome で XPath が正しく機能しているかテストする便利な技があったことを知ったのでメモです。
$x を使用する
Chrome のコンソールから、$x
を使えば OK。
たとえば/html/body
でちゃんと body 要素が取得できるか確認したい場合は、
$x('/html/body')
としてやれば body 要素がちゃんと返ってきます。こいつは便利。知らなかった。。
まとめ
スクレイピングする時ぐらいしか使わないかもしれませんが、もし複雑な XPath を書く必要に迫られて困っている人は試してみてください!
参考
procとlambdaの挙動の違い
Ruby の Proc オブジェクトはKernel#proc
(またはProc#new
)で作るパターンとKernel#lambda
で作るパターンがありますが、それらは地味に結構挙動が違っていたりします。
そこで今回はこのKernel#proc
とKernel#lambda
でつくった時の Proc オブジェクトの挙動の違いについて見ていこうと思います。
return の時の挙動が違う
def proc_hoge proc { return 1; puts 'hoge' }.call 'hogehoge' end def lambda_hoge -> { return 1; puts 'hoge' }.call 'hogehoge' end proc_hoge #=>1 lambda_hoge #=> 'hogehoge'
こんな感じで proc のreturn
はProc#call
を読んだコンテキストから抜けます。
よって、もしトップレベルで
proc { return 1; puts 'top level!' }.call #=> LocalJumpError
とやるとLocalJumpError
となります。
一方、lambda の方はKernel#lambda
で作成したオブジェクトの制御を抜けるだけです。
トップレベルで呼び出しても問題ありません。
-> { return 1; puts 'hoge' }.call #=> 1
break の時の挙動が違う
def proc_hoge proc { break 1; puts 'hoge' }.call 'hogehoge' end def lambda_hoge -> { break 1; puts 'hoge' }.call 'hogehoge' end proc_hoge #=>LocalJumpError lambda_hoge #=> 'hogehoge'
次にbreak
の挙動ですが、proc の場合はbreak
で常にLocalJumpError
が発生します。トップレベルだろうがメソッドの中だろうがエラーです。
一方、lambda の場合はreturn
と同様、制御を抜けるだけです。
これはかなり挙動が違うので注意する必要がありそうですね。
対策としてはnext
を使うとどちらの場合でも制御を抜けるだけになるので、それを使うといいと思われます。
def proc_hoge proc { next 1; puts 'hoge' }.call 'hogehoge' end def lambda_hoge -> { next 1; puts 'hoge' }.call 'hogehoge' end proc_hoge #=> 'hogehoge' lambda_hoge #=> 'hogehoge'
引数に対する厳密さが違う
proc { |x, y| [x, y] }.call(1, 2, 3) #=> [1, 2] proc { |x, y| [x, y] }.call(1) #=> [1, nil] proc { |x, y| [x, y] }.call([1, 2]) #=> [1, 2] # lambda { |x, y| x }.call(1, 2, 3)と同じ -> (x, y) { x }.call(1, 2, 3) #=> ArgumentError -> (x, y) { x }.call(1) #=> ArgumentError -> (x, y) { x }.call([1, 2]) #=> ArgumentError
proc に引数を渡すときは
- 仮引数の数より多く引数が渡された時、余分な引数は無視する
- 引数の数が足りない時は
nil
を渡す - 配列が 1 つだけ渡されると展開される
のような特徴があります。
一方、lambda の場合は上記はすべてArgumentError
になります。なかなか手厳しいですね。
まとめ
以上がKernel#proc
とKernel#lambda
でつくった時の Proc オブジェクトの主な違いです。全体的に lambda でつくった Proc オブジェクトのほうがよりメソッドぽい雰囲気がしますね。
参考は安定のこちら。いつもありがとうございます。
Rubyのcase文はwhen節に渡すオブジェクトによって振る舞いが異なる
今回は Ruby の case 文についてです。
Ruby の case 文はどのように値を比較するか
Ruby の case 文では case 節に比較対象となる値を、when 節に case 節の値と比較する値を記述します。
んで、when 節の値をレシーバとして、===
メソッドによって比較が行われ、最初に真となった when 節に続く処理が実行されます。
hoge = 'hoge' case hoge when 'foo' puts 'foooo!' when 'bar' puts 'baaar!' when 'hoge' puts 'hogeee!' else puts "わかりまへーん" end #=> hogeee!
このように===
で比較するわけで、数値や文字列を比較する時は単純に==
的な比較を行います。
しかし、実はいくつかのクラスでは===
の振る舞いが異なります。具体的にはRange
, Regexp
, Proc
, Module
とClass
です。
Range クラスのオブジェクトと比較する場合
score = 43 case score when (1..40) puts '追試ですよー' when (41..80) puts '微妙ですよー' when (81..1000) puts 'いい感じですよー' end #=> 微妙ですよー
Range
の===
では引数が自身の範囲内に含まれていたら真を返します。今回はscore = 43
なので追試は間逃れています。
Regexp クラスのオブジェクトと比較する場合
lang = 'ruby' case lang when /php/ puts 'phpですよー' when /ruby/ puts 'rubyですよー' else puts 'そんなの知りませんよー' end #=> rubyですよー
Regexp
の===
は引数の文字列がマッチした場合に真を返します。今回は簡単な例ですが、ちゃんと正規表現を書けばかなり柔軟な比較ができそうです。
Proc クラスのオブジェクトと比較する場合
obj = 'hoge' case obj when proc { |x| x.is_a? String } puts 'Stringでしたよー' when proc { |x| x.is_a? Numeric } puts 'Numericでしたよー' else puts 'よくわからないですよー' end #=> Stringでしたよー
Proc
の===
では右辺の値を引数にしてProc
の処理を実行します。
よって上記のように条件判断のためのProc
オブジェクトを仕込んでおくと、case に渡された値を引数として処理を実行するので、柔軟な条件分岐が可能になります。
Class や Module クラスのオブジェクトと比較する場合
obj = 'hoge' case obj when String puts 'Stringでしたよー' when Numeric puts 'Numericでしたよー' else puts 'よくわからないですよー' end #=> Stringでしたよー
Class
やModule
の===
では右辺のオブジェクトが左辺のクラス(もしくはそのサブクラス)のインスタンスだった場合、真を返します。
なのでString === String
はfalse
ですが、String === 'hoge'
はtrue
になります。
これは地味に間違えそうなやつですね。
まとめ
クラスによって比較の仕方が違うというのは、知らないとハマりポイントになりそうなので注意が必要です。また、例えばクラスの異なるオブジェクト同士を比較する場合は、どちらをレシーバにするかによって結果が変わる可能性もあるのでそこも注意が必要そうです。
今回も参考にしたのは安定のパーフェクト Ruby でございました。
Rubyのメソッド探索についてまとめてみた
今回はパーフェクト Ruby で「Ruby のメソッド探索」について学んだのでそのメモです。
オブジェクトに対してメソッド呼び出しが行われる際、どのようにメソッドが呼び出されているのか、書いてみたいと思います。
自身のメソッドを呼び出した時
class BaseClass def hello :hello end end base_object = BaseClass.new base_object.hello #=> :hello
これは最も単純なケース。
自身のクラスのhello
メソッドを探しにいって、あれば実行します。
親クラスのメソッドを呼び出した時
class InheritClass < BaseClass end inherit_object = InheritClass.new inherit_object.hello #=> :hello
この場合、まず Ruby のインタプリタはinherit_object
のクラスInheritClass
を参照しますが、ここにhello
メソッドはありません。なので、次にその親クラスであるBaseClass
を探しにいってそこで見つかったhello
メソッドを呼び出します。
特異メソッドを呼び出した時
def base_object.hello :singleton_method_hello end base_object.hello #=> :singleton_method_hello
base_object
の特異メソッドにhello
を定義した場合。これはbase_object
の特異クラスのメソッドなので上図のような関係になります。
特異クラスはオブジェクトのクラスよりも先にメソッド探索されるようになっています。よって今回の場合、インタプリタはまずbase_object
の特異クラスを参照し、そこにあるhello
メソッドを呼び出します。
ちなみにbase_object
の特異クラスにはbase_object.singleton_class
でアクセス可能(特異クラスはそれを定義したり、確認しようとしたタイミングで作成される)です。
クラスに include したモジュールのメソッドを呼び出した時
module HelloModule def hello_from_module :hello_from_module end end class InheritClass include HelloModule end inherit_object = InheritClass.new inherit_object.hello_from_module #=> :hello_from_module
モジュールをクラスにinclude
した場合、そのモジュールの機能を取り込んだクラスが自身と親クラスの間に挿入されます。そして自分、モジュールの機能を取り込んだクラス、親クラスの順に探索します。
今回の場合はモジュールの機能を取り込んだクラス内でhello_from_module
を発見し、それを呼び出しています。
ちなみに複数のモジュールをinclude
した場合は後からinclude
したモジュールが前に挿し込まれる感じになります。
クラスに extend したモジュールのメソッドを呼び出した時
module HelloModule def hello_from_module :hello_from_module end end class InheritClass extend HelloModule end inherit_object = InheritClass.new inherit_object.hello_from_module #=> NoMethodError: undefined method ... InheritClass.hello_from_module #=> :hello_from_module
モジュールをクラスにextend
した場合、レシーバ(今回はInheritClass
)の特異クラスに対してextend
したモジュールがinclude
されます。
よって上図のような関係になり、モジュールで定義しているメソッドがInheritClass
のクラスメソッドとして使えるようになります。inherit_object
のメソッド探索は直線部なので、破線部にあるhello_from_module
は見つかりません。
存在しないメソッドを呼び出した時
method_missing
メソッドが定義されているクラスを探し続けます。
それが見つからない場合は最終的にBaseObject#method_missing
を呼び出し、NoMethodError
が発生します。
まとめ
特異メソッド、特異クラス、mixin など何気なく使っているものの動作を図で整理できてよかったです。 なにか間違えていたらご指摘いただけると幸いです。
今回も参考にしたのはこちら。