当底层数据不是UTF-16时,有效地实现DbDataReader.GetChars()

本文关键字:实现 有效地 DbDataReader GetChars 数据 UTF-16 | 更新日期: 2023-09-27 18:17:49

我需要为ADO实现DbDataReader.GetChars()。. NET提供程序,注意单元格中的数据可能不是UTF-16,实际上可能是许多不同编码中的任何一种。

这个实现是专门针对'长数据'的,源数据在服务器上。我与服务器的接口(实际上无法更改)是为单元请求一个字节范围。服务器不以任何方式解释这些字节,它只是二进制数据。

我可以用明显的实现特殊情况下的UTF-16LE和UTF-16BE,但对于其他编码,没有直接的方法将请求"获取我UTF-16码单位X到X + Y"转换为请求"获取我字节X'到X' + Y'编码Z"。

一些消除明显实现的"需求":

  • 我不希望在任何时候检索给定单元的所有数据到客户端,除非有必要。单元可能非常大,请求几千字节的应用程序不应该处理分配的数百兆内存来满足请求。
  • 我希望相对有效地支持GetChars()暴露的随机访问。在第一个请求代码单元10亿到10亿+ 10的情况下,我看不出有任何方法可以避免从服务器检索单元中的所有数据,直到所请求的代码点,但是随后请求代码单元10亿+ 10到10亿+ 20,甚至代码点999亿999千到10亿都不应该意味着重新检索所有数据。

我猜绝大多数应用程序实际上不会"随机"访问长数据单元,但是如果这样做的话,避免糟糕的性能会很好,所以如果我找不到一个相对简单的方法来支持它,我想我不得不放弃它。

我的想法是保持#{UTF-16编码单元}-> #{字节的数据在服务器编码}的映射,更新它,因为我从单元检索数据,并使用它来找到一个"关闭"的地方开始从服务器请求数据(而不是从开始每次检索。顺便说一句,. net框架中缺少类似于c++的std::map::lower_bound的东西,这让我很沮丧)。不幸的是,我发现生成这个映射非常困难!

我一直在尝试使用Decoder类,特别是Decoder. convert()来转换数据碎片,但我不知道如何可靠地告诉源数据的给定字节数映射到确切的X UTF-16码单元,因为'bytesUsed'参数似乎包括源字节,这些字节只是存储到对象的内部状态,而不是作为字符输出。这导致我在尝试解码从部分代码点开始或以部分代码点中间结束时出现问题,并给我垃圾。

所以,我的问题是,是否有一些"技巧"我可以用来完成这一点(弄清楚#bytes到#codeunits的确切映射,而不诉诸于在循环中转换之类的东西,逐字节减少源的大小)?

当底层数据不是UTF-16时,有效地实现DbDataReader.GetChars()

您知道您的服务器可能提供哪些编码吗?我问这个问题是因为有些编码是"有状态的",这意味着给定字节的含义可能取决于它前面的字节序列。例如(源),在编码标准ISO 2022-JP中,两个字节0x24 0x2c可能意味着一个日文平假名字符"GA"()或两个ASCII字符"$"answers"$",根据"移位状态"——前面的控制序列的存在。在一些unicode前的"shift - jis"日文编码中,这些移位状态可以出现在字符串中的任何位置,并将应用于所有后续字符,直到遇到新的移位控制序列。在最坏的情况下,根据该站点的说法,"通常,只有通过从头开始线性读取非unicode文本才能可靠地检测到字符边界"。

即使c#使用的UTF-16编码(理论上是无状态的),由于代理对和组合字符的存在,也比通常实现的更复杂。代理对是一对char,它们一起指定一个给定的字符,例如 ;这些是必需的,因为有超过ushort.MaxValue个unicode码点。组合字符是应用于前面字符的变音符标记序列,例如字符串"Ĥ=T²+V²"。当然,它们可以共存,尽管不美观: *,这意味着一个抽象的UTF-16"文本元素"可以由一个或两个"基本"字符加上一些变音符号或其他组合字符组成。从用户的角度来看,所有这些都构成了一个单一的字符,因此永远不应该分割或孤立。

所以一般的算法是,当你想从偏移量K处开始从服务器获取N个字符时,对于一些"足够大"的E,从K-E处开始获取N+E,然后向后扫描,直到找到第一个文本元素边界。遗憾的是,对于UTF-16,微软没有提供一个API来直接做到这一点,人们需要对他们的方法进行逆向工程

internal static int GetCurrentTextElementLen(String str, int index, int len, ref UnicodeCategory ucCurrent, ref int currentCharCount)
在StringInfo.cs

有点麻烦,但还是可行的。

对于其他有状态的编码,我不知道如何做到这一点,并且向后扫描以找到第一个字符边界的逻辑将特定于每种编码。对于像Shift-JIS系列中的编码,您很可能需要向后扫描任意远。

不是真正的答案,但是对于评论来说太长了。

您可以对所有单字节编码尝试您的算法。我的电脑里有95种这样的编码:

        var singleByteEncodings = Encoding.GetEncodings().Where((enc) => enc.GetEncoding().IsSingleByte).ToList();  // 95 found.
        var singleByteEncodingNames = Encoding.GetEncodings().Where((enc) => enc.GetEncoding().IsSingleByte).Select((enc) => enc.DisplayName).ToList();  // 95 names displayed.
        Encoding.GetEncoding("iso-8859-1").IsSingleByte // returns true.

这在实践中可能很有用,因为许多旧的数据库只支持单字节字符编码,或者没有为它们的表启用多字节字符。例如,SQL Server数据库的默认字符编码是iso_1,即ISO 8859-1。但请看一位微软博主的警告:

使用IsSingleByte()来尝试找出编码是否是单个字节的代码页,但是我真的建议你不要对编码做太多的假设。假设是1对1关系的代码,然后试图寻找或备份,或者其他东西可能会混淆,编码不利于这种行为。回退,解码器和编码器可以改变单个调用的字节计数行为,编码有时可以做意想不到的事情。

我弄清楚了如何处理可能丢失的转换状态:我在映射中保留了解码器的副本,以便在从相关偏移重新启动时使用。这样我就不会丢失它在内部缓冲区中保留的任何部分代码点。这也让我避免添加特定于编码的代码,并处理编码的潜在问题,如dbc提出的Shift-JIS。

解码器是不可克隆的,所以我使用序列化+反序列化来复制。