【问题标题】:How to efficiently overwrite parts of a string by index in .NET?如何在.NET中通过索引有效地覆盖字符串的一部分?
【发布时间】:2011-03-28 10:01:11
【问题描述】:

在我的 .NET 程序中,我允许用户定义由业务逻辑计算的值的“字段”。这些字段具有位置和长度,因此它们都可以插入到给定索引处的单个输出字符串中。我还允许用户指定此输出字符串的默认内容。如果没有定义字段来替换给定位置,则输出默认字符

我的问题是,我怎样才能有效地做到这一点? StringBuilder 类有一个 Insert(int index, string value) 方法,但这每次都会延长输出字符串而不是覆盖它。我是否必须使用 StringBuilder[int index] 索引器一次设置每个字符,这是否效率低下?由于我将多次执行此操作,因此我希望它尽可能快。

谢谢。

【问题讨论】:

  • " 由于我将多次这样做,我希望它尽可能快 "。定义“很多”?每个按钮点击数千次?那么这是过早的优化。夜间批处理作业每小时数百万次?还有过早的优化(一个多小时的一百万次点击大约每秒 278 次)。如果这个字符串操作被证明是一个瓶颈,我会惊呆的。
  • 没错,但如果可能的话,编写高效的代码仍然很好。我只是在检查我没有做一些非常低效的事情。作为 .NET 开发人员,您总是听说过糟糕的字符串操作会导致性能损失。该程序的核心实际上将用于多个项目。第一个涉及文件转换。输出文件包含基于输入文件生成的值。我相信每个输入文件可能包含数百甚至数千条记录。但是使用此代码的未来应用程序可能会有更重的工作负载。
  • 您可能最好考虑将字段附加到流,而不是构建字符串并编写它。使用System.IO.StringWriter,如果出于任何原因需要,您始终可以将输出作为字符串获取。
  • 我实际上是在使用 StreamWriter 来输出文件。但是我使用 StringBuilder 来构建传递给 StreamWriter 的各个部分。
  • 我很奇怪,并分析了 Split 与 StringBuilder 解决方案,Split-Zip 解决方案表现更好:) 这里是代码:pastebin.com/pKsaexWX

标签: .net string stringbuilder


【解决方案1】:

一次只做一个角色可能是你最好的选择。我这样说是因为在 StringBuilder 上调用 InsertRemove 会导致字符向右/向左移动,就像在任何可变索引集合(如 List<char>)中的类似方法一样。

也就是说,这是一种让您的生活更轻松的扩展方法的绝佳候选者。

public static StringBuilder ReplaceSubstring(this StringBuilder stringBuilder, int index, string replacement)
{
    if (index + replacement.Length > stringBuilder.Length)
    {
        // You could throw an exception here, or you could just
        // append to the end of the StringBuilder -- up to you.
        throw new ArgumentOutOfRangeException();
    }

    for (int i = 0; i < replacement.Length; ++i)
    {
        stringBuilder[index + i] = replacement[i];
    }

    return stringBuilder;
}

使用示例:

var builder = new StringBuilder("My name is Dan.");
builder.ReplaceSubstring(11, "Bob");

Console.WriteLine(builder.ToString());

输出:

我叫鲍勃。

【讨论】:

  • 感谢使用 ++i 而不是 i++,这让我在 for 循环中发疯。
【解决方案2】:

StringBuilder 类可让您构建可变字符串。在执行Insert 之前尝试使用Remove 函数。由于它是随机访问的,它应该非常快。只要StringBuilder 保持相同的容量,它就不会花时间在内存中复制字符串。如果您知道字符串会变长,请尝试在调用New StringBuilder()时将容量设置为更大

【讨论】:

  • 使用RemoveInsert 涉及移动内存。
  • 另外,如果字段被定义为(位置,长度)touple,你需要做一些数学运算,如果插入的文本比文本短或长,它会替换。
  • +1 我没想过将 Remove 方法与 Insert 方法结合使用。这至少是一种选择。谢谢。
【解决方案3】:

只要字符串是不可变的,对它的每次操作都会导致 GC 负载,甚至 StringBuilder 插入/删除调用。 我会通过插入点剪切源字符串,然后用需要插入的数据“压缩”它。 之后,您可以在列表中连接字符串,以获得结果字符串。

这是一个执行拆分/压缩操作的示例代码。 它假定,字段被定义为(位置、长度、值)的元组。

public class Field
{
    public int pos { get; set; }
    public int len { get; set; }
    public string value { get; set; }
    public string tag { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        var source = "You'r order price [price] and qty [qty].";
        var fields = new List<Field>();
        fields.Add(new Field()
        {
            pos = 18, 
            len = 7, 
            value = "15.99$",
            tag = "price"
        });
        fields.Add(new Field()
        {
            pos = 37-3,
            len = 5,
            value = "7",
            tag = "qty"
        });
        Console.WriteLine(Zip(Split(source, fields), fields));
        Console.WriteLine(ReplaceRegex(source, fields));

    }

    static IEnumerable<string> Split(string source, IEnumerable<Field> fields)
    {
        var index = 0;
        foreach (var field in fields.OrderBy(q => q.pos))
        {
            yield return source.Substring(index, field.pos - index);
            index = field.pos + field.len;
        }
        yield return source.Substring(index, source.Length - index);
    }
    static string Zip(IEnumerable<string> splitted, IEnumerable<Field> fields)
    {
        var items = splitted.Zip(fields, (l, r) => new string[] { l, r.value }).SelectMany(q => q).ToList();
        items.Add(splitted.Last());
        return string.Concat(items);
    }
    static string ReplaceRegex(string source, IEnumerable<Field> fields)
    {
        var fieldsDict = fields.ToDictionary(q => q.tag);
        var re = new Regex(@"\[(\w+)\]");
        return re.Replace(source, new MatchEvaluator((m) => fieldsDict[m.Groups[1].Value].value));
    }
}

顺便说一句,使用正则表达式替换特殊的用户标记会更好,例如 [价格]、[数量]?

【讨论】:

  • StringBuilders,不像普通的字符串,不是不可变的。
  • 但是它们对内部的字节数组进行操作,并且在数组的中间插入,会导致内存重新分配。
  • +1 以获得有趣的解决方案。但是,将我现有的代码更改为像这样工作的工作量太大,尤其是在我的最后期限很紧的情况下。
【解决方案4】:

我建议使用StringBuilder 类。但是,您可以使用字符串执行此操作,但可能会产生副作用。这里有几篇博文展示了如何操作字符串和可能的副作用。

http://philosopherdeveloper.wordpress.com/2010/05/28/are-strings-really-immutable-in-net/

http://philosopherdeveloper.wordpress.com/2010/06/13/string-manipulation-in-net-epilogue-plus-new-theme/

【讨论】:

    【解决方案5】:

    如果替换子字符串将成为一个很大的瓶颈,您可能希望完全放弃子字符串的事情。相反,将您的数据分解为可以独立修改的字符串。类似于以下内容:

    class DataLine
    {
        public string Field1;
        public string Field2;
        public string Field3;
    
        public string OutputDataLine()
        {
            return Field1 + Field2 + Field3;
        }
    }
    

    这是一个简单的静态示例,但我确信它可以变得更通用,以便如果每个用户以不同的方式定义字段,您就可以处理它。将数据分解为字段后,如果您仍需要修改字段中的单个字符,至少您不会触及整个数据集。

    现在,这可能会将瓶颈推向 OutputDataLine 函数,具体取决于您对数据执行的操作。但如果需要,可以单独处理。

    【讨论】:

      【解决方案6】:

      如果您的字符串已经针对长度进行了预格式化,那么 StringBuilder 类具有

      public StringBuilder Replace(string oldValue, string newValue, int startIndex, int count)
      

      只需设置您的起始索引和计数 = 1,这样您就可以替换该特定实例。

      您可以做的另一件事是使用 String.Format()。将所有预定义的字段转换为索引,这样您就可以得到一个像“This {0} is very {1}”这样的字符串,然后只需将参数与特定索引匹配并执行 String.Format(myString, myParams);

      -劳尔

      【讨论】:

        【解决方案7】:

        正如您所说,StringBuilder 具有 Insert 方法但没有 Overwrite 方法。
        所以我为我的项目创建了覆盖扩展方法,见下文。
        请注意,如果 StringBuilder 没有足够的空间容纳它,它将削减价值。但是,您可以轻松修改其逻辑。

            public static void Overwrite( this StringBuilder sb, int index, string value )
            {
                int len = Math.Min( value.Length, sb.Length - index );
                sb.Remove( index, len );
                sb.Insert( index, value.Substring( 0, len ) );
            }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2019-07-03
          • 2011-08-28
          • 2011-01-25
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-01-08
          相关资源
          最近更新 更多