Swift経験者からみたDart言語の不思議 〜クラス編〜

こんにちは決済認証システム開発事業部の冨永です。

普段はiOS・iPadアプリ開発を中心に業務に携わっており、ゴリゴリのSwift愛好家です。

業務上、iOS・Android両プラットフォームの開発を効率化する際に、 クロスプラットフォーム対応のフレームワークであるFlutterの選択肢があるかと思います。 FlutterではDartという言語が使われていますが、Swift経験者がDartを学ぶ際に、

「SwiftでいうXXXってどうやって実装するのだろう」

という疑問が度々生じました。

今回はクラスに焦点を当てて、整理した記事になります。 iOS開発経験者がFlutterに興味を持つきっかけになれば幸いです。

目次

クラスの宣言方法

Swiftのクラスの宣言方法は以下になります。

class Person {
  init(name: String) { // コンストラクタ
    self.name = name
  }
  let name: String
}


// 使用例
let person = Person(name: "Taro") 
print(person.name)  // Taro

対して、Dartのクラスの基本的な宣言方法は以下になります。 Dartではコンストラクタをクラス名と同じ名前で定義します。

class Person {
  Person(this.name);  // コンストラクタ
  String name;
}

// 使用例
void main() {
  Person person = Person("John");
  print(person.name);  // John
}

Swiftでは値セマンティクスと不変(イミュータブル)なオブジェクトは構造体(Struct)で簡単に作れたけど、Dartではどう作る?

不変(イミュータブル)オブジェクトと値セマンティクスは、オブジェクトを安全かつ効率的に扱い、バグの発生を大幅に減少させる、非常に重要な概念です。Swiftでは不変と値セマンティクスは構造体(Struct)で楽に作れる印象があり、Dartではどのように作るのだろうと思いました。

不変(イミュータブル)とは

不変性は、オブジェクトが一度作成された後、その状態を変更できない性質を指します。この性質により、オブジェクトは安全に共有されることが可能になり、予測可能な振る舞いが保証されます。

値セマンティクスとは

値セマンティクスは、オブジェクトがコピーされる際に、その実際のデータがコピーされる性質を指します。これにより、元のオブジェクトとコピーされたオブジェクトは互いに独立し、一方の変更が他方に影響を与えない状態が実現されます。

Swiftの場合は不変と値セマンティクスは構造体で楽に作れる

Swiftの構造体(Struct)は、この値セマンティクスをデフォルトで持っています。構造体のインスタンスがコピーされたとき、全てのフィールドが独立したコピーとして扱われるため、フィールドを変更しても他のインスタンスには影響を及ぼしません。また、プリミティブ型のフィールドに対してlet キーワードを付与することにより、簡単に不変を定義できます。

Swiftの実装例は以下になります。

struct Person { // 構造体はデフォルトで値セマンティクスになる ※例外あり
  let name: String  // let キーワードで不変
  let age: Int
}

参考:Value Semantics とは

Dartで不変(イミュータブル)なオブジェクトを作る場合

Dartでは、不変性を強制するために@immutableアノテーションを利用することができます。 このアノテーションは、クラスが不変であるべきというマーカーとして機能し、 不変でない場合、静的解析ツールがコンパイル時に警告を発することでこれを阻止します。

Dartで不変(イミュータブル)を実装した例は以下になります。

import 'package:meta/meta.dart';

@immutable // アノテーションにより不変でないオブジェクトの場合は警告してくれる
class Person {
  const Person(this.name, this.age); // コンストラクタにconstをつけることでコンパイル時定数に
  final String name; // 全てのフィールドをfinalにする
  final int age;
}

フィールドにfinalキーワードを使うことで、そのフィールドは初期化後変更できなくなります。これにより、オブジェクトのプロパティが不変になります。 また、constキーワードを使ってコンストラクタを定義することで、コンパイル時定数のオブジェクトを作成できます。このオブジェクトは、プログラムの実行中に状態が変更されることがないため、完全にイミュータブルです。

では、値セマンティクスを実装するにはどうすればよいでしょうか。

Dartで値セマンティクスなオブジェクトを作る場合

実装した例は以下になります。

import 'package:meta/meta.dart';

@immutable
class Person {
  const Person(this.name, this.age);  // コンストラクタにconstをつける
  final String name; // 全てのフィールドをfinalにする
  final int age;
  Person copyWith(String? name, int? age) {
    return Person(name ?? this.name, age ?? this.age); // 新規オブジェクトを返却する
  }
}

Swiftの場合は構造体(Struct)を使用していた際は何も考えずに値セマンティクスが満たされていましたが、 Dartの場合はcopyWithのような追加の実装が必要だということを留意する必要があると思いました。

SwiftでいうProtocolはDartでいうと。。。?

SwiftではProtocolを使うことでメソッド定義の他にプロパティの宣言を含めることができ、実装するクラスがそのプロパティ、メソッドを提供する必要があります。また複数のプロトコルに準拠することや、プロトコルに対してデフォルトの実装を加えることができます。 以下のSwiftの例では、FlyableとSwimmableプロトコルに加えて、nameプロパティのゲッターを定義しています。

Swiftの場合

protocol Flyable {
    func fly() // 定義のみのメソッド
}

extension Flyable {
  func fly() {
    print("flies") // デフォルト実装
  }   
}

protocol Swimmable {
    func swim()
}

protocol Named {
  var name: String { get } // プロパティ
}

struct Duck: Flyable, Swimmable, Named { // 複数のプロトコルに準拠が可能
    let name: String      
    func swim() {
        print("\(name) swims")
    }
}

let duck = Duck(name:"Daffy")
duck.fly()   // "flies"
duck.swim()  // "Daffy swims"

では、Dartではどのように実装するか。 Dart 3.0登場前までは、abstractを使ってインターフェースのような機能を実装していましたが、Dart 3.0からは、新たに導入されるabstract interfaceクラス修飾子を使用してクラスを純粋なインターフェースとして定義することができるようになります。abstract interfaceキーワードを用いることで、従来のabstractを使用した際に可能だったクラスの継承を制限でき、これによりコードの一貫性と安全性が向上します。ただし、デフォルト実装はinterfaceでは提供できないため、必要に応じて再利用可能なコードを効率的に組み込むためにミックスインを併用すること必要です。

Dartの場合

mixin Flyable{ // abstract interfaceにはデフォルト実装機能がないため、mixinを使用する
  void fly(){
    print("flies"); // デフォルト実装
  }
}

abstract interface class Swimmable { // インターフェース部分
  void swim();
}

abstract interface class Named {
  String get name;
}

class Duck with Flyable implements Swimmable, Named { // mixinの場合はwith、abstract interfaceの場合はimplementsを付与する
  Duck(this.name);
  
  @override
  final String name;

  @override
  void swim() => print("$name swims");
}

void main() {
  Duck duck = Duck("Daffy");
  duck.fly();   // "flies"
  duck.swim();  // "Daffy swims"
}

参考:https://dart.dev/language/class-modifiers

まとめ

以上になります。 Swiftで使用する基本的なクラス、構造体、プロトコルをDartで表現する方法を整理しました。 Dartにはクラス修飾子にabstract interfaceやmixinの他にbase, sealedなどもあり、Dart固有の表現については今後さらに深掘りしていきたいと思います! ではでは!

テコテックの採用活動について

テコテックでは新卒採用、中途採用共に積極的に募集をしています。 採用サイトにて会社の雰囲気や福利厚生、募集内容をご確認いただけます。 ご興味を持っていただけましたら是非ご覧ください。 tecotec.co.jp