4月2007

第3回ミーティングに行ってきました

昨日、ActiveBasicのミーティングがあったので参加してきました。やはり今回も、現状のABで不足している言語仕様の話がメインになっていました。その中でも、今回話題の中心になっていたのは「型」です。

「型」の何か、簡単に言えば、コレクション回りで使うときのキャストや、ジェネリック等です。これは一般的な言語(C++,Java…)を想像してもらって、ABもそのような感じになると考えてよいでしょう。説明すると少し長くなりますので、今回は省略します。

ところで、このあたりの「型」について、Objective-Cではどうなっているのかと言えば、かなり大胆なことをしています。まずひとつとして、id型の存在があります。id型というのは、インスタンスへのポインタを表す型です。つまり、あらゆるオブジェクトは、id型として宣言することができます。

id s = [[NSString alloc] initWithString:@"string"];
id n = [[NSNumber alloc] initWithInt:10];

こうすると、当然静的型チェックは、全く機能しなくなります。そして、id型として宣言されたものでも、そのクラスのメソッドは実行することができます。

id number = [[NSNumber alloc] initWithDouble:10];
int value = [number intValue];

ここで疑問が生じる方もいるでしょう。numberはid型として宣言されているのにも関わらず、どうしてNSNumberクラスのメソッドが実行できるのか。

なぜなら、Objective-Cは、常に動的な情報を元にメソッドの探索を行っているからです。簡単に説明すれば、numberのインスタンスには、自分がNSNumberクラスという情報を持っているので、その情報を元にNSNumberクラスにintValueメソッドを問い合わせていることになります。

ということは、当然ながらコンパイル前の状態では、id型として宣言されたnumberは、何のクラスなのかはわかりません。何のクラスかわからないということは、何のメソッドがあるのかもわかりません。それにも関わらず、numberに対してNSNumberのメソッドを実行するコードを書けるのはどうでしてでしょうか?

答えは簡単です。単に、id型は全てのクラスの全てのメソッドの実行するコードを書くことを許容しているからです。つまり、id型に対してどんなクラスのメソッドを書いたとしても、警告が出ることはありません。

id number = [[NSNumber alloc] initWithInt:10];
[number rangeOfString:@"string"];

rangeOfStringはNSStringクラスのメソッドなので、NSNumberにはありませんが、id型には全てのメソッドを書くことが許容されているので、コンパイルの時点で警告が出ることはありません。

そして、これを実行すると、実行時にデバッガが捕まえてくれます。ただし、実行には影響がないようにできているのが、Objective-Cのいいところです。このメソッドは無視されるだけです。

デバッガ出力:
2007-04-30 21:11:05.263 Test[7004] *** -[NSCFNumber rangeOfString:]: selector not recognized [self = 0x35d5d0]
2007-04-30 21:11:05.329 Test[7004] *** -[NSCFNumber rangeOfString:]: selector not recognized [self = 0x35d5d0]

Testというのはプロジェクト名で、NSCFNumberというのは、NSNumberのことです。詳しい説明は省きます。

ちなみに、Xcode(エディタ)のid型に対するコード補完機能はどうなっているのかと言えば、もちろんあります。大胆にも、メソッドのコード補完には、全てのメソッドが列挙されます。
コード補完
見た目のスクロールバー以上に、膨大なメソッドが列挙されます。

実を言うと、そもそもObjective-Cは、たとえどこにも定義されていないメソッドがコード中に書かれていたとしても、警告されるだけでコンパイルは通ってしまいます。

id number = [[NSNumber alloc] initWithInt:10];
[number aaaaaaa]; //警告が出るだけで、コンパイルは通る

つまり、こんなこともできます。

NSNumber *string [[NSString alloc] initWithString:@"string"];
NSLog([string subStringFromIndex:2]);

stringはNSNumberクラスとして定義していますが、インスタンスはNSStringです。もちろん、メソッドも正常に動作します。ただし、コンパイル時に警告は出るし、あたりまえですが普通はこんな使い方はしません。

このid型だけからもわかるように、Objective-Cにはほとんど静的型チェックがないことが特徴です。これには、デメリット、メリットが多々あると思います。例えばメリットといえば、型変換やジェネリックなんて面倒なことを考えなくてよいことです。デメリットは、当然型チェックができないことです。他にもいろいろあるでしょう。ただ、個人的には、Objective-Cの型にルーズな仕様の方が好きです。

次は、Objective-Cのポリモーフィズム辺りについて書こうと思いますが、開発者ミーティングについての話があまりにも少ないので、もしかしたらそちらを書くかもしれません。

Objective-Cについては、Appleのサイトで非常に詳細まで書かれた日本語訳を読むことが出来ます。量が多いので、私もまだぜんぜん読んでいません。ですが、非常に興味深い内容まで書かれています。
Objective-C プログラミング言語

最近使った項目

Mac OS Xの多くのアプリケーションには、「最近使った項目」というメニューがあります。名前からも分かる通り、メニューに最近使ったファイル名が表示され、それを選択することで、そのファイルを開けるものです。

最近使った項目

これが何かというと、実は、この「最近使った項目」は、項目のファイルパスが変更されたとしても、そのファイルを開くことができるのです。

例えば、subject.txtをアプリケーションで編集し、User/username/Desktopに保存したとします。この時subject.txtは当然、最近使った項目に表示されるようになります。そして、このsubject.txtを別のフォルダに移動してみます。例えば、DesktopからUser/username/Documentというようにです。

こうするとファイルパスが変更されてしまったので開けないと勝手に想像してしまいますが、実はそうではありません。なんと「最近使った項目」は、パスが変更されても同じファイルを開いてくれるのです。

さらに、例えば、”subject.txt”を”document”という全く異なるファイル名に変更したとします。しかし、これも最近の項目として開くことが出来ます。ちなみに、メニューのところには、”subject.txt”ではなく、変更後の”document”と表示されるようになります。

読む気もしないのですが、この辺にそれを実現するための方法が書かれています。

Introduction to Launch Services Concepts and Tasks

どうやら、CocoaではなくCarbonAPIのようなので、少し残念です。

Grapher

OS Xには、非常に高性能なグラフソフトが標準で付いてきます。適当に打ち込んだ方程式もグラフになったりして面白いです。

こんな感じで、さまざまなグラフを描くことができます。
グラフ

媒介変数を使ったグラフも描けます。
うずまき

これはGrapher付属のサンプルのうちの一つですが、このような3次元のグラフも描くことが可能です。
3次元

さらにGrapherは、グラフを描くだけでなく、解を求めたり微分する機能もついているので、計算機にも使えそうです。

そして、一番面白そうな機能がこれです。なんと、パラメータをアニメートすることができるのです。そして、それを動画として出力することができるのです。ちなみに、下の動画は、上から2番目の画像の媒介変数Tをアニメートしたものです。

uzumaki.mov (QuickTime 32KB 320×240)

さすがに出力された動画そのままでアップロードするわけにもいかないので、iLifeのiMovieHDで早送りと変換の編集を施しました。そういえば、iMovieHDもなかなか使えそうな動画編集ソフトなので、こちらも何かあったら使いたいですね。

アニメートしたグラフを作るときの注意としては、パラメータ(動かしたい変数)を方程式のところで定数定義するところです。「T=0」などと定義することでパラメータと認識してくれます。この定義でないと、パラメータとして認識されないようなので、注意しましょう。

というわけで、Grapherについてググってもあまり使っている人はいない様子なので、あまりよく知らなかった人はぜひ活用してみましょう。

基本型のオブジェクト

昨日の投稿で、基本型はArrayに入れることはできないので、基本型もオブジェクトにする必要があると言いました。しかし、今のActiveBasicの仕様では、基本型をオブジェクトにしてしまうと、基本型と同じように振る舞うことができなくなってしまいます。ですから、基本型をオブジェクトに置き換えることは出来ません。

仕様が変われば話は終わりですが、ひとつ対処法が浮かんでいます。それは、Cocoaでも使われているNSNumberの、数字をラップするクラスを作るように、基本型を全てラップできる、ひとつのクラスを用意する方法です。こうすると、基本型を普通に扱い、Arrayに入れるときだけ、このインスタンスを生成し、入れることになります。ちょっと微妙なのですが、今の仕様ではこれくらいが限度でしょう。

基本型の配列

ライブラリの方ではArrayListが最低限実装されている訳ですが、これはオブジェクトのインスタンスを入れるものなので、基本型の変数を入れられるはずもありません。

この前、TextReaderでも実装してみようと思っていたのですが、どうやら基本型の配列が必要のようです。どういうことかといえば、TextReder.Readメソッドなどがその例です。今の書き方をするのならば、こういう引数になります。

Function Read(buffer As *Char, index As Long, count As Long) As Long

他の書き方もあった気がしますが、どちらにしろこんな感じでしょう。ただ、このやり方はライブラリの目指している方向とはだいぶ異なると思っています。わかっている方も多いでしょうが、どう見ても危ないからです。

これを改善するには、やはり配列や基本型の扱いについて考えなければならないでしょう。その前に、要はどんな記述になればいいかといえば、.NETみたいな感じで、配列で渡せればいいのです。

Function Read(buffer[] As Char, index As Long, count As Long) As Long

結局[]はArrayクラスのことでしょうが、Arrayクラスはオブジェクトを入れることが前提です。つまり、Char型などの基本型を入れることができません。そうなると、基本型をラップしたようなオブジェクトを作成しなければなりません。

.NETでは、すべての型がラップされています。さて、ActiveBasicもそうしたほうがいいのでしょうか?少なくとも言えることは、今の最新のβ版でもこれをすることは不可能です。なぜなら、オブジェクトは無条件で参照型になってしまうからです。例えば、Char型と同じ意味を持つ、Charクラスを作って、それを基本型として使おうとしても、同じ動作を期待することは出来ません。

Dim c1 As Char
Dim c2 As Char
c2 = c1

Charをクラスにしてしまえば、c2に同じ値を入れる訳ではなく、c2の参照をc1にする意味になってしまいます。

オブジェクトがALL参照になったのはうれしいですが、やはり値渡しできるオブジェクトも必要かもしれません。現在ライブラリは.NET Frameworkを元に進められているわけですが、基本的にはまるまるコピーです。こういうABではできない時には、少し改良して移植してしまいたいところでもありますが、少し改良してしまっても動くような.NET Frameworkではありません。思いがけないところで、クラス同士が連携していて、そう簡単に改良する訳にもいかないのです。

さて、まだうまく整理がつかないので、もう少し考える必要がありそうです。次のミーティングも来週ですし、今のうちにいろいろ用意しておきたいですね。

よーく見てみれば

System.IOのTextReader/WriteはStream関係なしに実装できそうですね。もう少し調査の必要はありそうですが、DirectoryInfoはまだ実装できないので、こちらから手を回すのも一つの手かもしれませんね。

Synchronizedはちょっと中で何をしてスレッドセーフにしているのかは全く想像付かないのですが、とりあえず明日あたりにそれ以外のところをやってみようと思います。

オブジェクトの保持 -2-

Cocoaのためのコーディングガイドライン フレームワーク開発者向けの助言と技術 (ポッチンルーム)

なにげにさらっと役立つことがたくさん書かれているページ(ADCの翻訳)ですが、これを見ていて「そういえばそうだなー」と思ったことがありました。それは「オブジェクトをオートリリースする」の項です。

これは以前のオブジェクトの保持にも関係することですが、例えば、オブジェクトを作成して返す、NSStringのsubstringFromIndexがあります。

- (NSString *)substringFromIndex:(unsigned)anIndex

これは、レシーバの文字列のanIndexから最後まで抜き出した、新しいNSStringを返すメソッドです。さて、これの戻り値であるNSStringは、誰が解放責任を持っているでしょうか?解放責任を持つのは、オブジェクトを生成(alloc,copy)したオブジェクトにあるので、このメソッドを実行したオブジェクトにはありません。すると、この戻り値はメソッド内で生成されたわけですから、これの解放責任は、このメソッドのレシーバであるNSStringのインスタンスにあるわけです。よって、このメソッドを実行する場合には、特にオブジェクトの解放に気を使う必要はありません。

逆に言えば、自分でこういうメソッドを作成する場合には、解放するようにしなければならないのです。解放するには、autoreleaseという大変便利なものがあるので、それを実行すればよいだけです。イメージとしては下のような感じです。

- (NSObject *)method
{
    NSObject *obj = [[NSObject alloc] init]
    //何らかの処理
    return [obj autorelease];
}

自分ではまだこういう解放し忘れしたことはなかった思いますが、かなりの注意が必要ですね。Cocoaの基本ですね。

さて、ガベージコレクションが搭載されるObjective-C2.0そしてCocoaが入った、 OS X 10.5 Leopardは10月に延期されてしまったので、メモリ管理ともあともう少し付き合わなければなりませんね。

理想的なコードに近づきつつあります -2-

さて、以前似たようなタイトルの記事を書きましたが、今回もさらに簡潔なコードが書けるようになってきたので紹介します。まずは、コードを見てもらいましょう。これを実行すると、ディレクトリ内の今日更新したファイルやフォルダが列挙されます。

Dim dir = New DirectoryInfo("D:¥SVN¥bin")
Dim dirs = dir.GetFileSystemInfos() 'As ArrayList
Dim s As String
Dim i As Long
For i = 0 To ELM(dirs.Count)
	Dim fileInfo = dirs[i] As FileSystemInfo
	If fileInfo.LastWriteTime > DateTime.ToDay Then
		s = s + fileInfo.LastWriteTime.GetDateTimeFormats() + " | " + fileInfo.FullName + Ex"¥r¥n"
	End If
Next
MessageBox(0, s, "今日更新したファイル一覧", MB_OK)

実行結果:
result

以前と変わった点は、ArrayListからオブジェクトを取り出すときに、ポインタでなくなった点です。これは、つい最近搭載された動的型情報が可能にしたと思われます。しかし、依然としてIEnumeratorを使うことはまだできなかったのですが、インターフェースをインスタンス変数や戻り値に使用できるようになったので、もう一歩と言ったところでしょうか。

あとは、この前正常に動作しなくてやらなかった、DateTimeクラスによるファイルの更新時間を表示しています。表示するだけではつまらなかったので、今日更新されたものだけを表示するようにしてみました。DateTime.ToDayで今日の日付と、fileInfo.LastWriteTimeでfileinfoの最後の更新日時が取得できるので、それを比較します。
If fileInfo.LastWriteTime > DateTime.ToDay Then

そしてもう一つ、今日か昨日あたりに追加された機能ですが、戻り値から直接メソッドを実行できるというか、参照できる機能です。ちょっと説明しにくいので見てもらえばわかりますが、以下の部分です。

fileInfo.LastWriteTime.GetDateTimeFormats()

これは、FileSystemInfoクラスのLastWriteTimeプロパティを使い、最終更新日の日付がDateTimeで取得されます。そして、そこから続けてDateTimeクラスのGetDateTimeFormatsメソッドが実行できるのです。以前のActiveBasicの記述ならばこうなります。

Dim date = fileInfo.LastWriteTime As DateTime
date.GetDateTimeFormtas()

最近は、Unicode化,オブジェクトのALL参照化など大きな変化がありましたが、ようやくここで落ち着いてきたような気がします。どちらも私はほとんど関与しなかったので、Unicodeの作業をほとんどやってくださったイグトランスさん、並びに言語開発の山本さんともに感謝ですね。ガベージコレクションも、最新の更新でとりあえず実用レベルにまではなったと思うで、いい感じですね。

というわけで、何かブログばかり更新していた私ですが、とりあえずIOの方をさっさと完了したいところです。しかし、まだEnumのビット演算のオーバーロードにバグがあるので、それが修正されてからになるでしょう。やはり技術が足りないと、活躍の範囲が狭まるので、今のうちにこの前から目をつけている、ガベージコレクションについての知識でも付けておきたいですね。それ以外にも、言語以外のWindowsについてとかですね。

まあ、パソコンばっかりやってるわけにもいかないんですけどね。

これはいいガイドラインですね

はてなの人気記事に上がってたやつですが、これはかなりいいですね。ぜひ、全てに目を通したいものです。

アップル ヒューマンインタフェースガイドライン

同サイトには、他にも日本語訳がされており、Cocoaのためのコーディングガイドラインといのものあり、Cocoaをやっている私にはどちらも重要な文章です。

lineRangeForRange:

NSStringのメソッドにlineRangeForRange:という、行の範囲を求めるメソッドが用意されています。

- (NSRange)lineRangeForRange:(NSRange)aRange

ちなみに、NSRangeは文字列の範囲を表す構造体です。locationが開始位置でlengthが長さです。

typedef struct _NSRange {
   unsigned int location;
   unsigned int length;
} NSRange;

さて、このlineRangeForRangeですが、行の範囲を求めるものなので、戻り値のNSRangeに行の開始位置と長さが入ります。しかし、セレクタを見てみると、こちらもNSRangeになっています。さて、一体何を指定するのでしょう?

ヘルプには、こう書かれています。

Returns the smallest range of characters representing the lines containing a given range.

訳:指定した範囲を含む行の範囲を返します。

ちょっと訳に自信がなかったので、他のサイトも見ましたが、だいたいこんな感じの意味です。つまり、範囲で指定したところが含まれている行を取得できる訳です。

これで思うのが、なぜセレクタが範囲なのかということです。何も、範囲を指定するのではなく、NSRangeで言えばlocationに当たる部分、先頭から数えた文字数を引数に取り、それが含まれる行を返すメソッドのが自然なのではないかということです。こんなイメージです。ただ、これだとメソッド名と内容が一致しないので、こういうメソッドならば名前を改める必要があるでしょう。

- (NSRange)lineRangeForRange:(int)location

どうしてNSRangeを指定するのか不思議に思いつつ使っていたのですが、例えば、指定した範囲が複数行に渡っている場合、その複数行が含まれる行全てが返ってくるようになっています。

NSString *s = [NSString stringWithString:@"012¥n45¥n78¥n9abc"];
NSRange range = [s lineRangeForRange:NSMakeRange(5, 3)];
NSLog([NSString stringWithFormat:@"%d, %d", range.location, range.length]);

この場合、結果は(4,6)になります。文字列で言えば「45¥n78¥n」の6文字です。セレクタで指定した(5,3)は、文字列の「56¥n7」の範囲を示しているので、それが含まれた行が返るわけです。

この範囲を指定するような使い方をすることはまだしていませんが、1行ずつ読み取っていく場合の、こういう使い方ならします。

NSString *string
int length = [string length];
NSRange range = NSMakeRange(0, length);
while (range.location < length)
 {
	range = [string lineRangeForRange:NSMakeRange(range.location, 0)];
	NSLog([stirng substringWithRange:range]);
	range.location = NSMaxRange(range); 'location+length
}

結局length0で指定しているので、範囲ではなく先頭からの文字数で指定してる感じで使っています。まあ、今はlength0にして使っていますが、いずれプログラミングをしているうちに、範囲指定であったほうが便利なことがあるかもしれませんね。