2020/11/11 Understanding null safetyの訳パート2

 

<<前のページへ

この例では、ifステートメントのthenブランチは、objectに実際にList(リスト)が含まれている場合にのみ実行されます。したがって、Dartはobjectが宣言されたObject型の代わりにList型に昇格させます。これは便利な機能ですが、かなり制限されています。nullセーフティの前は、次の機能的に同一のプログラムは機能しませんでした。

//https://dartpad.dev/
//null-safety導入前

bool isEmptyList(Object object) {
  if (object is! List) return false;
  return object.isEmpty; // <-- Error!
}
//↑確かにこの関数定義で
//The getter 'isEmpty' isn't defined for the type 'Object'
//↑のエラーが出る。(null-safety導入前)

//https://nullsafety.dartpad.dev/dart
//null-safety導入後

bool isEmptyList(Object object) {
  if (object is! List) return false;
  return object.isEmpty; // <-- Error!
}

void main() {
  List<int> list1=[1,2,3];
  print(isEmptyList(list1)); //false
}
//エラーは出ない。(null-safety導入後)

もう一度言いますが、.isEmptyの呼び出しは、objectがList型の値を持つ時のみ可能です。

繰り返しますが、オブジェクトにリストが含まれている場合にのみ.isEmpty呼び出しに到達できるため、このプログラムは動的に正しいものです。

しかし、type promotionのルールは、returnステートメントが、オブジェクトがリストである場合にのみ2番目のステートメントに到達できることを意味することを理解するほど賢くはありませんでした。

For null safety, we’ve taken this limited analysis and made it much more powerful in several ways.

工事中🏗

 


Contents

Reachability analysis

まず、タイププロモーションは早期のreturnやその他の到達不能コードパスについて賢くないという長年の苦情を修正しました。

工事中🏗

nullの安全性の下では、

// Using null safety:
bool isEmptyList(Object object) {
  if (object is! List) return false;//1行目
  return object.isEmpty;//2行目
}

↑Swiftのguard let文みたいなことか。

上記のコードは完全に有効です。

objectにセットされた値がList型でない場合if文はfalseを返して関数(isEmptyList)を終了させます(1行目)。

(objectにセットされた値がList型の場合、)(null-safety導入後は)Dartは2行目でobjectをList型とみなします。

これはnullabilityに関連しないコードも含めて非常に良い改正です。


Never for unreachable code

この到達可能性分析(reachability analysis)をプログラムすることもできます。

新しいボトムタイプのNeverには値がありません。

(String、bool、intは同時にどのような値ですか?)←意味不明

では、式がNever型を持つ(式の型がNever型である)とは、どういう意味を持つのでしょう?

これは、式が評価を正常に終了できないことを意味します。

ですから、例外をスローするか、中止するか、式の結果を期待する周囲のコードが実行されないようにする必要があります。

実際、throw式の静的な型はNever型です。

Never型はcoreライブラリで宣言されており、Never型を型宣言(type annotation)で使うことは可能です。

もしかすると、下記のような、特定の種類の例外を簡単にスローできるようにするためのヘルパー関数を用意している人もいるかもしれませんね。

// Using null safety:
Never wrongType(String type, Object value) {
  throw ArgumentError('Expected $type, but was ${value.runtimeType}.');
}

この関数は以下のようにして使えます。

// Using null safety:
class Point {
  final double x, y;

  bool operator ==(Object other) {
    if (other is! Point) wrongType('Point', other);
    return x == other.x && y == other.y;
  }

  // Constructor and hashCode...
}

==に渡されたotherがPoint型の場合、if文のthenブランチは実行されず、次の行のreturn文ではotherはPoint型にpromote(ダウンキャスト)される。よってother.x,other.yにアクセスできる。

このプログラムはエラー無しで解析されます。最後の==の行で、otherの.xと.yにアクセスしています。

関数にreturnやthrowがない場合でも、Point型にpromote(ダウンキャスト)されます。

制御フロー分析(control flow analysis)は、wrongType()メソッドの返り値の型がNever型と宣言されていることを認識しています。

これは、ifステートメントのthenブランチをどうにかして中止する必要があることを意味します。

2行目に到達できるのはotherに渡された値がPoint型である時だけなので、DartはotherをPoint型にpromote(ダウンキャスト)します。

つまり、独自のAPIでNeverを使用すると、Dartの到達可能性分析を拡張できます。これについては、ローカル変数を使用して簡単に説明しました。Dart

 


Definite assignment analysis

これについては、ローカル変数を使用して簡単に説明します。Dartでは、non-nullableなローカル変数が読み取られる(アクセスされる)前に常に初期化されていなければなりません。

私たちはdefinite assignment analysis(絶対的割り当て分析)を使用して、可能な限り柔軟に対応します。

Dart言語は全ての関数のボディを解析し、全ての制御フロー(プログラムの流れ)においてローカル変数と引数への代入(割り当て)を追跡します。

変数へのアクセスに繋がる全てのpath(プログラムの流れ)において代入が行われている限り、変数は初期化されたとみなされます。

これにより、変数がnon-nullable型の場合でも、初期化子なしで変数を宣言し、複雑な制御フローを使用して後で初期化できます。


definite assignment analysis(絶対的割り当て分析)を使用して、final変数をさらに柔軟にすることもできます。

null-safety導入前は、ローカル変数をインタレスティングな方法で初期化したい場合、そのローカル変数をfinalで宣言することは困難な場合もありました。

// Using null safety:
int tracingFibonacci(int n) {
  final int result;
  if (n < 2) {
    result = n;
  } else {
    result = tracingFibonacci(n - 2) + tracingFibonacci(n - 1);
  }

  print(result);
  return result;
}

↑のコードはresult変数がfinalなのに、初期化してないので(null-safety導入前は)エラーになっていました。

よりスマートなflow analysisを備えたnull-safety導入後はこのプログラムは問題ありません。

分析により、resultはすべての制御フローパスで1回だけ確実に初期化されることが確認されるので、変数をfinalにするための制約が満たされていることがわかります。(つまり変数をfinalにできる、ということ。)


Type promotion on null checks

よりスマートなフロー分析は、null可能性に関係のないDartコードをも支援します。

しかし、現在これらの変更を行っているのは偶然ではありません。(null-safetyの支援でもある)

私たちは型を「nullable型のセット」と「non-nullable型のセット」に分割しました。

nullable型の変数の値がnullの場合、フロー分析によってアプリのクラッシュが回避される、ただそれだけです。

しかし、nullable型の変数の値がnullでない場合、メソッドを呼び出すために、その値をnon-nullable型へと移すことができるといいですよね。

多分、「明示的なキャスト無しで、移すことができたら便利ですよね。」という意味だと思われる。

フロー分析は、ローカル変数とパラメーターに対してこれを行うための主要な方法の1つです。

私たちはtype promotionを

==null

!=null

(つまりnullチェック)にも拡張しました。nullチェックしてnullじゃないことを確認した部分もtype promotionが行われるようにした。

nullable型の変数の値をnullチェックして、nullでないことを確認した場合、Dartはその変数を対応するnon-nullable型にpromoteします。

// Using null safety:
String makeCommand(String executable, [List<String>? arguments]) {
  var result = executable;
  if (arguments != null) {
    result += ' ' + arguments.join(' ');
  }
  //return result;
  //多分↑の一文を書き忘れていると思われる。
}

https://dart.dev/null-safety/understanding-null-safety#type-promotion-on-null-checks

↑のページに記載されているサンプルコードは↑

だがこれをhttps://nullsafety.dartpad.dev/dartで動かすと、

The body might complete normally, causing 'null' to be returned, but the return type is a potentially non-nullable type

上記エラーが出る。多分return 文を忘れているのだと思われる。

ここで、arguments引数はnullable型です。通常だとnullable型への.join()メソッドの呼び出しは禁止されるところですが、

しかし、値がnullでないことを確認するifステートメントでその(.join()メソッドの)呼び出しを保護しているため、DartはargumentsをList<String>?からList<String>へとpromoteして、join()メソッドの呼び出しを可能にします。

あるいは、promoteしたargumentsを、non-nullableなListを引数として受け取る関数に渡したりします。

これはかなりマイナーなことのように聞こえますが、nullチェックに関するこのフローベースのプロモーションにより、ほとんどの既存のDartコードがnull-safety導入後も機能する、ということです。

ほとんどのDartコードは動的に正しく、メソッドを呼び出す前にnullをチェックすることで、null参照エラーのスローを回避します。

工事中🏗

そしてもちろん、これはnon-nullableなtype-promotionでも機能します。

一度型がnon-nullable型にpromoteされた場合、冗長な(不要な)nullチェックを再度するとwarning(警告)が発生します。

もちろん、到達可能性(reachablity)について行うスマートな分析でも機能します。上記の関数は、次のように記述できます。

// Using null safety:
String makeCommand(String executable, [List<String>? arguments]) {
  var result = executable;
  if (arguments == null) return result;
  return result + ' ' + arguments.join(' ');
}

↑コンパイルエラー無し。(null-safety導入後)

Dart言語は、どのような表現がpromotionを引き起こすかについてもより賢いです。

もちろん、明示的な== nullまたは!= nullは機能します。

明示的なasを使ったキャスト、または代入、または!演算子によるアンラップも間も無くpromotionを発生させるようになるでしょう。

一般的な目標は、コードが動的に正しく、静的にそれを理解することが合理的である場合、分析はそれを行うのに十分賢いはずであるということです。


Unnecessary code warnings

「賢いreachability analysis」と「プログラムの中でどこにnullが現れるかを知ること」は、確実にnullを処理するコードを追加することを助けます。

しかし、同じ分析を不要なコードを見つけることにも使うことができます。

null-safety導入前は下記のようなコードを書いていたでしょう。

// Using null safety:null-safety導入後
String checkList(List list) {
  if (list?.isEmpty) {
    return 'Got nothing';
  }
  return 'Got something';
}
/*
警告発生
The receiver can't be null, so the null-aware operator '?.' is unnecessary
*/

(null-safety導入前は)Dartはnull-aware演算子( ?. )が必要かどうかを知るすべがありませんでした。

(null-safety導入前は)上記サンプルでは、関数にnullを渡すことができます。

null-safety導入後は、引数はnon-nullableなList型と型宣言されていますので、引数listにnullが渡される可能性はありません。

ということでnull-safety導入後は上記サンプルでは、null-aware演算子( ?. )が働くことは無いので、通常の( . )演算子で十分です。

ということで、あなたのコードをシンプルにする助けとして、不要なコードに対してwarning(警告)を発生させるようにしました。

non-nullable型に対してnull-aware演算子、あるいは==null、または!=nullを使っている場合に警告が出ます。

そしてもちろん、この警告はnon-nullable型のpromotionに関してもあてはまります。

一度変数がnon-nullable型へとpromoteされた時、その後あなたが余分なnullチェックコードを書いている場合、警告が発生します。

// Using null safety:null-safety導入後
checkList(List? list) {
  if (list == null) return 'No list';
  if (list?.isEmpty) {//←ここに対して警告発生
    return 'Empty list';
  }
  return 'Got something';
}
/*
The receiver can't be null, so the null-aware operator '?.' is unnecessary
*/

一つ前の行ですでにlistがnullでないことが確認されていますので、

list?.isEmpty

という条件に対して、警告が発生します。

これらの警告の目的は、無意味なコードをクリーンアップすることだけではありません。

nullの不要なチェックを削除することで、残りの意味のあるチェックを目立たせることができます。 コードを見て、nullがどこに流れるかを確認できるようにしてください。


Working with nullable types

今私たちはnullをnullable型の中に囲いこみました。

フロー分析を使用すると、null以外の値をフェンスを越えさせて、non-nullableな側に安全に渡して使用することができます。

これは大きな一歩ですが、ここで止まってしまった場合、結果として得られるシステムは依然として非常に限定的です。

フロー分析は、ローカル変数とパラメーターでのみ役立ちます。

ダートがヌルセーフティの前に持っていた柔軟性をできるだけ取り戻そうとするために、そしていくつかの場所でそれを超えるために、私たちは他のいくつかの新機能を持っています。


Smarter null-aware methods

Dartのnull-aware演算子はnull-safetyよりも古くから存在するものです。

// Without null safety:null-safety導入前
String notAString = null;
print(notAString?.length);

レシーバー(↑のサンプルでのnotAString)がnullの時、プロパティアクセスはスキップされて、式の評価はnullとなります。

↑のサンプルでいうと、notAString?.lengthへのアクセスはスキップされて、

notAString?.length

はnullと評価されます。なので、上記のサンプルではnullと表示(print)されます。

null-aware演算子は、nullable型をDartで使用できるようにするための優れたツールです。

nullable型に対してメソッドを呼び出すことはできませんが、null-aware演算子を使用することはできます。

null-safety導入後のプログラムはこちらです↓。

// Using null safety:
String? notAString = null;
print(notAString?.length);

これで一つ前のサンプルと同じ挙動となります。


しかし、Dartでnull-aware演算子を使用したことがある場合、メソッドチェーンでそれらを使用するときに厄介なことに遭遇した可能性があります。

例えば、String?型の文字列(nullの可能性もある文字列)の長さが偶数かを調べたい場合、以下のようなコードになるでしょう。

// Using null safety:null-safety導入後
String? notAString = null;
print(notAString?.length.isEven);

このプログラムは?.を使用しますが、実行時に例外をスローします。

.isEvenのレシーバーはnotAString?.lengthの結果ですが、notAString?.lengthはnullと評価されます。

なので、notAString?.length.isEvenはnull reference errorを発生させます。

上記説明はnull-safety導入前の説明だと思われる。

//null-safety導入前(https://dartpad.dev/)

void main(){
  String notAString = null;
  print(notAString?.length.isEven);
}

//TypeError: Cannot read property 'get$isEven' of null
//↑コンパイルエラー出ない。
//↑実行時エラーが出る。
//notAStringがnullのとき、notAString?.lengthがnullになるので、
//null.isEvenの呼び出しとなるので実行時エラー。
//そうならないように必ず
//notAString?.length?.isEven
//と書かないといけない。(null-safety導入前)

(2021年2月16日時点:ドキュメントで↑のように説明されているが、実際

//null-safety導入後(https://nullsafety.dartpad.dev/dart)
void main(){
  try{
    String? notAString = null;
    print(notAString?.length.isEven);
  }catch(e){
    print(e);
  }
}
//null(例外は発生しない)

↑のコードをhttps://nullsafety.dartpad.dev/dartで動かしみると、例外はスローされずnullと表示される。

この解説が書かれた時点では例外がスローされていたのだろう多分。)

Dartでは、null-aware演算子を一度使用した後、チェーン内のすべてのプロパティまたはメソッドに適用する必要があるという難しい方法をあなたは学んだかもしれません↓。

String? notAString = null;
print(notAString?.length?.isEven);

これはうんざりしますが、さらに悪いことに、これでは重要な情報がわかりにくくなります。以下のサンプルを考えてみましょう。

// Using null safety:null-safety導入後
showGizmo(Thing? thing) {
  print(thing?.doohickey?.gizmo);
}

ここで質問です:

Can the doohickey getter on Thing return null? It looks like it could because you’re using ?. on the result. But it may just be that the second ?. is only there to handle cases where thing is null, not the result of doohickey. You can’t tell.

工事中🏗よくわからない。結局null-safety導入後は必要ない知識なはず笑

多分、nullが表示された時、その理由が「thingがnullだった」からか、それとも「thingはnullではなかったが、doohickeyがnullだった」からか、どちらかはっきりしない、ということか?

これを解決するために、C#の同じ機能のデザインのアイデアを借りてきました。

(多分null-safety導入後の話)メソッドチェーンの中でnull-aware演算子を使う時、レシーバーがnullと評価された場合、残りのメソッドチェーン全体はショートサーキット(短絡)されてスキップされます。

これは、もしdoohickeyがnon-nullable型なら、あなたは↓のように書くべきだということを意味します。

// Using null safety:
showGizmo(Thing? thing) {
  print(thing?.doohickey.gizmo);
}

実際に、↑のように書かない場合、不要なコードに関する警告が表示されます。

null-safety導入後にもし↓のようなコードを見たなら、

// Using null safety:
showGizmo(Thing? thing) {
  print(thing?.doohickey?.gizmo);
}

その時は、doohickeyの返り値の型はnullable型である、ということがわかります。

doohickeyがnon-nullable型のとき、doohickey?.gizmoと書くと警告が出る。

ということでdoohickey?.gizmoと書いてあるということは、こう書いて警告が出なかった、と推測できる。

ということはdoohickeyはnon-nullable型ではない、つまりnullable型である、と推測できる。

それぞれ?.が、 nullがメソッドチェーンに流れ込む可能性のある一意のパスに対応します。

↑のように書いているが、どちらの?.でnullが返ったかわかるのか?わからないんじゃないのか?

これにより、メソッドチェーン内のnull-aware演算子がより簡潔かつ正確になります。

//null-safety導入後
void main(){
  String? notAString = null;
  print(notAString?.length.isEven); //null
}
//↑null-safety導入後はshort-circuitedの仕組みがあるので、
//.lengthのようなnon-nullable型のプロパティ・ゲッター・メソッドには
//null-aware演算子(?.)は不要。

//null-safety導入後
class FanOfSoccer{
  FanOfSoccer({this.team});//(2)
  String? team;
}

void main(){ 
  FanOfSoccer? fan1=FanOfSoccer(team:'fctokyo');
  FanOfSoccer? fan2=null;
  FanOfSoccer? fan3=FanOfSoccer();
 
  print(fan1?.team?.length);//7
  print(fan2?.team?.length);//null
}
//fan2もteamもnullable型(nullになる可能性がある)の場合は、
//上記のように両方ともnull-aware演算子(?.)が必要。(null-safety導入後)

//null-safety導入後
class FanOfSoccer{
  FanOfSoccer({this.team});
  String? team;
}

void main(){ 
  FanOfSoccer? fan1=FanOfSoccer(team:'fctokyo');
  FanOfSoccer? fan2=null;
 
  print(fan1?.team.length);
  print(fan2?.team.length);
}
//fan2もteamもnullable型(nullになる可能性がある)の場合は、
//両方ともnull-aware演算子(?.)が必要。
//teamのnull-aware演算子を消すと
//An expression whose value can be 'null' must be null-checked before it can be dereferenced
//のエラーが発生する。(null-safety導入後)

 


//null-safety導入後(https://nullsafety.dartpad.dev/dart)
class FanOfSoccer{
  FanOfSoccer({this.team});
  String? team;
}

void main(){ 
  FanOfSoccer? fan1=FanOfSoccer(team:'fctokyo');
  FanOfSoccer? fan2=null;
    print(fan1.team?.length);//(1)
    print(fan2.team?.length);//(2)
}
//fan1,fan2のnull-aware演算子(?.)を消してみた。
//上記コードでコンパイルエラーが出るのは(2)のみ。
//(1)ではコンパイルエラーが出ない。(null-safety導入後)
//Flow analysisでfan1がnullじゃないことが確認できるから、ということか。

//「Flow analysisがある分エラーが出にくい」ということは言えると思う。
//ただそのことと、安全性の高いコードを書くことは別の話だろうから、
//開発者がnull安全について考える必要はあるだろう。
//基本的にnullable型へのnullチェックは必要だと思われる。

//結局Flow analysisによってエラーが出るか出ないかは、
//Dart言語の開発進捗状況により変わっていくっぽい。
//なので厳密には今動作を確認しても、後で挙動が変わることもある、ということ。
//ただ基本的には、Flow analysisの進歩でこれまで必要だった
//nullチェックなどがいらなくなることはあるだろうが、
//その逆はあまり無いのではないか、と予想される。
//(「いらなくなる」とは「無くてもエラーが出ない」という意味で
//安全性のために必要かどうかはまた別の話。)

 

 


さらに、他にいくつかのnull対応演算子を追加しました。

// Using null safety:null-safety導入後

// Null-aware cascade:
receiver?..method();

// Null-aware index operator:
receiver?[index];

null-aware関数呼び出し演算子はありませんが、次のように書くことができます。

// Allowed with or without null safety:
function?.call(arg1, arg2);

次ページへ>>

参考

https://dart.dev/null-safety/understanding-null-safety

 

カテゴリーDart

コメントを残す

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