StreamReaderのReadLine/ReadLineAsyncメソッド、まぁ便利です。
改行コードがLF/CR/CR+LFどれであっても、いい感じに読み込んでくれます。
が、…このメソッド、CRが来るだけでは処理を返してくれません!!!!
CRを読み込んだ後、他の文字が来るか、もしくはもうEOFだということがわかるまで、なんと処理を返さず待機するのです!
なんでやねん!CR読み込んだ時点で1行確定でしょ!!すぐに返してよ!!!!
この動作、テキストファイル等、すでにすべてのデータが揃っているストリームに対しては問題無いですね。
ですが、例えばネットワークなどでCRを区切りとしている系のAPIだと、応答が返ってきているはずなのに何も来ない…とか、1回分遅れて応答が帰ってくるように見える…ということが発生します。
CRの次の文字が来るまで返さないので、CRが来ただけでは、最新の1行分の応答がStreamReaderにバッファされたままになるのです。
先日、PJLinkのC#ラッパーを書いていてこの問題にぶち当たりました。
PJLinkとは、プロジェクタ等をLAN経由で操作するための標準仕様ですが、コマンドや応答の終端をCRとすることになっているのです。
つまり、
→コマンド1を送る
(←コマンド1の応答が返ってくるけどStreamReaderにバッファされるだけ)
→コマンド2を送る
←コマンド1の応答がやっと返される(そしてコマンド2の応答はStreamReaderにバッファされる)
みたいな。
しかたがないので、改行コードがCRな1行を読み込んだら、すぐに返すクラスを書いてみた。
長いけどこんなん。
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading.Tasks;
namespace ShComp
{
    /// <summary>
    /// 改行コードがCRの場合に、1行を読み込んですぐに応答を返すメソッドを提供します。
    /// </summary>
    class CRLineReader : IDisposable
    {
        private StreamReader _reader;
        private bool _isEof;
        private int _bufferStart;
        private int _bufferEnd;
        private char[] _buffer;
        public CRLineReader(Stream stream, int capacity = 136)
        {
            _reader = new StreamReader(stream);
            _buffer = new char[capacity];
        }
        /// <summary>
        /// CR区切りの1行を非同期に読み込みます。CR以外の改行コードは、改行コードとみなしません。
        /// </summary>
        public async Task<string> ReadLineAsync()
        {
            if (_isEof) return null;
            var sb = new StringBuilder();
            if (_bufferEnd > _bufferStart)
            {
                if (AppendFromBuffer(sb))
                {
                    return sb.ToString();
                }
            }
            _bufferStart = 0;
            while (true)
            {
                _bufferEnd = await _reader.ReadAsync(_buffer, 0, _buffer.Length);
                if (_bufferEnd == 0)
                {
                    _isEof = true;
                    return sb.ToString();
                }
                if (AppendFromBuffer(sb))
                {
                    return sb.ToString();
                }
            }
        }
        private bool AppendFromBuffer(StringBuilder sb)
        {
            for (int i = _bufferStart; i < _bufferEnd; i++)
            {
                if (_buffer[i] == 'r')
                {
                    sb.Append(_buffer, _bufferStart, i - _bufferStart);
                    _bufferStart = i + 1;
                    return true;
                }
            }
            sb.Append(_buffer, _bufferStart, _bufferEnd - _bufferStart);
            return false;
        }
        public void Dispose()
        {
            _reader.Dispose();
        }
    }
}
バッファサイズが136なのは、PJLinkの応答の最大長が136だから。
PJLink、ASCIIな文字しか使わないから、StreamReader介さず直接Streamから読み込んでもいい気がするけど、まぁ。
