2020/9/19 Dart: Sound null safetyの訳

Sound null safety

Dart言語にnull安全が導入されます。(現在ベータ版です(2021年2月時点))

null安全を導入した場合、デフォルトでは普通に型宣言したら、それはnon-nullable型になります。つまり普通に宣言した変数は、nullを持つことができない。普通に宣言した変数の値がnullということはない。

null安全を導入することで、null-dereference errorが実行時に発生することがなくなり、代わりにコード編集時の静的解析エラーとなります。

次のスクリーンショットに示すように、通常の開発環境でnullセーフティを試すか、既存のコードを移行してnullセーフティを使用するか、WebアプリDartPad with NullSafetyでヌルセーフティの使用を練習できます。

Screenshot of DartPad null safety snippet with analysis errors

important

null安全はまだテクニカルプレビューなので、製品版で使用しないでください。特にFlutterフレームワークはまだnull安全に対応していません。(2020年9月19日時点)(2021年2月時点で削除されている。)


Null safety principles

Dart null安全サポートは、次の3つのコア設計原則に基づいています。

Non-nullable by default.明示的にnull値をとることができると示さない限り、non-nullable型とみなされます。このデフォルトは、APIでnull以外が断然最も一般的な選択であることが調査で判明した後に選択されました。

Incrementally adoptable. 何をnull safetyにマイグレートするか、そしていつマイグレートするかを選択できます。同じプロジェクトでnull-safeなコードとnon-null-safeなコードを混在させることができます。徐々にマイグレートしていくことが可能です。マイグレーションを助けるツールを提供しています。

Fully sound. Dartのnullセーフティは健全であり、コンパイラの最適化を可能にします。型システムがnullではないと判断した場合、値がnullになることはありません。プロジェクト全体とその依存関係をnullセーフティに移行すると、健全性のメリットを最大限に活用できます。バグが少なくなるだけでなく、バイナリが小さくなり、実行が高速になります。


A tour of the null safety feature

nullの安全性に関連する新しい演算子とキーワードには、?、!、およびlateが含まれます。 Kotlin、TypeScript、またはC#を使用したことがある場合、nullの安全性の構文は見覚えがあるかもしれません。

これは仕様によるものです。Dart言語は驚くことではないことを目指しています。

Creating variables

変数宣言時に ? ,あるいは late を使用して、その変数がnull可型か、null不可型かを示します。

//null安全導入後は以下の変数の値がnullになることはありません。
var i = 42; // Inferred to be an int.
String name = getFileName();
final b = Foo();

var, finalで宣言して、なおかつnullable型と示さない場合non-nullable型となる。

null安全が導入されてないこれまででは、上記の変数宣言でこれらの変数の値がnullになることはあった。現時点(2021年2月時点)でもそう。

//null安全導入前

void main(){
  var i=10;
  i=null;
  print("${i}"); //null
}
//null安全導入後

void main(){
  var i=10;
  i=null;
  print("${i}");
}

//エラー発生。
//A value of type 'Null' can't be assigned to a variable of type 'int' - line 17
//「int型の変数にNull型の値(null)を代入することはできません」

以下全てnull安全導入後の話。

null可型の変数を宣言したい場合、変数の型宣言の後ろに ? を付けます。

int? aNullableInt = null; //←問題無い。

Swiftのオプショナル型。Optional<Int>型、あるいはInt?型。

If you know that a non-nullable variable will be initialized to a non-null value before it’s used, but the Dart analyzer doesn’t agree, insert late before the variable’s type:

変数宣言時にlateキーワードをつけると、

  • lateを付けた変数は即時に初期化する必要がなくなります。
  • ランタイムは、late変数を遅延初期化します。たとえば、null不可のインスタンス変数を計算する必要がある場合、late修飾子を追加すると、インスタンス変数が最初に使用される時まで計算が行われるタイミングを遅らせます。
class IntProvider {
  late int aRealInt;
  
  IntProvider(){
    aRealInt=calc();
  }
}

int calc(){
  return 100;
}


void main(){
  var int1=IntProvider(); //(A)
  print("${int1.aRealInt}"); //(B)
}

上記のcalc()関数はただ100を返すだけなので「lateを付けて何の意味があるの?」という感じだが、calc()関数で行われる処理が「非常に計算量が多かったり時間がかかる可能性がある」処理の場合、必ず必要だとは限らないタイミングである「インスタンス生成時(Aのタイミング)」ではなく、「int1.aRealIntが最初に参照される時点(Bのタイミング)」でcalc()関数が実行されて初期化(initialize)が行われることが意味を持つ、ということ。


Using variables and expressions

null-safetyを導入すると、Dart analyzerは、non-null値が必要な場所でnullable値を発見した時にエラーを発生させます。

アナライザーは、関数内の変数または式がnull許容型であるが、null値を持つことができない場合を認識できることがよくあります。

↑説明が抽象的で非常にわかりにくい。non-nullable型変数にnullを代入しようとするとエラーが出る、ということか?

non-nullable型変数にnullable型変数を代入する時点でエラーなのか?

こんなのコードを示して説明すれば一発なのに、示さずに文章で説明するのはどういう感覚なんだろうか。

null不可型の変数の値をnullにしようとするとエラーを発生させます。工事中🏗

//2021/2/15(nullsafety.dartpad.dev)
//↓エラー出る。intOrNullの返り値が
//int?型だから、ということだろう。
/*
void main(){
  int a=0;
  int? b=3;
  a=intOrNull();
  print(a);
}

int? intOrNull(){
  return null;
}
*/
/////////////////
//↓エラー出ない。
/*
void main(){
  int a=0;
  int? b=3;
  a=intOrNull() ?? 0;
  print(a);//0
}

int? intOrNull(){
  return null;
}
*/
/////////////////
//↓エラー出ない。
//bはint?型なのでエラー出るのではないのか?
/*
void main(){
  int a=0;
  int? b=3;
  a=b;
  print(a);//3
}
*/
/////////////////
//↓エラー出る。
//A value of type 'int?' can't be assigned to a variable of type 'int'
//それは一つ上もそうだと思うが。
/*
void main(){
  int a=0;
  int? b=null;
  a=b;
  print(a);
}
*/
/////////////////
//↓エラー出る。
//The argument type 'int?' can't be assigned to the parameter type 'num'
void main(){
  int a=0;
  int? b=null;
  int c = a+b;
  print(c);
}

/////////////////
//エラー出ない。
//null可型とnull不可型の計算自体がだめな訳ではない。
//つまりこの場合も(コンパイルエラー出ないので)自分でnullチェックする必要がある。
/*
void main(){
  int a=1;
  int? b=3;
  int c = a+b;
  print(c);//4
}
*/
//あくまでDartpadでの結果なのでFlutterでも同じかは確認しないとわからない。

 

アナライザーはアプリケーション全体のフローをモデル化できないため、グローバル変数またはクラスフィールドの値を予測できません。

値がnullになる可能性のある変数や式は、nullであるかどうかを調べることを徹底しましょう。if文、あるいは??演算子、あるいは?.演算子を使用してnull値を適切に扱いましょう。

下記の例では、??演算子を使用して、null不可型である変数valueに、nullが代入されないようにしています。

int value = aNullableInt ?? 0;
//aNullableIntがnullの場合、valueに0が代入される。
//aNullableIntがnullでない場合、aNullableIntの値がvalueに代入される。

int definitelyInt(int? aNullableInt) {
  if (aNullableInt == null) {
    return 0;
  }
  return aNullableInt; // Can't be null!
}

↑がSwiftのオプショナルバインディングによるアンラップと同じことをやっていることになる。

引数aNullableIntの値はnullの可能性があるので、aNullableIntの値がnullであるかどうかをif文でチェックしている。

aNullableIntの値がnullの場合は0を返す。

aNullableIntの値がnullでない場合はaNullableIntの値を返す。

とすることで、関数definitelyIntは確実にint型の値を返す。(関数definitelyIntがnullを返す可能性は無い。)


null可型の変数の値がnullではないとわかっている時は、変数の後ろに「!」演算子をつけることで、null不可型として扱うようにDartに知らせることができます。

int? aNullableInt = 2;
int value = aNullableInt!; // `aNullableInt!` is an int.
// This throws if aNullableInt is null.

important:nullの可能性がある変数に「!」を付けてはいけません。


//過去のサンプルコード
void main(){
  int? int1=10;
  int? int2=20;
  int int3=int1!+int2!;  //←null可型の変数に「!」を付けてnull不可型にする。
  print("${int3}");  //←30
}

void main(){
  int? int1=10;
  int? int2=20;
  int int3=int1+int2;
  print("${int3}");
}

//エラー発生。
//An expression whose value can be 'null' must be null-checked before it can be dereferenced - line 31
//「nullの可能性がある式は、参照する前に値がnullでないかをチェックする必要があります。」
//↑過去のサンプル。多分その時は↑のエラーが出てたのだと思うが、
//2021年2月時点ではこのエラーが出ない。

↑null可型の変数同士を(nullチェック無しで)計算することはできません。

「null可型の変数」と「null不可型の変数」を(nullチェック無しで)計算することはできません。

そういう状況で計算を行いたい場合は、

(1)if文、??演算子、?.演算子などを使用して、nullチェックをして、非null値同士の計算になることを確実にする。(Swiftのオプショナルバインディング的なこと。)

(2)「!」演算子を使用して強制的にnon-null値であることを示す。(Swiftの強制オプショナルアンラップ)

上記のような対応が必要です。

(2021年2月15日)Dartpad(null-safetyバージョン)だと「null可型の変数」と「null不可型の変数」を(nullチェック無しで)計算することができている。

ただ、上記の注意は必要なはず。なので、開発者が自分で気を付けるしかない。


( ! 演算子でできることを超えて)nullable型の変数の型を変える必要がある場合、typecast operator(as)を使うことができます。

下記のサンプルはnum?型をint型に変換するためにasを使っています。

return maybeNum() as int;

null-safetyを導入した場合、nullの可能性のあるオペランドに対してメンバーアクセスオペレータ( . )は使えません。代わりにnull-awareバージョンのメンバーアクセスオペレータ( ?. )を使いましょう。

double? d;
print(d?.floor()); // Uses `?.` instead of `.` to invoke `floor()`.

↑のように説明されているが、

https://nullsafety.dartpad.dev/

で↓のコードを実行させてもエラーは出ない(2021年2月15日時点)。

void main(){ 
  double? a=4.6;
  print(a.floor()); //4
}

つまり「エラーが出る」という意味ではなく、「nullチェックの無いコードを書くべきではない、nullチェック(null-awareバージョンのメンバーアクセスオペレーターを使うこと)をするべきだ」という意味だと思われる。

なので開発者がnullチェックを忘れないようにする必要がある。

 

 

void main(){
  double? d1;
  print(d1.floor()); 
}
//An expression whose value can be 'null' must be null-checked before it can be dereferenced

///null可型(double?型)のd1に対してdouble型のメソッドfloor()を実行しようとしています。d1の値がnullの場合実行できないので、エラーが出ています。

///✖️こういう場合(2021年2月15日、間違い→)「!」を使ってnull不可型と示すわけですが、以下のように

///void main(){
///  double? d1=76.21;
///  double d2=d1 as double;
///  print(d1.floor()); //76
///}

///as演算子を使ってnull可型からnull不可型へキャストすることも可能です。


null安全を導入した場合、null可型の変数のプロパティやメソッドにアクセスするしたい時は「?.」演算子(null-aware演算子)を使用しましょう。

void main(){
  double? d1=76.21;
  print(d1?.floor()); //76
}

Understanding list, set, and map types

List(リスト)、Set(セット)、およびMap(マップ)は、Dartプログラムで一般的に使用されるコレクションタイプであるため、それらがnullの安全性とどのように相互作用するかを知る必要があります。

Dartコードがこれらのコレクションタイプを使用する方法の例を次に示します。

  • ColumnなどのFlutterのレイアウトウィジェットは、List<Widget>型のchildrenプロパティを持つものが多いです。
  • The Veggie Seasons sample uses a Set of VeggieCategory to store a user’s food preferences.
  • GitHub DatavizサンプルのfromJson()メソッド。このメソッドはMap<String,dynamic>として提供されるJSONデータからオブジェクトを生成します。

List and set types

 

List<String> list1=[];

と宣言した場合、list1自体null不可型だし、list1の全ての要素もnull不可型。


List<String>? list2;

と宣言した場合、list2自体はnullの可能性がある。list2の各要素はnull不可型。


List<String?> list3;

と宣言した場合、list3自体null不可型、list3の要素はnull可型なので、list3の要素の値がnullの可能性はある。


List<String?>? list4;

と宣言した場合、list4自体nullの可能性もあるし、list4の要素の値がnullの可能性もある。

List<String?> list5;

var list6=<String?>['aaa','bbb','ccc'];

list5のような宣言の仕方の他にlist6のように宣言してもList<String?>型の変数を宣言できる。


Map types

void main(){
  var myMap = <String, int>{'one': 1};
  var uhOh = myMap['two'];
  print(uhOh); //←null
}

Mapに関してはほとんど私たちの予想通りの挙動になりますが、一つだけ、「keyにより問い合わせた結果がnullになる」ということはあります。

上記のサンプルではuhOhの値はnullであり、uhOhの型は int? です。

uhOh.runtimeTypeはNull型と表示される。まあドキュメントに書いてあるのでint?型なのだろう。

リストやセットと同様に、マップにはさまざまなタイプがあります。

Type Can the map
be null?
Can an item (int)
be null?
Map<String, int> No No*
Map<String, int>? Yes No*
Map<String, int?> No Yes
Map<String, int?>? Yes Yes

*  mapの全ての要素がint型であっても、不正なキー(key)による問い合わせの結果はnullになります。(上記サンプル)

マップルックアップはnullを返す可能性がある(int?型)ため、null不可型変数(value)に割り当てることはできません。

//null安全導入後

int value = <String, int>{'one': 1}['one']; 
// エラー発生。
//A value of type 'int?' can't be assigned to a variable of type 'int'

valueはnull不可型。mapはnullを返す可能性(不正なキーによる問い合わせ)があるため。

じゃあどうすれば良いか?

一つは変数valueをnull可型で宣言する↓

void main(){
  int? value = <String, int>{'one': 1}['one']; //OK
}

あるいは、強制アンラップ(Swiftでの笑)する。

void main(){
  int value = <String, int>{'one': 1}['one']!; //強制アンラップすればOK
}

安全なアプローチは「keyを使ったMapへの問い合わせ結果」がnullでない場合にだけその結果を使うことです。結果を

if文、

あるいは

??演算子

を使ってチェックすることが必要です。

当たり前の話ですが、安全なのは、「不正なキーによる問い合わせをしない」ことです。

var aList = <String, int>{'one': 1};
...
int value = aList['one'] ?? 0;
//↑aList['one']の値がnullなら0を返す、nullでないならその値を返す。
//??演算子を使用してnullでないかをチェックしている。

Enabling null safety

Null-safetyは現在ベータ版の機能です。DartまたはFlutterSDKの最新のベータチャネルリリースを要求して使用することをお勧めします。

最新のリリースを見つけるには、Dart SDKアーカイブのベータチャネルセクション、またはFlutterSDKアーカイブのベータチャネルセクションを参照してください。

null-safetyをサポートする言語バージョンを要求するようにSDK constraints(制約)を設定します。 たとえば、pubspec.yamlファイルには次の制約がある場合があります。

environment:
  sdk: ">=2.12.0-0 <3.0.0"

上記の2.12SDK制約は-0で終わります。 このセマンティックバージョニング表記の使用により、2.12.0-29.10.betaベータプレリリースなどの2.12.0プレリリースが可能になります。

nullの安全性に関する問題を見つけた場合は、フィードバックをお寄せください。


Where to learn more

nullの安全性の詳細については、次のリソースを参照してください。

まあまとめると、実行時エラーがでないようにnullかどうかをチェックしましょう。気をつけましょう、ということに尽きますね。

 

 

参考

https://dart.dev/null-safety

コメントを残す

メールアドレスが公開されることはありません。