2021/2/18 Understanding null safetyの訳パート4

<<前ページへ

Late final variables

lateとfinalと一緒に使うこともできます。

// Using null safety:
class Coffee {
  late final String _temperature;

  void heat() { _temperature = 'hot'; }
  void chill() { _temperature = 'iced'; }

  String serve() => _temperature + ' coffee';
}

通常のfinalなフィールドとは違い、「フィールドの宣言時」あるいは「コンストラクタのイニシャライゼーションリスト」により初期化する必要がありません。

実行時に遅延初期化できます。

初期化は一度だけ行えます。そしてそれは実行時にチェックされます。

一度初期化したフィールドに再度値を代入しようとすると、2度目以降の代入は例外がスローされます。

上記のサンプルで言うとheat()メソッドとchill()メソッドを両方とも実行するような感じですね。

これ(late+final)は、最終的に初期化され、その後は不変(immutable)になる状態をモデル化するための優れた方法です。

言い換えると、Dartの他の変数修飾子と組み合わせた新しいlate修飾子は、KotlinのlateinitとSwiftのlazyの特徴空間のほとんどをカバーします。

少しローカルな遅延評価が必要な場合は、ローカル変数で使用することもできます。

参考

https://dart.dev/null-safety/understanding-null-safety#late-final-variables


Required named parameters

(多分、これはコンパイルエラーが出ます、

こうすれば問題ありません、これも大丈夫です、こういうこともできます、

ということを示そうとしているセクションなのだと思われる。)

non-nullbale型引数に、(自動初期化も含め)nullを渡されないようにするために、タイプチェッカーは全てのオプションパラメータに対し、

「nullable型にする」

か、あるいは、

「デフォルト値を設定する」

かのどちらかを要求します。

//サンプル4-1
//null-safety導入後(https://nullsafety.dartpad.dev/)

void someFunc({int a,}){//←コンパイルエラー発生。
  print(a);
}

void main(){
  someFunc();
}

//↑non-nullable型のnamed-paraneterの場合、
//呼び出しで引数省略すると、引数にnullをセットすることになる。
//それはできないので、それを防ぐために
//(1)引数aにデフォルト値を設定する。
//(2)引数aの型をint?型(nullable型)にする。
//(1),(2)のどちらかをしないといけない。
//どちらもしてないこのコードでは下記のコンパイルエラーが出る。
//The parameter 'a' can't have a value of 'null' because of its type, and no non-null default value is provided
//引数aはnon-nullable型なのでnullをセットすることはできません、
//そしてaにnullでないデフォルト値が設定されていません。

 

//サンプル4-2
//null-safety導入後(https://nullsafety.dartpad.dev/)
//↓aにデフォルト値を設定するとコンパイルエラーは出なくなる。
void someFunc({int a=0,}){
  print(a);
}

void main(){
  someFunc();//0
}

 

//サンプル4-3
//null-safety導入後(https://nullsafety.dartpad.dev/)
//↓aの型をint?型にするとコンパイルエラーは出なくなる。
void someFunc({int? a,}){
  print(a);
}

void main(){
  someFunc();//null
}

 

//サンプル4-4
//null-safety導入後(https://nullsafety.dartpad.dev/)
//↓このように宣言すると引数aはnamed parameterだが
//必須パラメータとなる。

void someFunc({required int? a,}){
  print(a);
}

void main(){
  someFunc();
  //↑よって引数無しで呼び出そうとするとコンパイルエラーが出る。
  //The named parameter 'a' is required, but there's no corresponding argument 
  //aは必須引数ですが、対応する引数が渡されていません。
}

 

//サンプル4-5
//null-safety導入後(https://nullsafety.dartpad.dev/)
//↓このように宣言すると引数aはnamed parameterだが
//必須パラメータとなる。
//non-nullable型引数で、requiredを付けて必須パラメータにした。
//これも問題無い。
//この場合、必ずnull以外の引数が渡されるので、
//デフォルト値を設定する必要は無い。
//requiredを付けずにデフォルト値を設定したのがサンプル4-2
void someFunc({required int a,}){
  print(a);
}

void main(){
  someFunc(a:8,);//8
}

 

ではnullable型のnamed parameterでデフォルト値無しにしたい場合はどうでしょう?(つまりサンプル4-3のケース)

これは、呼び出し元(caller)が常に引数に値を渡すように要求することを意味します。 つまり、named parameterだがオプションではないパラメーター(必須パラメータ)にしたい、ということです。

↑なぜそうなるのかさっぱりわからない。

Dartのパラメータをテーブル(表)で可視化してみました。

             mandatory    optional
            +------------+------------+
positional  | f(int x)   | f([int x]) |
            +------------+------------+
named       | ???        | f({int x}) |
            +------------+------------+

理由はよくわかりませんが、Dartは長い間上記の表の三つのコーナーをサポートしてきましたが、左下の「named+mandatory」な引数はサポートしていませんでした。

mandatory:義務的な、強制の、必須の、命令の、委任の、

null-safetyのためにこの「named+mandatory」もサポートする必要があります。

関数宣言時に引数の前にrequiredを付けることで、必須のnamed parameterを宣言することができます。

function({int? a, required int? b, int? c, required int? d}) {}

このコードで、全ての引数はnameを指定して渡す必要があります(←a,cを省略する場合は除く)。(named parameterだから。named parameterについてはこちらをご覧ください。)

引数aとcはオプションなので省略する(引数に値を渡さない)こともできます。引数bとdは省略できません。(引数に値を渡さないといけない)

「引数が必須かどうか」はnull可能性とは独立しています。

あなたはnullable型の必須named parameterを持つことができます。(サンプル4-4)

あなたはデフォルト値を設定すれば、non-nullable型のオプションnamed parameterを持つことができます。(サンプル4-2)

これもまた、null-safetyに関係なくDartをより良くしていると思う機能の一つです。それは私にとって、言語がより完成されていると感じるようになります。

参考

https://dart.dev/null-safety/understanding-null-safety#required-named-parameters


Working with nullable fields

これらの新機能は多くの一般的なパターンをカバーし、ほとんどの場合nullの関係する作業を非常に簡単にします。

しかし、それでも、nullableフィールドは依然として難しくなる可能性があります。

フィールドを「late+non-nullable型」にすることができる場合、あなたは金色です。

しかし多くのケースであなたはフィールドが値を持っているかチェックする必要があり、そのためにはフィールドをnullable型にする必要があります。そうするとnullを監視できます。

あなたは次のコードを問題なく機能すると予想するかもしれません。

// Using null safety, incorrectly:
class Coffee {
  String? _temperature;

  void heat() { _temperature = 'hot'; }
  void chill() { _temperature = 'iced'; }

  void checkTemp() {
    if (_temperature != null) {
      print('Ready to serve ' + _temperature + '!');
    }
  }

  String serve() => _temperature! + ' coffee';
}

checkTemp()メソッドの中で、_temperatureの値がnullかを確認しています。

そしてnullでない場合それにアクセスして+演算子を使おうとしています。

しかしこれはできません(許されません)。

//サンプル4-6
//null-safety導入後
//https://dart.dev/null-safety/understanding-null-safety#working-with-nullable-fields

// Using null safety, incorrectly:
class Coffee {
  String? _temperature;

  void heat() { _temperature = 'hot'; }
  void chill() { _temperature = 'iced'; }

  void checkTemp() {
    
    if (_temperature != null) {
      print('Ready to serve ' + _temperature + '!');
      //↑この行でコンパイルエラー。
      //↑if文でnullチェックしているが、フィールドなので、+演算子を使うところで
      //「nullable型への+演算子適用」ということでコンパイルエラー。
    }
    
  }

  String serve() => _temperature! + ' coffee';
}

void main(){
  
}
/*
The argument type 'String?' can't be assigned to the parameter type 'String' 
*/

Flow analysisに基づいたtype promotion(ダウンキャスト)はクラスのフィールドにに対しては適用されません。

なぜなら静的分析は、「フィールドのnullチェックをする時点」と「フィールドにアクセス(読み取り)をする時点」の間にフィールドの値が変更されていないことを証明できないからです。

(異常なケースでは、フィールド自体が、2回目に呼び出されたときにnullを返すサブクラスのゲッターによってオーバーライドされる可能性があることを考慮してください。)

したがって、健全性を重視しているため、フィールドはプロモートせず、上記のメソッドはコンパイルされません。

これでは面倒くさいですね。

このサンプルのように単純なケースでのベストな方法は、以下のようにフィールド使用時に、! を使って強制アンラップすることです↓。

//サンプル4-7
void checkTemp() {
  if (_temperature != null) {
    print('Ready to serve ' + _temperature! + '!');
    //↑これでコンパイルエラーは出なくなる。
  }
}

冗長に思えますが、一応これが現時点のDartの振る舞いです。(2021年2月18日時点)


役立つもう1つのパターンは、最初にフィールドをローカル変数にコピーしてから、代わりにそれを使用することです。

// Using null safety:
void checkTemp() {
  var temperature = _temperature;
  if (temperature != null) {
    print('Ready to serve ' + temperature + '!');
  }
}

 

//サンプル4-8
//null-safety導入後
//https://dart.dev/null-safety/understanding-null-safety#working-with-nullable-fields

// Using null safety, incorrectly:
class Coffee {
  String? _temperature;

  void heat() { _temperature = 'hot'; }
  void chill() { _temperature = 'iced'; }

  void checkTemp() {
    var temperature = _temperature;//フィールドの値を一時変数にセット。
    if (temperature != null) {
      print('Ready to serve ' + temperature + '!');
    }
    //↑これならFlow analysisでtype promotionしてくれる
    //ので、コンパイルエラーは出ない。
  }

  String serve() => _temperature! + ' coffee';
}

void main(){
  Coffee coffee1=Coffee();
  coffee1.heat();
  coffee1.checkTemp();//Ready to serve hot!
}

type promotionはローカル変数には適用されますので、上記コードは問題なく機能します。

(多分checkTemp()の中で、フィールドの)値を変更する必要があるときは、ローカル変数に代入するのではなく、フィールドに値をセットすることを忘れないでください。

参考

https://dart.dev/null-safety/understanding-null-safety#working-with-nullable-fields


Nullability and generics

ほとんどの現代的な静的型付け言語と同様、Dartもgeneric(ジェネリック)型とgenericメソッドの仕組みがあります。

それらはそれらは、直感に反しているように見えるいくつかの方法でnull可能性と相互作用しますが、関係性(影響)を検討すると意味があります。

implication:包含、含蓄、含み、裏の意味、連座、密接な関係、掛かり合い

まず最初に注目したいことは、「この型はnullable型ですか?」という質問は、もはや「yes,no」で答えられるシンプルな質問では無い、ということです。

 

// Using null safety:
class Box<T> {
  final T object;
  Box(this.object);
}

main() {
  Box<String>('a string');
  Box<int?>(null);
}

Boxクラスの定義で、型引数のTはnullable型ですか?それともnon-nullable型ですか?

上記のサンプルを見るとわかるように、両方のケースがあります。

答えは、「Tはpotentially nullable typeです。」ということです。

potentially nullabel typeは、「nullable型になる可能性もある型」ということか。

↑工事中🏗

genericクラスやgenericメソッドのボディ内で、potentially nullable typeは、nullable型とnon-nullable型の両方の制限を持っています(制限を受ける)。

前者(nullable型の受ける制約)は、「Objectクラスで定義されているいくつかのメソッドをのぞいた他のメソッドを呼び出せない」ということです。

後者は(non-nullable型の受ける制約)は、「使用される前に、(non-nullable型である)あらゆるクラスのフィールドや関数のローカル変数を初期化しないといけない」ということです。

これは型引数の利用を非常に困難にさせます。

 

実際にいくつかのパターンを見てみましょう。

型引数を任意の型でインスタンス化できる、コレクションのようなクラスでは、制限に対処する必要があります。

ここでのサンプルのようにほとんどのケースで、それは必要な時に、その型引数の型の値に確実にアクセスできるようにすることです。

幸いなことに、コレクションのようなクラスでは、その要素に対してメソッドを呼び出すことは滅多にありません。

値にアクセスしない場所では、型パラメーターを利用してnullableにすることができます。

// Using null safety:
class Box<T> {
  T? object;
  Box.empty();
  Box.full(this.object);
}

↑objectフィールドの宣言で、型を

T?

にしているのに注目してください。明示的にnullable型と宣言していますので、初期化しなくても問題ありません。

 

ここでobjectの型をT?と宣言したので、nullの可能性を排除する必要が出てくる場合があります。正しいやり方は、!演算子を使うのではなく、明示的に

as T

とキャストすることです。

// Using null safety:
class Box<T> {
  final T? object;
  Box.empty();
  Box.full(this.object);

  T unbox() => object as T;
  //↑objectはT?型なのでasでキャストしてT型を返すようにしている。
}

!演算子は値がnullの時常に例外をスローします。しかし、もし型引数Tがnullable型に指定されてしてインスタンス化された場合、nullはそのTに対して完全にvalidな(有効な)値です。(だから!演算子で例外をスローされるのは避けないといけない)

//サンプル4-10
//null-safety導入後(https://nullsafety.dartpad.dev/)

// Using null safety:
class Box<T> {
  final T? object;
  Box.empty();//←この行でコンパイルエラー発生。
  Box.full(this.object);

  T unbox() => object as T;
}

// Using null safety:
main() {
  var box = Box<int?>.full(null);
  print(box.unbox());
}
//サンプルコピペして動かそうとすると、
//Box.empty();の行でコンパイルエラー。
//All final variables must be initialized, but 'object' isn't
//finalでnullable型のフィールドを初期化しないと、結局
//「一番最初に自動的にnullで初期化されてその後ずっとnullを
//保持し続けるフィールド」になるので、意味が無い、ということ
//でコンパイルエラーを出していると思われる。
//フィールド宣言の先頭にlateを付けるとコンパイルエラーは出なくなるが、
//lateが無いとそういうことになる。

 

//サンプル4-11
//null-safety導入後(https://nullsafety.dartpad.dev/)

// Using null safety:
class Box<T> {
  final T? object;
  Box.empty():object=null;//←initialization listで初期化。
  Box.full(this.object);

  T unbox() => object as T;
}

// Using null safety:
main() {
  var box = Box<int?>.full(null);
  //型引数Tにint?を設定している。
  //まあこういうこともできる、ということだろう。
  print(box.unbox());//null
}
//ということでBox.empty()でも、
//initialization listで初期化する。
//これでコンパイルエラーは出なくなる。

 

//サンプル4-12
//null-safety導入後(https://nullsafety.dartpad.dev/)

// Using null safety:
class Box<T> {
  final T? object;
  Box.empty():object=null;//←initialization listで初期化。
  Box.full(this.object);

  T unbox() => object!;//←!演算子でアンラップ
}

// Using null safety:
main() {
  var box = Box<int?>.full(null);
  print(box.unbox());//実行時エラーが発生する。
  /*
  Uncaught TypeError: Cannot read property 'toString$0' of nullError: TypeError: Cannot read property 'toString$0' of null
*/
}

//unboxメソッドの返り値をobject!にしてみる。
//この場合Tにint?(nullable型)が設定されていても、
//object!は例外をスローする。
//プログラムの意図として、「Tにnullable型を設定した場合、
//unboxメソッドはnullを返しても良いようにしたい」のであれば、この実装は「間違い」ということになる。

 

このプログラムはエラーを出さずに実行されるべきです。

as Tでキャストすれば、それが実現できます。!演算子を使うと例外がスローされてしまいます。


他のジェネリック型には、適用できる型引数の種類を制限するいくつかの制限があります。

// Using null safety:
class Interval<T extends num> {
  T min, max;

  Interval(this.min, this.max);

  bool get isEmpty => max <= min;
}

もし制限がnon-nullable型なら、型引数もnon-nullable型になります。

上記のサンプルだと、「型引数T型はnum型のサブクラスに限る」、ということ。

num型はnon-nullable型、そのサブクラスもnon-nullable型になる、ということ。

ということは、型引数T型のフィールドはnon-nullable型の制約を受ける、ということになります。

 

 

 

参考

https://dart.dev/null-safety/understanding-null-safety#nullability-and-generics


 

 

コメントを残す

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