String.Substring 相对于其他字符串处理方法有多快?

4
我正在使用VB.NET处理一个长的定长记录。最简单的方法似乎是将整个记录加载到字符串中,并使用Substring按位置和长度访问字段。但是,每次调用Substring方法时似乎会发生一些冗余处理。这使我想知道是否可以使用基于流或数组的方法获得更好的结果。
内容最初作为包含UTF8字符数据的字节数组开始。我想到了几种其他方法,如下所示。
  1. 将字符串加载到StringReader中,并一次读取一块
  2. 将字节数组转换为字符数组,并在数组内按位置访问字符
  3. (这个方法似乎很愚蠢,但我还是提出来了)将字节数组复制到内存流中,并使用StreamReader
这绝对是过早优化;即使Substring方法稍微慢了几毫秒,它也可能是完全可接受的。但在编码之前,我想问一下是否有人能想到使用其他方法的原因。
4个回答

6

substring 的主要开销在于将子字符串剪切成一个新的字符串。使用 Reflector,您可以看到这一点:

private unsafe string InternalSubString(int startIndex, int length, bool fAlwaysCopy)
{
    if (((startIndex == 0) && (length == this.Length)) && !fAlwaysCopy)
    {
        return this;
    }
    string str = FastAllocateString(length);
    fixed (char* chRef = &str.m_firstChar)
    {
        fixed (char* chRef2 = &this.m_firstChar)
        {
            wstrcpy(chRef, chRef2 + startIndex, length);
        }
    }
    return str;
}

现在要到达那个位置(注意这不是Substring()),需要通过5个长度检查等步骤。
如果您多次引用相同的子字符串,则将所有内容提取出来并丢弃巨大的字符串可能会更有价值。这样做会产生存储所有这些子字符串的数组开销。
如果通常只有一次访问,则使用Substring,否则考虑分区。也许System.Data.DataTable会有用?如果您正在进行多次访问并解析为其他数据类型,则DataTable对我来说更具吸引力。如果您每次只需要一个记录保留在内存中,则Dictionary<string,object>应足以容纳一个记录(字段名必须唯一)。
或者,您可以编写自定义的通用类来处理固定长度的记录读取。指示每个字段的起始索引和字段类型。字段的长度是由下一个字段的开始推断出来的(最后一个字段的例外情况,其长度可以从总记录长度推断出来)。类型可以使用int.Parse()double.Parse()bool.Parse()等方法进行自动转换。
RecordParser r = new RecordParser();
r.AddField("Name", 0, typeof(string));
r.AddField("Age", 48, typeof(int));
r.AddField("SystemId", 58, typeof(Guid));
r.RecordLength(80);

Dictionary<string, object> data = r.Parse(recordString);

如果您喜欢反射技术:
[RecordLength(80)]
public class MyRecord
{
    [RecordFieldOffset(0)]
    string Name;

    [RecordFieldOffset(48)]
    int Age;

    [RecordFieldOffset(58)]
    Guid Systemid;
}

只需遍历属性,其中可以获取PropertyInfo.PropertyType以了解如何处理记录中的子字符串; 您可以从属性中提取偏移量和总长度; 并返回填充有数据的类的实例。 基本上,您可以使用反射来提取信息,从我的先前建议中调用RecordParser.AddField()和RecordLength()。

然后将所有内容包装成一个整洁简洁的类:

RecordParser<MyRecord> r = new RecordParser<MyRecord>();
MyRecord data = r.Parse(recordString);

甚至可以称之为r.EnumerateFile("path\to\file"),并使用yield return枚举语法来解析记录。

RecordParser<MyRecord> r = new RecordParser<MyRecord>();
foreach (MyRecord data in r.EnumerateFile("foo.dat"))
{
    // Do stuff with record
}

谢谢您提供的内部信息。这正是我想知道的内容。您是在建议我创建一个DataTable,其列与我的记录格式相匹配,然后按顺序读取记录并在读取时填充DataTable吗?这是一个有趣的建议,我之前没有考虑过。 - John M Gant
我的假设是您需要读取多个记录并且需要同时在内存中保存多个记录。如果您只有一个记录,那么Dictionary<string,object>应该足够了,对吗?您可能可以编写一个通用类来处理这个问题。我会将这个评论整合到我的答案中。 - Colin Burnett
抱歉,我想我的所有代码都是用C#编写的。我不懂VB,所以我不太确定能翻译多少(比如 yield return 语法),但我认为应该可以。 - Colin Burnett

3
最快的方法可能是使用流技术,因为假设您可以按顺序读取每个字段,它只会在内存中保留您需要的内容,并且记住您在处理过程中的位置

1

你想要做的事情听起来像是一个解析任务。如果我理解正确,你正在加载一个包含多个字段及其值的巨大字符串。对于这种特定的场景,Substring 不会特别高效。对于每个字段及其值,你需要在较大的字符串中调用 Substring 并指定特定的位置和长度。这是相当大的开销。

作为替代方案,你可以实现一个简单的解析器,一次从头到尾处理你的字符串,并在单个通道中检索每个字段和值。这样的解析器不需要非常复杂...只需要一个简单的 1 字符前瞻解析器可能就足够了。你甚至可能不需要将输入进行标记化...你可以以流式方式处理它,提取一个字段,然后提取它的值,将其放入某个容器中,然后继续进行。

如果你的输入字符串比仅仅是一系列字段和值更加复杂(即结构化),那么可能需要一个更复杂的解析器。有许多工具,如 antler,提供了框架,可以为你生成语法,生成解析器,并提供一个漂亮的 API 来消耗你解析的内容。


0

你是如何首先读取记录的?

你是逐个字符还是逐行阅读的?

在阅读时,您可以即时处理数据,因此不需要使用子字符串。

如果必须先读取然后处理,则将其读入字符串并使用StringReader,它将允许您逐个字符或按一定数量的字符进行阅读。


它最初是一个UTF-8字节数组。它是一个单独的记录,没有行。将整个数组读入字符串并使用StringReader进行处理是我的一个选项。这是您推荐的吗? - John M Gant

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接