【问题标题】:Trim whitespace from the end of a StringBuilder without calling ToString().Trim() and back to a new SB从 StringBuilder 的末尾修剪空白而不调用 ToString().Trim() 并返回到新的 SB
【发布时间】:2021-01-07 15:46:06
【问题描述】:

什么是在不调用 ToString().Trim() 的情况下从 StringBuilder 的末尾修剪空白并返回到新的 SB new StringBuilder(sb.ToString().Trim()) 的有效方法。

【问题讨论】:

  • 这个问题在于它在标题中使用了非常主观的词“最快”。这听起来像是一场比赛。
  • 我知道,因为我是在对自己的问题做了一些回答和几个小时的测试后自己完成的,但我不知道马上做是“好的”
  • NicholasPetersen,@Bobo 的回答如此简单,是因为软件开发中的最佳实践首先是让它发挥作用。然后,如果有任何相关问题,您就知道需要优化的地方。否则,它是过度杀伤优化。我这么说是因为你在这里说话的方式在我看来很傲慢。这并不是因为我们 C# 程序员不关心性能。这是因为我们根据客户的要求专注于重要的事情。一旦它工作,一旦客户需要,我们就会得到性能优化。
  • @WillMarcouiller 我不是故意要听起来傲慢。我确实觉得我们 C# 开发人员对性能和不浪费有合理的关注,在这样的情况下总是被殴打完全,我们应该收到零抨击的情况,但它就是,在这里我们又被打了。注意:1)我花了大约 5 分钟来编写这个函数,2)它是一种扩展方法,可以在剩下的日子里使用,3)在某些情况下,这将是数千倍的性能,但是你就像我提出了一些丑陋的微优化代码一样。
  • @NicholasPetersen 也许提供一些指标可以将其移出主观领域。我同意如果您打算保留StringBuilder,您的方法会很快。如果您打算在需要修剪之前丢弃它并使用生成的string,那么该字符串上的TrimEnd() 会更快。我很想看看您的代码比任何其他实现快 数千倍 倍的情况。

标签: c# stringbuilder


【解决方案1】:

下面是扩展方法,可以这样调用:

sb.TrimEnd();

此外,它还返回 SB 实例,允许您链接其他调用 (sb.TrimEnd().AppendLine())。

public static StringBuilder TrimEnd(this StringBuilder sb)
{
    if (sb == null || sb.Length == 0) return sb;

    int i = sb.Length - 1;

    for (; i >= 0; i--)
        if (!char.IsWhiteSpace(sb[i]))
            break;

    if (i < sb.Length - 1)
        sb.Length = i + 1;

    return sb;
}

注意事项:

  1. 如果为 Null 或 Empty,则返回。

  2. 如果实际上不需要 Trim,我们说的是一个非常快的返回时间,最昂贵的调用可能是对 char.IsWhiteSpace 的单次调用。因此,在不需要时拨打TrimEnd 几乎是零费用,而不是这些ToString().Trim() 回到SB 路线。

  3. 否则,如果需要修剪,最昂贵的事情是多次调用char.IsWhiteSpace(在第一个非空白字符上中断)。当然,循环向后迭代;如果全部都是空格,您最终会得到 SB.Length 为 0。

  4. 如果遇到空格,i 索引将保留在循环之外,这允许我们使用它适当地削减长度。在StringBuilder 中,这非常有效,它只是设置了一个内部长度整数(内部char[] 保持相同的内部长度)。

更新:请参阅 Ryan Emerle 的精彩笔记如下,它纠正了我的一些误解(SB 的内部运作比我想象的要复杂一些):

StringBuilder 在技术上是char[] 块的链接列表,所以 我们不会在 LOH 中结束。调整长度在技术上并不像 就像更改结束索引一样简单,因为如果您进入不同的 chunk 必须保持容量,因此可能需要一个新的块 分配。不过,您只在最后设置了 Length 属性, 所以这似乎是一个很好的解决方案。 来自 Eric Lippert 的相关细节:https://stackoverflow.com/a/6524401/62195

另外,请参阅这篇讨论 .NET 4.0 新 StringBuilder 实现的好文章:http://1024strongoxen.blogspot.com/2010/02/net-40-stringbuilder-implementation.html

更新:以下说明了更改 StringBuilder 长度时会发生什么(这里对 SB 进行的唯一实际操作,并且仅在需要时进行):

StringBuilder sb = new StringBuilder("cool  \t \r\n ");

sb.Capacity.Print(); // 16
sb.Length.Print();  // 11
        
sb.TrimEnd();

sb.Capacity.Print(); // 16
sb.Length.Print();  // 4 

您可以看到内部数组 (m_ChunkChars) 在更改 Length 后保持相同的大小,事实上,您可以在调试器中看到它甚至没有覆盖(在本例中为空格)字符。他们都是孤儿。

【讨论】:

  • 您是否考虑解释该代码,以及为什么它符合问题的要求?这样可以帮助未来的读者学习。
  • 对,索引器访问内部 char 数组(参见 sb.Capacity 的大小); StringBuilder 实际上只是一个带有 Length 字段的美化 char[],它充当指向要添加到内部数组的位置的指针。重要的是,此方法对 SB 所做的唯一操作是在需要时更改 Length 字段,但这不会使内部 char[] 的大小减小(它只会增长)。如果是这样,那将需要一个新的数组分配和复制,这会破坏目的。因此:完整的 Trim 没有意义(而且无论如何都很少需要),因为这需要更改内部数组。
  • StringBuilder 在技术上是char[] 块的链接列表,所以我们不会在LOH 中结束。调整长度技术上不像更改结束索引那么简单,因为如果您移动到不同的块,则必须维护Capacity,因此可能需要分配一个新块。不过,您只在最后设置了 Length 属性,所以这似乎是一个很好的解决方案。
  • 我们在序列化 270 MB JSON 字符串时遇到问题。切换到此方法后,发布版本所需的时间从 22 分钟变为 12 秒。
  • 感谢@EmanuelStrömgren!我建议不要使用 TrimStart,因为我不相信 sb.Remove 是高效的,尽管我可以接受纠正。似乎更好的方法是等到 sb 必须被序列化为字符串并在那时修剪它,即当 sb.ToString 被调用时,因为它允许传入开始索引。我写了一个扩展方法在这里:github.com/copernicus365/DotNetXtensions/blob/master/…
【解决方案2】:

你可以试试这个:

StringBuilder b = new StringBuilder();
b.Append("some words");
b.Append(" to test   ");

int count = 0;
for (int i = b.Length - 1; i >= 0; i--)
{
    if (b[i] == ' ')
        count++;
    else
        break;
}

b.Remove(b.Length - count, count);
string result = b.ToString();

当有空格时,它只会遍历结尾,然后跳出循环。

甚至像这样:

StringBuilder b = new StringBuilder();
b.Append("some words");
b.Append(" to test   ");

do
{
    if(char.IsWhiteSpace(b[b.Length - 1]))
    {
         b.Remove(b.Length - 1,1);
    }
}
while(char.IsWhiteSpace(b[b.Length - 1]));

string get = b.ToString();

【讨论】:

    【解决方案3】:
    public static class StringBuilderExtensions
    {
        public static StringBuilder Trim(this StringBuilder builder)
        {
            if (builder.Length == 0)
                return builder;
    
            var count = 0;
            for (var i = 0; i < builder.Length; i++)
            {
                if (!char.IsWhiteSpace(builder[i]))
                    break;
                count++;
            }
    
            if (count > 0)
            {
                builder.Remove(0, count);
                count = 0;
            }
    
            for (var i = builder.Length - 1; i >= 0; i--)
            {
                if (!char.IsWhiteSpace(builder[i]))
                    break;
                count++;
            }
    
            if (count > 0)
                builder.Remove(builder.Length - count, count);
    
            return builder;
        }
    }
    

    【讨论】:

    • 好主意,问题是,我相信从一开始就修剪它不是高性能的。因此,在获取字符串时进行最终修剪操作似乎是一个更好的主意。所以想象一下,如果你的方法返回一个字符串,比如说TrimToString,如果开始需要修剪,你可以使用 ToString 重载来设置开始索引以开始获取字符串(并以正常方式首先修剪结尾)。我已经用了一段时间了,很快就会看到新帖子。
    • 从末尾删除空格 - 如果我将设置长度(如您的示例中),则性能更高。但一开始你将 StringBuilder 转换为字符串,但我想返回 StringBuilder,这是我使用 Remove 的原因。如果你想返回字符串,你可以让你的方法更高效 - 记住有效的开始索引,记住有效的结束索引(不设置长度,不要调用删除),在方法调用的结尾 ToString(startValidIndex, Length - validStartIndex - validEndIndex )
    • 我把这个用于 TrimStart 和 Petersen 的 TrimEnd - 我认为这是真的可以做到的最好的一个
    【解决方案4】:

    要进行全面修剪,在StringBuilder 级别上执行此操作不是高效/可取的,而是在ToString 时间执行此操作,就像此TrimToString 实现一样:

        public static string TrimToString(this StringBuilder sb)
        {
            if (sb == null) return null;
    
            sb.TrimEnd(); // handles nulle and is very inexpensive, unlike trimstart
    
            if (sb.Length > 0 && char.IsWhiteSpace(sb[0])) {
                for (int i = 0; i < sb.Length; i++)
                    if (!char.IsWhiteSpace(sb[i]))
                        return sb.ToString(i);
                return ""; // shouldn't reach here, bec TrimEnd should have caught full whitespace strings, but ...
            }
    
            return sb.ToString();
        }
    

    【讨论】:

    • 一会儿。在 .net core 2.1 上,我应该在 ToString 中添加第二个参数:return sb.ToString(i, sb.Length - i)
    • @СергейРыбаков 无需查看该代码示例,但是,如果需要修剪,您的代码需要预先了解,不仅如此,还需要知道有多少起始字符是空白。扩展的重点是让它处理所有这些。
    • 对不起。我的意思是在.net core 2.1 中没有ToString 有一个参数。只有2个论点。我在第一条评论中的示例允许在 .net core 2.1 上使用您的 TrimToString
    • 在 .NET 4.7.2 上也没有过载。
    【解决方案5】:
    StringBuilder myString = new StringBuilder("This is Trim test ");
    
    if (myString[myString.Length - 1].ToString() == " ")
    {              
        myString = myString.Remove(myString.Length - 1, 1);
    }
    

    【讨论】:

    • 1) 这不会修剪多个尾随空格,2) 它检查的唯一空格是空格,3) 无需将第 1 行中的字符转换为字符串,只需将其作为char,如果您要走那条路线(` == ' '`),4)我必须检查在SB结束时Remove如何工作,但肯定不会比仅更改长度更快,因为其他人在下面提出了建议。
    猜你喜欢
    • 1970-01-01
    • 2011-08-11
    • 2011-11-21
    • 1970-01-01
    • 1970-01-01
    • 2021-09-16
    • 1970-01-01
    • 2011-04-07
    • 1970-01-01
    相关资源
    最近更新 更多