koudenpaのブログ

趣味のブログです。株式会社はてなでWebアプリケーションエンジニアをやっています。職業柄IT関連の記事が多いと思います。

C#でバイト単位でファイルを読んでみる

社内の勉強会でGoのメモリアロケーションの話があって、最後に『チャレンジ問題』があった。

こんなの。

チャレンジ問題 ファイルの内容すべてをバイト列に読み込む関数 ReadFile(*File).Read などを用いて実装してください ヒント: リアロケーションが起きないようにするにはどうすればいいだろうか

そういえばC#書いててもファイル読み込みとかStreamReaderとか使ってて低レベルな読み込みしないなぁ、どんな感じになのかなぁ、と思ったのでちょっと見てみた。

結果、.NETでIOに使う Stream には Length っていう身もふたもないプロパティがあったのでその分だけメモリを確保すればよかった。

面白みはなかった。

気が向いたらソースコード眺めてみようかな、とは思った*1

Length プロパティはSeekできないStreamだと例外を吐く模様。ローカルファイルの読み込みならSeekできるだけで、ネットワーク越しのアクセスだと多分使えない。IOは奥が深い。

using System;
using System.IO;
using System.Text;

namespace dotnet_sandbox
{
    // ファイルの内容すべてをバイト列に読み込む関数 `ReadFile` を `(*File).Read` などを用いて実装してください
    // ヒント: リアロケーションが起きないようにするにはどうすればいいだろうか
    // 答え合わせは `os.ReadFile` を確認してください
    // Go 1.15 (ioutil) と1.16の実装の違いを確認し、ベンチマークしてみましょう。
    // PrometheusはGo 1.16でテストが落ちるようになりました ([https://github.com/prometheus/prometheus/issues/8403 #8403])。理由を調べてみましょう。
    class Program
    {
        static byte[] ReadToEnd(Stream stream)
        {
            var buf = new Byte[stream.Length];
            var i = 0;
            int b;
            // ReadByte はStreamが終端だと -1 を返すという現代とは思えないレガシーな仕様
            while ((b = stream.ReadByte()) >= 0)
            {
                // アンセーフにしてポインタを進めていったほうが早いのか? 分からん
                // そもそも長さが分かっているなら Read メソッドに buf  を渡せば一気に読めそう
                buf[i++] = (byte)b;
            }
            return buf;
        }

        static void Main(string[] args)
        {
            using (var stream = new FileStream(args[0], FileMode.Open))
            {
                var bytes = ReadToEnd(stream);
                Console.WriteLine($"{args[0]} length = {bytes.Length}");
                Console.WriteLine(Encoding.UTF8.GetString(bytes));
            }
        }
    }
}

*1:こういう時は大体眺めない