viva Cocoa Objective-C 入門 第8章 ファイル入出力
ホーム メール
 
目次 < 前ページ 次ページ >

 この章ではファイル入出力について学習します。
 コンピュータにとってアプリケーションが携わっているデータを一時保存しておき、そしてそのデータを再び呼び出して続きの作業を行うという一連の作業は当然かつ不可欠な機能です。本書では各機能の難易度よりも各機能の必要度にしたがって説明をする順番を決めています。そこで、この章ではコンピュータがただの電子計算機から脱却し大きな発展の源となったデータの読み込みと書き込み(書き出し、保存)について、まずはテキストベースで行う方法を説明していきたいと思います。

ファイル入出力

 前章でデリゲートとターゲットアクションというObjective-Cで最もよく使われる2つのデザインパターンの説明をいたしました。

  1. デリゲートではText Viewの文字列を変更すると、その変更された文字列がモデルオブジェクトに格納されると同時に、そのモデルオブジェクトに格納された文字列がコンソールウィンドウにも表示されるようにいたしました。
  2. ターゲットアクションではText Viewの文字列を変更すると、その変更された文字列がモデルブジェクトに格納され、同時にメインウィンドウのクローズボックスに文字列が変更されたことを表すドットマークが表示されるようにしました。そしてIndicate(表示)ボタンをクリックするとモデルオブジェクトに格納されている文字列が、コンソールウィンドウに表示され、メインウィンドウのクローズボタンに表示されていた、編集されたことを(未保存であることを)表すドットマークが消えるようにいたしました。

 以上、2つのステップは、アプリケーションMyClipにとって最も大事な機能になる、データの読み込みと書き込み(書き出し、保存)を搭載するための準備をしていました。そしてこのデータの読み込みと書き込みのことを冒頭でも説明しましたが、ファイル入出力と呼びます。

ファイル入出力の種類

 Objective-Cにはファイル入出力の方法として次の3つが用意されています。

■ テキストデータをテキストのまま読み込んで、テキストのまま書き込みを行う。

 一般的にコンピュータのデータはテキストデータとバイナリデータの2通りに分かれます。テキストデータは人の目に文字として見えるデータのことです。一方バイナリデータのバイナリとは2進数のことを意味しています。つまりバイナリデータは2進化されたデータのことで、例の0101010101というデータ形式のことになります。テキストデータでのファイル入出力は、

などの理由で結構人気があります。この章でもこのテキスト形式でのファイル入出力をMyClipに搭載しながら説明を行っていきたいと思います。また最初にこのテキスト形式でのファイル入出力を説明するためにリッチテキストフォーマットに対応しているText Viewをわざわざノーマルテキストだけの対応に変えていたのです。なお今後は、今まで「搭載」と呼んでいた機能の追加のことを、プログラミングらしく「実装」という呼び方をする場合もあります。

■ テキストデータとそのほかの形式のデータをテキストデータとバイナリデータを織り交ぜた形で読み込みと書き込みを行う。

 この方法はObjective-Cではplistと呼ばれています。Objective-Cのplist形式はどちらかといういとテキストデータの亜種的存在ですが非常に優秀でCocoaアプリケーションの標準データ形式となっている面もあります。しかし本書ではこの形式でのファイル入出力については特に説明を行わないことにいたしました。理由はデータ形式として中途半端ではないだろうかという思慮からですが、実際には著者2人ともがこのデータ形式をあまり好きではないという単純な理由からでもあります :-)

■ テキストデータとそのほかの形式のデータをバイナリデータとして読み込みと書き込みを行う。

 このバイナリデータでのファイル入出力については第10章で説明いたします。順序的にプレーンテキスト(属性を持たない簡素なテキストデータ)でのファイル入出力の説明をはじめに持ってきましたが、本格的なデータの読み込みと書き込みを行うにはこのバイナリデータによるファイル入出力を行うことになります。バイナリデータでのファイル入出力はプレーンテキスト、属性付きテキスト、画像データ、音楽データなどすべてのデータ形式に対応しています。

 ファイル入出力には名前のとおりにハードディスクなどのストレージに対してファイルという形でデータの読み込みと書き込みを行う方法とURLで表せるネットワーク上のストレージに対してデータの読み込みと書き込みを行う方法があります。しかし現実的にはネットワーク上のストレージに対して制限なしにアクセスできるという環境は実質存在しないと考えてよいでしょう(ローカルエリアネットワーク上であれば存在するかもしれませんが)。そこで基礎編ではハードディスクなどへのファイル入出力とネットワーク上のURLからのファイル入力だけを扱うことにいたしました。ネットワーク上のストレージへの書き込みは実践編でWeb APIを使った実例をいくつか紹介しています。ぜひ参考にしてください。

残念ながら本コーナーでは上記の「実践編」が掲載されることはありません。ご了承ください。

テキストデータによるファイルの読み込みと書き込み

この項の説明文と画像はXcode 3.1をベースに掲載しています。

 では始めましょう。MyClipで保存しなければならないデータはModelクラスのstringインスタン変数に格納されているNSString型の文字列データです。コーディングするための情報を集めなければならないクラスはNSStringクラスということになります。製品ドキュメント(Xcode 3.2ではデベロッパドキュメント)でNSString Class Referenceを開いてください。NSStringクラスは重要なクラスなので冒頭の説明が長いと思いますが下にスクロールすると「Tasks」という項目が始まると思います。タスクはコンピュータ用語としては「仕事の単位」という意味になります。1番目のグループが「Creating and Initializing Strings 」となっていますので「すぐに見つかった」とぬか喜びしそうですが、よく見ると2番目のグループが「Creating and Initializing Strings from a File 」となっています。ローカルストレージのファイルからの読み込みメソッドはどうやらこのグループにありそうです。そして続いて「Creating and Initializing a String from an URL」というグループと「Writing to a Fie or URL」というグループがリファレンスに載っています。テキストデータによるファイル入出力についてのメソッドはこの3つのグループから見つけることができそうです。
 なお3つ目のグループのWritingという単語ですが、コンピュータ用語してこの1語でデータの書き込み(書き出し)という意味になります。

NSStringクラスはFoundationクラス群のなかでNSObjectの次に重要かつ良く使われるクラスです。

 まず「Creating and Initializing a String form a File 」グループを見てください。この中に赤い文字で「Deprecated in Mac OS X v10.4」と書かれているメソッドがいくつかあります。「Mac OS X 10.4以降での使用は推奨しない」という意味になると思いますが、実際には推奨しないではなく10.4以降では使えないと思ってください。

図 NSString Class Reference 1

 これで「ファイルからの読み込みで」使えるメソッドは残り4つになりました。実際にメソッド名をクリックして詳細も確認してみましょう。完全なメソッドシグネチャが分からなければコーディングもできません。

 次項ではローカルストレージからの各読み込みメソッドと書き込みメソッドの説明をいたします。

ローカルストレージ
 ストレージはもともとハードディスクなどの補助記憶装置のことを表す言葉です。したがってローカルストレージという呼び方はおかしいものかもしれません。しかし最近はネットワーク上のデータ保存場所を表すネットワークストレージという言葉が登場してきています。そのためここでは、このネットワークストレージと区別するためにローカルストレージという言葉を使っています。

テキストデータによるファイル入出力メソッドの説明

 この項では読み込みメソッドと書き込みメソッドの説明をしていきます。なおここで説明する読み込みメソッドはローカルストレージからの読み込みメソッドになります。

■ 読み込みメソッド

 では読み込みメソッドの説明をはじめます。説明の順番はリファレンスでの掲載順序とは同じではありません。説明しやすい順序にしています。

	- (id)initWithContentsOfFile:(NSString *)path
		encoding:(NSStringEncoding)enc
			error:(NSError **)error

 先頭がマイナスで始まっているインスタンスメソッドです。第1引数のパス(どこにある何というファイルかを示す文字列)から第2引数のテキストエンコード(文字コード)を使って文字列データを読み込みます。その際にエラーが起こった場合には第3引数で指定されている変数にエラーの内容が書き出されます。このerror引数についてはのちほどもう少し詳しく説明いたします。なお第2引数のNSStringEncodingという型は列挙型から得られる整数値をtypedefしているにすぎません。リファレンスではNSStringEncodingの部分がクリックできるようになっています。リンクをクリックして内容を確認しておいてください。またこれらの引数の説明は各メソッド詳細ページのParametersグループに書かれています。
 なお第2引数のencodingというラベルは文字コードを意味するテキストエンコーディングからきています。それ以外の場合ではencodeとはデータをコード化するという意味で使われます。そしてコードからデータを復元することはdecodeと呼びます。なおテキストエンコーディングは「ファイルエンコーディング」「文字エンコーディング」と呼ばれる場合もあります。

	+ (id)stringWithContentsOfFile:(NSString *)path
		encoding:(NSStringEncoding)enc
			error:(NSError **)error

 先頭がプラスで始まるクラスメソッドです。メソッド名がクラス名で始まっていますのでコンビニエンスコンスラクタになります。つまりこのメソッドで作成された(読み込まれた)データ(オブジェクト)は一時オブジェクトになります。したがって継続して使う場合はretainなどの何かの手段を講じておく必要があります。引数の内容と意味は前述のインスタンスメソッドとまったく同じです。

	- (id)initWithContentsOfFile:(NSString *)path
		usedEncoding:(NSStringEncoding *)enc
		    error:(NSError **)error

 最初のインスタンスメソッドとほぼ同じですが第2引数の名前(ラベル)がusedEncodingになっています。読み込もうとするファイルの文字コードを自動的に判別してusedEncodingの引数に代入されます。
※ということになっていますが状況によっては動作が不安定です。

	+ (id)stringWithContentsOfFile:(NSString *)path
		usedEncoding:(NSStringEncoding *)enc
		    error:(NSError **)error

上記のメソッドのコンビニエンスコンストラクタです。

 以上の4つがローカルストレージからの読み込みメソッドの候補ですが今回は2番目の文字コードを自動判別しないコンビニエンスコンストラクタを使います。安定していますし、わざわざインスタンスを作らなくてもクラスから直接オブジェクトが作れるのでコンビニエンスという言葉どおりに便利です。

■ 書き込みメソッド

 書き込みメソッドは「Writing to a File or URL」グループから探すことになります。しかし掲載されている4つのメソッドのうち2つは先ほども説明したDeprecated(非推奨された。すなわち推奨されない)メソッドです。そして残り2つのうちひとつは「 ToURL」という文字からもネットワークストレージへ書き込むためのメソッドだということが分かります。結果として残るメソッドはひとつしかありません。メソッド名をクリックして詳細ページを表示してください。正確なメソッドシグネチャは次のとおりです。

	- (BOOL)writeToFile☹NSString *)path
		atmically:(BOOL)useAuxiliaryFile
			encoding:(NSStringEncoding)enc
				error☹NSError **)error

 先頭がマイナスで始まるインスタンスメソッドになっています。戻り値としてYESかNOだけを保持するBOOL型を返します。第1引数のpathで指定した場所とファイル名に第3引数で指定された文字コードを使って文字列データを書き出します。第4引数のエラー処理については読み込みメソッドの場合と同じです。元に戻って第2引数のラベルのatomicallyはコンピュータ用語として「排他的に〜」という意味合いになります。この第2引数をYESにすればデータはまず予備ファイルに書き出されます。そして無事に書き出しが終われば指定されたファイルと置き換えられます。万が一途中でエラーが起こっても書き出し場所にあるひとつ前に保存されたファイルは守られることになります。NOを指定すると指定されたファイルへデータが直接書き出されます。途中で何かのエラーが起こればファイルが壊れる可能性があります。最後に無事書き出しが終われば戻り値としてYESを返します。書き出しに失敗した場合には戻り値としてNOが返され、同時にerrorで指定した変数へエラーの内容が書き出されます。

■ エラー引数について

 error引数にエラーコードを受け入れるためには変数をあらかじめ用意しておかなければなりません。しかしerror引数にNULLを指定するとエラーメッセージの受け入れ機能じたいが無効となります。もし簡単に済ませたい場合にはNULLを指定するとよいでしょう。 また読み込みメソッドの場合にはerror引数にnilを指定するとエラーが起こった場合に戻り値としてnilが返されます。Objective-CではNULLは何も存在しないことを表す記号ですが、nilはどのオブジェクトも指し示していない空のオブジェクト型のポインタになります。したがってid型の戻り値として使用することができます。またこのように戻り値がid型の場合にはerror引数にNULLを指定すると戻り値の型と整合性がとれなくなりアプリケーションが実行時エラーを起こす場合がありますのでnilを指定しておくことをおすすめします。なお書き込みメソッドの場合はエラーが起こった場合にはまず戻り値としてBOOL型のNOが返されます。したがってerror引数はNULLで大丈夫です。
 このnilとNULLの違いは大事なことですが、いま理解することは少し難しいかもしれません。のちほどMyClipにファイル入出力を実装するところでこのerror引数にnilを指定することを利用したコードが出てきます。その時にご理解いただけたらと思います。

■ Xcodeのデフォルト文字コードの設定

 iPhon SDK 3.0もしくはXcode3.1.3以降をインストールされた場合にはunicode (UTF-8)がデフォルトの文字コードに設定されていると思います。しかし念のために確認しておきましょう。

「Xcode」メニューから「環境設定...」を選んで環境設定パネルを表示します。

 環境パネルが表示されましたら「テキスト編集」タブを選んでください。右下に「デフォルトのファイルエンコーディング:」という項目があると思います。もしUnicode (UTF-8)になっていないようでしたらポップアップメニューでUnicode (UTF-8)に変更してください。変更をした場合には「適用」もしくは「OK」ボタンを押してから環境設定パネルを閉じます。変更を行わなかった場合はどのボタンを押しても結果は同じです。

図 Xcode 環境設定 テキスト編集


【Xcode 3.1の場合】
 環境パネルが表示されましたら「テキスト編集」タブを選んでください。右下に「デフォルトのファイルエンコーディング:」という項目があると思います。もしUnicode (UTF-8)になっていないようでしたらポップアップメニューでUnicode (UTF-8)に変更してください。変更をした場合には「適用」もしくは「OK」ボタンを押してから環境設定パネルを閉じます。変更を行わなかった場合はどのボタンを押しても結果は同じです。

図 Xcode環境設定 テキスト編集 Xcode 3.1

 今後はCocoaアプリケーションを作成する場合には、特別な理由のないかぎりこのUTF-8をデフォルト文字コードとして使ってください。

テキストデータによる入出力メソッドのコーディング

■ コーディング

 では上記2つのメソッドをアプリケーションMyClipに実装します。前章までですでにMyClipのコードを色々と変更して試された読者もおられるでしょう。そこでここでMyClipの4つのコードファイルのすべてを掲載しなおします。読者が変更されたプロジェクトを残しておきたい場合はバックアップをとっておいてください。

 ではMyClipプロジェクトを立ち上げて4つのコードファイルを次のようにコーディングします。強調箇所が今回追加・変更されるコードです。読書がさらに変更を加えられていた場合もこのサンプルどおりのコードに一度戻してください。

■ MyClip コードリスト

Model.h
#import <Cocoa/Cocoa.h> @interface Model : NSObject { NSString *string; } - (void)setString:(NSString *)aString; - (NSString *)string; @end
※このファイルに 変更箇所はありません。

Model.m
#import "Model.h" @implementation Model - (void)setString:(NSString *)aString { [string release]; string = aString; } - (NSString *)string { return string; } @end
※このファイルに変更箇所はありません。

Controller.h
#import <Cocoa/Cocoa.h> #import "Model.h" @interface Controller : NSObject { Model *model; IBOutlet NSTextView *textView; IBOutlet NSButton *button; } - (void)readFromFile; - (void)writeToFile:(id)sender; @end
※ファイルの読み込みメソッドの宣言が追加されPush Buttonに接続されるメソッド名が変わりました。

Controller.m
#import "Controller.h" static NSString *filePath = @"/MyClip.txt"; @implementation Controller - (id)init { self = [super init]; if (self) { model = [[Model alloc] init]; [self readFromFile]; } return self; } - (void)awakeFromNib { [textView setString:[NSString string]]; [textView setDelegate:self]; [button setTarget:self]; [button setAction:@selector(writeToFile:)]; if ([model string]) [textView setString:[model string]]; } - (void)textDidChange:(NSNotification *)aNotification { [model setString:[[textView string] copy]]; [[textView window] setDocumentEdited:YES]; } - (void)readFromFile { [model setString:[[NSString stringWithContentsOfFile:filePath encoding:4 error:nil] copy]]; } - (void)writeToFile:(id)sender { [[model string] writeToFile:filePath atomically:YES encoding:4 error:NULL]; [[sender window] setDocumentEdited:NO]; } - (void)dealloc { [model release]; [super dealloc]; } @end
※コントローラオブジェクトの役割はモデルとビューを同期させることとモデルデータのファイル入出力に責任をもつことでした。したがって今回のコードの追加と変更のほとんどはこのController.mで行われることになります。

■ 実行

 コーディングが終わりましたら「ビルドして進行」をクリックして実行してみてください。今回はコンソールウィンドウを表示しておく必要はありません。Text Viewに文字列を入力してindicateボタンを押すとModelオブジェクトのデータ、つまりText Viewに表示されている内容と同じ内容のテキストデータがファイルへ書き出されます。ここで一度MyClipを終了してみてください。そしてもう一度起動します。さきほど保存したテキストデータがText Viewに表示されます。

 おめでとうございます。これでMyClipは実質的なアプリケーションになったと言えるでしょう。しかしまだまだ手を加えなければならない点は一杯あります。なお終了させたアプリケーションをもう一度起動する場合は「実行」メニュー→「実行」を選ぶかcommand + option + Rで起動できます。「ビルドして進行」ボタンを押して起動させてもらっても大丈夫です。Xcodeはファイルに変更があるかどうかを常に管理しています。ファイルに変更がない場合はビルド作業は行わずに進行(実行)されるだけです。

 データは起動ディスクの第一階層に作成された「MyClip.txt」とうファイルに書き込まれています。このファイルをダブルクリックしてみてください。OS Xに付属している「テキストエディット」でファイルが開かれ中を見ることができると思います。MyClipを終了させてからテキストエディットでこの文字列を書き変えて保存してみてください。そしてもう一度MyClipを起動します。今度はテキストエディットで書き変えた文字列が表示されます。

 次にMyClip.txtをデスクトップに移動してMyClipアプリケーションを再起動してみください。今度は読み込むデータが存在しないのでText Viewは白紙のまま表示されます。文字を入力してindicateボタンを押せば、また新しいMyClip.txtがハードディスクに作成されます。デスクトップに移動したMyClip.txtをハードディスクに戻してMyClipアプリケーションを再起動させて元のデータを表示させることもできます。

テキストデータによる入出力メソッドのコード説明

 この項では、前項でコーディングしたコードの説明をしていきます。


■ コード説明

 Model.hとModel.mについては変更・追加はありません。何かの都合で読者が変更されていた場合はサンプルどおりのコードに戻してください。独自で変更されたデータを残しておきたい場合はコーディングするまえにバックアップをとっておいてください。

Controller.h
ひとつだったメソッド宣言が2つに増えています。
	- (void)readFromFile;
	- (void)writeToFile:(id)sender;
 はじめのメソッドはファイルからデータを読み込むメソッドです。
2つめのメソッドはPush Buttonに割り当てていたindicateメソッドの名前をwriteToFileに変更しています。そしてController.mではこのメソッドをモデルのデータをコンソールに表示するのではなくファイルに保存するメソッドとして実装し直しています。

Controller.m
追加・変更箇所を上から順に説明していきます。
	static NSString *filePath = @"/MyClip.txt";

■ オブジェクト定数

 @"/MyClip.txt"で文字列オブジェクトを作成して変数filePathに代入しています。@"○○○"という記述方法については第3章のコラムで軽く説明いたしましたが作成された文字列はNSString型オブジェクト定数と呼ばれるものになります。オブジェクト定数は、一度作成するとアプリケーション終了まで存在し続け、relese メソッドでもガーベジコレクションでも解放することはできません。
 オブジェクト定数のリファレンスカウンタはつねにUINT_MAXという記号定数でdefineされたunsigned intの最大値4294967295になることになっていますが、実際には処理系によって違うみたいです。Xcode 3.1.3(iPhone SDK 3.0)ではsigned intの最大値2147483647になります。

 気になる方は試しにController.mのwriteToFileメソッドに

	NSLog(@"filePathカウント=%d", [filePath retainCount]);
 というコードを追加してコンソールを表示してから「ビルドして進行」をクリックしてみてください。MyClipウィンドウが現れましたらindicateボタンを押します。コンソールにタイムスタンプ、アプリケーション名などに続いて
	filePathカウント=2147483647

 と表示されることと思います。このオブジェクト定数には何度releaseメソッドを送ってもこの値が変わることはありません。なおretainCountメッセージはレシーバのオブジェクトのリファレンスカウンタの値を返すメソッドです。

 テストが終わりましたら、NSLogではじまるテスト用コードを削除してController.mを保存し直しておいてください。


■ static記憶クラス指定子

 C言語の復習になりますが、static記憶クラス指定子は使う場所によって意味が違ってきます。filePathはグローバル変数です。代入されている値が定数であっても違う値に入れ代えることはできます。なお不必要になったオブジェクト定数はそのままアプリケーション終了時までメモリのどこかに残ることはまえにも言ったとおりです。

 グローバル変数の有効範囲はその翻訳単位(コードファイル単位)内ですが、Aという翻訳単位のfilePathというグローバル変数を、Bという翻訳単位で使いたい場合はBファイルでextern記憶クラス指定子を先頭につけて、

	extern filePath;

 と定義すれば使えるようになります。逆に言えばextern指定子を使わないかぎりはほかの翻訳単位からは使えません。しかしこのように使えないに仕様になっているにも関わらず、同じ名前のグローバル変数をほかの翻訳単位でも定義していた場合にはコンパイル時に二重定義エラーを起してしまいます。大きなプログラムは複数のプログラマで作ることもあります。各人が自分の担当しているコードファイルでグローバル変数を定義していくとほかのプログラマが担当しているコードファイルのグローバル変数と同じ名前になる可能性が出てきます。これは困ったことです。

 そこでこの問題の解決策としてグローバル変数の先頭にstatic指定子をつけておくという方法があります。このsitatic指定子の付けられたグローバル変数はコンパイル時にリンカに渡されなくなります。つまりそのグローバル変数だけはほかのコードファイルの翻訳結果(オブジェクトファイル)とは結合されないということです。その結果同じ名前のグローバル変数が存在していても二重定義エラーは起こらなくなります。これで各人がほかのプログラマのコーディングを気にすることなく安心してグローバル変数を定義していくことができるようになります。

オブジェクトファイル
 ファイル単位ごとに翻訳(コンパイル)されたものを「オブジェクトファイル」と呼びますが、このオブジェクトファイルはオブジェクト指向プログラミングとは何の関係もありません。そしてこのオブジェクトファイルがリンカによってリンクされたものがプログラムの実行ファイルになります。

 このようにstatic指定子を使えばほかのコードファイルからはextern指定子を使ってもそのグローバル変数を使うことはできません。一般的にグローバル変数とstatic指定子はこのようにセットで使われるものだと思って頂いて結構です。もしほかのコードファイルのグローバル変数を使いたい場合は、そういう場合こそ事前に打ち合わせをしておけば良いだけということになります。

	- (id)init
	{
	    self = [super init];
	    if (self) {
			model = [[Model alloc] init];
			[self readFromFile];
	    }
	    return self;
	}
    

 自作メソッドのreadFromFileを呼び出しています。

	[self readFromFile];

 このようにする代わりにこの場所に、のちほど出てくるNSStringのファイル読み込みのメッセージ式を直接記述してもかまいません。しかしwriteToFileメソッドと対応するかたちでreadFromFileメソッドが存在するほうがファイル全体が見やすくなる(読みすくなる)と思ってこのようにいたしました。

 コーディングは短ければ必ず良いということはありません。分かりやすく(読みやすく)するためにかえって行数が増えることもあります。またコードを変に短くすることでかえってCPUへの負荷が増えたり速度低下が起こる場合があります。そのことについては実例が出てくるたびに説明したいと思います。

	- (void)awakeFromNib
	{
		[textView setString:[NSString string]];
		[textView setDelegate:self];
		[button setTarget:self];
		[button setAction:@selector(writeToFile:)];
		if ([model string])
			[textView setString:[model string]];
	}

 次のコードでText Viewにデフォルトで表示される英文を消去するために空文字を生成してその空文字をText Viewの値として設定しています。

	[textView setString:[NSString stirng]];

 5章では[textView setString:@""]にしていましたが、@""はオブジェクト定数ですのでいつまでもメモリを占有し続けます。NSStringのstringメソッドは空文字を返すコンビニエンスコンストラクタです。生成された空文字はText Viewの内容を白紙にしたあとに破棄されます。

 本章ではテキストデータのファイル入出力を行うというテーマにあわせて、できるだけNSStringクラスの説明するようにしたいと思っています。そこでこのstringメソッドも使ってみることにしました。ここで@""とstringメソッドのどちらを使ったほうが良いかは微妙です。@""はたしかにメモリを無駄に占有しますがawakeFromNibが実行されるのはたったの一度だけです。したがって@""は1個作られるだけにすぎません。逆にCPUの負荷という点から見るとstringメソッドは空文字を作成するときと破棄するときの2度CPUへ負荷がかりますが、@""はオブジェクド作成時の1度しかCPUへの負荷はかかりません。とは言ってもその負荷はどちらの場合も負荷とは呼べないほどの微細なものだろうとは思います。

 NSStrigのオブジェクト定数は表面的記述が@""であるだけで内部では通常どおりの手順を踏んでオブジェクトが作成されています。したがって作成時にはCPUへの負荷が当然かかります。

 しかしあえてどちらが良いかと言えば@""を使うほうが良いように思います。プログラミングではメモリやCPUの負荷などのことをリソース(資源)と呼びます。そしてリソースをどれほど必要とするかということをコストと呼びます。今回の場合は空文字というおそらく占有メモリが最小の1個のオブジェクト定数の無駄よりも微細とはいえ1回多いCPUへの負荷ほうがコストが高いのではないかという気がします。しかしここはNSStringクラスの説明ということでstringメソッドを使ってみました。また今回のケースをコストの例として説明しましたが、あまりにも微細なコストですのでどちらを使っても問題はない(差はない)と思います。

 なおグローバル変数filePathで@"/MyClip.txt"というオブジェクト定数を使っていますが、このグローバル変数はアプリケーション終了時まで変更されることはありません。そしてまたアプリケーション終了時まで残っていてもらわなければグローバル変数としての役目も果たせません。したがってこのオブジェクト定数がアプリケーション終了時までメモリを占有していることは無駄とはなっていません。

 なお文字列の使用メモリサイズを調べるには通常どおりsizeof演算子が使えます。NSStringクラスには文字列の使用メモリサイズを調べるメソッドはありません。注意点としてはsizeof演算に続く式(今回は文字列)に@マークは付けてはいけません。@マークをつけるとオブジェクトのポインタと解釈されて中の文字列をどのように変更しても値はいつも4バイトになります。次のNSLog関数をwriteToFileメソッドに追加して""の間に色々な文字列をいれてビルドし直して試してみるとUnicode UTF-8が各文字や文字列でどのようにメモリを使っているかが分かります。なお空文字の使用メモリサイズは1バイトです。 &&&&NSLog(@"文字列は=%dバイトです。", sizeof ""); 確認が終わりましたら上記のコードは削除してController.mを保存し直しておいてください。

	[button setAction:@selector(writeToFile:)];

 ボタンアクションを接続するメソッド名が変わりましたのでSEL型を取得する@selectorの引数もindicate:からwriteToFile:に変更しています。

	if ([model string])

 Modelオブジェクトのstringインスタン変数にはNSString型のオブジェクトが入っているかnilポインタが入っています。nilポインタの場合はBOOL値ではNO(偽)と評価されます。NSStringオブジェクトが入っていればYES(真)と評価されてつぎのメッセージ式が実行されることになります。

	[textView setString:[model string]];

このメッセージ式はメインウィンドウのText Viewに文字列を設定します。しかしNSTextView(正確にはNSText)のsetStringメソッドは引数としてnilポインタを受け付けません。もしnilポインタを渡してしまった場合は実行時エラーが起こりアプリケーションは暴走状態になります。それを防止するために前述のif文でModelオブジェクトのstringインスタン変数にNSStringオブジェクトが格納されているかどうかを確認していたわけです。

 なおこの実行時エラーはObjective-Cが動的バインディング(実行時結合)になっていることが原因だと言えます。しかし動的バインディングのおかでコーディングが楽になっていることも事実です。

 このメッセージ式についてもうひとつ注意して頂きたいことがあります。それはこのメッセージ式がawakeFromNibメソッドの中に記述されているということです。initメソッドの中や、あとから出てくるreadFromFileメソッドの中にこのメッセージ式は書けません。それはこのメッセージ式が実行される時点でレシーバのText Viewのインスタンス化(正確にはオブジェクトの復元と言ったほうが良いのですが、今の段階ではインスタンス化としておきます)が完了しているかどうかは分からないからです。しかしawakeFromNibはxibファイルに含まれてるすべてのオブジェクトのインスタンス化が完了されてから実行されるメソッドです。
 したがってこの時点ではText Viewオブジェクトが間違いなく存在していることになります。※Controllerオブジェクト自体がxibファイルに含まれていることも思い出してください。

	- (void)readFromFile
	{
		[model setString:[[NSString
			stringWithContentsOfFile:filePath
				encoding:4 error:nil] copy]];
	}

 保存されているファイルからデータを読み込むために新しく追加されたメソッドです。

	[model setString:[[NSString stringWithContentsOfFile:filePath
    		encoding;4 error:nil] copy]];

 Modelオブジェクトにstringインスタン変数の値を設定するめのsetString:メソッドをメッセージとして送っています。メソッドの引数としてはNSStringの「Creating and Initializing a String from a File」グループから選んだ次のメソッドを使っています。

	+ (id)stringWithContentsOfFile:(NSString *)path
    		encoding:(NSStringEncoding)enc error:(NSError **)error

 第1引数のパスにはグローバル変数のfilePathを、第2引数の文字コードにはUTF-8を表す4(※1)を、そして第3引数のエラコードにはnilを設定しています。  第一引数のパスについては説明不要だと思います。第2引数については次の項で説明いたします。ここでは第三引数についてだけ説明いたします。

 「エラー引数」で説明したように、この引数には、もし読み込みができなかった場合のエラーメッセージを受け取るための変数を指定します。しかしエラーメッセージが必要のない場合はNULLを引数として指定することによってエラーメッセージ機能自体を無効にしてしまうこともできますが、このメソッド自体がid型を返すことになっています。

 MyClip.txtはwritToFileメソッドを実行するときに初めて作成されます。したがって初めてMyClipを起動した場合やMyClip.txtを移動したり削除した場合は読み込むべきファイルが存在しないことになります。このようなときに読み込みメソッドのerror引数にNULLを指定していると返すべき値がないかid型ではない不定値を返そうとします。するとその時点で実行時エラーが起こることになります。

 このことを回避するためには読み込みメソッドを実行する前にNSFileManagerというオブジェクトを使ってMyClip.txtの存在を確かめるという手法もあります。多くの書籍やADCのドキュメントでもこの手法が説明されていると思います。私も自身のサイトでそのように説明しています。ところがリファレンスを良く読むとこの引数にnilを指定すると戻り値としてnilポインタを返すことが分かりました。

 nilポインタはid型として通用します。そして戻り値を受け取る側のNSStringもnilポインタを受け付けます。これでわざわざNSFileManagerを使ってMyClip.txtの存在を確かめることなくこの読み込みメソッドをいきなり実行することができるようになります。

 次に問題となるのがこの読み込みメソッドがコンビニエンスコンストラクタだということです。戻り値で返された値は一時オブジェクトになっています。ファイルから文字列データを取得してモデルオブジェクトのインスタン変数に代入してもその文字列はすぐに消えてしまいます。

 このような場合には、読み込みメソッドの戻り値に対してretainメッセージを送ってリファレンスカウンタを2にしておくのが一般的な手法です。しかし今回はcopyメソッドを使いました。Text Viewから文字列を取得するときにcopyメソッドを使っています。それに合わせてここでもcopyメソッドを使うことにいたしました。copyメソッドは元のオブジェクトを複製してリファレンスカウント1の新しいオブジェクトを作ります。

64bit対応の新しい整数値NSUInteger

 この項では読み込みメソッドの「encoding」というラベルの付いた第2引数についての説明をいたします。そしてそれに関連して64bitコンピュータに対応するためにObjective-Cに採用されたNSUIntegerとNSIntegerについても説明いたします。そしてそのあとにController.mで変更された最後の箇所であるwriteToFileメソッドについての説明を続けます。

 「メソッド説明」のときにも述べましたがencodingというラベルは「ファイルエンコーディング」「テキストエンコーディング」「文字エンコーディング」などと呼ばれている「文字コード」のことを表しています。encodingではなく、encodeの場合は「データをほかの形式に変換する」という意味になります。なおここではこの文字コードについての説明は省略させていただきます。

 この第2引数のNSStringEncodingという型は、NSUIntegerという整数値を表す型をtypedefしています。製品ドキュメントでNSStringEncodingを検索してみてください。最初にtypedefされていることが書かれています。次に「Discussion」の項でstring Encodingsというリンクをクリックすると詳細ページが開きます。NSStringEncodingが列挙型であること、UTF-8を表すNSUTF8StringEncodingが整数値の4であることが分かります。

図 NSStringEncoding


	[NSString stringWithContentsOfFile:filePath
		encoding:4 error:nil]

 というコードでは第2引数をNSUTF8StringEncodingと記述するほうが正式と言えます。しかし4と記述してもまったく問題はありません。列挙型ですのでどちらを使っても意味の違いは生じません。そのことよりもこの引数の型が実際はNSUIntegerであるということのほうが実際には大事であるという気がします。


■ NSUInteger

 64bitコンピュータに対応するためにObjective-Cに新しく導入された整数値を表す型です。32bitコンピュータでアプリケーションを動作させた場合はこの型はint型として働きます。そして64bitコンピュータでアプリケーションを動作させた場合はこの型はlong型として働きます。大変ありがたい型ですが、リファレンスによるとMac OS X 10.5以降で利用可となっています。さきほど「大事である」と言ったのはこのことだったのです。Mac OS X 10.4 Tigerでアプリケーションを動作させる予定がなければまったく問題はありません。

図 NSUInteger

 この新しい整数値型にはunsigned intの代わりをするNSUIntegerとsigned intの代わりをするNSIntegerが用意されています。現在Cocoaフレームワークの中で使われているint値の多くがこのNSUIntegerとNSIntegerに書き替えられています。なおこの新しい整数値型は純粋なCの標準関数ではこのまま使うことはできません。C標準関数で使うためには手を加えなければなりませんが、そのことについての説明は本書では省略させて頂きます。

 さて再びController.mのコード説明に戻ります。

 最後に残ったwriteToFile:メソッドはプッシュボタンのアクションに対応していたindicate:メソッドを変更したものです。

	- (void)writeToFile:(id)sender
	{
		[[model string] writeToFile:filePath
			atomically:YES
				encoding:4 error:NULL];
		[[sender window] setDocumentEdited:NO];
	}

 [model string]メッセージで取得したstringインスタン変数の値に対して書き込みメソッドをメッセージとして送っています。

	[[model string] writeToFile:filePath atomically:YES
    		encoding:4 error:NULL];

 このメソッドには4つの引数がありますが第1引数と第3引数の意味はすでに分かって頂けていると思います。第2引数についても「メソッド説明」で一度説明していますが念のためにもう一度説明をいたします。

 この引数にYESを指定するとレシーバのオブジェクトのデータがいったん予備ファイルに書き出されます。そして無事に予備ファイルに書き出されればfilePathで指定されたファイルは予備ファイルと置き換えられます。書き出しに失敗した場合は置き換えは行われませんのでfilePathにあるファイルは最後に正常に書き出せた状態のまま保たれていることになります。
 この引数にNOを指定した場合はfilePathで指定されたファイルへ直接データを書き込みます。もし書き込みが途中で失敗した場合はもとのファイルは壊れる可能性があります。

 第4引数についても「メソッド説明」で説明しましたが、もう一度説明いたします。 リファレンスによると「もしエラーの詳細に興味がなければNULLを渡すことができる(pass in NULL)」となっています。ただし「nil」を使えるとは書かれていませんのでここはリファレンスどおりにNULLを指定しています。またこの書き込みメソッドは書き込みに成功するとYESを、失敗するとNOを戻り値として返しますのでnilを指定しても意味はありません。

 以上でコードの説明も終わりましたが、最後にIndicateボタンの名前をSaveに変えておきましょう。

ボタンの名前をコードから変更する

 ここではプッシュボタンの名前(title)を「Indicate(表示)」から「Save(保存)」へとコーディングによって変更したいと思います。次のコードをController.mのawakeFromNibメソッドに追加してください。

	[button setTitle:@"Save"];

 コードを追加する場所はawakeFromNibの中ならどこでもかまいませんが、一応最後の行ということにしておきます。コーディングが終わりましたら「ビルドして進行」クリックしてください。

図MyClipアプリケーションウィンドウ

 これで@"Save"というオブジェクト定数がまた増えることになりましたが、まだまだ問題になる範囲ではありません。

 そしてこれはのちほどまた説明することになると思いますがObjective-CにおいてというよりもAppleとして(もしくはコンピュータ製造業全体として)リソース(コスト)としてメモリの占有率よりもCPUへの負荷のほうを大事に考えている面があります。このことは当然のことかもしれません。そして覚えておいたほうが良いことのように思います。

 最後にもう一度awakeFromNibについて説明いたします。
プッシュボタンもControllerもMainMenu.xibに含まれているオブジェクトです。xibファイルに含まれているオブジェクトはアプリケーションが起動する時、もしくはそのxibファイルが元の状態に復元される時に順次実体化されていきます。逆に言えばControllerが実体化していてもButtonオブジェクトはまだ実体化されていない可能性があります。

 このため、xibファイルに含まれているすべてのオブジェクトに、xibファイルに含まれているすべてのオブジェクトの実体化 (復元)が終わった時点でawakeFromNibメソッドが一斉に送られることになっています。

 逆に言えば、xibファイルに含まれているオブジェクト同士の最初のメッセージ送信はこのawakeFromNibの中で行うようになっていると言えます。またこのことからもControllerオブジェクトがxibファイルに含まれている必要があることが分かります。もしControllerオブジェクトがxibファイルに含まれていなければawakeFromNibメソッドを受け取ることができず、xibファイルに含まれているビューバーツなどへいつメッセージ送信を行えば良いか分からなくなるからです。また、ビューパーツの更新はControllerオブジェクトの大事な責務のひとつであることも思い出してください。



目次 < 前ページ 次ページ >


Copyright 2006 - 2010 viva Cocoa. All Rights Reserved.