Dartツアー(1)Dart基本形から組み込み型まで

技術,プログラミング言語

Flutterを始めてみましたが、コードはDart言語で書きます。という訳で、Dart言語のツアーを見て勉強しながら、自分用にメモをまとめてみました。元のツアーが目次もない長大なセクションになっているので、こちらも長くなってしまっています。目次は付けますけど。

オリジナルがかなり長いので、今回は冒頭から組み込み型までを対象としています。全体からすると1/4程度でしょうか。

なお、コード部分はDart Padを埋め込んでいるので、コードを実行することが可能です。また、自由に書き換え得て動作を確認することができます。書き換えても元の内容には影響しませんので、好きにいじってもらって構いません。

なお、日本語訳はたとえば以下が見つかりました。ただ、少し古いバージョンのもののようで、今現在公開されているオリジナルで追加されたものはありません。

A basic Dart program

Dartプログラムの基本形は以下になります。

行末にセミコロンが必要とか、関数宣言の構文はC言語に近い感じです。ただ、#include文とかはありません。ライブラリを読み込むimport文はありますが、基本となるライブラリは明示的に指定しなくて良いようです。

また、変数宣言時に初期化もする場合、型推論があるので初期化も型を明記する必要はありません。文字列中に’$変数名’で変数展開ができることがわかります。

重要な概念

全てはオブジェクト

  • 変数に入るものは全てオブジェクト
  • オブジェクトは全てクラスのインスタンス
    • 数値も関数もNULLもオブジェクト

数値もオブジェクトということで、数値型の変数もメソッドが呼び出せます。もちろん、即値でも同じです。Javaとは違うところです。

型推論を伴う強い型付き言語

Dartは強い型付き言語ですが、型推論があるので型アノテーションは必要に応じて書きます。

書きたい場合は、varの代わりに型名を書けば良いようです。

null安全(null safety)

Dart 2.12から導入されたnull安全(null safety)を有効にすると、変数にnullが入らないことが保証されます。型名の末尾に"?"をつけると変数にnullが許容されます。

つまり、int型は数値であってnullは入りませんが、int?型はnullも許容されます。nullがないことをプログラマが保証する場合には、変数名の末尾に"!"を付けます。もしnullだった場合は例外がスローされます。

なお、コンパイル時にnullでないことが明らかな場合は"!"が無くてもエラーにはならず、逆に無くても良いと教えてくれます。?付き型の変数から?無し型の変数に代入する際に必ずつけるものではないことに注意が必要です。まあ、付けても問題はないですけど。

なんでもOKのObject?型とdynamic型

どのような型でも許されることを明示したい場合はObject?型を使うようにとされています。実行時まで型チェックを延期したい場合はdynamic型を使うようにとされています。

まあ、正直分からない部分も多いです。全ての型はObject型がベースなので当然どんな型もObject型で受け取れるし、nullも含めた場合は?が必要なのは分かります。実際に、あまりこんなコードを書くこともない気はしますが、こんなことが可能です。

あとはdynamic型ですね。単純に上のソースのObject型の部分をdynamicに書き換えれば、同じように動きます。dynamic型の詳細は別ページに書かれています。

もちろん、dynamic型とObject型が理論的に違うものであることはわかるのですが、具体的な使い分けとかがいまいちよく分かりません。それぞれ別の概念のものを有用だからと持ってきたけど、今ひとつ融合できていない印象です。

あまり考えすぎても良くないので、便利に使えば良いのでしょう。ただ、静的型付け言語の考え方が染み込んでいるので、あまり有意義な使い方を思いつかなかったりします。実際、調べると弊害もあるのでなるべく使わないようにガイドラインにも書いてある、とあります。考え方の基準として、どの型でも受け付ける場合はObject型、そうではなくて一つだけではないいくつか型だけ受け付ける時はdynamicとあります。

どうしてもdynamicを使わないと処理できない時に、そのリスクを理解した上で使いなさい、と。型推論に十分な情報がない場合にdynamicが使われるとあるので、ある意味最後の手段かもしれません。

以下は適当に書いてみたサンプルです。add関数は2つのパラメータがどちらもintならintで和を返し、doubleならdoubleで和を返します。なお、片方がint、もう片方がdoubleの場合、自動的にintがdoubleとして解釈されるので、doubleを返します。それ以外の型だった場合は例外になります。もっとも、2行目をコメントアウトしても同じように動きます。処理系がうまく処理してくれるようです。

GENERIC

今時の言語の多くにあるGENERICです。ロートルには慣れない概念です。典型的なのが"List<int>"みたいな奴です。中身に整数を持つリストとなります。なんでもありのリストなら"List<Object>"になります。

とりあえずサンプル。文字列で空白区切りの数字列を与えて、それをintを入れるリストに入れてからループで表示しています。エラー処理とかサボってます。

関数と変数

関数はトップレベルの関数だけでなく、クラスやオブジェクトに関連づけた関数とか、入れ子の関数も作れる、とあります。同時に、変数も同様です。関数はstaticメソッドとinstanceメソッド、変数は静的変数とインスタンス変数がそれぞれあります。インスタンス変数はフィールドとかプロパティとも呼びます。また、パラメータや戻り値にすることも可能です。(第1級関数)

javaの様なprivateとかpublicというキーワードはなく、名前が"_"で始まるとライブラリ内のプライベートなものとして扱われます。

変数や関数の名前は他言語と同様で"_"か文字で始まり、2文字目以降はそれに加えて数字も使うことができます。

なお、ガイドラインでは無闇にセッター/ゲッターを定義せずにフィールドをそのまま使え、となっているそうです。

あまり意味はないサンプルですが、関数の中で関数を定義して呼び出す例になります。

実際には関数の中の関数は”int incNum(int n) => n + 1;”という様な表記にしてしまうことが多いかと思います。

式と文

Dartは式と文があります。式は値を持ち、文は持ちません。文は1つ以上の式から成りますが、式は直接は文を含みません。まあ、C言語と同じです。よく引き合いに出るのが、三項演算子の式は値を持ちますが、if文は値を持たない、という例でしょうか。

WarningsとErrors

Dart処理系では処理時の問題をWarningとErrorに分けています。Warningはコードが正常に働かないかもしれないという指摘であって、プログラムを実行します。Errorはコンパイル時と実行時があり、コンパイル時のエラーは実行できません。実行時エラーは実行中に起きた問題で、例外が発生します。

キーワード

あらかじめ定義されているキーワードを名前に使うことは避けるべきとされています。ただ、キーワードの種類によって、いくつか種類があります。

文脈キーワード

特定の場所でのみ意味を持つので、その他の場所では名前に使うこともできます。

showasyncsyncon
hide
文脈キーワード

組み込み識別子

クラス名や型名、インポートプレフィックスには使えません。

abstractimportasstatic
exportinterfaceextentionlate
externallibraryfactorymixin
typedefoperatorcovariantFunction
partgetrequireddeferred
dynamicimplementsset
組み込み識別子

非同期処理用の予約語

async、async*、sync*でマークされた関数内では名前に使えません。

awaityield
非同期処理用の予約語

予約語

上記3種の他は、予約語なのでどこであっても名前には使えません。

elseenuminassert
superextendsisswitch
breakthiscasethrow
catchfalsenewtrue
classfinalnulltry
constfinallycontinuefor
varvoiddefaultwhile
rethrowwithdoif
return
予約語

変数

  • 変数は参照を格納する。
  • 変数の方は推論されるが、明示する事で指定した型に変更できる。
  • オブジェクトが一つの型に限らない場合はObject型か、必要に応じてdnamicを指定する。
  • 別の方法として推論される型を明示的に宣言する。
Note

スタイルガイドではローカル変数にはvarの使用を推奨している。という注釈がついていますが、リンク先を見てもどこに書いてあるのかわかりにくい気がします。より直接的にはこちらのリンク先を示してくれた方がわかりやすい気がします。Typesセクションの中ではあるので間違っているわけではないですが。

こちらには今時は関数は小さくすることが多いので、型を明示しないことで、コードを読む人はより重要である変数名と初期値に注目するとしています。サンプルとして挙げられている例は型の指定が複雑なので、簡潔にvarと初期値だけにした方が簡潔で分かりやすいという趣旨の様です。

推論される型が問題ない場合は初期化部分で自明なので省略してしまった方がわかりやすい、問題がある場合は指定しろ、ということの様です。
なお別のところで初期化してない変数は型アノテーションをつけることを推奨しています。varの推奨は初期化している場合になります。昔の言語と違って、今時の言語では宣言時に初期化してしまうことが普通なので、多くの場合はvarで済むということかと。

初期値

  • 初期化されていない変数の初期値はnull。
  • 数値型でもnull(数値もオブジェクトなので)。
  • null safetyが有効の場合は、必ず初期化する。
  • 宣言時に初期化しなくても良いが、最初に使う時までには初期化する。
  • トップレベル変数とクラス変数は最初に使用されるときに初期化コードが実行される。

上のコードで宣言時の型アノテーションをint?ではなくintにしても、4行目が有効な状態ならばエラーにならず実行できます。しかし、4行目をコメントアウトするとコンパイル時エラーにしてくれます。極力nullableな型ではなくて普通の方を使いなさい、ということかと。

遅延初期化変数(Late variables)

  • Dart 2.12で追加されたキーワード。
  • 使い方は2つ。
    • 宣言後に初期化されるnullでない変数の宣言
    • 遅延初期化
  • 初期化している変数にlateキーワードを付けると、初期化は実際に使用する際に後ろ倒し。
    • 実際に使われていなければ、初期化もされない(lateなしなら宣言時に初期化)

要はDart処理系は使用前に初期化されていないことを警告するが、誤って警告することもあるので、後で初期化していることが自明ならそれをあらかじめ書いておく、と解釈しました。

とりあえず、遅延初期化のサンプルです。動かしてみればわかりますが、lateVariablesの方は13行目のprint 文の前に初期化されています。また、13行目をコメントアウトしてみると、初期化自体が呼ばれないことがわかると思います。

これを積極的に使って何かするということはあまりやらない気はしますが、場合によっては使い所があるかもしれません。

Finalとconst

  • どちらも定数の宣言に使用する。
    • finalは一度だけ設定できる(実行時に初期化)
    • constはコンパイル時に設定できる
    • 厳密にはfinalは変更できない変数、constは定数

どちらも再代入ができないのは同じですが、値が決まるのが実行時かコンパイル時かという違いがあります。変数と定数の違いというのは、変数は上で触れたように参照を格納するものとなっています。一方、定数は参照ではなくてそのものです。また、final変数は初期化時に関数を呼び出してその返り値を代入できますが、const定数はコンパイル時に決まるので、初期化時に関数を呼び出してもエラーになります。四則演算とかはコンパイル時にもできるので問題ありません。

上は動作確認のサンプルです。3行目と6行目がエラーになるのはまあ当然と思います。13行目は問題なく、14行目はエラーになります。finalで変更できないのは格納している参照であって参照している先ではないので、bListが参照しているリストの要素は変更できます。が、そのリストを別のリストに置き換える(つまり、格納している参照を別のものに変更する)ことはできません。

問題は18行目です。これはエラーにしてもらいたかったのですが、エラーにはならず。その上、実行してもランタイムエラーにもならずに15行目のprint文の出力を最後に何も出てきません。ここをコメントアウトすれば普通に22行目まで実行されます。エラーも警告も出ないのがよくわかりませんが、まあ、やってはダメということだと解釈しておきます。

note

一応、デバッグプリントを突っ込んで、どこまで実行されているのか確認してみましたが、17行目と18行目の間に入れたデバッグプリントは実行されているので、18行目を実行中におかしなことが起きている様です。

finalとconstの使い分けについては、Effective Dart をチラッと眺めても特に指針はない様です。極力不変の変数、メンバーにはfinalをつけようという指針はある様です。最近の言語ではありがちな指針です。大前提としてコンパイル時に決まるのがconstですので、システムに関連した何らかの定数はconst、そうではない不変の値は何でもfinalで良い様に思います。

組み込み型

Dartが標準でサポートしている組み込み型は以下。

  • Numbers (int, double)
  • Strings (String)
  • Booleans (bool)
  • Lists (List)
  • Sets (Set)
  • Maps (Map)
  • Runes (Runes)
  • Symbols (Symbol)
  • null値 (Null)

その他、dartで特別な意味がある型は以下。

  • Object: null以外のすべてのクラスの親クラス。
  • FutureとStream: 非同期処理で使う。
  • Iterable: for-inループやgenerator functionで使う。
  • Never: 式が正しく評価できないことを示す。
  • dynamic: 静的型チェックを無効にすることを示す。通常はOnjectかObject?型を使う。
  • void: 値を使用しないことを示す。関数の戻り値に使う。

Numbers

  • 数値を表す。
  • intとdoubleがある、どちらもnum型のサブタイプである。
  • 整数リテラルは必要に応じて自動的に浮動小数に拡張される。
  • 文字列をint.parse()やdouble.parse()で数値に変換し、数値を.toString()やtoStringAsFixed()で文字列に変換できる。
  • int型はビットシフトや論理演算ができる。
  • 数値リテラルや多くの算術演算式はコンパイル時定数である。

Strings

  • Dart文字列はUTF-16である。
  • シングルクオートやダブルクオートで文字列を作成できる。
  • 文字列中に${式}を入れると展開できる。式が識別子単体であれば{}で囲まなくて良い。
    • 展開時にはそのオブジェクトのtoString()メソッドを呼び出す。
  • 文字列は文字列リテラルを並べたり、+演算子で連結できる。
  • シングルクオート3つ('’’)で囲むと複数行文字列を作成できる。
  • 文字列リテラルはコンパイル時定数である。(null,数値,文字列,bool値として評価される場合)

Booleans

  • 論理値リテラルはtrue,falseの2つだけで、どちらもコンパイル定数である。
  • Dartではifやassertも条件式に非論理値は使えない。明示的にチェックする。

具体的には以下の様にチェックします。最近の言語ではよくある仕様と感じます。

Lists

  • 多くのプログラミング言語で一般的なコレクションで、他の言語でいう配列や序列のあるグループである。
  • ListリテラルはJavaScriptの配列リテラルの様に見える。
  • DartではlistはList<int>と推論する。非整数値をリストに加えるとエラーになる。
  • 最後の要素の後に区切りのカンマがあっても良い。
  • Listのインデックスは0から始まり、list.length – 1までとなる。
  • コンパイル次定数としてリストを作るには、constキーワードをリストの前に付ける。
  • Dart 2.3からスプレッド演算子(…/…?)が使える。
  • コレクションの中でifやforが使える。

スプレッド演算子のサンプルです。なお、+演算子でリストの連結もできます。

こちらはifやforのサンプルです。ifはelseも書けました。

Sets

  • Setは順序のないユニークな要素のコレクションである。
  • Dart処理系はsetをSet<String>と推論する。
  • Map(後述)とSetのリテラルの構文と似ているため、{}はMapリテラルとして扱われる。
  • add()/addAll()で要素を追加する。
  • .lengthでサイズを取得する。
  • コンパイル次定数にするには、constキーワードをリテラルの前につける。
  • スプレッド演算子やcollection if/forが使える。

Maps

  • キーと値を関連づけるオブジェクトである。(いわゆる連想配列)
  • キーも値もどんなオブジェクトでも構わない。
  • キーはユニークでなければならないが、値はユニークでなくても良い。
  • 初期化をしていればDart処理系がキーや値の型を推論してくれる。それに反した操作をするとエラーになる。
  • 作成はMapリテラルとMapコンストラクタの2通りで可能である。
    • コンストラクタで作成する場合、C#やJavaのようなnewキーワードは不要(つけてもよい)。
  • 既存のMapにキーと値のペアを使いするのはJavaScript同様(代入)である。
  • 値の取得もJavaScriptと同様である。
    • 存在しないキーを指定した場合はnullを返す
  • lengthを使うとキーと値のペア数を返す
  • コンパイル字定数として作成するには、mapリテラルの前にconstを付ける。
  • 初期化時にスプレッド演算子やcollection if/forを使用できる。

例によって一通りのサンプルを書いてみました。Map(連想配列)は最近の言語あるあるなので、特に変わった話はないです。キーを重複させてもコンパイラはinfoを出すだけで実行は可能です。重複キーで有効になるのは、後の方でしたが、仕様なのかたまたまかは確認していません。コンストラクタを使うとリテラルを可能なら使えとinfoが出ます。

Runeと書記素クラスタ

  • DartではRuneを使うことで文字列内のUnicodeコードポイントを扱うことができる。
  • charactersパッケージを使うとUnicode書記素クラスタと呼ばれるユーザが認識している文字そのものを見たり、操作できる。

ツアーに乗っているサンプルをもう少し事情を分かりやすくしたものを用意しました。この部分はUnicodeについての知識がないとよくわからないかとは思います。ただ、日本語を扱う日本のプログラマは半角文字と全角文字の問題に置き換えれば捉えやすいかと思います。

この実行結果がこんなです。

実行結果

message変数の中身はユーザの認識としては4文字なのですが、実行結果を見ればわかるように、String.lengthの認識は7文字です。これは、普通の英数字の"Hi “の3文字と、Unicodeの絵文字であるデンマーク国旗がU+1F1E9,U+1F1F0という4バイトコードになっているためです。この辺の話はUnicodeとかUTF-16とかの話をしないといけないのでざっくり除きます。そのため、Stringとしては3バイト+4バイト=7バイトで7文字という扱いになっています。実際に6行目をの"message.length – 1″を"- 4″にすることで旗が表示されます。

このように6行目は文字列の最後の1文字を出力しようとしていますが、その「1文字」の想定がユーザが思っている旗の文字ではなくて、1バイトになっています。日本版Excelマクロで言えばlen()ではなくてlenb()です。

一方、7行目はユーザの認識するところの「1文字」で認識してくれます。ですので、最後の文字を取ってくるlastを使うことで、正しく旗の文字を取ってこれます。

逆に言えば、Stringを使う場合は、常に文字コードを意識していないと思った感じで動きません。言うなれば半角文字(1文字1バイト)と全角文字(1文字2バイト)と同じ問題になります。日本語文字列の最後の1文字を取るときは、その文字列が半角文字なのか全角文字なのかを考慮した上でプログラムを書かないといけないのと理屈は同じです。そこをサポートしてくれるのがcharactersパッケージになります。

なお、よく使う日本語はちゃんと考慮して扱ってくれるので、普段はそんなに気にする必要はありません。例えば、サンプルを書き換えてメッセージを「こんにちは」にすれば、以下の様に思った通りに動いてくれます。

「こんにちは」での実行結果

認識通りに5文字と判断されていますし、6行目も7行目も同じ結果です。こういう問題になる範囲とならない範囲の見極めが、UnicodeやUTF-16の知識がないと難しい話になります。厳密に言えば、いついかなる時もcharactersパッケージを使えば問題ないし、理想を言えばそうすべきという話にはなります。

まあ滅多に引っかからないかといえばこれまた微妙な話もあります。Macで新しいフォルダを作成した際にデフォルトで設定される「名称未設定フォルダ」という最後の「ダ」ですが、これは「ダ」に見えて「ダ」ではありません。

Macの「名称未設定フォルダ」の最後の文字は「ダ」ではなくて「タ」と濁点の合成

お前は何を言っているのだと怒られそうですが、普通に使っている1文字のダではなくて、タと濁点の合成文字というものになっています。日本人的には濁点、半濁点付きの文字は個別に欲しいと思いますが、アルファベット圏の外国人的にはそんな個別に文字を用意するのは無駄だから元の文字+濁点や半濁点という合成文字でいいじゃないか、という意見があるのです。

とはいえ、Unicode的にも濁点、半濁点付きの文字がちゃんと定義されているので合成文字を使う必要性はないし、Windowsは濁点、半濁点付きの文字を使っていますが、Appleはなぜか合成文字を採用しています。ですので、Macでファイルシステムを操作していると、もしかすると変なところでハマる可能性はあります。

message is 名称未設定フォルダ. (length:10)
End of String: ゙
The last character: ダ

実際にMacのフォルダ名をコピペしてサンプルを実行すると濁点を1文字と数えた10文字扱いで、文字列としての最後は濁点、charactersによる最後の文字は合成文字の「ダ」と認識されます。手描き書類で濁点とかも1マスに書かされることがありますが、あれと同じことです。ただ、見た目的には普通に1文字に見えます。

合成を使っておらず、基本多言語面にある文字なら問題ないと思います。絵文字でも使えるものと使えないものがありますので、絵文字なら不可とも一概に言えません。Unicodeの文字コードは以下で確認できます。

note

これは別にDartに限った話ではなく、コンピュータシステム一般の問題になります。なまじUnicodeが普及したので変な問題が起きたりします。UnicodeとかUTF-16を使っておけばOKという雑な日本語化、多言語化しか考えてないケースが多く、問題が起きたり起きなかったりするのが厄介です。

Symbols

  • SymbolオブジェクトはDartプログラムで宣言された演算子や識別子を表す。
  • #に続けて識別子を書いたものがSymbolである。
  • Symbolリテラルはコンパイル時定数である。

ライブラリを書く場合に気にするくらいで、普段のプログラムで使うことはないとのことなので見なかったことにします。

次回

次回は関数です。

2022-03-04技術Dart,プログラミング言語

Posted by woinary