从StringBuilder的末尾删除空格,而不调用ToString().Trim()方法并将其返回到一个新的SB中。

29
如何高效地从 StringBuilder 的末尾删除空格,而不使用 ToString().Trim() 方法并将其转换为新的 StringBuilder(new StringBuilder(sb.ToString().Trim()))?

2
这个问题是它在标题中使用了非常主观的词语“最快”。这让它听起来像是一场比赛。 - crthompson
1
我知道这一点,因为在自己提出问题并经过几个答案和几个小时的测试后,我自己完成了它,但我不知道立即完成它是“可以”的。 - Fredou
1
@WillMarcouiller 我并不是想表现得傲慢。我认为我们这些关心性能和避免浪费的C#开发人员总是在恰好像这样的情况下受到攻击,这些情况下我们本应该没有受到任何指责,但事实却是如此,我们又被攻击了。注意:1)编写这个函数花费了我大约5分钟的时间,2)它是一个可以用于余生的扩展方法,3)在某些场景下,这将比其他方法快上数千倍,然而你却像我提供了一些丑陋的微小优化代码一样对待它。 - Nicholas Petersen
3
也许提供一些度量指标可以将这个问题从主观领域移开。如果你打算继续使用 StringBuilder 来实现你的方法,那么你的方法会很快。但如果你想在需要修剪之前就放弃它并使用结果字符串,那么对该字符串应用 TrimEnd() 方法会更快。我很想看到一个情况,证明你的代码比其他任何实现方法都快上数千倍 - Ryan Emerle
1
哦,得了吧!在寻找“最快”的解决方案中可能有什么是基于“观点”的?我很难找到一个更客观的目标! - TaW
显示剩余14条评论
7个回答

54
以下是一个扩展方法,所以您可以这样调用它:
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. 如果为空,则返回。
  2. 如果不需要修剪,那么返回时间非常快,可能最昂贵的调用是单个调用char.IsWhiteSpace。因此,当不需要时,调用TrimEnd几乎没有任何费用,而不是这些ToString().Trim()返回SB路线。
  3. 否则,如果需要修剪,则最昂贵的事情是多次调用char.IsWhiteSpace(在第一个非空格字符上中断)。当然,循环向后迭代;如果全部都是空格,您将以SB.Length为0结束。
  4. 如果遇到空格,则保留i索引在循环外,这使我们可以相应地裁剪长度。在StringBuilder中,这非常有效,它只是设置一个内部长度整数(内部char[]保持相同的内部长度)。
更新:请参见以下由Ryan Emerle提供的优秀注释,纠正了我的一些误解(SB的内部工作比我描述的要复杂一些):

StringBuilder在技术上是char[]块的链接列表,因此我们不会进入LOH。调整长度并不像简单地更改结束索引那样简单,因为如果移动到不同的块中,则必须维护Capacity,因此可能需要分配新块。尽管如此,您只在最后设置Length属性,因此这似乎是一个很好的解决方案。 来自Eric Lippert的相关详细信息:https://dev59.com/KWw15IYBdhLWcg3wkcuq#6524401

另外,请参见这篇很好的文章,讨论.NET 4.0的新StringBuilder实现:http://1024strongoxen.blogspot.com/2010/02/net-40-stringbuilder-implementation.html 更新:以下说明了更改StringBuilder长度时会发生什么(这里唯一真正执行的操作,而且仅在需要时才执行):
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)保持相同的大小,实际上,在调试器中可以看到它甚至不会覆盖(在这种情况下是空格)字符。它们只是被遗弃了。


1
你是否考虑解释一下那段代码,以及它为什么符合问题的要求?这样可以帮助未来的读者学习。 - Andrew Barber
1
索引器访问内部字符数组(请参见sb.Capacity以获取其大小); StringBuilder实际上只是一个带有Length字段的char[],该字段充当指针,指向要添加到内部数组的位置。重要的是,此方法对SB执行的唯一操作是在需要时更改Length字段,但这不会使内部char[]减小(它只会增长)。如果是这样,那将需要新的数组分配和复制,这将违背其目的。因此:完全修剪没有意义(而且很少需要),因为这需要修改内部数组。 - Nicholas Petersen
2
StringBuilder 在技术上是一个 char[] 块的链表,因此我们不会陷入 LOH。调整长度并不像简单地更改结束索引那样“技术性”,因为如果您移动到另一个块中,则必须维护 Capacity,因此可能需要分配新块。尽管如此,您只在最后设置 Length 属性,因此这似乎是一个很好的解决方案。 - Ryan Emerle
2
我们在序列化一个270 MB的JSON字符串时遇到了问题。切换到这种方法后,Release版本所需的时间从22分钟减少到了12秒。 - cskwg
1
谢谢@EmanuelStrömgren!我建议不要使用TrimStart,因为我认为sb.Remove的性能不佳,尽管我可能需要纠正。更好的方法似乎是等到sb必须序列化为字符串并在那时修剪它,即当调用sb.ToString时,因为它允许传递起始索引。我在这里编写了一个扩展方法:https://github.com/copernicus365/DotNetXtensions/blob/master/DotNetXtensions/src/XStringBuilder.cs#L314 - Nicholas Petersen
显示剩余8条评论

3
你可以尝试这个方法:
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();

1

我为Nicholas Petersen的版本进行了扩展,以包含可选的附加字符:

/// <summary>
/// Trims the end of the StingBuilder Content. On Default only the white space char is truncated.
/// </summary>
/// <param name="pTrimChars">Array of additional chars to be truncated.</param>
/// <returns></returns>
public static StringBuilder TrimEnd(this StringBuilder pStringBuilder, char[] pTrimChars = null)
{
    if (pStringBuilder == null || pStringBuilder.Length == 0)
        return pStringBuilder;

    int i = pStringBuilder.Length - 1;

    var lTrimChars = new HashSet<char>();
    if (pTrimChars != null)
        lTrimChars = pTrimChars.ToHashSet();

    for (; i >= 0; i--)
    {
        var lChar = pStringBuilder[i];
        if ((char.IsWhiteSpace(lChar) == false) && (lTrimChars.Contains(lChar) == false))
            break;
    }

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

    return pStringBuilder;
}

编辑:在Nicholas Petersen的建议下:

/// <summary>
/// Trims the end of the StingBuilder Content. On Default only the white space char is truncated.
/// </summary>
/// <param name="pTrimChars">Array of additional chars to be truncated. A little bit more efficient than using char[]</param>
/// <returns></returns>
public static StringBuilder TrimEnd(this StringBuilder pStringBuilder, HashSet<char> pTrimChars = null)
{
    if (pStringBuilder == null || pStringBuilder.Length == 0)
        return pStringBuilder;

    int i = pStringBuilder.Length - 1;

    for (; i >= 0; i--)
    {
        var lChar = pStringBuilder[i];

        if (pTrimChars == null)
        {
            if (char.IsWhiteSpace(lChar) == false)
                break;
        }
        else if ((char.IsWhiteSpace(lChar) == false) && (pTrimChars.Contains(lChar) == false))
            break;
    }

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

    return pStringBuilder;
}

我建议发送HashSet,而不是在每次调用时分配和初始化它。 - Nicholas Petersen

1
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重载来设置开始索引以从中获取字符串(并先用正常方式修剪结尾)。我已经使用这个方法一段时间了,稍后会发布新的帖子介绍它。 - Nicholas Petersen
从末尾删除空格-如果我设置长度(就像您的示例中一样),性能会更好。但是在开始时,您将StringBuilder转换为字符串,但我想返回StringBuilder,这就是我使用Remove的原因。如果您想返回字符串,可以使方法更加高效-记住有效的起始索引,记住有效的结束索引(不要设置长度并且不要调用remove),在方法结束时调用ToString(startValidIndex,Length-validStartIndex-validEndIndex) - Smagin Alexey
我用了 TrimStart,而 Petersen 用了 TrimEnd - 我认为这是最好的实现了。 - Boppity Bop

1
要进行完整的修剪,不建议在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) - Сергей Рыбаков
@СергейРыбаков,不看代码示例,你的代码仍然需要预先了解是否需要修剪,并且不仅如此,还需要知道有多少起始字符是空格。扩展的目的是为了处理所有这些问题。 - Nicholas Petersen
1
对不起,我的意思是在 .net core 2.1 中没有只有一个参数的 ToString 方法,只有两个参数的。我在第一条评论中的示例允许在 .net core 2.1 上使用您优秀的 TrimToString 方法。 - Сергей Рыбаков
在.NET 4.7.2上,也没有重载。 - Alex from Jitbit

0
如果您知道要删除多少个空格,可以尝试使用StringBuilder.Remove(int startIndex, int length),而不需要创建扩展方法。
希望能对您有所帮助!

-1
StringBuilder myString = new StringBuilder("This is Trim test ");

if (myString[myString.Length - 1].ToString() == " ")
{              
    myString = myString.Remove(myString.Length - 1, 1);
}

1
  1. 这不会去除多个尾随空格,2) 它只检查一个空格符,3) 不需要将第一行中的字符转换为字符串,只需将其作为字符进行比较,如果你按照这条路线进行(` == ' '),4) 如果在SB的末尾使用Remove方法,我得检查一下它的工作方式,但肯定不会比像其他人建议的那样改变长度更快。
- Nicholas Petersen

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