修正日: 07/06/20
練習問題5
解答と補足編
計算方法はおなじみWikiPediaを参考にしました。BMIの計算式は
BMI = 体重(Kg)/身長(m)/身長(m)
です。したがって体重を計算するには
体重(Kg) = BMI*身長(m)*身長(m)
簡単ですね。
次にウィンドウのデザインはこんな感じ。
今回はメタルウィンドウにしてみました。Interface BuilderのInspectorウィンドウのNSWIndowの設定で「Has texture」をチェックするとメタルになります。実はどういう場合にメタルウィンドウを使うか、Apple Human Interface Guidelineできちんと決めてあって、ここでメタルを使って良いかというとたぶんダメなんじゃないかと思いますが(ちょっと微妙)、まあこういうことも簡単に出来ますよ、というサンプルです。
次、ソースコード。簡単ですね。身長はcmで入力しているので100で割ってm単位に変換するのを忘れないでください。
- (IBAction)calcBMI:(id)sender
{
//A
[bmiField setFloatValue:
calculateBMI([heightField floatValue], [weightField floatValue])];
}
- (IBAction)calcWeight:(id)sender
{
[weightField setFloatValue:
calculateWeight([heightField floatValue], [bmiField floatValue])];
}
float calculateBMI(float pHeight, float pWeight)
{
return pWeight/(pHeight/100)/(pHeight/100);
}
float calculateWeight(float pHeight, float pBMI)
{
return pBMI*(pHeight/100)*(pHeight/100);
}
「行の終わりには;」というのにはそろそろ慣れたのではないかと思いますが、行末に必ず;があるというルールがあるので、Aのように文の途中で改行を入れることもできます。例えばこんなのもOK。
[bmiField
setFloatValue:
calculateBMI
(
[heightField
floatValue],
[weightField
floatValue]
)
];
ただし変数名や関数名の途中に改行を入れるのはダメです。こんな極端なのは読みにくいだけですが、適宜改行を入れることでソースが読みやすくなりますのでいろいろ工夫してみてください。
これで今回の目標はクリアしたのですが、ここで一つ大切なことを学びましょう。「関数とメソッドの違い」です。
上記の解答例ではcalculateBMI()関数に体重と身長を渡して計算させています。でも引き数を渡すのは面倒なのでさぼって、関数の中で体重と身長をテキストフィールドから直接取り出すようにしてみましょう。例えばこんな感じです。このコードをコピペして、実際に試してみてください。
- (IBAction)calcBMI:(id)sender
{
[bmiField setFloatValue: calculateBMI()];
}
float calculateBMI(void)
{
return [weightField floatValue]
/([heightField floatValue]/100)
/([heightField floatValue]/100);
}
・・・いかがでしょうか?コンパイルでエラーが出たはずです。
error: 'weightField' undeclared (first use in this function)
error: 'heightField' undeclared (first use in this function)
(weightFieldが宣言されてないよ!)
なぜでしょうか?weightFieldとheightFieldはちゃんと定義してあるはずです。calcBMI:メソッドで実行したときはうまく動いたのに?
実は関数とメソッドは似て非なるものなのです。「Become an Xcoder」の中でクラスは変数とメソッドの集合、というような説明があったはずですが(正確な表現は覚えていませんが)、メソッドはクラスの一部なのに対して、関数はクラスとは関係のないプログラムなのです。図で書くとこんな感じです。
クラスの一部であるメソッドはクラスが持っている変数やメソッドに自由にアクセスできますが、部外者の関数は変数を勝手に使ったりメソッドを呼び出したりすることはできないのです。
では上記のような機能を実装するにはどうすれば良いのでしょうか?以下の三つの方法が考えられます。
- 関数の中では計算だけ行って、textFieldへのセットはアクションの中で行う
- 関数への引き数としてtextFieldのポインタも渡す
- 関数ではなくメソッドにする
1は最初のサンプルで採用している手法ですね。一番常識的で、汎用性の高い方法です。クラスの仕様に依存する部分がないので、別のソフトで必要になってもすんなりと流用できます。
2はよほどの理由がない限りは推奨できません。実は関数はメソッドに比べて呼び出し処理がほんの少し速いので、少しでもスピードを稼ぎたい場合はこのように関数を使う可能性もありますが、1,000万回、2,000万回繰り返してようやく1秒の違いが出るかどうかというレベルなので(正確な数値は忘れました)、普通は気にする必要はありません。そんなことを気にするよりは、わかりやすいコードを書く方がずっと生産的です。
で、問題は3です。いままで関数の書き方しか勉強していなかったので、ここではメソッドを自分で作る、ということを勉強しましょう。メソッドを作るには以下のように書きます。
- (float)calculateBMI
{
}
簡単ですね。上記の関数をメソッドに書き直してみるとこうなります。
- (float)calculateBMI
{
return [weightField floatValue]
/([heightField floatValue]/100)
/([heightField floatValue]/100);
}
このメソッドはクラスの一部なので、クラスの中身である「weightField」や「heightField」を自由に利用することができます。さらに、このメソッドを利用する場合はこのようにします。
- (IBAction)calcBMI:(id)sender
{
float aBMIValue = [self calculateBMI];
[bmiField setFloatValue: aBMIValue];
}
- (float)calculateBMI
{
return [weightField floatValue]
/([heightField floatValue]/100)
/([heightField floatValue]/100);
}
calculateBMIの前の「self」ってなんでしょうか?Cocoaであるオブジェクトにメッセージを送る場合の書式を思い出してください。例えばこうです。
[bmiField setFloatValue: aBMIValue];
「bmiField」というオブジェクトに対して「setFloatValue」というメッセージを送っています。だから
[self calculateBMI];
はオブジェクト「self」さんに「calculateBMI」というメソッドを送っています。では「self」というオブジェクトは何者でしょうか?もうわかると思いますが、オブジェクト自分自身です。自分自身が持っている「calculateBMI」というメソッドを実行したいので、「calculateBMIを実行しろ>俺」と命令しているわけです。
この「calculateBMI」メソッドは引き数を何もとらなくてちょっと寂しいので引き数をとる場合の書き方も紹介します。身長と体重からBMIを計算するメソッドであればこんな感じです。
- (float)calcBMIFromHeight:(float)pHeight andWeight:(float)pWeight
{
return pWeight/(pHeight/100)/(pHeight/100);
}
- (IBAction)calcBMI:(id)sender
{
[self calcBMIFromHeight: [heightField floatValue]
andWeight: [weightField floatValue]];
}
いかがでしょうか?最初のcalculateBMI()関数は引き数の順番を覚えていないといけませんでしたが、このようなメソッドの書き方をすると引き数に何を渡せば良いかわかりやすいですね。引き数を増やしたい場合は
- (float)method:(float)param1 for:(float)param2
and:(float)param2 onemore:...
このようにどんどん増やしていくことができます。わかりやすいメソッド名を付けようとすると、名前がどんどん長くなって一見面倒くさいですが、あとあと必ず役に立ちますので、面倒がらずに意味を理解しやすいメソッド名を付けるように心がけましょう。
最後にソースコードのまとめ。
- (IBAction)calcBMI:(id)sender
{
float aBMIValue;
//関数で計算
aBMIValue = calculateBMI( [heightField floatValue],
[weightField floatValue] );
//引き数を渡さないでメソッドで計算
aBMIValue = [self calculateBMI];
//引き数を渡してメソッドで計算
aBMIValue = [self calcBMIFromHeight: [heightField floatValue]
andWeight: [weightField floatValue]];
//値をテキストフィールドに表示
[bmiField setFloatValue: aBMIValue];
}
//関数で計算する
float calculateBMI(float pHeight, float pWeight)
{
return pWeight/(pHeight/100)/(pHeight/100);
}
//引き数を渡さないでメソッドで計算する
- (float)calculateBMI
{
return [weightField floatValue]
/([heightField floatValue]/100)
/([heightField floatValue]/100);
}
//引き数を渡してメソッドで計算する
- (float)calcBMIFromHeight:(float)pHeight
andWeight:(float)pWeight
{
return pWeight/(pHeight/100)/(pHeight/100);
}
以上、長くなりましたがいかがでしょうか?関数とメソッドの違い、メソッドを自分で定義する方法についてわかりましたか?サンプルをこちらにおいておきますので中身をチェックしてみてください。
次回は「ウィンドウを閉じるとアプリも終了する」機能を追加してみます。