クラスを理解する
このページで学ぶこと
- クラスとオブジェクト指向の考え方がつかめる
- インスタンスフィールドを持つクラスを書ける
- 3 種類のコンストラクタ(generative / named / factory)を使い分けられる
extendsを使ってクラスを継承できる
なぜクラスを学ぶのか
Flutter のコードを見ると、必ず class という単語が出てきます。
class MyApp extends StatelessWidget { ... }
class Counter extends Notifier<int> { ... }Flutterはオブジェクト指向(Object-Oriented, OO)の考え方で設計されています。
オブジェクト指向のざっくりした考え方
オブジェクト指向は「データ(フィールド)と、そのデータに対する操作(メソッド)を一つの箱にまとめよう」という発想です。例えば「ユーザー」を扱うとき:
// ❌ データと操作がバラバラ
String userName = 'とんぺい';
int userAge = 20;
String greet(String name) => 'こんにちは、$name さん';
bool isAdult(int age) => age >= 18;これをクラスにまとめると、以下のようになります。
// ✅ データと操作を User クラスにまとめる
class User {
String name;
int age;
User(this.name, this.age);
String greet() => 'こんにちは、$name さん';
bool isAdult() => age >= 18;
}
final user = User('とんぺい', 20);
print(user.greet()); // こんにちは、とんぺいさん
print(user.isAdult()); // trueインスタンス変数(フィールド)
クラスの中に書いた変数を**インスタンス変数(フィールド)**と呼びます。
class User {
String name; // フィールド
int age; // フィールド
User(this.name, this.age);
}finalで書き換え不可にする
一度決めたら変えられたくない値に変数修飾子finalをつけます。
class User {
final String name;
final int age;
User(this.name, this.age);
}
final user = User('とんぺい', 20);
// user.name = 'ぺーとん'; // final nameなのでコンパイルエラーゲッターとセッター
ゲッター:値を「計算して返す」メソッド
ゲッター(“getter”)は、フィールドの皮を被せた関数のようなものです。呼び出し側からは普通のフィールドと同じように見えます。 通常の関数の場合、呼び出されるたびに計算(「評価」)しますが、ゲッターの場合は呼び出されても最初に計算した値をいい感じに使い回してくれます(「キャッシュ」)。
セッター:値を「受け取って反映する」メソッド
書き込みはできるが、読み出しはできないフィールドを更新するのに用いられたりします。
class Temperature {
double _celsius = 0; //プライベート
set celsius(double value) {
if (value < -273.15) {
throw ArgumentError("絶対零度よりはい温度は設定できません");
}
_celsius = value;
}
}
final temp = Temperature();
t.celsius = 25; //set celsiusが呼ばれるclass Rectangle {
final int width;
final int height;
Rectangle(this.width, this.height);
int get area => width * height;
bool get isSquare => width == height;
}
final rect = Rectangle(4, 3);
final area = rect.area; // 12
final isSquare = rect.isSquare; // falseいつ使う?
ざっくりこんな使い分けになります。
| やりたいこと | 書き方 |
|---|---|
| 値をそのまま公開したいだけ | 普通のフィールド |
| 他のフィールドから計算した値 を見せたい | ゲッター(get) |
内部は _xxx に隠し、読み取りだけ許したい | _xxx + ゲッター(セッターなし) |
| フィールド書き込みだけ許し、読み出しはできないようにしたい | _xxx + セッター(ゲッターなし) |
| 書き込み時に バリデーションや変換 を挟みたい | ゲッター + セッター |
Dartのコンストラクタ
コンストラクタは、クラス(設計図のようなもの)からインスタンス(実態)を作るときに呼ばれる特別な関数です。以下にDartでよく使うコンストラクタの書き方を紹介します。
① Generative コンストラクタ(基本形)
class User {
final String name;
final int age;
// this.xxx と書くと、引数をそのままフィールドに代入できる
User(this.name, this.age);
}
final user = User("とんぺい", 20);this.nameは「引数で受け取った値をそのままthis.nameに代入する」というショートカット記法です。
長く書くと
User(String name, int age): this.name = name, this.age = age;② Named コンストラクタ(名前付きコンストラクタ)
コンストラクタに名前を付けて複数用意できます。用途ごとにインスタンスの生成手順を変えたいときに便利です。
class User {
final String name;
final int age;
User(this.name, this.age);
User.guest(): name = 'ゲスト', age = 0;
User.fromJson(Map<String, dynamic> json): name = json['name'] as String,
age = json['age'] as int;
}
final u1 = User('とんぺい', 20);
final u2 = User.guest();
final u3 = User.fromJson({'name': 'ぺーとん', 'age': 18});③ Factory コンストラクタ
factory を付けると、コンストラクタの中で return を自由に書ける ようになります。
generative/namedコンストラクタが「その場で新しいインスタンスを作る」のに対し、factoryコンストラクタは「なにを返すか自由にコントロールできる」のが違いです。
class User {
final String name;
final int age;
factory User.fromInput(String? name, int? age) {
if (name == null || age == null) {
return User("ゲスト", 0);
}
return User(name, age);
}
}
final u1 = User.fromInput('とんぺい', 20); // User('とんぺい', 20)
final u2 = User.fromInput(null, null); // User('ゲスト', 0)継承(extends)
クラスには「別のクラスを土台にして、新しいクラスを作る」仕組みがあります。これを継承と呼びます。
Dartでは extends を使って書きます。
なぜ継承を使うのか
例えば、「動物」というクラスと「犬」というクラスを考えてみましょう。
犬も動物の一種なので、名前や年齢は共通して持っています。これを毎回書き直すのは面倒です。
// ❌ 同じフィールド・メソッドを何度も書いている
class Animal {
final String name;
Animal(this.name);
void breathe() => print('$name は呼吸している');
}
class Dog {
final String name; // Animalと同じ…
Dog(this.name);
void breathe() => print('$name は呼吸している'); // Animalと同じ…
void bark() => print('$name: ワン!');
}extends を使うと、共通部分を 親クラス(スーパークラス) にまとめられます。
// ✅ Animal を継承して Dog を作る
class Animal {
final String name;
Animal(this.name);
void breathe() => print('$name は呼吸している');
}
class Dog extends Animal {
Dog(super.name); // 親のコンストラクタに name を渡す
void bark() => print('$name: ワン!');
}
final dog = Dog('ポチ');
dog.breathe(); // ポチ は呼吸している(Animalから継承したメソッド)
dog.bark(); // ポチ: ワン!(Dog独自のメソッド)super で親クラスを呼ぶ
super は「親クラス」を指すキーワードです。コンストラクタやメソッドの中で使います。
class Animal {
final String name;
Animal(this.name);
void introduce() => print('私は $name です');
}
class Dog extends Animal {
final String breed; // 犬種
// 親のコンストラクタを super(...) で呼ぶ
Dog(String name, this.breed) : super(name);
void introduce() {
super.introduce(); // 親の introduce() を先に呼ぶ
print('犬種は $breed です');
}
}
final dog = Dog('ポチ', '柴犬');
dog.introduce();
// 私は ポチ です
// 犬種は 柴犬 です補足:
Dog(super.name)と書くと、: super(name)を省略できます(Dart 2.17以降)。短く書けるので最近はこちらがよく使われます。
@override でメソッドを上書きする
親クラスのメソッドを子クラスで書き換えたいときは @override をつけます。
class Animal {
final String name;
Animal(this.name);
void sound() => print('$name: …');
}
class Cat extends Animal {
Cat(super.name);
@override
void sound() => print('$name: ニャー');
}
final cat = Cat('タマ');
cat.sound(); // タマ: ニャーFlutterでの例
実は、Flutterのコードで出てくる extends StatelessWidget もまったく同じ仕組みです。
// StatelessWidget を継承して、自分のWidgetを作っている
class MyApp extends StatelessWidget {
const MyApp({super.key}); // 親のコンストラクタに key を渡す
@override
Widget build(BuildContext context) {
return const MaterialApp(home: Text('Hello'));
}
}ここまで読めば、extends StatelessWidget が「StatelessWidget を土台にして、MyApp という新しいクラスを作っている」という意味だとわかりますね。
まとめ
| やりたいこと | 書き方 |
|---|---|
| 別のクラスを土台にして新しいクラスを作る | class 子 extends 親 |
| 親のコンストラクタを呼ぶ | super(...) または super.xxx |
| 親のメソッドを呼ぶ | super.メソッド名() |
| 親のメソッドを上書きする | @override をつけて同名で定義 |
確認しよう
- フィールドとコンストラクタを持つクラスを書けた
- generative / named / factory の3種類のコンストラクタを使い分けられた
-
extendsで別のクラスを継承できた -
@overrideでメソッドを上書きできた
AIに聞いてみよう
「Dartの
extendsとimplementsとwithの違いを初心者向けに教えてください」