0
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Flutter入門 第4回】Dart のクラスとオブジェクト指向 ― class・継承・抽象クラス・mixin

0
Posted at

はじめに

Flutter入門シリーズ第4回は Dart のクラスとオブジェクト指向 です。

Flutter のウィジェットはすべてクラスで構成されています。クラスの定義方法、継承、抽象クラス、mixin といったオブジェクト指向の仕組みを理解することは、Flutter アプリ開発の土台になります。

この記事では、Dart のオブジェクト指向に関する機能をコード例とともに一通り学びます。すべてのコード例は void main() {} を含む完全な形で掲載しているので、DartPad にそのまま貼り付けて実行できます。


クラスの基本定義

クラスはフィールド(データ)とメソッド(処理)をまとめた設計図です。class キーワードで定義し、クラス名() でインスタンスを生成します。

class Dog {
  String name = '';
  int age = 0;

  void bark() {
    print('ワンワン!');
  }

  void introduce() {
    print('名前: $name、年齢: $age歳');
  }
}

void main() {
  var dog = Dog();
  dog.name = 'ポチ';
  dog.age = 3;
  dog.introduce(); // 名前: ポチ、年齢: 3歳
  dog.bark();      // ワンワン!
}

コンストラクタ

基本のコンストラクタ

クラス名と同じ名前の関数がコンストラクタです。インスタンス生成時に値を渡せます。

class Dog {
  String name;
  int age;

  Dog(String name, int age) {
    this.name = name;
    this.age = age;
  }

  void introduce() {
    print('名前: $name、年齢: $age歳');
  }
}

void main() {
  var dog = Dog('ポチ', 3);
  dog.introduce(); // 名前: ポチ、年齢: 3歳
}

this を使った省略記法

Dart では、コンストラクタ引数に this.フィールド名 と書くことで、フィールドへの代入を省略できます。Flutter のコードで最も頻繁に見かける書き方です。

class Dog {
  String name;
  int age;

  Dog(this.name, this.age);

  void introduce() {
    print('名前: $name、年齢: $age歳');
  }
}

void main() {
  var dog = Dog('ハチ', 5);
  dog.introduce(); // 名前: ハチ、年齢: 5歳
}

名前付きコンストラクタ

Dart ではコンストラクタのオーバーロード(同名で引数違いの定義)ができません。代わりに 名前付きコンストラクタ を使います。

class Point {
  double x;
  double y;

  Point(this.x, this.y);

  // 名前付きコンストラクタ: 原点を生成
  Point.origin()
      : x = 0,
        y = 0;

  // 名前付きコンストラクタ: X 軸上の点を生成
  Point.onXAxis(double x)
      : x = x,
        y = 0;

  @override
  String toString() => 'Point($x, $y)';
}

void main() {
  var p1 = Point(3, 4);
  var p2 = Point.origin();
  var p3 = Point.onXAxis(7);

  print(p1); // Point(3.0, 4.0)
  print(p2); // Point(0.0, 0.0)
  print(p3); // Point(7.0, 0.0)
}

上の例で使われている : x = 0, y = 0 の部分は イニシャライザリスト と呼ばれ、コンストラクタ本体が実行される前にフィールドを初期化します。


ゲッターとセッター(get / set)

Dart では getset キーワードを使って、フィールドのようにアクセスできる計算プロパティを定義できます。

class Rectangle {
  double width;
  double height;

  Rectangle(this.width, this.height);

  // ゲッター: 面積を計算して返す
  double get area => width * height;

  // ゲッター・セッター: 周囲長
  double get perimeter => 2 * (width + height);

  set perimeter(double value) {
    // 現在の縦横比を維持しつつ周囲長を変更
    double ratio = width / height;
    // perimeter = 2 * (width + height), width = ratio * height
    // value = 2 * (ratio * h + h) = 2 * h * (ratio + 1)
    height = value / (2 * (ratio + 1));
    width = ratio * height;
  }
}

void main() {
  var rect = Rectangle(4, 3);
  print('面積: ${rect.area}');           // 面積: 12.0
  print('周囲長: ${rect.perimeter}');    // 周囲長: 14.0

  rect.perimeter = 28; // 周囲長を2倍にする
  print('幅: ${rect.width}');            // 幅: 8.0
  print('高さ: ${rect.height}');         // 高さ: 6.0
  print('面積: ${rect.area}');           // 面積: 48.0
}

計算の確認:

  • 初期状態: width=4, height=3area = 4*3 = 12.0perimeter = 2*(4+3) = 14.0
  • perimeter = 28 を設定: ratio = 4/3height = 28 / (2 * (4/3 + 1)) = 28 / (2 * 7/3) = 28 / (14/3) = 6.0width = (4/3) * 6.0 = 8.0
  • 結果: area = 8.0 * 6.0 = 48.0

private(_ アンダースコアプレフィックス)

Dart のアクセス制御は ライブラリ(ファイル)単位 です。フィールド名やメソッド名の先頭に _(アンダースコア)を付けると、そのライブラリの外からアクセスできなくなります。

DartPad のような単一ファイル環境ではすべて同じライブラリに属するため、_ を付けても同一ファイル内からアクセスできます。実際のプロジェクトで別ファイルに分割したときに効果が現れます。

class BankAccount {
  String owner;
  double _balance; // private フィールド

  BankAccount(this.owner, this._balance);

  double get balance => _balance; // ゲッターで読み取りのみ公開

  void deposit(double amount) {
    if (amount > 0) {
      _balance += amount;
      print('$owner: ${amount}円 入金 → 残高 ${_balance}円');
    }
  }

  void withdraw(double amount) {
    if (amount > 0 && amount <= _balance) {
      _balance -= amount;
      print('$owner: ${amount}円 出金 → 残高 ${_balance}円');
    } else {
      print('$owner: 出金できません(残高不足)');
    }
  }
}

void main() {
  var account = BankAccount('田中', 1000);
  print('残高: ${account.balance}円'); // 残高: 1000.0円
  account.deposit(500);                // 田中: 500.0円 入金 → 残高 1500.0円
  account.withdraw(200);               // 田中: 200.0円 出金 → 残高 1300.0円
  account.withdraw(2000);              // 田中: 出金できません(残高不足)
}

注意: Java や C++ のような privateprotectedpublic キーワードは Dart にはありません。_ プレフィックスがアクセス制御の唯一の手段です。


継承(extends)

extends キーワードで既存クラスを継承し、フィールドやメソッドを引き継げます。

class Animal {
  String name;

  Animal(this.name);

  void speak() {
    print('$name は鳴きます');
  }
}

class Cat extends Animal {
  String color;

  Cat(String name, this.color) : super(name);

  @override
  void speak() {
    print('$name$color): ニャー');
  }
}

class Dog extends Animal {
  Dog(String name) : super(name);

  @override
  void speak() {
    print('$name: ワンワン!');
  }

  void fetch() {
    print('$name はボールを取ってきた!');
  }
}

void main() {
  var cat = Cat('ミケ', '三毛');
  var dog = Dog('ポチ');

  cat.speak(); // ミケ(三毛): ニャー
  dog.speak(); // ポチ: ワンワン!
  dog.fetch(); // ポチ はボールを取ってきた!

  // 親クラスの型で扱える(ポリモーフィズム)
  List<Animal> animals = [cat, dog];
  for (var animal in animals) {
    animal.speak();
  }
  // ミケ(三毛): ニャー
  // ポチ: ワンワン!
}

super(name) は親クラスのコンストラクタを呼び出しています。@override アノテーションは親クラスのメソッドを上書きしていることを明示します。省略しても動作しますが、付けることが推奨されています。


抽象クラス(abstract class)

abstract class はインスタンス化できないクラスです。サブクラスに実装を強制するメソッド(抽象メソッド)を定義できます。

abstract class Shape {
  // 抽象メソッド(本体なし): サブクラスで必ず実装する
  double area();
  double perimeter();

  // 通常のメソッドも定義できる
  void describe() {
    print('面積: ${area()}, 周囲長: ${perimeter()}');
  }
}

class Circle extends Shape {
  double radius;

  Circle(this.radius);

  @override
  double area() => 3.14159265 * radius * radius;

  @override
  double perimeter() => 2 * 3.14159265 * radius;

  @override
  String toString() => 'Circle(半径: $radius)';
}

class RectangleShape extends Shape {
  double width;
  double height;

  RectangleShape(this.width, this.height);

  @override
  double area() => width * height;

  @override
  double perimeter() => 2 * (width + height);

  @override
  String toString() => 'Rectangle(${width}x$height)';
}

void main() {
  // var shape = Shape(); // エラー: 抽象クラスはインスタンス化できない

  List<Shape> shapes = [
    Circle(5),
    RectangleShape(4, 3),
  ];

  for (var shape in shapes) {
    print(shape);
    shape.describe();
    print('---');
  }
  // Circle(半径: 5.0)
  // 面積: 78.53981625, 周囲長: 31.4159265
  // ---
  // Rectangle(4.0x3.0)
  // 面積: 12.0, 周囲長: 14.0
  // ---
}

計算の確認:

  • Circle(5): area = 3.14159265 * 25 = 78.53981625perimeter = 2 * 3.14159265 * 5 = 31.4159265
  • RectangleShape(4, 3): area = 12.0perimeter = 14.0

インターフェース(implements)

Dart には interface キーワードがありません。すべてのクラスが暗黙的にインターフェースとして機能しますimplements を使うと、指定したクラスのすべてのメソッドとフィールドのゲッター・セッターを実装する義務が生まれます。

extends(継承)との違いは、implements親の実装を一切引き継がない 点です。

class Printable {
  void printInfo() {
    print('Printable');
  }
}

class Savable {
  void save() {
    print('保存しました');
  }
}

class Document implements Printable, Savable {
  String title;

  Document(this.title);

  // Printable のメソッドを実装(親の実装は引き継がれない)
  @override
  void printInfo() {
    print('ドキュメント: $title');
  }

  // Savable のメソッドを実装
  @override
  void save() {
    print('「$title」を保存しました');
  }
}

void main() {
  var doc = Document('設計書');
  doc.printInfo(); // ドキュメント: 設計書
  doc.save();      // 「設計書」を保存しました
}

mixin(with キーワード)

mixin は 複数のクラスにコードを再利用する仕組み です。Dart は単一継承(extends は1つだけ)ですが、with で複数の mixin を適用できます。Flutter では SingleTickerProviderStateMixin など、mixin が頻繁に使われます。

mixin Swimming {
  void swim() {
    print('$runtimeType は泳げます');
  }
}

mixin Flying {
  void fly() {
    print('$runtimeType は飛べます');
  }
}

class Animal {
  String name;
  Animal(this.name);
}

class Duck extends Animal with Swimming, Flying {
  Duck(String name) : super(name);

  void introduce() {
    print('$name はアヒルです');
  }
}

class Fish extends Animal with Swimming {
  Fish(String name) : super(name);
}

void main() {
  var duck = Duck('ドナルド');
  duck.introduce(); // ドナルド はアヒルです
  duck.swim();      // Duck は泳げます
  duck.fly();       // Duck は飛べます

  var fish = Fish('ニモ');
  fish.swim();      // Fish は泳げます
  // fish.fly();    // エラー: Fish には Flying mixin がない
}

mixin に制約を付ける(on キーワード)

on を使うと、特定のクラスを継承しているクラスでのみ使える mixin を定義できます。

class Musician {
  String name;
  Musician(this.name);

  void perform() {
    print('$name が演奏します');
  }
}

mixin Vocalist on Musician {
  void sing() {
    print('$name が歌います');
  }
}

mixin Guitarist on Musician {
  void playGuitar() {
    print('$name がギターを弾きます');
  }
}

class SingerSongwriter extends Musician with Vocalist, Guitarist {
  SingerSongwriter(String name) : super(name);
}

void main() {
  var artist = SingerSongwriter('田中');
  artist.perform();     // 田中 が演奏します
  artist.sing();        // 田中 が歌います
  artist.playGuitar();  // 田中 がギターを弾きます
}

enum の基本

enum(列挙型)は、固定された値の集合を定義するときに使います。

enum Color {
  red,
  green,
  blue,
}

void main() {
  var color = Color.green;

  print(color);        // Color.green
  print(color.name);   // green
  print(color.index);  // 1

  // switch 文との組み合わせ
  switch (color) {
    case Color.red:
      print('赤');
      break;
    case Color.green:
      print('緑');
      break;
    case Color.blue:
      print('青');
      break;
  }
  // 緑

  // 全ての値を取得
  print(Color.values); // [Color.red, Color.green, Color.blue]
}

拡張 enum(Dart 2.17 以降)

Dart 2.17 以降では、enum にフィールドやメソッドを追加できます。

enum Planet {
  mercury(name: '水星', distanceFromSun: 57.9),
  venus(name: '金星', distanceFromSun: 108.2),
  earth(name: '地球', distanceFromSun: 149.6),
  mars(name: '火星', distanceFromSun: 227.9);

  final String name;
  final double distanceFromSun; // 百万km

  const Planet({required this.name, required this.distanceFromSun});

  @override
  String toString() => '$name(太陽からの距離: ${distanceFromSun}百万km)';
}

void main() {
  for (var planet in Planet.values) {
    print(planet);
  }
  // 水星(太陽からの距離: 57.9百万km)
  // 金星(太陽からの距離: 108.2百万km)
  // 地球(太陽からの距離: 149.6百万km)
  // 火星(太陽からの距離: 227.9百万km)
}

toString() のオーバーライド

toString() をオーバーライドすると、print() や文字列補間でオブジェクトを表示したときの出力をカスタマイズできます。

class Student {
  String name;
  int grade;
  List<String> subjects;

  Student(this.name, this.grade, this.subjects);

  @override
  String toString() {
    return 'Student(名前: $name, 学年: $grade年, 科目: ${subjects.join(", ")})';
  }
}

void main() {
  var student = Student('佐藤', 2, ['数学', '英語', '物理']);
  print(student);
  // Student(名前: 佐藤, 学年: 2年, 科目: 数学, 英語, 物理)

  // 文字列補間でも同じ
  print('生徒情報: $student');
  // 生徒情報: Student(名前: 佐藤, 学年: 2年, 科目: 数学, 英語, 物理)
}

実践的な例: 動物クラスの継承階層

ここまでの知識を組み合わせた、より実践的な例です。

abstract class Animal {
  String name;
  int age;

  Animal(this.name, this.age);

  void speak(); // 抽象メソッド

  @override
  String toString() => '$runtimeType(名前: $name, 年齢: $age歳)';
}

mixin Pet {
  String? ownerName;

  void showOwner() {
    if (ownerName != null) {
      print('飼い主: $ownerName');
    } else {
      print('飼い主なし(野良)');
    }
  }
}

class Cat extends Animal with Pet {
  bool isIndoor;

  Cat(String name, int age, {this.isIndoor = true}) : super(name, age);

  @override
  void speak() {
    print('$name: ニャー');
  }
}

class Dog extends Animal with Pet {
  String breed;

  Dog(String name, int age, this.breed) : super(name, age);

  @override
  void speak() {
    print('$name: ワンワン!');
  }

  void fetch() {
    print('$name$breed)はボールを取ってきた!');
  }
}

class Parrot extends Animal with Pet {
  List<String> phrases;

  Parrot(String name, int age, this.phrases) : super(name, age);

  @override
  void speak() {
    for (var phrase in phrases) {
      print('$name: 「$phrase」');
    }
  }
}

void main() {
  var cat = Cat('ミケ', 3);
  cat.ownerName = '田中';

  var dog = Dog('ポチ', 5, '柴犬');
  dog.ownerName = '佐藤';

  var parrot = Parrot('ピーちゃん', 2, ['おはよう', 'こんにちは']);

  List<Animal> animals = [cat, dog, parrot];

  for (var animal in animals) {
    print(animal);      // toString() が呼ばれる
    animal.speak();
    if (animal is Pet) {
      (animal as Pet).showOwner();
    }
    print('---');
  }
  // Cat(名前: ミケ, 年齢: 3歳)
  // ミケ: ニャー
  // 飼い主: 田中
  // ---
  // Dog(名前: ポチ, 年齢: 5歳)
  // ポチ: ワンワン!
  // 飼い主: 佐藤
  // ---
  // Parrot(名前: ピーちゃん, 年齢: 2歳)
  // ピーちゃん: 「おはよう」
  // ピーちゃん: 「こんにちは」
  // 飼い主なし(野良)
  // ---
}

実践的な例: 図形クラスの面積計算

import 'dart:math';

abstract class Shape {
  String get shapeName;
  double area();
  double perimeter();

  @override
  String toString() =>
      '$shapeName → 面積: ${area().toStringAsFixed(2)}, 周囲長: ${perimeter().toStringAsFixed(2)}';
}

class Circle extends Shape {
  double radius;
  Circle(this.radius);

  @override
  String get shapeName => '円(半径$radius)';

  @override
  double area() => pi * radius * radius;

  @override
  double perimeter() => 2 * pi * radius;
}

class Triangle extends Shape {
  double a, b, c; // 3辺の長さ

  Triangle(this.a, this.b, this.c);

  @override
  String get shapeName => '三角形(${a}x${b}x$c)';

  @override
  double area() {
    // ヘロンの公式
    double s = (a + b + c) / 2;
    return sqrt(s * (s - a) * (s - b) * (s - c));
  }

  @override
  double perimeter() => a + b + c;
}

class Square extends Shape {
  double side;
  Square(this.side);

  @override
  String get shapeName => '正方形(一辺$side)';

  @override
  double area() => side * side;

  @override
  double perimeter() => 4 * side;
}

void main() {
  List<Shape> shapes = [
    Circle(5),
    Triangle(3, 4, 5),
    Square(6),
  ];

  for (var shape in shapes) {
    print(shape);
  }
  // 円(半径5.0) → 面積: 78.54, 周囲長: 31.42
  // 三角形(3.0x4.0x5.0) → 面積: 6.00, 周囲長: 12.00
  // 正方形(一辺6.0) → 面積: 36.00, 周囲長: 24.00

  // 面積が最も大きい図形を探す
  shapes.sort((a, b) => b.area().compareTo(a.area()));
  print('\n面積が最も大きい図形: ${shapes.first.shapeName}');
  // 面積が最も大きい図形: 円(半径5.0)
}

計算の確認:

  • Circle(5): area = pi * 25 ≈ 78.54perimeter = 2 * pi * 5 ≈ 31.42
  • Triangle(3, 4, 5): s = 6area = sqrt(6*3*2*1) = sqrt(36) = 6.00perimeter = 12.00
  • Square(6): area = 36.00perimeter = 24.00

練習問題

練習問題1: 従業員クラス

以下の仕様で Employee クラスと Manager クラスを作成してください。

Employee クラス:

  • フィールド: name(String)、salary(double)
  • コンストラクタ: this 省略記法を使用
  • メソッド: work()"$name は働いています" と出力
  • toString()"Employee(name, 給与: salary円)" の形式

Manager クラス(Employee を継承):

  • 追加フィールド: department(String)
  • work() をオーバーライド → "$name は $department 部門を管理しています" と出力
  • toString()"Manager(name, 部門: department, 給与: salary円)" の形式

期待する出力:

Employee(田中, 給与: 300000.0円)
田中 は働いています
Manager(佐藤, 部門: 開発, 給与: 500000.0円)
佐藤 は 開発 部門を管理しています
模範解答
class Employee {
  String name;
  double salary;

  Employee(this.name, this.salary);

  void work() {
    print('$name は働いています');
  }

  @override
  String toString() => 'Employee($name, 給与: ${salary}円)';
}

class Manager extends Employee {
  String department;

  Manager(String name, double salary, this.department) : super(name, salary);

  @override
  void work() {
    print('$name$department 部門を管理しています');
  }

  @override
  String toString() => 'Manager($name, 部門: $department, 給与: ${salary}円)';
}

void main() {
  var emp = Employee('田中', 300000);
  print(emp);    // Employee(田中, 給与: 300000.0円)
  emp.work();    // 田中 は働いています

  var mgr = Manager('佐藤', 500000, '開発');
  print(mgr);    // Manager(佐藤, 部門: 開発, 給与: 500000.0円)
  mgr.work();    // 佐藤 は 開発 部門を管理しています
}

練習問題2: 抽象クラスと mixin

以下の仕様で、乗り物のクラス階層を作成してください。

Vehicle 抽象クラス:

  • フィールド: name(String)
  • 抽象メソッド: move() → 移動方法を出力

Electric mixin:

  • メソッド: charge()"$runtimeType を充電中..." と出力

Car クラス(Vehicle を継承):

  • move()"$name は道路を走ります" と出力

ElectricCar クラス(Vehicle を継承、Electric mixin を適用):

  • move()"$name は静かに道路を走ります" と出力

期待する出力:

ガソリン車 は道路を走ります
テスラ は静かに道路を走ります
ElectricCar を充電中...
模範解答
abstract class Vehicle {
  String name;
  Vehicle(this.name);
  void move();
}

mixin Electric {
  void charge() {
    print('$runtimeType を充電中...');
  }
}

class Car extends Vehicle {
  Car(String name) : super(name);

  @override
  void move() {
    print('$name は道路を走ります');
  }
}

class ElectricCar extends Vehicle with Electric {
  ElectricCar(String name) : super(name);

  @override
  void move() {
    print('$name は静かに道路を走ります');
  }
}

void main() {
  var car = Car('ガソリン車');
  var eCar = ElectricCar('テスラ');

  car.move();     // ガソリン車 は道路を走ります
  eCar.move();    // テスラ は静かに道路を走ります
  eCar.charge();  // ElectricCar を充電中...
}

練習問題3: 拡張 enum

以下の仕様で Season enum を作成してください。

Season enum:

  • 値: spring, summer, autumn, winter
  • フィールド: japaneseName(String)、averageTemp(int、平均気温)
  • メソッド: describe()"$japaneseName(平均気温: $averageTemp℃)" と出力

期待する出力:

春(平均気温: 15℃)
夏(平均気温: 30℃)
秋(平均気温: 18℃)
冬(平均気温: 5℃)
模範解答
enum Season {
  spring(japaneseName: '春', averageTemp: 15),
  summer(japaneseName: '夏', averageTemp: 30),
  autumn(japaneseName: '秋', averageTemp: 18),
  winter(japaneseName: '冬', averageTemp: 5);

  final String japaneseName;
  final int averageTemp;

  const Season({required this.japaneseName, required this.averageTemp});

  void describe() {
    print('$japaneseName(平均気温: $averageTemp℃)');
  }
}

void main() {
  for (var season in Season.values) {
    season.describe();
  }
  // 春(平均気温: 15℃)
  // 夏(平均気温: 30℃)
  // 秋(平均気温: 18℃)
  // 冬(平均気温: 5℃)
}

まとめ

機能 キーワード 用途
クラス定義 class フィールドとメソッドをまとめる
コンストラクタ省略 this.フィールド名 フィールドへの代入を簡略化
名前付きコンストラクタ ClassName.name() 複数の生成パターンを用意
ゲッター/セッター get / set 計算プロパティやアクセス制御
private _ プレフィックス ライブラリ単位のアクセス制限
継承 extends 親クラスの実装を引き継ぐ(単一継承)
オーバーライド @override 親のメソッドを上書き
抽象クラス abstract class サブクラスに実装を強制
インターフェース implements すべてのメソッドを再実装する契約
mixin mixin + with 複数クラスへのコード再利用
列挙型 enum 固定された値の集合

次回(第5回)は Dart のコレクション(List・Map・Set) を学びます。Flutter の UI 構築ではリストデータの操作が頻繁に登場するため、しっかり押さえておきましょう。


参考


@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!

0
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?