【问题标题】:Stop checking condition once requirement has been met once in a loop在循环中满足要求后停止检查条件
【发布时间】:2025-12-09 19:40:01
【问题描述】:

/* 编辑:请注意,此代码是一个示例,我的问题通常与(C#)编码有关,因为我经常发现自己遇到循环某些内容的情况,并且根本不需要检查 if 语句一点也不。 */

我怀疑这是否可能,但我想确定。

我有一个任务,我必须读取文件并使用变量。 这些项目在 txt 文件中的一行中,以逗号分隔。我得到的文件中的项目被 \t 隔开,有时还有空格。 一个例子:“\t Boeing 737-800”(注意两个空格)

这是我为摆脱制表符(和空格)而制作的功能:

private string RemoveTab (string text)
{
    string tempText = "";
    foreach (char c in text)
    {
        if (c == ' ' && tempText == "")
        {
            //nothing has to be done
        }
        else if (c != '\t')
        {
            tempText += c;
        }
    }
    return tempText;
}

现在回到真正的问题: 一旦所有的制表符和空格都被跳过,它就不需要再检查任何东西了。尤其不是第一个 if 语句。有没有办法可以禁用第一个 if 语句?

在这个例子中,我怀疑它会对性能产生任何重大影响。好吧,我知道它没有。但由于我的 i5-4570 在玩《守望先锋》和《战地风云 4》等游戏时遇到问题,我想让事情变得尽可能高效。而且我认为在不可能满足要求的情况下检查很多东西可能会对 CPU 密集型应用程序的 CPU 使用率产生一些影响。

【问题讨论】:

  • 当你知道你已经完成了所有的工作后,为什么不直接回来呢?诚然,您不应该建立这样的字符串(使用StringBuilder),您可能会发现TrimStart 可以满足您的需求...
  • 感谢您在这种情况下的建议!我将研究 StringBuilder 的事情以及如何使用它。但这并不能回答我的问题:)
  • 你无法通过猜测瓶颈在哪里获得良好的性能,也无法通过学习数十亿条“永远做 X”的规则。您设定绩效目标。你编写清晰易懂的代码。然后,您衡量性能并如果它没有达到目标,您确定瓶颈并开始评估那些地方的替代方案。
  • 大部分情况下,我同意你的看法。类比:如果你投票给选举,你就是数百万人中的一员。但归根结底,所有这些单身人士加起来是一笔巨款。如果我可以通过直接编写高效的代码来防止进行大量检查,那么我以后就不必通过代码来搜索可以节省一点性能的地方。这也是我非常感谢 Jon Skeet 的建议的原因。但是还有很多其他情况,我会通过数组而不是字符串 :)

标签: c# performance visual-studio-2015


【解决方案1】:

不要重新发明*。要删除开头和结尾的空格,请使用 string.Trim(' ')(或等效的 string.TrimStartstring.TrimEnd,如果适用)。

其余的,可以使用 LINQ 简单地完成:

var noTabsString = new string(text.Trim(' ').Where(c => c != '\t').ToArray());

使用字符串连接来构建任意长的字符串通常不是一个好主意,无论如何你都应该使用StringBuilder

但我会完全避开你的方法;利用 string 实现 IEnumerable<char> 的事实,并在过滤掉所有不需要的字符后实例化一个 string

更新回答您的具体问题,如果不再需要循环内的if 条件,则无法停用它。您唯一能做的就是将循环分成两部分,但我不建议您这样做,除非您检查的条件被证明是不可接受的瓶颈。

一种方法是使用以下模式:

using (var enumerator = sequence.GetEnumerator())
{
     while (enumerator.MoveNext())
     {
         if (veryExpensiveCheck) { ... }
         else if (cheapCheck)
         {
             ...
             break; //veryExpensiveCheck not needed anymore.
         }
     }

     while (enumerator.MoveNext())
     {
         if (cheapCheck) { ... }
     }
}

可读性下降,因此,再次强调,除非确实有必要并且您有经验数据证明第一个选项是不可接受的,否则不要这样做。

【讨论】:

  • 感谢您在这种特殊情况下的建议,我一定会研究 StringBuilder。但遗憾的是它没有回答我的问题。我编辑了我的帖子以(希望)明确我的问题是什么:)
  • 编译器将字符串上的foreach优化为for(;;),它通过字符串字符数组进行索引,这比规范的using/MoveNext()/Current组合(它对数组也做同样的事情)。在优化基于 MoveNext() 的概念代码时,这会撤消该优化,因此虽然这是在 IEnumerable<T> 上优化这种拆分循环的方法,但通常在字符串上这样做应该从重新写入 for(;;) 开始。
【解决方案2】:

一旦所有的制表符和空格都被跳过,它就不需要再检查任何东西了。

值得注意的是,您的代码仅在初始部分跳过空格,但在任何地方都跳过了制表符。目前尚不清楚您是想要那个还是您在此处描述的内容。我将假设您拥有的代码是正确的,就其结果而言。

这是一种足够常见的模式,您希望对循环的一部分执行某些操作,而对其余部分执行其他操作。这很容易通过从foreach 中删除语法糖来完成,所以:

foreach (char c in text)
{
    if (c == ' ' && tempText == "")
    {
        //nothing has to be done
    }
    else if (c != '\t')
    {
        tempText += c;
    }
}

变成:

using(var en = text.GetEnumerator())
{
    while (en.MoveNext())
    {
        char c = en.Current;
        if (c == ' ' && tempText == "")
        {
            //nothing has to be done
        }
        else if (c != '\t')
        {
            tempText += c;
        }
    }
}

现在我们已经分解了foreach,我们可以进一步轻松地对其进行更改。

using(var en = text.GetEnumerator())
{
    while (en.MoveNext())
    {
        char c = en.Current;
        if (c != ' ')
        {
            do
            {
                if (c != '\t')
                {
                    tempText += c;
                }
            } while (en.MoveNext());
            return tempText;
        }
    }
    return ""; // only hit if text was all-spaces
}

现在我们只检查c 是否为空格,直到它第一次找到非空格并为其余的枚举执行不同类型的循环。 (如果您也只想在开始时跳过标签,那么将其从内部循环中取出并进行初始测试c != ' ' && c != '\t')。

(这值多少钱是另一个问题。我发现与此类似的更改可以单独考虑产生明显的改进,但除非输入字符串非常大或代码经常被命中,否则它不会很重要——在更广泛的应用环境中对本身不明显的事物进行明显的改变,在更广泛的环境中也不是明显的改变)。

这是适用于任何foreach 的一般情况。在这里,我们还可以做两件事。

一个是我们可以删除using,因为我们知道字符串的枚举器在其Dispose() 中没有任何作用。仅当您真的确定可以时才这样做。

另一个是我们可以从foreach 更改为迭代。考虑到我们也可以将您的原始逻辑写成:

for(int i = 0; i < text.Length; ++i)
{
    char c = text[i]; // We could also have done char[c] arr = text.Chars and used that. The compiler does the equivalent.
    if (c == ' ' && tempText == "")
    {
        //nothing has to be done
    }
    else if (c != '\t')
    {
        tempText += c;
    }
}

从这个起点我们可以做到:

for(int i = 0; i < text.Length; ++i)
{
    char c = text[i];
    if (c != ' ')
    {
        do
        {
            c = text[i];
            if (c != '\t')
            {
                tempText += c;
            }
        } while(++i < text.Length);
        return tempText;
    }
    return "";
}

或者:

int i = 0
while(i < text.Length)
{
    char c = text[i];
    if (c != ' ')
    {
        break;
    }
    ++i;
}
while(i < text.Length)
{
    char c = text[i];
    if (c != '\t')
    {
        tempText += c;
    }
    ++i;
}

这两种模式都执行两个半循环但使用索引而不是foreach。许多人发现这更简单,并且在某些情况下它更高效(特别是对于数组和字符串 [请注意,编译器将数组或字符串类型变量上的 foreach 转换为规范的 MoveNext()/Current 组合,而是转换为索引 @ 987654338@ 在幕后,你不想失去那个优化])虽然不是一般的(它不适用于无法索引到的 IEnumerable&lt;char&gt;

【讨论】:

    【解决方案3】:

    您可以通过一种方式来组合条件检查,即 first if 应该始终为 false。

    if (!(c == ' ' && tempText == "") && (c != '\t')){
         tempText += c;
    }
    

    希望这会有所帮助。

    【讨论】:

    • 确实让它更短了。我也想这样做。但我想知道我是否可以完全停止检查 (c == ' ' && tempText == "")。如果我可以禁用 if 语句,我可以在构建大型程序时保护 cpu 资源:)
    【解决方案4】:

    您可以考虑用 Action&lt;&gt; 对象替换循环体,该对象封装了所需的功能并且可以在运行时进行更改。

    private string RemoveTab(string text)
    {
        string tempText = String.Empty;
    
        // An Action is an object that encapsulates a method that does
        // not return a value. If you need to return something, use a Func<>.
    
        // Create an Action<> that will be used in the loop until the initial
        // condition has ceased.  This Action<> will replace itself with the
        // subsequent Action<>.
        Action<char> Process = new Action<char>(c =>
        {
            if ((c != ' ') && (c != '\t'))
            {
                tempText += c;
                // Replace the Action with new functionality for subsequent
                // iterations of the loop.
                Process = new Action<char>(c1 =>
                {
                    if (c1 != '\t')
                    {
                        tempText += c1;
                    }
                });
            }
        });
    
        // Now the loop will use the Process Action<>, which will change
        // itself to new behaviour once the initial condition no longer holds.
        foreach (char c in text)
        {
            Process(c);
        }
        return tempText;
    }
    

    对于像您在问题中用作示例的简单问题,我不会推荐这种方法,但对于更复杂的场景,它可能值得考虑。但是,请始终考虑到将来有人可能不得不修改代码,如果您太聪明,可能不会太高兴。

    【讨论】: