我意识到这确实为时已晚,但我自己偶然发现了StreamReader 中的这个令人难以置信的缺陷;使用StreamReader 时无法可靠地寻找的事实。就个人而言,我的具体需求是具有阅读字符的能力,但如果满足某个条件则“备份”;这是我正在解析的一种文件格式的副作用。
使用ReadLine() 不是一个选项,因为它只在非常琐碎的解析工作中有用。我必须支持可配置的记录/行分隔符序列并支持转义分隔符序列。另外,我不想实现自己的缓冲区,所以我可以支持“备份”和转义序列;那应该是StreamReader 的工作。
此方法按需计算底层字节流中的实际位置。它适用于 UTF8、UTF-16LE、UTF-16BE、UTF-32LE、UTF-32BE 和任何单字节编码(例如代码页 1252、437、28591 等),无论是否存在前导码/BOM。此版本不适用于 UTF-7、Shift-JIS 或其他可变字节编码。
当我需要寻找底层流中的任意位置时,我直接设置BaseStream.Position,然后调用DiscardBufferedData() 以使StreamReader 重新同步,以便下一次Read()/Peek() 调用。
还有一个友情提示:不要随意设置BaseStream.Position。如果你将一个字符一分为二,你将使下一个 Read() 无效,对于 UTF-16/-32,你也会使这个方法的结果无效。
public static long GetActualPosition(StreamReader reader)
{
System.Reflection.BindingFlags flags = System.Reflection.BindingFlags.DeclaredOnly | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.GetField;
// The current buffer of decoded characters
char[] charBuffer = (char[])reader.GetType().InvokeMember("charBuffer", flags, null, reader, null);
// The index of the next char to be read from charBuffer
int charPos = (int)reader.GetType().InvokeMember("charPos", flags, null, reader, null);
// The number of decoded chars presently used in charBuffer
int charLen = (int)reader.GetType().InvokeMember("charLen", flags, null, reader, null);
// The current buffer of read bytes (byteBuffer.Length = 1024; this is critical).
byte[] byteBuffer = (byte[])reader.GetType().InvokeMember("byteBuffer", flags, null, reader, null);
// The number of bytes read while advancing reader.BaseStream.Position to (re)fill charBuffer
int byteLen = (int)reader.GetType().InvokeMember("byteLen", flags, null, reader, null);
// The number of bytes the remaining chars use in the original encoding.
int numBytesLeft = reader.CurrentEncoding.GetByteCount(charBuffer, charPos, charLen - charPos);
// For variable-byte encodings, deal with partial chars at the end of the buffer
int numFragments = 0;
if (byteLen > 0 && !reader.CurrentEncoding.IsSingleByte)
{
if (reader.CurrentEncoding.CodePage == 65001) // UTF-8
{
byte byteCountMask = 0;
while ((byteBuffer[byteLen - numFragments - 1] >> 6) == 2) // if the byte is "10xx xxxx", it's a continuation-byte
byteCountMask |= (byte)(1 << ++numFragments); // count bytes & build the "complete char" mask
if ((byteBuffer[byteLen - numFragments - 1] >> 6) == 3) // if the byte is "11xx xxxx", it starts a multi-byte char.
byteCountMask |= (byte)(1 << ++numFragments); // count bytes & build the "complete char" mask
// see if we found as many bytes as the leading-byte says to expect
if (numFragments > 1 && ((byteBuffer[byteLen - numFragments] >> 7 - numFragments) == byteCountMask))
numFragments = 0; // no partial-char in the byte-buffer to account for
}
else if (reader.CurrentEncoding.CodePage == 1200) // UTF-16LE
{
if (byteBuffer[byteLen - 1] >= 0xd8) // high-surrogate
numFragments = 2; // account for the partial character
}
else if (reader.CurrentEncoding.CodePage == 1201) // UTF-16BE
{
if (byteBuffer[byteLen - 2] >= 0xd8) // high-surrogate
numFragments = 2; // account for the partial character
}
}
return reader.BaseStream.Position - numBytesLeft - numFragments;
}
当然,这使用反射来获取私有变量,因此存在风险。但是,此方法适用于 .Net 2.0、3.0、3.5、4.0、4.0.3、4.5、4.5.1、4.5.2、4.6 和 4.6.1。除了这个风险之外,唯一的另一个关键假设是底层字节缓冲区是byte[1024];如果 Microsoft 以错误的方式更改它,则该方法会因 UTF-16/-32 而中断。
已针对填充有 Ažテ?(10 字节:0x41 C5 BE E3 83 86 F0 A3 98 BA)的 UTF-8 文件和填充有 A?(6 字节:0x41 00 01 D8 37 DC)的 UTF-16 文件进行了测试。重点是沿着byte[1024] 边界强制分割字符,它们可能是所有不同的方式。
更新 (2013-07-03):我修复了该方法,该方法最初使用了其他答案中的损坏代码。此版本已针对包含需要使用代理对的字符的数据进行测试。数据被放入 3 个文件中,每个文件都有不同的编码;一种 UTF-8、一种 UTF-16LE 和一种 UTF-16BE。
更新 (2016-02):处理二等分字符的唯一正确方法是直接解释底层字节。 UTF-8 处理得当,UTF-16/-32 可以工作(给定 byteBuffer 的长度)。