【Dart】enumとsealedクラスのパターンマッチングについて

こんにちは。エキサイト株式会社でアプリエンジニアをしている岡島です。 今回はDartenumとsealedクラスについて勉強したことをまとめていこうと思います。

環境

Dart: 3.4.0

enumのパターンマッチング

基本的な使用例

enum Weather { sunny, rainy, cloudy }

String getWeatherAdvice(Weather weather) {
  return switch (weather) {
    Weather.sunny => "日焼け止めを塗ろう!",
    Weather.rainy => "傘を忘れずに持って行こう!",
    Weather.cloudy => "念の為傘を持って行こう",
  };
}

void main() {
  print(getWeatherAdvice(Weather.rainy)); // 出力:傘を忘れずに持って行こう!
}

enumは列挙型と呼ばれ、定数に意味を持たすことができ、まとめて管理することができます。

enumの網羅性チェック

enumの大きな利点は、コンパイラが網羅性チェックを行ってくれる点です。

例えば以下の例です。

enum Weather { sunny, rainy, cloudy, snowy } //snowyを追加

String getWeatherAdvice(Weather weather) {
  return switch (weather) {
    Weather.sunny => "日焼け止めを塗ろう!",
    Weather.rainy => "傘を忘れずに持って行こう!",
    Weather.cloudy => "念の為傘を持って行こう",
    // Weather.windyのケースが欠けている
  };
}

このようにWeatherで列挙した値がswitch文で抜けている場合コンパイラが検知してくれて以下のようなメッセージを出してくれます。

The type 'Weather' is not exhaustively matched by the switch cases since it doesn't match 'Weather.snowy'.

これにより、enumで扱っている値が抜けている場合や、新しい値の追加したときなどにバグを防ぐことができます。

sealedクラスのパターンマッチング

Dart 3から導入されたSealedクラスは、enumよりも柔軟にデータを扱うことができます。

基本的な使用例

sealed class Shape {}

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

class Rectangle extends Shape {
  Rectangle(this.width, this.height);
  final double width;
  final double height;
}

class Triangle extends Shape {
  Triangle(this.base, this.height);
  final double base;
  final double height;
}

double calculateArea(Shape shape) {
  return switch (shape) {
    Circle(radius: final r) => 3.14 * r * r,
    Rectangle(width: final w, height: final h) => w * h,
    Triangle(base: final b, height: final h) => 0.5 * b * h,
  };
}

void main() {
  print(calculateArea(Circle(5))); // 出力: 78.5
  print(calculateArea(Rectangle(4, 5))); // 出力: 20.0
  print(calculateArea(Triangle(3, 4))); // 出力: 6.0
}

この例ではShapeというsealedクラスと、CircleRectangleTriangleというサブクラスを定義しています。それぞれの形の面積を計算する関数calculateAreaでは、Dartのsealedクラスのパターンマッチングを利用したswitch文を書いています。

サブクラスの構造に基づいたパターンマッチング

String describeShape(Shape shape) {
  return switch (shape) {
    Circle(radius: final r) => "A circle with radius $r",
    Rectangle(width: final w, height: final h) when w == h => "A square with side $w",
    Rectangle(width: final w, height: final h) => "A rectangle with width $w and height $h",
    Triangle(base: final b, height: final h) => "A triangle with base $b and height $h",
  };
}

このようにwhenなどの条件式を用いたswitch文も書くことができます。 この例では、Rectangleのケースで条件(when w == h)を使用して、正方形の場合を特別に扱っています。

詳しくはドキュメントの分岐についてをご参照ください。

dart.dev

sealedクラスの網羅性チェック

sealed class Shape {}
// ... 既存のクラス ...
class Pentagon extends Shape {
  Pentagon(this.side);
  final double side;
}

double calculateArea(Shape shape) {
  return switch (shape) {
    Circle(radius: final r) => 3.14 * r * r,
    Rectangle(width: final w, height: final h) => w * h,
    Triangle(base: final b, height: final h) => 0.5 * b * h,
    // Pentagonのケースが欠けている
  };
}

sealedでも、enumと同じようにコンパイラが網羅性のチェックを行ってくれます。この例では、Pentagonのケースが無いことについてコンパイラが検出してくれます。

最後に

このように、enumやsealedを利用し、パターンマッチングとswitch文を適切に使用することで、より安全でバグの少ないコードを書くことを意識していこうと思います。