Reader / Writer を理解したい
Go の io.Reader、io.Writer で学べ
Reader というのは外部システムからプログラムのメモリ上にデータを読み込むという理解であっていますか?
Reader から変数へ読み込み(Reader からバイトスライスにデータを読み込む)
はい、その理解は基本的に正確です。Readerはデータソース(これは外部システム、ファイル、ネットワーク接続、あるいはメモリ内の他の部分でさえもよい)からデータを読み取り、プログラムがそれを操作できる形(通常はメモリ上のデータ構造)に読み込む機能を提供します。
ただし、一般的には「Reader」は一部のプログラミング言語やライブラリにおける具体的なインターフェイスやクラスを指すものであり、その詳細な動作や機能はその言語やライブラリによって定義されます。そのため、具体的な動作や能力は使用しているシステムによります。
たとえば、Go言語のio.ReaderインターフェイスはReadメソッドを定義しています。このメソッドはバイトスライスを引数にとり、データソースからそのスライスにデータを読み込みます。読み込みが成功すると、読み込んだバイト数とエラー(存在する場合)を返します。
一方、Java言語のjava.io.Reader抽象クラスも同様の目的を果たしますが、そのメソッドや動作は異なります。このクラスは主に文字データの読み込みに使用され、バイトではなく文字を操作します。また、異なる具体的なReaderクラスが異なる種類のデータソースからの読み取りをサポートしています。
なぜ Reader はバイト列でデータを読み込むのでしょうか?文字列や数値として読み込まないのですか?
データをバイト列(バイナリデータ)として読み込む主な理由は、その方がより一般的で汎用的だからです。ReaderやWriterのような抽象的なインターフェースをバイナリデータに対して定義することで、あらゆる種類のデータに対する操作を一貫した方法で行うことができます。
バイト列は、あらゆる種類のデータ(テキスト、画像、音声、動画、シリアライズされたオブジェクトなど)を表現するのに使用できます。したがって、ReaderやWriterがバイト列を基本のデータ単位として扱うことで、これらのインターフェースを任意のデータ形式に対して使用できます。
一方、文字列や数値としてデータを直接扱うと、そのデータ形式に対してのみ有効となります。例えば、文字列を直接扱うReaderはテキストデータに対しては機能しますが、画像データや音声データに対しては使えません。これは、テキストデータとそれ以外のデータ形式では、その内部表現が大きく異なるためです。
しかし、バイト列としてデータを読み込んだ後は、必要に応じてそのデータを任意の形式(文字列や数値など)に変換することができます。この変換処理は通常、特定のデコーダ(例えばTextDecoderやDataView)を使用して行います。
したがって、ReaderやWriterがバイト列を扱うことで、あらゆる種類のデータに対する一般的で汎用的な操作を可能にしています。
Reader から読み込むと消費される
import { StringReader } from "https://deno.land/std@0.195.0/io/mod.ts";
import { iterateReader } from "https://deno.land/std@0.195.0/streams/iterate_reader.ts";
// StringReader
const decoder = new TextDecoder();
const r = new StringReader("Hello, Deno!");
const it = iterateReader(r, { bufSize: 1 });
const results = [];
for await (const x of it) {
results.push(decoder.decode(x));
}
console.log(results);
/* => ["H", "e", "l", "l", "o", ",", " ", "D", "e", "n", "o", "!"] */
// 1 度目の iterateReader で消費されているので一文字も出力されない
const it2 = iterateReader(r, { bufSize: 2 });
const results2 = [];
for await (const x of it2) {
results2.push(decoder.decode(x));
}
console.log(results2);
多くの Reader の実装では、一度読み取ったデータは「消費」され、再度読み取ることはできません。具体的には、StringReaderの内部ではカーソル(現在の読み取り位置)が管理されており、readメソッドが呼び出されるたびにカーソルが進みます。そのため、すべてのデータを一度に読み取った後に再度readを呼び出しても、カーソルは既に末尾に達しているため、新たに読み取るデータはありません。
つまり、同じReaderを用いて別の読み取りを試みても、前の読み取りですべてのデータが消費されているため、新たに読み取ることができるデータはありません。
読み込んだデータの消費について StringReader と iterateReader の実装に潜る
StringReader
string を受け取って array buffer に変換して Buffer のコンストラクタに渡している。
-
Uint8Array:uint8の配列
// ref: https://github.com/denoland/deno_std/blob/2fc511fc422b4e1d147c65b736f2794f9d0df969/io/string_reader.ts
export class StringReader extends Buffer {
constructor(s: string) {
const encodedStr: Uint8Array = new TextEncoder().encode(s);
console.log(encodedStr);
const arrayBuffer: ArrayBufferLike = encodedStr.buffer;
console.log(arrayBuffer);
super(arrayBuffer);
}
}
// StringReader
const decoder = new TextDecoder();
const r = new StringReader("Hello, Deno!");
const it = iterateReader(r, { bufSize: 1 });
const results = [];
for await (const x of it) {
results.push(decoder.decode(x));
}
console.log(results);
/* => ["H", "e", "l", "l", "o", ",", " ", "D", "e", "n", "o", "!"] */
const it2 = iterateReader(r, { bufSize: 2 });
const results2 = [];
for await (const x of it2) {
results2.push(decoder.decode(x));
}
console.log(results2);
出力
$ deno run main.ts
Hello, Deno!
Uint8Array(12) [
72, 101, 108, 108, 111,
44, 32, 68, 101, 110,
111, 33
] 12
ArrayBuffer {
[Uint8Contents]: <48 65 6c 6c 6f 2c 20 44 65 6e 6f 21>,
byteLength: 12
} 12
[
"H", "e", "l", "l",
"o", ",", " ", "D",
"e", "n", "o", "!"
]
[]
ArrayBuffer について
ArrayBufferはJavaScriptで使われる型の一つで、一連のバイトデータを表す汎用的な固定長のバイナリデータバッファを示します。ArrayBufferはそのままでは直接データを操作することはできません。代わりに、それをビューとして見るための「TypedArray」(Int8Array, Uint8Array, Float32Array など)や DataView を使用します。
これらのビューはArrayBuffer上のバイト列を特定の数値型として扱います。例えば、Int8Arrayは8ビット整数としてデータを扱い、Float32Arrayは32ビット浮動小数点数としてデータを扱います。このようにして、JavaScriptでバイナリデータを効率的に扱うことができます。
このようなバイナリデータの操作は、例えばWebSocketやXHR、File API、Canvas、WebGLなど、JavaScriptでバイナリデータを扱う必要がある多くのAPIで使用されます。また、WebAssemblyと連携して高速な数値計算を行う際にも重要となります。
なお、上記のnew TextEncoder().encode(s).bufferで得られるのがArrayBufferです。encodeメソッドによって得られるのはUint8Arrayですが、そのbufferプロパティを参照することで元のArrayBufferにアクセスすることができます。
iterateReader
使われ方
const decoder = new TextDecoder();
const r = new StringReader("Hello, Deno!");
const it = iterateReader(r, { bufSize: 1 });
const results = [];
for await (const x of it) {
results.push(decoder.decode(x));
}
iterateReader の実装
Buffer
- 全体
-
Buffer.constructor- ArrayBuffer 型の引数を byte array に変換して保持している
- 基本的に Reader はバイト列で読み込んだデータを保持することが多い(Deno や Go で実装されている Reader はそう)
-
Buffer.readの実装- 受け取ったバッファに対して Reader が読み込んだデータをコピーする
- コピーして消費する実態は
Buffer.readSync
-
Buffer.readSyncの実装- L114-L116 で
this.#offしてReader.#bufを読み始める場所(offset)を進めている部分が「消費」の正体。一度 Reader.read でソースデータを読み込んでしまうと、2 度目は読み込めない(もちろん Reader.read で読み込んだデータを変数に格納すればそっちでデータにアクセスできるようになる)。 -
nreadは実際にコピーできたバイト数なので、Reader.read(buffer)で渡した buffer のサイズに依存する。今回のサンプルコードでは、buffer のサイズを 1 にしてあるので、Reader.#bufからは 1 バイトずつしかコピーされない。
- L114-L116 で
処理の流れ
import { StringReader } from "./reader.ts";
// import { iterateReader } from "https://deno.land/std@0.195.0/streams/iterate_reader.ts";
import { iterateReader } from "./iterate_reader.ts";
const decoder = new TextDecoder();
// load data to Reader
const r = new StringReader("Hello, Deno!");
// Read data from Reader
const it = iterateReader(r, { bufSize: 1 });
const results = [];
for await (const x of it) {
results.push(decoder.decode(x));
}
console.log(results);
/* => ["H", "e", "l", "l", "o", ",", " ", "D", "e", "n", "o", "!"] */
// 1 度目の iterateReader で消費されているので一文字も出力されない
const it2 = iterateReader(r, { bufSize: 2 });
const results2 = [];
for await (const x of it2) {
results2.push(decoder.decode(x));
}
console.log(results2);
- iterateReader
export async function* iterateReader(
r: Reader,
options?: {
bufSize?: number;
}
): AsyncIterableIterator<Uint8Array> {
const bufSize = options?.bufSize ?? DEFAULT_BUFFER_SIZE;
const b = new Uint8Array(bufSize);
while (true) {
const nread = await r.read(b);
if (nread === null) {
break;
}
console.log(`b: ${b.slice(0, nread)}`);
yield b.slice(0, nread);
}
}
- Reader によるデータの読み込み(データはバイト列として保持される)
- データソースを読み込んだ Reader を iterateReader に渡しイテレータの初期化
-
for awaitで iterateReader から yield されるデータを変数 results へ格納していく。- iterateReader の中身:
Reader.read(buffer)を実行し、Reader に保持されているデータを固定長の buffer に読み込む。
- iterateReader の中身:
- イテレータで取り出したデータを results に格納
- results を出力
ここ丁寧にやる