🎃

共変性と反変性の簡単な説明とDartにおける例

2022/02/20に公開

共変性と反変性の簡単な説明

以下、基本原則(リスコフの置換原則)を前提とし、

原則と同じく、基底型変数を派生型に置き換えられることを共変(きょうへん)と呼ぶ。

一方で、呼び出し構造により見かけ上互換性の向きが反対になっていて、派生型変数を基底型に置き換えられることを反変(はんぺん)と呼ぶ。

具体的な用例

基底型の戻り値を持つ関数型やクラスの型に、派生型の戻り値を持つ関数やクラスのインスタンスが代入できる。
→ 戻り値の基底型が派生型に置き換えられる。
→ 戻り値に共変性がある。

派生型の引数を持つ関数型やクラスの型に、基底型の引数を持つ関数やクラスのインスタンスが代入できる。
→ 引数の派生型が基底型に置き換えられる。
→ 引数に反変性がある。

Dart における例

共変

repository.dart
class Repository {
  const Repository(Api api);
  final Api api;
}
api.dart
// 基底クラス
abstract class Api {
  Future<String> fetch();
}

// 派生クラスその1
class ApiProd implements Api {
  
  Future<String> fetch() async {
    final response = await http.get('https://www.example.com/data');
    return response.body;
  }
}

// 派生クラスその2
class ApiMock implements Api {
  
  Future<String> fetch() async {
    return '{"code":"OK"}';
  }
}
main.dart
final Repository repository;
if (F.flavor == Flavor.mock) {
  repository = Repository(
    // 共変: 基底クラス [Api] の引数に派生クラス [ApiMock] を指定している
    ApiMock(),
  );
} else {
  repository = Repository(
    // 共変: 基底クラス [Api] の引数に派生クラス [ApiProd] を指定している
    ApiProd(),
  );
}

反変

article.dart
import 'package:freezed_annotation/freezed_annotation.dart';

part 'article.freezed.dart';

/// 記事の内容を格納するUnionクラス
///
/// 基底クラス

class Article with _$Article {
  /// テキストのみの記事
  ///
  /// 派生クラスその1
  const factory Article.text({
    required String id,
    required String content,
  }) = TextArticle;

  /// 写真のみの記事
  ///
  /// 派生クラスその2
  const factory Article.picture({
    required String id,
    required Uri url,
  }) = PictureArticle;
}

/// IDとタイトルを合わせてBase64化し、ローカルにキャッシュするファイル名を生成
///
/// 派生クラス間で生成方法は変わらないため、基底クラス [Article] を引数に取る
String generateFileNameFromArticle(Article article) {
  final seed = '${article.id}:${article.title}';
  final bytes = utf8.encode(seed);
  return base64.encode(bytes);
}
main.dart
/// 派生クラス [TextArticle] をフェッチし、ローカルにキャッシュして返すメソッド
///
/// ファイル名生成の関数の引数 [generateFileName] は、派生クラス [TextArticle] を引数に取る
Future<TextArticle> _fetchTextArticle({
  required String id,
  required String Function(TextArticle) generateFileName,
}) async {
  final article = await _remoteDataSource.fetchTextArticle(id: id);

  final fileName = generateFileName(article);
  await _localDataSource.save(article: article, fileName: fileName);

  return article;
}

await _fetchTextArticle(
  id: id,
  // 反変: 引数が派生クラス [TextArticle] の関数の引数 [generateFileName] に、
  //  引数が基底クラス [Article] の関数 [generateFileNameFromArticle] を指定している
  generateFileName: generateFileNameFromArticle,
);

定義方法を変えることで反変の関係だったものを共変の関係に変えることもでき、また、個人的な感想として、共変の関係の方が直感的で設計しやすいと感じる。そのため、あまり反変の関係で設計することはなさそう。

参考

https://medium.com/dartlang/dart-declaration-site-variance-5c0e9c5f18a5

https://qiita.com/CodeOne/items/730480791c2e98b40d66#反変性がサポートされると

https://zenn.dev/suin/scraps/adf781330c9888

https://qiita.com/mkosuke/items/42c19d7edbf111f7fb71#futureortは戻り値に使用しない

GitHubで編集を提案

Discussion