💡

Flutter開発の準備運動「クラスに関する文法知識」これを理解しないと読むのが辛い文法まとめ

2024/11/04に公開

はじめに

みなさん、こんにちは。

今回はDartのクラスに関連する文法についてまとめています。

他の言語を習得済みの方の差分理解の手助けになれば幸いです。

(特にJavaなどのクラスベースの言語を習得をした方)

クラス

概要

  • 全てのオブジェクトはクラスのインスタンスであり、null以外の全てのクラスはObjectクラスのサブクラス
  • オブジェクトリテラルはない(Mapがそれに相当)

Dartはクラスベースのオブジェクト指向言語であり、クラスが活用されています。JavaScriptにあるようなオブジェクトリテラルの表現はなく、全てのオブジェクトはクラスから生成されたインスタンスになっています。

class宣言

classキーワードで宣言します。もちろんクラス名は先頭大文字です。メンバはコンストラクタ、インスタンス変数、メソッドです。順番は問われませんが、コンストラクタが先頭になっていることが多いように思います。その一方でDart公式のサンプルはインスタンス変数、コンストラクタ、メソッドの順番になっています。

コンストラクタ宣言

コンストラクタの引数をthis.インスタンス変数名とすることで、インスタンス化時にインスタンス変数を初期化できます。

インスタンス変数宣言

インスタンス変数は型を指定して宣言します。型を省略することも可能ですが、警告が出ます(DartPadで確認)。変更予定がない場合はfinalキーワードをつけます。

メソッド宣言

クラス内で関数宣言と同じ形式でメソッドを宣言します。

// 宣言
class Item {
  // コンストラクタ
  Item(this.title, this.price);
  
  // インスタンス変数
  final String title;
  int price;

  // メソッド
  String createMessage(String phrase){
    return phrase + ': [$title, $price]';
  }
}

インスタンス

概要

  • インスタンス化はnew不要
  • インスタンス変数名を指定することで参照、更新
  • メソッド名を指定することで実行
  • カスタムgetter、setterを提供する機能がある。getおよびsetキーワードを利用する

インスタンス化

インスタンス化する際はnewキーワードは使わず、コンストラクタを指定して行います。

final item = Item('abc', 200);

インスタンス変数の参照

インスタンス.インスタンス変数名 とすることでインスタンス変数を参照することができます。インスタンス変数がfinalではない場合、=で代入することで値を更新することができます。

// 参照
print(item.title);
// 更新
item.price = 10;

メソッドの呼び出し

メソッドの呼び出しはインスタンス.メソッド名(引数)とします。

// 引数を渡してメソッドを実行
item.createMessage('message');

カスタムgetter、setter

クラスには独自のgetter、setterを用意することもできます。インスタンス変数をprivateにしている場合は、インスタンスから直接アクセスできないのでカスタムgetterとsetterを用意します。

カスタムgetterはgetキーワードで宣言します。

戻り値型 get カスタムgetter名 => インスタンス変数名;

カスタムsetterはsetキーワードで宣言します。

set カスタムsetter名(引数){
    インスタンス変数を更新する処理
}

カスタムgetter、setterを持つクラスの例

class Car {
  Car(this._name, this._fuel);

  // privateはインスタンス変数
  final String _name;
  int _fuel;

  // カスタムgetter
  String get name => _name;
  int get fuel => _fuel;

  // カスタムsetter
  set fuel(int newFuel) {
    _fuel = newFuel > 0 ? newFuel + _fuel : _fuel;
  }
}

コンストラクタの特徴的な仕様

概要

  • constantコンストラクタ:不変なインスタンスを生成
  • 名前付きコンストラクタ:複数のコンストラクタを定義
  • factoryコンストラクタ:インスタンス生成をカスタマイズ

クラスのコンストラクタには通常のもの以外にいくつかの種類があります。

constantコンストラクタ

constantコンストラクタは不変なインスタンスを生成するためのコンストラクタです。処理によってインスタンス変数の値を変更することができません。よってインスタンス変数はfinalで宣言します。

コンストラクタ呼び出しにはconstキーワードを付与し、コンパイル時にインスタンスが生成されます。タイミングがコンパイル時というのがポイントであり実行時ではないため、実行時の状況によって動的に変わる値をコンストラクタの引数に渡すことはできません。

void main() {
  // インスタンス化はconstを付与
  final brown1 = const Color(165, 42, 42);
  final brown2 = const Color(165, 42, 42);
  final yellow = const Color(255, 255, 0);
  print(brown1 == brown2); // -> true(同じインスタンスが再利用されてるので)
}

// 色クラス
class Color {
  // constantコンストラクタ
  const Color(this.red, this.green, this.blue);

  // インスタンス変数は全てfinal
  final int red;
  final int green;
  final int blue;
}

名前付きコンストラクタ

複数のコンストラクタを宣言する際は名前付きコンストラクタとします。クラス名.コンストラクタ名(引数リスト)とすることで宣言できます。名前付きコンストラクタは複数宣言することができます。

// Itemクラスの名前付きコンストラクタ
Item.newItem(this.name);

インスタンス化はクラス名.コンストラクタ名(引数);で行います。

void main() {
  // インスタンス化
  final newItem = Item.newItem('abc');
  final dummyItem = Item.dummyItem('abc');
}

// 商品クラス
class Item {
  // 通常のコンストラクタ
  Item(this.name, this.price, this.onSale);

  // 名前付きコンストラクタ(新商品用)
  Item.newItem(this.name)
      : price = 100,
        onSale = false;

  // 名前付きコンストラクタ(ダミー商品用)
  Item.dummyItem(this.name)
      : price = 0,
        onSale = false;

  // インスタンス変数
  String name;
  int price;
  bool onSale;
}

factoryコンストラクタ

factoryコンストラクタはインスタンス生成をカスタマイズできる仕組みです。コンストラクタに処理を持たせ任意のインスタンスを返却することができます。factoryキーワードを付与して宣言し、自身の型かサブクラス型のインスタンスを戻り値に指定します。

// ファクトリーコンストラクタ(returnでインスタンスを返す)
factory User.simple() {
  return User();
}

処理は=>を利用した省略表現も利用可能です。

// ファクトリーコンストラクタ(省略記法も可能)
factory User.allow() => User();

void main() {
  // ファクトリーコンストラクタの呼び出し
  final user = User.simple();
}

class User {
  // キャッシュ保持のためのクラス変数
  static final Map<String, User> _cache = {};

  // 通常のコンストラクタ
  User();

  // ファクトリーコンストラクタ(returnでインスタンスを返す)
  factory User.simple() {
    return User();
  }

  // ファクトリーコンストラクタ(省略記法も可能)
  factory User.allow() => User();

  // ファクトリーコンストラクタ(子クラスのインスタンス)
  factory User.admin() => Admin();

  // ファクトリーコンストラクタ(キャッシュからインスタンスを返却)
  factory User.fromCache(String key) {
    // 保存済みのインスタンスを取得
    final cachedObj = _cache[key];

    // 既存のインスタンスを取得できたらそれを返す
    if (cachedObj != null) {
      return cachedObj;
    // なければインスタンスを生成し、キャッシュに保存しつつ返す
    } else {
      final user = User();
      _cache[key] = user;
      return user;
    }
  }
}

// 子クラス
class Admin extends User {}

継承・インターフェース

概要

  • extendsキーワードで継承
  • abstractキーワードで抽象クラス
  • sealedクラスは継承可能なクラスを制限
  • abstract interface classでインターフェース

Dartの継承はクラスの拡張という言い方をします。ここでは言い慣れてる継承で統一します。classの修飾子を利用するにはDart3以上が必要です。

継承

extends キーワードでクラスの継承を行います。

// 子クラス
class Admin extends User {

子クラスのコンストラクタは親クラスのコンストラクタを呼び出す必要があります。デフォルトコンストラクタが有効な場合は暗黙的に呼び出されます。

親クラスのコンストラクタ呼び出しは、2通りの書き方があります。親クラスの通常のコンストラクタを呼び出す場合は子クラス(super.親のインスタンス変数名);と書きます。

// コンストラクタ(親クラスのコンストラクタを呼び出す)
Admin(super.name);

上記の書き方はもう一つの書き方の糖衣構文として用意されたものです。もう一つの書き方は親クラスの名前付きコンストラクタも指定することができます。子クラス名(引数) : super(引数);と書きます。

// コンストラクタ(別の書き方、名前付きコンストラクタの呼び出し)
Admin() : super.named();

// 糖衣構文をもう一つの書き方で直した例
Admin(String name) : super(name);

親クラスのメソッドを子クラスでオーバーライドすることができます。@overrideアノテーションを付けることが推奨されています。

// オーバーライド

void showMessage() {
  print('This is admin user $name');
}

全体像

// 親クラス
class User {
  String name;

  // 通常のコンストラクタ
  User(this.name);
  
  // 名前付きコンストラクタ
  User.named():name = 'xxx';

  void showMessage() {
    print('I am $name');
  }
}

// 子クラス
class Admin extends User {
  // コンストラクタ(親クラスのコンストラクタを呼び出す)
  Admin(super.name);
  
  // コンストラクタ(別の書き方、名前付きコンストラクタの呼び出し)
  // Admin() : super.named();

  // オーバーライド
  
  void showMessage() {
    print('This is admin user $name');
  }
}

抽象クラス

abstract キーワードで抽象クラスを宣言することができます。他の言語と同様に抽象クラスには具象メソッドの定義も可能です。抽象クラス自体をインスタンス化することはできません。

抽象メソッドの宣言は戻り値型 メソッド名(引数);です。抽象クラスを継承した具象クラスでは全ての抽象メソッドをオーバーライドします。

// 抽象クラス
abstract class Animal {
  // インスタンス変数
  String name;
  
  // コンストラクタ
  Animal(this.name);

  // 具象メソッド
  void sayHello() {
    print('hello');
  }

  // 抽象メソッド
  void eat();
}

// 子クラス(具象クラス)
class Dog extends Animal {
  Dog(super.name);

  
  void eat() {
    print('I eat meat');
  }
}

sealedクラス

sealedクラスは継承可能なクラスを同一ファイル内に限定する機能です。sealedキーワードでsealedクラスを宣言し、子クラスは同一ファイル内でのみ定義可能です。

// sealedクラス
sealed class AppState {}

// 子クラス
class StartAction extends AppState {}
class InputMessage extends AppState {}
class LodingData extends AppState {}

sealedクラスはswitch式などと組み合わせて利用されます。switch式では全てのパターンを網羅しなければコンパイルエラーになるため、子クラス全てを条件に指定します。

sealedクラスの定義は1ファイルにまとまるので、分岐パターンを理解しやすくなります。

final appState = LodingData();

// switch式と組み合わせて利用
final message = switch (appState) {
  StartAction() => 'スタートしました',
  InputMessage() => '入力しました',
  LodingData() => 'ロード中です',
};

print(message);

インターフェース

Dartには明確なインターフェースの言語仕様が存在しません。クラスを宣言すると暗黙的にインターフェースも定義されます。これを利用しインターフェースを作成します。

// 暗黙的なインターフェース(通常のクラス宣言)
class Account {
  // 抽象メソッドの定義はできない
}

他の言語にもあるような抽象メソッドのみを定義したインターフェースを作成するにはabstract interface classキーワードで宣言します。もちろんインスタンス化もできません。

// 純粋なインターフェース
abstract interface class Account {
  // 抽象メソッド
  void doSome();
}

implementsキーワードで実装クラスを定義します。インターフェースをabstract interface classキーワードで宣言した場合は、抽象メソッドのオーバーライドが必要です。実装クラスはインスタンス化できます。

// 純粋なインターフェースの実装クラス
class User implements Account{
  
  void doSome(){
    print('I do something');
  }
}

可視性・修飾子

概要

  • Dartには可視生をコントロールするキーワードはない。デフォルトはpublicで_(アンダーバー)始まりで命名するとprivateとして扱われる。可視性はファイル単位。_でprivate
  • フィールドのlate修飾子
  • partで生成されたコードを取り込む

Dartで見かける見慣れないキーワードを見ていきます。

「_」でプライベート

Dartはファイル単位で可視性を設定できます。_を先頭に付与するとそのファイル内でのみ参照可能になります。

// プライベートなクラス
class _SampleClass{
  // プライベートなインスタンス変数
  int _sampleNum = 0;
  
  // プライベートなメソッド
  void _doSome() => print();
}

lateで遅延初期化

変数やインスタンス変数にlateキーワードを付与することで、遅延初期化を示すことができます。コンパイラに対して遅延初期化を示すことで初期化せずともコンパイラのチェックを避け、コンパイルが通ります。

大きなデータなど初期化処理のコストが高い場合に有効です。またいつ使うか分からない、使うか分からない、すぐには使わないといったデータの初期化を後回しにして、値を利用する直前にインスタンス化することでメモリの節約になります。

// 遅延初期化する変数
late final title;
class Item{
  // 遅延初期化するインスタンス変数
  late String name;
  
  // 初期化用のメソッド
  void initialize(String data){
    name = data;
  }
}

partで生成コードの取り込み

Flutterのライブラリによってはコード生成を行うことがあります。コード生成したものはpartキーワードで取り込みます。

おわりに

Dartはクラスベースのオブジェクト指向であり、基本は他のクラスベースの言語と同じような書き方でした。その一方でインターフェースは仕組みを持っていないというのは気づきでした。

コンストラクタの書き方は特徴的なものがあり、この辺りは慣れが必要であると考えます。

Discussion