Flutter講義:その2(Dart言語について②)

引き続きDart言語の言語構造のお話。

前回(その1)ではざっくりとした言語の基本構造の説明になったが、今回ももう少し基本部分を説明していく。初心者向け、という看板は出していないが、まだ初心者に向けて書いているような気分になっている。

制御構造(Control Statements)

プログラムにおける前提として、3種類の制御構造がある、とされている。詳しくは下記リンクを見ていただくとして...。

algo.jeita.or.jp

  • 順次処理(スタートからおわりまで順繰りに処理がされていく)
  • 分岐処理(処理内に「分岐」、条件によって処理が枝分かれする)
  • 反復処理(処理内に「反復」、繰り返しの処理が存在する)

という処理があり、これらを組み合わせることで複雑な処理を作ることができる。

前項で説明している「関数」や、次回以降に説明する「クラス」もこの制御構造の組み合わせで作られる、と理解してよい。

この項では制御構造のうち、分岐・反復処理で使うキーワードを紹介していく。

分岐処理

処理がある条件によって「分岐する」場合に使われるキーワード。

if文(if~、if~else、if~elseif~else)

基本的には2つの分岐(例:右か左か、上か下か、YesかNoか、etc)を表現するときに使われる。

一応3つ以上の複数分岐でも使うことができる。下に挙げた例が3つのゴールが存在するケース、つまり3つの分岐が存在するパターン。

例:

if (condition) {
  // codes…
} else if (anotherCondition) {
  // another codes…
} else {
  // some other codes…
}

switch文(switch~case)

基本的には3つ以上の分岐(例:右か左かまっすぐか、上下右左のいずれか、etc)を表現する。

例:

switch (variable) {
  case value1:
    // code 1...
    break;
  case value2:
    // code 2...
    break;
  case valueX:
   // code X...
  default:
    // default code...
    break;
}

if文とswitch文の使い分け

if~elseif~elseでもswitchでも3つ以上の分岐が表現できるのであれば、どちらを使ってもよいのでは?という質問は多い。

ローレベル(コンパイルされたプログラムのメモリ使用とかそんなレベル)で言うと、3分岐以上の(同等の)処理を書く場合にif文に比べてswitch文のほうが高速だ、ということらしく、それを理由に「3分岐以上はswitch、2分岐ならif」と言う人もいるらしいが、実際のところ、そこまで体感的には処理速度が向上したとか思うことはない。

むしろ、2つの要件からどちらを使うべきかを考えた方がいい、と私は考えている。その要件は、

  1. 最初から複数のルートが存在するならswitchを使い、目の前にある条件をクリアしないと次に進めないならifを使う
  2. フローチャートを書いたときに「条件」から2つしか矢印が出ない場合はif、3つ以上出る場合はswitchを使う

と言う要件だ。あまり考えたくない例だが、乗っていた電車が何らかのトラブルで止まってしまった場合のことを考えてみればいい。

運よく駅間で止まった電車が動き出し、ある駅に止まったとする。もちろんやることは一つだ。振替乗車を検討しなくてはならない。

ここで、複数の路線を選択でき、いずれも本来の目的地に到着できる場合であれば、今自分のいる駅からのルートは複数あるということになり、この場合はswitch文を使って条件分岐させるのが望ましいと言える。

逆に、選択できる路線がひとつしかない場合に、振替路線を使うことで目的地に到着はできるものの、かなり時間のロスが発生する場合(遠回り過ぎるとか、乗り換えが複雑すぎるとか)、もしかすると今乗っている電車の運転再開を待っていた方が目的地に早く到着できる可能性があるとか、こんな場合は「乗り換えるか待つか」という二択になる。

また、乗り換えが複雑になり、かつその途中にある別の路線でも遅延が発生していた、となると、「まずは今ここで待つか、それとも動いて様子を見るか」という、やっぱり二択になるし、動いた先でやっぱり止まっている、となればその時点で別の選択肢を選ぶことになる。この場合もif文のほうが向いている分岐である。

フローチャートでこの電車トラブルシミュレーションを書いてみるとわかりやすい...、と思うよ。

break/returnの違い

breakはswitch構文のみを「抜ける」。したがって、switch構文後に記載されたコード(上記例でいえばprint(’SOMETHING’)の部分)は実行される

returnはそのロジックブロック(≒メソッド)をreturn実行により終了させてしまう。switch構文より後に書かれたコードもすべて処理対象外となることに注意

反復処理

処理がある条件によって「繰り返される」場合に使われるキーワード。「ループ処理」とも呼ばれる。

for文(for~)

ある条件を満たすまで繰り返す場合に使う。

for (int i = 0; i < 10; i++)
  print i;
}
// 0から9までコンソールに表示する

Javaなどと同様に、forの後に続くカッコ内には、

  1. 初期化式(条件の初期値)
  2. 条件式(終了条件)
  3. 変化式(条件の値が変化する条件)

の順に記す。上記例で言えば、

  • 初期化式:i = 0 ... 変数i(整数型)は0からスタート
  • 条件式:i < 10 ... 変数iが10になったら終了(iの値が10未満なら継続)
  • 変化式:i++ ... 変数iの値は1ずつ増える(インクリメント演算子を使っている:後述)

となり、0~9までの(1ずつ加算された)値を表示して、つまり、for文の中カッコで囲まれている処理を10回繰り返す、ということになる。

初期値の変数を「ループカウンタ」という言い方をするケースもある(特に多言語において聞く)。

for~in文

リストなどに格納された全データを最初から最後まで取得する場合に使う。

List<String> fruits = [
    'apple',
    'orange',
    'lemon',
    'grape',
    'pineapple',
];
for (String fruit in fruits) {
    print('これは$fruitです。');  // 変数の値を文字列に結合させる方法は別項にて解説予定
}
// コンソール上には「これは〇〇です」がfruits変数に格納した値の順に5つ表示される

まず、リストfruits(複数形にしてある)に果物を5つ格納している。

次に、for (String fruit in fruits)としているが、日本語に直訳すると、「fruits(リスト)変数に含まれる値をそれぞれ(添え字順に)fruit(単数形)と言うString型変数に格納して」となる。

そしてfruit(単数形)変数に格納された値を使って中カッコ内の処理を行う、ということになる。

forEach文

for~in文同様に、リストなどに格納された全データを最初から最後まで取得する。挙動としてはfor~inと全く同じになる。

List<String> fruits = [
    'apple',
    'orange',
    'lemon',
    'grape',
    'pineapple',
];
fruits.forEach((fruit) {  // 無名関数になっていることに留意。無名関数については別項にて説明予定
    print('これは$fruitです。');
});
// コンソール上には「これは〇〇です」がfruits変数に格納した値の順に5つ表示される
// なお、この無名関数の方法はDartでは推奨されておらず、本来はこんな感じで書くのが望ましいらしい
// fruits.forEach(print);
// この場合コンソール上は果物名だけが表示される

for~inとforEachの使い分けについて

上記サンプルコードのコメントにも書いた通り、「無名関数」とは、という説明が必要だが、それを省いたうえで簡単に説明すると、forEachを使うケースは割と限られていて、繰り返し処理をする場合はfor~inのほうが(最初は)使い勝手がいいはず。

また、もう一つ説明が必要な点として、「本来の推奨される書き方(Effective Dart)」があるのだが、それについても説明を加えてからのほうが使い分けについては説明しやすいはず。現時点ではforEachは限定的に使い、リストとセットに関してはfor~inを使う(マップは実はどっちでもOK)と説明をしておく。

while文・do~while文

どちらも、「条件を満たすまで繰り返す」のは一緒。

for文との違い

for文の場合は一定回数繰り返す場合に使う。ループカウンタの概念や、リストの長さを制限値とするなど、繰り返し回数が「決まっている」ことが使用条件になる。

while文・do~while文は、繰り返し回数が決まっていない場合に使われる(ただし、繰り返しを終了させるための条件のみ指定する必要がある)。

while文とdo~while文の違い(使い分け)

while文は、終了条件を満たせばその場で終了する。表記を日本語に翻訳すると、「終了条件を満たすまで」が最初に書かれ、そのあとに処理が始まる、というスタイル(終了条件が前に書かれるので「前置条件」型と呼ばれることがある)。

条件を満たす「まで」と言っているが、もし繰り返しが始まる前に終了条件をすでに満たしている場合は、繰り返し処理が一度も行われないことになる。

do~while文も、終了条件を満たせば終了するが、終了条件は最後に書かれるため(後置条件型)、終了条件を現状満たしているかどうかにかかわらず、必ず繰り返し処理が1度実行されることになる。

少なくとも繰り返し処理が1度でも実施されてほしい状況の場合はdo~while文を、終了条件を優先するのであればwhile文を使う、という使い分けができる*1

演算子(Operators)

演算子については他の言語と同じようなものがあり、それ自体に特記するようなことはない。詳しくは下記言語リファレンスを参照のこと。

dart.dev

例:(型指定をしない変数を使う)

final name = ‘hoge’;

final nameTimes20 = name * 20;

print(nameTimes20);

// →コンソール表示:「hogehoge….(hogeを20回繰り返す)」
// 文字列を20回繰り返す、という挙動になる

ただし、カスタム演算子(Custom Operator)を作成することができる。実際にはある特定の演算子をオーバーライドする形で演算子の意味を変えてしまうようなイメージになる。

オーバーライドに関して別項で述べたあとに、実際のカスタム演算子についても述べることにするので、現時点では演算子もカスタマイズ可能である、という認識だけ持っていればよい。

今回の講義はここまで

リテラルについての説明をする予定だったが、準備不足なので次回まとめます。さらにNull(-safety)に関する話題について。公開はまた2~3週間くらい後を予定しております。

*1:他の言語でも同じ使い分けをすることになる