2021/2/15 Learn (null-safety) with Snippets! 8〜14の訳

<< 2021/2/15 Learn (null-safety) with Snippets! 1〜7の訳(前ページ


Snippets 8: The assertion operator

nullableな式をnon-nullableな変数に代入したい場合、代入演算子を使うことができます。(` ! `付きで。)

式の後ろに` ! `を付けることで、Dartに対してその式の値がnullでないことを示すことができます。

これはnon-nullableな変数へ安全に代入する方法です。

Note:nullでないことが確実な時に上記の方法を使ってください。値がnullである式に` ! `を付けると例外がthrow(スロー)されます。

` ! `を追加して、下記のコードの3つの間違った割り当て(代入)を修正してみてください。

↑のように書いているが、実際動かしてみると、最初の文はエラーは出ない。(2021年2月15日時点)

ただ、そのような挙動の違いがあっても、基本的な考え方として「nullableな変数はnullチェックが必要」ということは言えるはず。

int? couldReturnNullButDoesnt() => -3;

void main() {
  int? couldBeNullButIsnt = 1;
  List<int?> listThatCouldHoldNulls = [2, null, 4];
  
  int a = couldBeNullButIsnt!;
  int b = listThatCouldHoldNulls.first!;
  int c = couldReturnNullButDoesnt()!.abs();
  
  print('a is $a.');
  print('b is $b.');
  print('c is $c.');
}
/*
a is 1.
b is 2.
c is 3.
*/

上記コードで

The '!' will have no effect because the receiver can't be null

変数aの文で上記警告が出る。警告、エラー表示の設定次第なのかもしれないが、何なんだろうかこれは。



Snippets 9: The late keyword

クラス内のフィールドをnon-nullableにすべき場面ではあるが、すぐに値を割り当てる(代入する)ことができない場合があります。
そのような場合は、 `late`キーワードを使用してください。

これは、Dartに次のことを伝える方法です。

  • 今すぐにそのフィールドに値を代入しようとはしていない。
  • しかし後にそのフィールドに値を代入するつもりだ。
  • そしてそのフィールドにアクセスする前に確実に値を代入したい。

クラスのフィールドを`late`を付けて宣言して、そのフィールドに値がセットされる前にアクセスされた場合、`LateInitializationError`がスローされ、開発者に問題が示されます。

`late`キーワードを使って下記のサンプルコードを正しく修正しましょう。

後で少し楽しみたい場合は、「description(説明)」を設定する行をコメントアウトしてみてください。

class Meal {
  String description;//←エラー発生
  
  void setDescription(String str) {
    description = str;
  }
}

void main() {
  final myMeal = Meal();
  myMeal.setDescription('Feijoada!');
  print(myMeal.description);
}
/*
error
Non-nullable instance field 'description' must be initialized
non-nullableなインスタンスフィールド'description'は初期化しないといけません。
*/

まず修正前のコード。上記のエラーが出る。

初期化しないと、「インスタンス生成時に自動的にnullで初期化される」のがこれまでのデフォルトの挙動。しかし、descriptionはString型、つまりnon-nullable型なので、nullで初期化できない。

ということで上記のエラーが発生する。


class Meal {
  late String description;
  void setDescription(String str) {
    description = str;
  }
}

void main() {
  final myMeal = Meal();
  myMeal.setDescription('Feijoada!');
  print(myMeal.description);
}
/*
Feijoada!
*/

descriptionフィールドの宣言にlateキーワードを追加するとエラーが出なくなる。

上記のように、non-nullable型であっても、アクセスされるまでに値をセットすれば問題無い。


解説で言われている通り、setDescriptionメソッド実行部分をコメントアウトする。

class Meal {
  late String description;
  void setDescription(String str) {
    description = str;
  }
}

void main() {
  try{
    final myMeal = Meal();
    //myMeal.setDescription('Feijoada!');
    print(myMeal.description);
  }catch(e){
    print(e);
  }
}
/*
LateInitializationError: Field 'description' has not been initialized.
*/

エラーがスローされる、ということなので、tryブロックで囲む。

解説の通り、`LateInitializationError`がスローされている。

 



Snippets 10: Late circular references

`late`キーワードは、循環参照(circular references)のようなトリッキーなパターンに対して非常に有用です。

以下のように二つのオブジェクトが互いにnon-nullableな参照を保持するような状況です。

`late`キーワードを使って下記のサンプルを修正しましょう。

class Team {
  late final Coach coach;
}

class Coach {
  late final Team team;
}

void main() {
  final myTeam = Team();
  final myCoach = Coach();
  myTeam.coach = myCoach;
  myCoach.team = myTeam;
  
  print ('All done!');
}
/*
All done!
*/

 

lateキーワードを付けた時、`final`を消しましたか?

もし消したなら、もう一度付けてください。

`late`を付けたフィールドはfinalにできます。finalを消す必要はありません。

late`final`を付けたフィールドに値がセットされると、その後そのフィールドはread-only(読み取り専用)になります。

 



Snippets 11: Late and lazy

`late`が役立つ別のパターンは次のとおりです。高価なnon-nullableフィールドの遅延初期化(lazy initialization)。

まずこのコードを変更せずに実行してみてください。

次に`_cache`を` late`フィールドにすると何が変わると思いますか?

//修正前

int _computeValue() {
  print('(2)Computing value...');
  return 3;
}

class CachedValueProvider {
  final _cache = _computeValue();
  int get value => _cache;
}

void main() {
  print('(1)Calling constructor...');
  var provider = CachedValueProvider();
  print('(3)Getting value...');
  print('(4)The value is ${provider.value}!');
}
/*
(1)Calling constructor...
(2)Computing value...
(3)Getting value...
(4)The value is 3!

*/

(修正前)

CachedValueProviderのインスタンス生成時に_computeValue関数が呼び出されるので上記のような順番になる。


//修正後

int _computeValue() {
  print('(2)Computing value...');
  return 3;
}

class CachedValueProvider {
  late final _cache = _computeValue();
  int get value => _cache;
}

void main() {
  print('(1)Calling constructor...');
  var provider = CachedValueProvider();
  print('(3)Getting value...');
  print('(4)The value is ${provider.value}!');
}
/*
(1)Calling constructor...
(3)Getting value...
(2)Computing value...
(4)The value is 3!
*/

(修正後)

lateキーワードを付けたので、provider.valueに最初にアクセスされる時に_cacheの初期化が行われる。つまり_computeValue関数が呼び出される。

よって上記のような順番になる。


興味深い事実:_cacheフィールドの宣言に`late`を追加すると、_computeValue関数を`CachedValueProvider`クラスの定義内に移動することができ、そうしてもコードは機能します。

`late`を付けたフィールドの初期化の式にはインスタンスメソッドを使うことができます。

//解説通りに、_computeValue()の定義をクラス定義内に移動させたサンプル

class CachedValueProvider {
  late final _cache = _computeValue();
  int get value => _cache;
  
  int _computeValue() {
    print('(2)Computing value...');
    return 3;
  }
}

void main() {
  print('(1)Calling constructor...');
  var provider = CachedValueProvider();
  print('(3)Getting value...');
  print('(4)The value is ${provider.value}!');
}
/*
(1)Calling constructor...
(3)Getting value...
(2)Computing value...
(4)The value is 3!
*/

多分インスタンス生成完了後でないとメンバメソッドを呼び出すことができない、ということだと思われる。

なのでコンストラクタ呼び出し時に_cacheをメンバメソッド(_computeValue)で初期化することはできない、と。

ただ`late`キーワードを付ければ、メンバメソッドの呼び出しは、「インスタンス生成時」ではなく「一番最初にゲッターvalueにアクセスされた時」になる。

「一番最初にゲッターvalueにアクセスされた時」は「インスタンス生成完了後」なので、インスタンスメソッドを使った初期化が可能、ということだと思われる。

まあこれはこれで、こういうことがあるのだろうが、Flutterに関してはあまりこれが問題になることは無いような気がする。



Implicit animations(null safe)

//lateキーワードが無いバージョン

class DiscData {
  static final _rng = Random();

  final double size; //初期化が無いとエラー
  final Color color; //初期化が無いとエラー
  final Alignment alignment; //初期化が無いとエラー

  DiscData() {
    color = Color.fromARGB(
      _rng.nextInt(200),
      _rng.nextInt(255),
      _rng.nextInt(255),
      _rng.nextInt(255),
    );
    size = _rng.nextDouble() * 40 + 10;
    alignment = Alignment(
      _rng.nextDouble() * 2 - 1,
      _rng.nextDouble() * 2 - 1,
    );
  }
}

DiscDataクラスのフィールドがfinalでなおかつnon-nullable型。

フィールドがfinalでなおかつnon-nullable型の場合、初期化が必要(lateが無い場合)。

late無しで初期化も無いと、

All final variables must be initialized, but 'alignment', 'color', and 1 others aren't

のエラーが発生する。


//lateキーワードを付ければ解決
class DiscData {
  static final _rng = Random();

  late final double size;
  late final Color color;
  late final Alignment alignment;

  DiscData() {
    color = Color.fromARGB(
      _rng.nextInt(200),
      _rng.nextInt(255),
      _rng.nextInt(255),
      _rng.nextInt(255),
    );
    size = _rng.nextDouble() * 40 + 10;
    alignment = Alignment(
      _rng.nextDouble() * 2 - 1,
      _rng.nextDouble() * 2 - 1,
    );
  }
}

lateキーワードを付けると、フィールド宣言時の初期化が無くても、最初のアクセスまでに初期化できれば問題無い。


animationについて。

//https://nullsafety.dartpad.dev/
//implicit animation
//null-safe無しでやる。
//まずAnimatedContainerのサンプル。
/*
import 'package:flutter/material.dart';
import 'dart:math';

class DiscData {
  static final _rng = Random();

  DiscData(this.size, this.color, this.alignment);

  final double size;
  final Color color;
  final Alignment alignment;

  //static DiscData createInstance() {
  factory DiscData.random() {
    return DiscData(
        _rng.nextDouble() * 350 + 40,
        //200.0,
        Color.fromARGB(
          _rng.nextInt(200),
          _rng.nextInt(255),
          _rng.nextInt(255),
          _rng.nextInt(255),
        ),
        Alignment(
          _rng.nextDouble() * 2 - 1,
          _rng.nextDouble() * 2 - 1,
        ));
  }
/*
  DiscData() {
    color = Color.fromARGB(
      _rng.nextInt(200),
      _rng.nextInt(255),
      _rng.nextInt(255),
      _rng.nextInt(255),
    );
    size = _rng.nextDouble() * 80 + 10;
    alignment = Alignment(
      _rng.nextDouble() * 2 - 1,
      _rng.nextDouble() * 2 - 1,
    );
  }

 */
}

void main() async {
  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Container(
          color: Color(0xFF15202D),
          child: SizedBox.expand(
            child: VariousDiscs(),
          ),
        ),
      ),
    ),
  );
}

class VariousDiscs extends StatefulWidget {
  @override
  _VariousDiscsState createState() => _VariousDiscsState();
}

class _VariousDiscsState extends State<VariousDiscs> {
  DiscData disc;

  @override
  void initState() {
    super.initState();
    _makeDisc();
  }

  void _makeDisc() {

    setState(() {
      disc = DiscData.random();
    });
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap:_makeDisc,
      child: Stack(
        children: [
          Center(
            child: Text(
              'Click a disc!',
              style: TextStyle(color: Colors.white, fontSize: 50),
            ),
          ),
          Align(
            alignment:Alignment(1.0,1.0,),
            child: AnimatedContainer(
              duration: Duration(milliseconds: 2000),
              decoration: BoxDecoration(
                color: disc.color,
                shape: BoxShape.circle,
              ),
              height: disc.size,
              width: disc.size,
            ),
          ),
        ],
      ),
    );
  }
}

//https://nullsafety.dartpad.dev/
//implicit animation
//null-safe無しでやる。
//次にAnimatedAlignのサンプル。
//色とサイズの変化のアニメーションはAnimatedContainerの担当なので、
//このサンプルだと色は一瞬で切り替わっている。位置はアニメーション。
//サイズは200.0で固定。サイズをランダムにしても、結局アニメーションにはならず
//一瞬でサイズが変わる。
import 'package:flutter/material.dart';
import 'dart:math';

class DiscData {
  static final _rng = Random();

  DiscData(this.size, this.color, this.alignment);

  final double size;
  final Color color;
  final Alignment alignment;

  //static DiscData createInstance() {
  factory DiscData.random() {
    return DiscData(
        _rng.nextDouble() * 350 + 40,
        //200.0,
        Color.fromARGB(
          _rng.nextInt(200),
          _rng.nextInt(255),
          _rng.nextInt(255),
          _rng.nextInt(255),
        ),
        Alignment(
          _rng.nextDouble() * 2 - 1,
          _rng.nextDouble() * 2 - 1,
        ));
  }
/*
  DiscData() {
    color = Color.fromARGB(
      _rng.nextInt(200),
      _rng.nextInt(255),
      _rng.nextInt(255),
      _rng.nextInt(255),
    );
    size = _rng.nextDouble() * 80 + 10;
    alignment = Alignment(
      _rng.nextDouble() * 2 - 1,
      _rng.nextDouble() * 2 - 1,
    );
  }

 */
}

void main() async {
  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Container(
          color: Color(0xFF15202D),
          child: SizedBox.expand(
            child: VariousDiscs(),
          ),
        ),
      ),
    ),
  );
}

class VariousDiscs extends StatefulWidget {
  @override
  _VariousDiscsState createState() => _VariousDiscsState();
}

class _VariousDiscsState extends State<VariousDiscs> {
  DiscData disc;

  @override
  void initState() {
    super.initState();
    _makeDisc();
  }

  void _makeDisc() {

    setState(() {
      disc = DiscData.random();
    });
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap:_makeDisc,
      child: Stack(
        children: [
          Center(
            child: Text(
              'Click a disc!',
              style: TextStyle(color: Colors.white, fontSize: 50),
            ),
          ),
          AnimatedAlign(
            duration:Duration(milliseconds: 1000),
            curve: Curves.easeInOut,
            alignment: disc.alignment,
            child: Container(
              decoration: BoxDecoration(
                color: disc.color,
                shape: BoxShape.circle,
              ),
              height: disc.size,
              width: disc.size,
            ),
          ),
        ],
      ),
    );
  }
}

 

 

参考

https://nullsafety.dartpad.dev/

の右上のメニューLearn with Snippets!

コメントを残す

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