記事内に広告が含まれています

C#と比較して学ぶDart言語仕様③ – クラス・非同期処理

dart_csharp_03 C#

前回の記事に続いて、Dartの言語仕様をC#と比較しながら簡単に解説していきます。

最後となる今回は、クラスと非同期処理について見ていきます。

※第1回と第2回の記事は下記から飛べます。

言語のバージョン

  • Dart 3.4.3
  • C# 12 (.NET 8)

クラス

クラスはDartとC#で異なる点が多いので、差異があるポイントをメインに解説していきます。

クラスの基本

以下はDartでの基本的なクラス定義です。

class Person {
  // インスタンス変数
  String name; // 通常の変数(getterとsetterを持つ)
  final int? age; // final修飾子あり(getterのみを持つ)
  DateTime? _birthday; // private変数

  // コンストラクタ
  Person(this.name, this.age);
  
  // インスタンスメソッド
  String showProfile() {
    return '名前:${name}, 年齢:${age}, 誕生日:${_birthday}';
  }
}

void main() {
  var person = Person('Taro', 30);
  person.name = 'Jiro';
  person._birthday = DateTime(1995, 10, 2);

  print(person.showProfile()); // -> 名前:Jiro, 年齢:30, 誕生日:1995-10-02 00:00:00.000
}

主なC#との差異は下記の通りです。

  • フィールドとプロパティの明確な区別がない
  • インスタンス変数は暗黙的にgetterとsetterを持つ
  • final修飾子が付いたインスタンス変数はgetterのみを持つ(必ずコンストラクタでの初期化が必要)
  • コンストラクタの初期化処理がシンプル書ける
  • 変数やメソッド名の先頭に_を付けると、他のライブラリ(ファイル)から参照できないように制限できる

下記は同様のクラスをC#で書いたものです。

    class Program
    {
        static void Main()
        {
            var person = new Person("Taro", 30, new DateTime(1995, 10, 2))
            person.Name = "Jiro";

            Console.WriteLine(person.ShowProfile()); // -> 名前:Jiro, 年齢:30, 誕生日:1995/10/02 0:00:00
        }
    }

    class Person
    {
        public string Name { get; set; }
        public int? Age { get; } // get-onlyプロパティ
        private DateTime? _birthday; // privateフィールド

        public Person(string name, int age, DateTime birthday)
        {
             Name = name;
             Age = age;
            _birthday = birthday;
        }

        public string ShowProfile()
        {
            return $"名前:{Name}, 年齢:{Age}, 誕生日:{_birthday}";
        }
    }

コンストラクタ

コンストラクタはDart特有のものについて紹介します。

名前付きコンストラクタ

Dartではクラス名.識別子とすることで、名前付きのコンストラクタを定義することができます。

C#でいうコンストラクタのオーバーロードに似ていますが、引数が全て同じでもコンストラクタを複数定義できる点が異なります。

class Person {
  String name;
  final int? age;
  DateTime? _birthday;

  // 通常のコンストラクタ
  Person(this.name, this.age);
  
  // 名前付きコンストラクタ
  Person.taro() : this.name = 'Taro', this.age = 30, this._birthday = DateTime(1990, 10, 1);
  Person.jiro() : this.name = 'Jiro', this.age = 25, this._birthday = DateTime(1995, 10, 2);
  
  String showProfile() {
    return '名前:${name}, 年齢:${age}, 誕生日:${_birthday}';
  }
}

void main() {
  var taro = Person.taro();
  var jiro = Person.jiro();
  print(taro.showProfile()); // -> 名前:Taro, 年齢:30, 誕生日:1990-10-01 00:00:00.000
  print(jiro.showProfile()); // -> 名前:Jiro, 年齢:25, 誕生日:1995-10-02 00:00:00.000
}

constコンストラクタ(定数コンストラクタ)

クラスのインスタンスをコンパイル時定数にしたい場合は、constコンストラクタを定義できます。

constコンストラクタを使うことでFlutterコンポーネントの不要なインスタンス生成を防ぐことができ、パフォーマンス改善に繋がるというメリットがあるようです。

class Person {
  // インスタンス変数(全てfinalである必要がある)
  final String name;
  final int? age;

  // constコンストラクタ
  const Person(this.name, this.age);
  
  String showProfile() {
    return '名前:${name}, 年齢:${age}';
  }
}

void main() {
  // クラス名の前にconstを付けるか、const変数に代入することでコンパイル時定数となる
  final taro = const Person('Taro', 30);
  const jiro = Person('Jiro', 25);
  print(taro.showProfile()); // -> 名前:Taro, 年齢:30
}

factoryコンストラクタ

特定の条件に基づいてインスタンスを返すコンストラクタで、新しいインスタンスを生成せずに既存のインスタンスを返すことなどができます。

シングルトンパターンの実装や、キャッシュの利用で使われることが多いようです。

下記はキャッシュがある場合に既存のインスタンスを返すクラスのサンプルコードです。

class User {
  static final Map<int, User> _cache = {};

  final int id;
  final String name;

  // プライベートコンストラクタ
  User._internal(this.id, this.name);

  // factoryコンストラクタ
  factory User(int id, String name) {
    if (_cache.containsKey(id)) {
      // キャッシュがある場合は既存インスタンスを返す
      return _cache[id]!;
    } else {
      // キャッシュがない場合はインスタンスを生成して返す
      final instance = User._internal(id, name);
      _cache[id] = instance;
      return instance;
    }
  }
}

void main() {
  var user1 = User(1, 'Taro'); // 新規インスタンス
  var user2 = User(1, 'Taro'); // キャッシュ
  var user3 = User(2, 'Hanako');

  print(user1 == user2); // true
  print(user1 == user3); // false
}

継承

Dartでもクラスの継承が可能です。extendsキーワードの後に継承するクラス名を指定します。

// スーパークラス
class Animal {
  String name;
  
  Animal(this.name);

  void eat() => print('$name is eating.');
}

// サブクラス
class Dog extends Animal {
  Dog(String name) : super(name);

  void sleep() => print('$name is sleeping.');

  // 親クラスのメソッドをオーバーライド
  @override
  void eat() => print('$name is eating dog food.');
}

void main() {
  Dog myDog = Dog('Pochi');
  myDog.eat();      // => Pochi is eating dog food.
  myDog.sleep();    // => Pochi is sleeping.

  // Animalクラスのインスタンス
  Animal myAnimal = Animal('Animal');
  myAnimal.eat();    // => Animal is eating.
}

メソッドをオーバーライドする際は@override属性を、スーパークラスのコンストラクタを呼び出すにはsuperキーワードを使用します。

同様の内容をC#で書き直したのが以下です。キーワード以外は特段大きな違いはありません。

public class Animal
{
    public string Name { get; set; }

    public Animal(string name)
    {
        Name = name;
    }

    public virtual void Eat() => Console.WriteLine($"{Name} is eating.");
}

public class Dog : Animal
{
    public Dog(string name) : base(name) { }

    public void Sleep() => Console.WriteLine($"{Name} is sleeping.");

    public override void Eat() => Console.WriteLine($"{Name} is eating dog food.");
}

public class Program
{
    public static void Main()
    {
        var myDog = new Dog("Pochi");
        myDog.Eat();    // => Pochi is eating dog food.
        myDog.Sleep();  // => Pochi is sleeping.

        var myAnimal = new Animal("Animal");
        myAnimal.Eat();   // => Animal is eating.
    }
}

暗黙のインターフェース

Dartではすべてのクラスに暗黙的にインターフェースが定義されています。

インターフェースを実装するには、implementsキーワードの後ろにクラス名を指定します。

extendsキーワードによる継承との違いは、すべてのインスタンス変数とメソッドをオーバーライドする必要がある点です。

// スーパークラス
class Animal {
  String name = 'Animal';
  void eat() => print('$name is eating.');
}

// サブクラス
class Dog implements Animal {
  // スーパークラスの変数とメソッドの実装が必須
  String name = 'Dog';
  void eat() => print('$name is eating dog food.');
  
  // サブクラス独自のクラス
  void sleep() => print('$name is sleeping.');
}

void main() {
  Dog myDog = Dog();
  myDog.eat();      // => Dog is eating dog food.
  myDog.sleep();    // => Dog is sleeping.

  Animal myAnimal = Animal();
  myAnimal.eat();    // => Animal is eating.
}

C#では明示的にinterfaceを定義して、クラスに実装させる必要があります。

// interface
public interface IAnimal
{
    public string Name { get; set; }
    public void Eat() => Console.WriteLine($"{Name} is eating.");
}

// IAnimalの実装クラス
public class Dog : IAnimal
{
    public string Name { get; set; } = "Dog";
    public void Eat() => Console.WriteLine($"{Name} is eating dog food.");

    public void Sleep() => Console.WriteLine($"{Name} is sleeping.");
}

public class Program
{
    public static void Main()
    {
        Dog myDog = new Dog();
        myDog.Eat();    // => Dog is eating dog food.
        myDog.Sleep();  // => Dog is sleeping.

        IAnimal myDog2 = new Dog();
        myDog2.Eat();    // => Dog is eating dog food.
        // myDog2.Sleep();  IAnimalにはSleepメソッドが定義されていないため呼び出せない
    }
}

ちなみにDart 3.0以降ではinterface修飾子が導入され、C#と似た挙動ができるようになっています。

abstract interface classはC#におけるinterfaceとほぼ同じです。

// interface修飾子を付けるとextendsでの継承が禁止される
interface class Animal {
  void eat() => print('Animal is eating.');
}

// abstract修飾子と組み合わせると、実装を持たない純粋なinterfaceを定義できる
abstract interface class Animal2 {
  void eat();
}

// interfaceの実装クラス
class Dog implements Animal {
  // メソッドの実装が必須
  void eat() => print('dog is eating.');
  void sleep() => print('dog is sleeping.');
}

// abstract interfaceの実装クラス
class Cat implements Animal2 {
  // メソッドの実装が必須
  void eat() => print('cat is eating.');
  void sleep() => print('cat is sleeping.');
}

void main() {
  Dog myDog = Dog();
  myDog.eat();      // => dog is eating.
  myDog.sleep();    // => dog is sleeping.

  Cat myCat = Cat();
  myCat.eat();      // => cat is eating.
  myCat.sleep();    // => cat is sleeping.
  
  Animal myAnimal = Animal();
  myAnimal.eat();    // => Animal is eating.
  
  // abstractクラスはインスタンス化できない
  //Animal myAnima2 = Animal2();
}

抽象クラス

abstract修飾子を付けることで抽象クラスを定義できます。

インスタンス化できなくなるなどの特徴もC#と同じため、Dart側のサンプルコードのみ記載します。

// 抽象クラス
abstract class Animal {
  void eat();
}

// 抽象クラスの継承
class Dog implements Animal {
  // メソッドの実装が必須
  void eat() => print('dog is eating.');
  void sleep() => print('dog is sleeping.');
}

void main() {
  Dog myDog = Dog();
  myDog.eat();      // => dog is eating.
  myDog.sleep();    // => dog is sleeping.

  // abstractクラスはインスタンス化できない
  //Animal myAnimal = Animal();
}

ミックスイン

DartはC#と同様にクラスの多重継承をサポートしていませんが、ミックスインという仕組みを使えば多重継承を実現させることができます。

mixinキーワードでミックスインを定義し、withキーワードを使うことでミックスインを実装することができます。

一見クラスと似ていますが、多重継承の可否以外にも、ミックスインとクラスには以下のような違いがあります。

  • コンストラクタを定義できない
  • インスタンス化できない
  • 明示的にメソッドをオーバーライドする必要がない
// ミックスイン
mixin Runnable {
  void run() => print('Running.');
}

mixin Flyable {
  void fly() => print('Flying.');
}

// ミックスインの実装クラス
class Dog with Runnable {
  void bark() => print('Dog is barking.');
}

class Bird with Flyable {
  void sing() => print('Bird is singing.');
}

class Duck with Runnable, Flyable {
  void quack() => print('Duck is quacking.');
}

void main() {
  Dog myDog = Dog();
  myDog.bark();   // => Dog is barking.
  myDog.run();    // => Running.

  Bird myBird = Bird();
  myBird.sing(); // => Bird is singing.
  myBird.fly();   // => Flying.

  Duck myDuck = Duck();
  myDuck.quack(); // => Duck is quacking.
  myDuck.run();   // => Running.
  myDuck.fly();   // => Flying.
}

C#ではインターフェースのデフォルト実装(C# 8.0以降)を使えば、ミックスインと同様の表現をすることができます。

public interface IRunnable
{
    void Run() => Console.WriteLine("Running.");
}

public interface IFlyable
{
    void Fly() => Console.WriteLine("Flying.");
}

public class Dog : IRunnable
{
    public void Bark() => Console.WriteLine("Dog is barking.");
}

public class Bird : IFlyable
{
    public void Sing()
    {
        Console.WriteLine("Bird is singing.");
    }
}

public class Duck : IRunnable, IFlyable
{
    public void Quack()
    {
        Console.WriteLine("Duck is quacking.");
    }
}

public class Program
{
    public static void Main()
    {
        var myDog = new Dog();
        myDog.Bark();   // => Dog is barking.
        ((IRunnable)myDog).Run();    // => Running.

        Bird myBird = new Bird();
        myBird.Sing(); // => Bird is singing.
        ((IFlyable)myBird).Fly();   // => Flying.

        Duck myDuck = new Duck();
        myDuck.Quack(); // => Duck is quacking.
        ((IRunnable)myDuck).Run();   // => Running.
        ((IFlyable)myDuck).Fly();   // => Flying.
    }
}

拡張メソッド

Dartにも拡張メソッドの機能があり、既存クラスに新しいメソッドを追加することができます。

以下はStringクラスに文字列を反転する拡張メソッドを追加する例です。

extension StringExtensions on String {
  // 文字列を反転する拡張メソッド
  String reverse() {
    return split('').reversed.join('');
  }
}

void main() {
  String text = 'hello';
  print(text.reverse());  // => olleh
}

C#で同様の拡張メソッドを追加できます。

public static class StringExtensions
{
    public static string Reverse(this string str)
    {
        return new string(str.ToCharArray().Reverse().ToArray());
    }
}

public class Program
{
    public static void Main()
    {
        string text = "hello";
        Console.WriteLine(text.Reverse()); // => olleh
    }
}

Enum

Dartにも列挙型がありますが、C#と異なり、コンストラクタ(constコンストラクタかfactoryコンストラクタのみ)やメソッドを持つことができます。

// 単純なEnum
enum Color {
  red, blue, green,
}

// 変数やコンストラクタが定義されたEnum
enum Direction {
  north('North'),
  south('South'),
  east('East'),
  west('West');

  // インスタンス変数(finalである必要がある)
  final String name;

  // constコンストラクタ
  const Direction(this.name);
}

void main() {
  Direction direction = Direction.north;
  print('Direction: ${direction.name}'); // => Direction: North

  // Enumの全ての名称を表示
  for (var direction in Direction.values) {
    print('Direction: ${direction.name}');
  }
}

C#ではEnumにメソッド等を定義できないため、拡張メソッドを使って似たような機能を実現させます。

public enum Direction
{
    North, South, East, West
}

// Direction列挙型のヘルパークラス
public static class DirectionHelper
{
    public static string GetName(this Direction direction)
    {
        return direction switch
        {
            Direction.North => "North",
            Direction.South => "South",
            Direction.East => "East",
            Direction.West => "West",
            _ => throw new ArgumentOutOfRangeException(nameof(direction)),
        };
    }
}

public class Program
{
    public static void Main()
    {
        var north = Direction.North;
        Console.WriteLine($"Direction: {north.GetName()}");  // => Direction: North

        // Enumの全ての名称を表示
        foreach (Direction direction in Enum.GetValues(typeof(Direction)))
        {
            Console.WriteLine($"Direction: {direction.GetName()}");
        }
    }
}

final修飾子

final修飾子が付いたクラスは継承が禁止されます。

C#でのsealed修飾子と同じで、abstractinterface修飾子との併用はできません。
Dart側のサンプルコードのみ記載します。

final class Animal {
  void eat() => print('Animal is eating.');
}

// finalが付いたクラスは継承できない(implementsも不可)
//class Dog exetnds Animal {
//  void eat() => print('dog is eating.');
//}

非同期処理

非同期処理の基本的な構文を中心に解説します。

Future

DartではFuture型、C#ではTask型と、非同期処理の戻り値の型が異なりますが、asyncawaitキーワードを使う点などは同じです。

import 'dart:async';

Future<void> fetchData() async {
  // 2秒間待つ
  await Future.delayed(Duration(seconds: 2));
}

void main() async {
  await fetchData();
}

C#でのサンプルコードです。その他の細かな違いとしてasyncキーワードの位置が両者で異なります。

class Program
{
    static async Task FetchData()
    {
        await Task.Delay(2000);
    }

    static async Task Main()
    {
        await FetchData();
    }
}

Stream

一連のデータを非同期に処理する機能をC#では非同期ストリームといいますが、DartではStream型を使うことでその機能を実現できます。

Stream型を返す関数はasync*キーワードとyield文を使用します。呼び出し側はawait forを使うことでStreamの値を順次受け取ることができます。

import 'dart:async';

// 非同期ストリームを生成する関数(非同期ジェネレータ)
Stream<int> generate() async* {
  for (int i = 0; i < 5; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i;
  }
}

void main() async {
  // 非同期ストリームから値を順次受け取る
  await for (int number in generate()) {
    print(number);
  }
}

C#ではIAsyncEnumerable<T>yield文を組み合わせることで非同期ストリームを生成でき、await foreachで非同期ストリームの値を受け取ります。

public class Program
{
    public static async IAsyncEnumerable<int> Generate()
    {
        for (int i = 0; i < 5; i++)
        {
            await Task.Delay(1000);
            yield return i;
        }
    }

    public static async Task Main()
    {
        await foreach (var number in Generate())
        {
            Console.WriteLine(number);
        }
    }
}

おわりに

以上、簡単ではありましたがDartとC#の主要な言語仕様の比較をしてみました。

どちらの言語も似ている仕様が多いので、得意な言語と比較しながら学ぶと新しい言語もスムーズに習得できるはずです。

第1回・第2回の記事も参考にしてみてください。

おすすめ書籍

Flutterを使ったアプリ開発を実践的に学べる入門書です。Dart言語の仕様も一通り紹介されているので初めてFlutter開発にチャレンジする方にもおすすめです。

タイトルの通り、実務で役に立つイディオムやパターンが豊富に紹介されており、初級者から中級者まで幅広くおすすめできる書籍です。(2024年7月に改訂版が発売しました)

C#
hiranote

コメント

タイトルとURLをコピーしました