のんびり精進

調べた情報などをおすそ分けできれば。

【Dart】シンプルに派生クラスを作る(別名の代わり)

クラスに別名を付けることができれば後述の問題が解決するのですが、今のところできないため、代わりに 元のクラスと機能は同じままで継承するシンプルな方法がないか調べてまとめた記事です。

※タイトルに「複製」という言葉を使っていましたが、同じものが出来上がるわけではないので変更しました。 id:ntaoo さんありがとうございました!

悩んだ問題

Flutter で provider を使うときに少し悩むことがあります。
同じ型の値を複数渡せないという問題です。
ListMap に入れるとできるのですが、ちょっとぎこちないやり方に思えます。

List 等を使わない場合、次のようになります。

MultiProvider(
  providers: [
    Provider<TextEditingController>(
      builder: (_) => TextEditingController(),  // a
    ),
    Provider<TextEditingController>(
      builder: (_) => TextEditingController(),  // b
    ),
  ],
  child: Hoge(),
)
class Hoge extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 二つとも b のオブジェクトになってしまう
    final titleController = Provider.of<TextEditingController>(context);
    final bodyController = Provider.of<TextEditingController>(context);

    return ...;
  }
}

使用箇所に最も近い TextEditingController (b のほう)しか取れません。

解決策

これは同じ型だから起こります。
それなら異なる型名を使えばいいですね。 そこで、クラスにエイリアス(別名)を付けることを考えたのですが、現時点ではできないことがわかりました。

代わりに同じ中身の別クラスを作ることなら可能です。

1) べたな方法

class TitleEditingController extends TextEditingController {}

2) 別の方法

class TitleEditingController = TextEditingController with Type;

with Type とは

もし 2 で with Type を付けて Type というものを mixin しないとエラーが出ます。

Classes can only mixin other classes.

Type の定義を見ると、中身のない抽象クラスになっています。

abstract class Type {}

形だけでも mixin しないといけないということですね。
これはこれであまり美しくない感じがしますが、仕方ないですね。

注意

先ほども書いたのですが、これはエイリアスではありません。
既成のクラスに Mixin 適用して作った新たなクラス/型です。

typedef for simple type aliases · Issue #2626 · dart-lang/sdk · GitHub

class Foo = Bar with Mixin syntax is used to create a new type. Such that Foo != Bar

元の型とは当然別物として扱われます。
名前だけ異なる同じ型だと考えて使うと失敗する場合があるので注意してください。

class Foo = Bar with Type;

print(Foo == Bar);  // false(同じ型ではない)

Bar bar = Foo();    // OK
Foo foo = bar;      // OK
foo = Bar();        // エラー

作ったクラスのオブジェクトを元のクラスの型として扱うこと(アップキャスト)は可能です。
そのため、TextEditingController を渡す部分に代わりに TitleEditingController を渡すことができます。

ダウンキャストについては、アップキャストしたものを本来の型にダウンキャストするのはOKですが、いきなりダウンキャストするのはエラーになります。

制限

プリミティブな型には使えない

プリミティブな型にも使えると便利なのですが、それはできないようです。
例えば、int を基にして myInt を作ろうとしてもエラーになります。

// Classes can't extend 'int'. というエラーが出る
class myInt = int with Type;

親のコンストラクタに引数があるなら継承する方法は不向き

親クラスが引数のあるコンストラクタを持っている場合、子クラスで親のコンストラクタを呼ばないといけないため、結局シンプルにできません。

Mixin する方法では const でなくなる

親が const コンストラクタを持っていても、with Type のほうの方法で作ったクラスのコンストラクタは const になりませんでした。

今後の改善

クラスにもプリミティブな型にも typedefエイリアスを付けられるようになりそうです。
このことは先ほどと同じ issue でコメントされています。

As of 02bb437 (Jan 11, 2019) it is part of the language specification that you can write type aliases like typedef C = D; and typedef D = Map<X, List>;, etc.

There's an implementation plan. Right now some other things are being pushed harder than this feature, but it's accepted and in the pipeline.

Note that you can not use this feature to create new types (similar to Haskell's newtype or Pascal's type Temperature = Integer;), it creates an additional notation for an existing type. So if you declare typedef D = C; then it is type correct to have things like D d = C();.

typedef for simple type aliases · Issue #2626 · dart-lang/sdk
https://github.com/dart-lang/sdk/issues/2626#issuecomment-464307003
https://github.com/dart-lang/sdk/issues/2626#issuecomment-464638272

続きの議論は下記 issue で行われています。

github.com

定義の例

typedef C = D<int>;
typedef E<X extends num> = Map<X, List<X>>;
typedef UserId = int;

このように定義した型は、新たな型という扱いではなくちゃんと別名になるようです。
しかしまだ仕様が確定したわけではないと思います。

上記コメントの時点から半年以上経っていて、いつになるのかわかりません。
他の新機能の導入に人手を取られているようです。
気長に待ちましょう。

Dart 2.13 以降の typedef

Dart 2.13 にて typedef が関数以外の型にも使えるようになりました。

medium.com

dart.dev

しかし typedef はあくまで別名であって元の型と同一なので、Provider.of<T> を使うときに別名で区別しようとしてもダメでした。