Dartの変数

変数

Dartの変数について学びます。以下に変数を作成・初期化する例を示します。

var name = 'Jack';Code language: JavaScript (javascript)

変数は参照を保持します。nameという変数には、値が「Bob」の文字列オブジェクトを指す参照が格納されます。

参照とはメモリ上のアドレスのことです。データをメモリに保管しやすくするため、各メモリ領域に住所番号のようなアドレスが割り当てられています。

コンパイラはnameの型を自動的にStringと推論しますが、手動で型を指定して推論結果を変更することも可能です。複数の型のオブジェクトを格納する必要がある変数の場合は、型をObjectと宣言します(必要に応じてdynamicも使用できます)。

Object name = 'Jack';Code language: JavaScript (javascript)

もう一つの記法は明示的に型を指定する方法です(名前はテキストなのでString型で保持できると分かっている場合、直接文字列型を記述します)。

String name = 'Jack';Code language: JavaScript (javascript)

Nullセーフティ(空値安全)

Nullセーフティは、値がnullの変数を誤って参照することで発生するエラーを防ぎます。このエラーはnull参照エラー(null dereference error)と呼ばれます。演算結果がnullとなる式に対しプロパティにアクセス、またはメソッドを呼び出した際に発生します。

例外が一つ存在します:null自身が対応するプロパティまたはメソッド(toString()hashCodeなど)の場合はエラーが発生しません。Nullセーフティにより、Dartコンパイラはコンパイル時点でこの種の潜在的なエラーを検知できます。

プログラミング経験のある方ならご存じの通り、null参照エラーは非常に頻出します。コンパイル段階でこのエラーを回避できれば、プログラムの安定性と堅牢性が大幅に向上します。

例:int型変数iの絶対値を取得したいとします。もしinullの場合、i.abs()を実行するとnull参照エラーが発生します。他のプログラミング言語ではこのコードは実行時にしかエラーを出しませんが、Dartコンパイラはこの不正な呼び出しを直接禁止し、事前に問題を遮断します。

Nullセーフティによる3つの核心的な変更点

1 許容null型:型を指定する際、末尾に疑問符を付けるだけで、その型がnullを許可するか制御できます。

String? name  // 許容null型:値はnullまたは文字列
String name   // 非許容null型:nullを保持できず、文字列のみ格納可能Code language: JavaScript (javascript)

一部のプログラミング言語ではstring nameにnullを代入できますが、Dartではできません。nullを格納したい場合はstring?型を使用する必要があります。

2 変数は使用前に初期化を完了させる必要がある。許容null変数のデフォルト値はnullで、自動的に初期化された状態と同等です。一方、非許容null型にはデフォルト値が存在せず、手動で初期値を代入することが強制されます。Dartは初期化されていない変数へのアクセスを禁止することで不具合を防ぎます。

void main() {
  int year;        // エラー:初期化されていない非許容null変数
  print(year);     // コンパイルエラー:yearが未初期化
}
Code language: Dart (dart)

上記コードはコンパイル時にエラーを出力します。

C:\dartdemo\firstdart>dart run
Building package executable...
Failed to build firstdart:firstdart:
bin/firstdart.dart:7:9: Error: Non-nullable variable 'year' must be assigned before it can be used.
  print(year);     // コンパイルエラー:yearが未初期化
        ^^^^Code language: JavaScript (javascript)

許容null変数の場合は以下の記述が可能です。

void main() {
  int? age;        // 許容null変数、デフォルト値はnull
  print(age);      // nullが出力され、エラーなし
  
  //age = 18;        // 手動代入
  //print(age);      // 18が出力
}Code language: JavaScript (javascript)

3 許容null型の式に対し直接プロパティアクセス・メソッド呼び出しを行ってはならない。こちらにも例外が存在し、nullが標準で持つプロパティ・メソッド(hashCodetoString())に限り呼び出しが許可されます。

void main() {
  String? name; // 許容null型、デフォルト値null

  // エラー:lengthに直接アクセスすると失敗
  // print(name.length);

  // 正常:null標準搭載メソッドは呼び出し可能
  print(name.toString());  // "null" を出力
  print(name.hashCode);    // 整数値を出力

  // 正常:null安全演算子を使用
  print(name?.length);     // nullを出力、エラーなし
}Code language: PHP (php)

デフォルト値

手動で値を代入していない許容null変数の初期値はデフォルトでnullとなります。数値型変数であってもデフォルト値はnullです。

Dartでは数値もその他すべてのデータと同じく、本質的にオブジェクトだからです。

void main() {
  int? lineCount;
  assert(lineCount == null);
}Code language: JavaScript (javascript)

注意:本番環境のコードではassert()による検証は無視されます。開発段階においてassert(条件)内の条件が成立しない場合、プログラムは例外をスローします。

Nullセーフティを有効にした後、非許容null変数は使用前に初期化を完了させる必要があります。

int lineCount = 0;

ローカル変数は宣言と同一行で代入する必要はないものの、使用する前に必ず値を代入しなければなりません。以下のコードは、print()実行時にlineCountが必ず非null値を持つとDartが解析できるため合法です。

int lineCount;

if (weLikeToCount) {
  lineCount = countLines();
} else {
  lineCount = 0;
}

print(lineCount);

Code language: Dart (dart)

上記コードのlineCount = countLines(); と lineCount = 0; は変数への代入処理であり「使用」ではない点に注意してください。使用とは変数の値を読み取る行為のことです。例えば以下のコードはエラーとなります。

void main() {
	int lineCount;

	print(lineCount);
}
Code language: Dart (dart)

コンパイルエラーが発生します。

トップレベル変数とクラスメンバ変数は遅延初期化が適用され、初めて参照された時のみ代入ロジックが実行されます。

late修飾子付き変数

late修飾子には二つの使用場面があります。

  1. 後から値を代入する非許容null変数を宣言する;
  2. 変数の遅延初期化を実装する。

大半のケースでDartのフロー解析は、非許容null変数が使用前に非null値を代入済みか判断できます。しかし一部状況で解析が失敗し、最も頻出するのがグローバル変数とインスタンス変数です。コンパイラは代入済みか判断できないためエラーを出力します。

変数使用前に必ず値が代入されると確信しているがコンパイラが検知できない場合、late修飾子を付与することでエラーを解消できます。

下記例のdescriptionはグローバル変数で、コンパイラは代入の有無を判別できません。絶対に値が代入されると確信している場合はlateを使用します。

late String description;

void main() {
  description = 'Feijoada!';
  print(description);
}
Code language: Dart (dart)

注意点

late変数を宣言したまま一度も値を代入しなかった場合、変数にアクセスした時点で実行時例外がスローされます。

late変数宣言時に初期値を直接記述した場合、初期化コードは変数が初めて参照されたタイミングのみ実行されます。この遅延初期化は二つの場面で非常に有用です。

  1. 変数が一度も使用されない可能性があり、かつ初期化処理の計算コストが大きい;
  2. インスタンス変数初期化時、初期化ロジックがthisにアクセスする必要がある。

下記例において、temperatureが一度も使用されなければ、負荷の大きいreadThermometer()関数は一切実行されません。

// temperatureに初めてアクセスした時のみreadThermometer()が呼ばれる
late String temperature = readThermometer(); // 遅延初期化
Code language: JavaScript (javascript)

final と const

変数の値を変更不可にしたい場合はfinalまたはconstを使用します。どちらも単独、または型注釈と組み合わせてvarの代わりに使えます。

  • final変数は一度だけ代入可能;
  • const変数はコンパイル時定数(const変数は暗黙的にfinalの性質を持つ)。

備考

インスタンスメンバ変数にはfinalを使用できますがconstは不可。クラス全体の定数にはstatic constを使用してください。

final変数の定義例を示します。

final name = 'Jack'; // 型注釈なし
final String nickname = 'Jason';
Code language: PHP (php)

final変数の値を上書きすることはできず、コンパイラが直接エラーを出力します。

void main() {
	final name = 'Jack'; // 型注釈なし
	final String nickname = 'Jason';
	
	name = 'Alice'; // エラー:final変数は1回のみ代入許可
}Code language: JavaScript (javascript)
C:\dartdemo\firstdart>dart run
Building package executable...
Failed to build firstdart:firstdart:
bin/firstdart.dart:6:2: Error: Can't assign to the final variable 'name'.
        name = 'Alice'; // エラー:final変数は1回のみ代入許可Code language: PHP (php)

constはコンパイル時定数を定義するために使用します。クラス内部に定数を定義する場合はstaticを付加しstatic constと記述します。const変数宣言時の代入値は、コンパイル時に確定可能な値でなければなりません:数値、文字列リテラル、他のconst変数、定数同士の四則演算結果などです。

const bar = 1000000; // 圧力単位(ダイン/cm²)
const double atm = 1.01325 * bar; // 標準大気圧
Code language: JavaScript (javascript)

constキーワードは定数変数宣言だけでなく、定数オブジェクトの作成や定数インスタンスを生成するコンストラクタ定義にも使えます。任意の変数は定数オブジェクトを値として受け取れます。

var foo = const [];
final bar = const [];
const baz = []; // const [] と同等
Code language: PHP (php)

const変数への代入式内では、上記bazの例のようにconstキーワードを省略可能です。

変数に定数オブジェクトを代入したことがあっても、その変数がfinalでもconstでもない場合は参照先を変更できます。

foo = [1, 2, 3]; // fooは当初定数空配列を指していたが、参照先を変更可能
Code language: JavaScript (javascript)

しかしconst変数に再代入することは許可されず、コンパイラがエラーを出力します。

// 静的解析:コードエラー
baz = [42]; // エラー:定数変数への再代入不可
Code language: JavaScript (javascript)

定数定義時には型判定・型キャスト(isas)、コレクションif、展開演算子(......?)を併用可能です。

void main() {
	const Object i = 3; // iはint値を保持するObject型定数
	const list = [i as int]; // 型キャスト使用
	const map = {if (i is int) i: 'int'}; // is判定+コレクションif
	const set = {if (list is List<int>) ...list}; // 展開演算子と併用
}Code language: JavaScript (javascript)

補足

final修飾オブジェクトは参照先を変更できないものの、オブジェクト内部のフィールドは変更可能です。対照的にconstオブジェクトは自身と内部の全データが完全に不変です。

constによる定数コレクション作成の詳細は、リスト・マップ・クラス関連ドキュメントを参照してください。

* ワイルドカード変数

バージョン注記:ワイルドカード変数を使用するにはDart 3.7以上の言語バージョンが必要です。

_と名付けられたワイルドカード変数は束縛のないローカル変数・引数で、単なるプレースホルダーとして動作します。初期化式が記述されている場合式自体は実行されますが、変数に保存された値を読み取ることはできません。同一スコープ内に複数の_変数を定義しても名前衝突は発生しません。

ワイルドカード変数とは何か

Dartにおいて単一アンダースコア_がワイルドカード変数です。

  1. 使用しない値を受け取るために使い、コンパイラに「この変数は読み取らない・使用しない」と伝える;
  2. コンパイラが「変数が定義されているが未使用」の警告を出力しない;
  3. _を読み取るとエラーとなるため、データを捨てるためだけに使用する。
// 1番目と3番目の値のみ取得、2番目は_で破棄
final [a, _, c] = [10, 20, 30];
print(a); // 10
print(c); // 30
// print(_); // エラー、ワイルドカードは読み取り不可Code language: PHP (php)

トップレベル宣言やライブラリのプライベートアクセスに影響するクラスメンバにはワイルドカード変数を使用できません。ブロックスコープ内の宣言のみ利用可能、例は下記の通りです。

  1. ローカル変数宣言
void main() {
  var _ = 1;
  int _ = 2;
}
Code language: JavaScript (javascript)
  1. forループ変数
for (var _ in list) {}
Code language: PHP (php)
  1. catch捕捉引数
try {
  throw '!';
} catch (_) {
  print('oops');
}
  1. ジェネリック型・関数ジェネリック引数
class T<_> {}
void genericFunction<_>() {}

takeGenericCallback(<_>() => true);
  1. 関数引数
Foo(_, this._, super._, void _()) {}

list.where((_) => true);

void f(void g(int _, bool _)) {}

typedef T = void Function(String _, String _);

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です