【Dart】バイナリのデータ変換: Uint8List, List<int>, ByteData, ByteBuffer, Base64, File, String

対象者

  • DartやFlutterでのアプリ開発において、バイナリデータの操作や変換に関心がある開発者
  • ファイル操作、ネットワーク通信、データのエンコードやデコードなど、低レベルのデータ処理について学びたいと考えているプログラマー
  • パフォーマンスを意識した効率的なデータ処理方法を探求しているエンジニア

はじめに

プログラミングの世界では、データは生命線です。特に、DartやFlutterを使ったアプリ開発では、バイナリデータの扱い方がアプリのパフォーマンスと直結します。画像を扱う、ファイルを操作する、サーバーとデータをやり取りする…これら全てのタスクに共通して必要なのが、バイナリデータの操作です。しかし、多くの開発者が直面するのは、Uint8ListList<int>ByteDataなどのクラス間でのデータ変換の複雑さです。

本記事では、これらのクラス間での変換方法を明快に解説し、あなたのDart/Flutter開発を一歩前進させるための知識を提供します。

Dartのバイナリ関連のクラス

DartやFlutterで扱われるバイトデータを操作するための主要なクラスには、Uint8ListList<int>ByteDataなどがあります。これらは、バイナリデータやファイルシステムの操作、ネットワーク通信などで頻繁に使用されます。それぞれのクラスの特徴を以下に説明します。

Uint8List

  • Uint8Listは、固定長の8ビット符号なし整数のリストです。これはTypedDataライブラリの一部で、バイナリデータを効率的に扱うために設計されています。
  • メモリ上で連続した領域を占めるため、大量のデータを扱う際にパフォーマンスの面で有利です。
  • バイトデータを直接操作する場合や、ファイル、ネットワーク通信からの生データの読み書きに適しています。

List<int>

  • List<int>は、整数のリストで、Dartの基本的なリスト構造です。各要素はint型で、これをバイトデータとして使用することができますが、各要素がバイトサイズ(0-255)に収まることを自分で保証する必要があります。
  • List<int>は汎用的で、Uint8Listよりも柔軟性がありますが、バイトデータを扱う際にはUint8Listの方が効率的です。
  • バイトデータ以外の目的で整数のリストを扱う場合にも使用されます。

ByteData

  • ByteDataは、バイトバッファ上の固定サイズのビューを提供します。これを使用すると、バイトデータ内の任意の位置から異なるデータ型(例えば、8ビット整数、32ビット浮動小数点数など)を読み書きできます。
  • エンディアン(バイトオーダー)を指定してデータを読み書きする機能を提供します。これは、異なるプラットフォーム間でデータを交換する際に重要になります。
  • バイナリデータを構造化して扱う場合や、特定のフォーマットのデータを解析、生成する場合に便利です。

ByteBuffer

  • ByteBufferは、バイトデータの生のバッファを表します。Uint8ListByteDataは、内部的にはByteBufferを使用しています。
  • 直接ByteBufferを使用することは少なく、通常はUint8ListByteDataを介してアクセスします。
  • バッファ全体を一括で操作する必要がある場合に使用されます。

これらのクラスは、Dartのdart:typed_dataライブラリによって提供されており、バイナリデータの効率的な操作を可能にします。用途に応じて適切なクラスを選択することが重要です。例えば、生のバイトデータを扱う場合はUint8List、複雑なバイナリ構造を解析する場合はByteDataが適しています。

Base64

Base64は、バイナリデータをテキスト形式で表現するためのエンコーディング方式です。データを64種類の印字可能なASCII文字に変換することで、バイナリデータをテキストベースのメディアで安全に転送・保存することが可能になります。Dartでは、dart:convertライブラリを通じてBase64のエンコードとデコード機能を提供しています。これにより、画像、ファイル、あるいは任意のバイナリデータを文字列として扱うことができるようになります。

バイトデータ間の変換

概要

Uint8List を中心にして、データ変換をすると良い気します。

  • Uint8List ↔ List<int>
  • Uint8List ↔ ByteData
  • ByteData↔ByteBuffer

準備

import 'dart:typed_data';
「import ‘dart:nativewrappers/_internal/vm/lib/typed_data_patch.dart’;」ではない。(なんじゃこりゃ)

Uint8List と List の変換

test('Uint8List と List<int> の変換', () {
  Uint8List uint8list1 = Uint8List.fromList([0, 1, 2, 3]);
  List<int> listInt = uint8list1;
  Uint8List uint8list2 = Uint8List.fromList(listInt);

  expect(uint8list1, uint8list2);
});

Uint8List と ByteData の変換

test('Uint8List と ByteData の変換', () {
  Uint8List uint8list1 = Uint8List.fromList([0, 1, 2, 3]);
  ByteData byteData = ByteData.view(uint8list1.buffer);
  Uint8List uint8list2 = byteData.buffer.asUint8List();

  expect(uint8list1, uint8list2);
});

Uint8List と ByteBuffer の変換

test('Uint8List と ByteBuffer の変換', () {
  Uint8List uint8list1 = Uint8List.fromList([0, 1, 2, 3]);
  ByteBuffer buffer = uint8list1.buffer;
  Uint8List uint8list2 = buffer.asUint8List();

  expect(uint8list1, uint8list2);
});

Uint8List、List、および String の相互変換

test('Uint8List、List<int>、および String の相互変換', () {
  String string1 = "Hello, World!";
  Uint8List uint8list1 = Uint8List.fromList(utf8.encode(string1));
  List<int> listInt = Uint8List.fromList(utf8.encode(string1));

  String string2 = utf8.decode(uint8list1);
  expect(string1, string2);

  String string3 = utf8.decode(listInt);
  expect(string1, string3);
});

ByteData と String の変換

test('ByteData と String の変換', () {
  ByteData byteData1 = ByteData(5);
  byteData1.setUint8(0, 72); // H
  byteData1.setUint8(1, 101); // e
  byteData1.setUint8(2, 108); // l
  byteData1.setUint8(3, 108); // l
  byteData1.setUint8(4, 111); // o
  Uint8List uint8list1 = byteData1.buffer.asUint8List();
  String string1 = utf8.decode(uint8list1);
  expect(string1, 'Hello');

  ByteData byteData2 = ByteData.sublistView(Uint8List.fromList(utf8.encode(string1)));
  expect(byteData1.lengthInBytes, byteData2.lengthInBytes);
  for (int i = 0; i < byteData1.lengthInBytes; i++) {
    expect(byteData1.getUint8(i), byteData2.getUint8(i));
  }
});

File と Uint8List の変換

test('File と Uint8List の変換', () async {
  final file = File('temp.txt');
  await file.writeAsString('Hello, World!');
  Uint8List uint8list1 = await file.readAsBytes();
  await file.writeAsBytes(uint8list1);

  Uint8List uint8list2 = await file.readAsBytes();
  expect(utf8.decode(uint8list2), 'Hello, World!');

  String string = await file.readAsString();
  expect(string, 'Hello, World!');

  await file.delete();
});

String と Base64 の変換

test('String と Base64 の変換', () {
  String originalString = 'Hello, World!';
  String encoded = base64Encode(utf8.encode(originalString));
  String decoded = utf8.decode(base64Decode(encoded));
  expect(decoded, originalString);
});

Uint8List と Stream<int>, Stream<List<int>> の変換

test('Uint8List と Stream<int>, Stream<List<int>> の変換', () {
  Uint8List uint8list = Uint8List.fromList([0, 1, 2, 3]);
  Stream<int> streamInt = Stream.fromIterable(uint8list);
  expect(
    streamInt,
    emitsInOrder([0, 1, 2, 3, emitsDone]),
  );
  Stream<List<int>> streamListInt = Stream.fromIterable([uint8list]);
  expect(
    streamListInt,
    emitsInOrder([
        [0, 1, 2, 3],
        emitsDone
    ]),
  );
});

Q&A

Q1: Uint8List の挙動がなんかおかしい

A1: 「import ‘dart:nativewrappers/_internal/vm/lib/typed_data_patch.dart’;」でインポートしてないか確認してください。そうなら「import ‘dart:typed_data’;」に修正することで、正しい挙動になります。この問題は、間違ったライブラリをインポートすることで発生するため、正しいパッケージを使用することで解決できます。

まとめ

DartでバイナリやString,Fileの変換処理を迷うときがあるので、一挙にまとめてみました。参考になれば幸いです!

参考