在C#中处理“循环初始化”的其他方法

时间:2010-08-29 19:57:39

标签: c# initialization goto do-while

首先,我会说我同意goto语句在很大程度上与现代编程语言中的更高级别结构无关,并且在适当的替代品可用时不应该使用。

我最近重新阅读了Steve McConnell的Code Complete原版,并忘记了他对常见编码问题的建议。几年前,当我第一次开始时,我已经读过它,并且不认为我意识到配方会有多么有用。编码问题如下:执行循环时,您经常需要执行循环的一部分来初始化状态,然后使用其他逻辑执行循环,并使用相同的初始化逻辑结束每个循环。一个具体的例子是实现String.Join(delimiter,array)方法。

我认为每个人第一个接受这个问题的都是这个。假设定义了append方法以将参数添加到返回值。

bool isFirst = true;
foreach (var element in array)
{
  if (!isFirst)
  {
     append(delimiter);
  }
  else
  {
    isFirst = false;
  }

  append(element);
}

注意:稍微优化一下就是删除else并将其放在循环的末尾。赋值通常是单个指令并等效于else,并将基本块的数量减少1并增加主要部分的基本块大小。结果是在每个循环中执行一个条件以确定是否应该添加分隔符。

我也看到并使用了其他处理这个常见循环问题的方法。您可以先在循环外执行初始元素代码,然后从第二个元素到结尾执行循环。您还可以将逻辑更改为始终附加元素然后添加分隔符,一旦完成循环,您只需删除添加的最后一个分隔符。

后一种解决方案往往是我更喜欢的解决方案,因为它不会复制任何代码。如果初始化序列的逻辑发生变化,您不必记得在两个地方修复它。然而,它需要额外的“工作”来做某事然后撤消它,至少导致额外的cpu周期,并且在很多情况下,例如我们的String.Join示例也需要额外的内存。

我很兴奋然后阅读这个构造

var enumerator = array.GetEnumerator();
if (enumerator.MoveNext())
{
  goto start;
  do {
    append(delimiter);

  start:
    append(enumerator.Current);
  } while (enumerator.MoveNext());
}

这样做的好处是,您没有重复的代码,也没有额外的工作。你开始循环进入第一个循环的执行的一半,那就是你的初始化。您只能使用do while构造模拟其他循环,但转换很容易并且阅读它并不困难。

所以,现在问题。我很高兴尝试将其添加到我正在处理的一些代码中,发现它不起作用。在C,C ++,Basic中工作得很好但是在C#中你不能跳转到不是父范围的不同词法范围内的标签。我很失望。所以我想知道,在C#中处理这个非常常见的编码问题(我主要在字符串生成中看到它)的最佳方法是什么?

或许更具体的要求:

  • 不要重复代码
  • 不要做不必要的工作
  • 比其他代码慢不超过2或3倍
  • 可读

我认为可读性是我说过的唯一可能会受到影响的因素。但它在C#中不起作用,那么下一个最好的东西是什么?

*编辑* 由于一些讨论,我改变了我的表现标准。性能通常不是限制因素,所以更正确的目标应该是不合理,不是最快的。

我不喜欢我建议的替代实现的原因是因为它们要么重复代码,留下改变一个部分而不是另一个部分的空间,或者对于我通常选择它的那个要求“撤消”需要额外思考和时间的操作撤消你刚刚做的事情。特别是对于字符串操作,这通常会让您因一个错误而打开,或者没有考虑空数组并尝试撤消未发生的事情。

9 个答案:

答案 0 :(得分:18)

就我个人而言,我喜欢Mark Byer的选择,但您可以随时为此编写自己的通用方法:

public static void IterateWithSpecialFirst<T>(this IEnumerable<T> source,
    Action<T> firstAction,
    Action<T> subsequentActions)
{
    using (IEnumerator<T> iterator = source.GetEnumerator())
    {
        if (iterator.MoveNext())
        {
            firstAction(iterator.Current);
        }
        while (iterator.MoveNext())
        {
            subsequentActions(iterator.Current);
        }
    }
}

这是相对简单的...给出一个特殊的最后动作稍微有些困难:

public static void IterateWithSpecialLast<T>(this IEnumerable<T> source,
    Action<T> allButLastAction,
    Action<T> lastAction)
{
    using (IEnumerator<T> iterator = source.GetEnumerator())
    {
        if (!iterator.MoveNext())
        {
            return;
        }            
        T previous = iterator.Current;
        while (iterator.MoveNext())
        {
            allButLastAction(previous);
            previous = iterator.Current;
        }
        lastAction(previous);
    }
}

编辑:由于你的评论涉及到这方面的表现,我将在这个答案中重申我的评论:虽然这个普遍的问题相当普遍,但通常是因为它是这样的性能瓶颈,值得微观优化。实际上,我不记得曾经遇到过循环机械成为瓶颈的情况。我确定它会发生,但 不是“常见”。如果我碰到它,我会特别注意特定代码,最好的解决方案将取决于完全代码需要做什么。

但是,一般来说,我认为可读性和可重用性很多比微优化更重要。

答案 1 :(得分:11)

对于您的具体示例,有一个标准解决方案:string.Join。这样可以正确添加分隔符,这样您就不必自己编写循环。

如果你真的想自己写一个你可以使用的方法如下:

string delimiter = "";
foreach (var element in array)
{
    append(delimiter);
    append(element);
    delimiter = ",";
}

这应该是合理有效的,我认为阅读是合理的。常量字符串“,”被实现,因此不会导致在每次迭代时创建新字符串。当然,如果性能对您的应用程序至关重要,那么您应该进行基准测试而不是猜测。

答案 2 :(得分:7)

你已经愿意放弃foreach。所以这应该是合适的:

        using (var enumerator = array.GetEnumerator()) {
            if (enumerator.MoveNext()) {
                for (;;) {
                    append(enumerator.Current);
                    if (!enumerator.MoveNext()) break;
                    append(delimiter);
                }
            }
        }

答案 3 :(得分:6)

您当然可以在C#中创建goto解决方案(注意:我没有添加null个检查):

string Join(string[] array, string delimiter) {
  var sb = new StringBuilder();
  var enumerator = array.GetEnumerator();
  if (enumerator.MoveNext()) {
    goto start;
    loop:
      sb.Append(delimiter);
      start: sb.Append(enumerator.Current);
      if (enumerator.MoveNext()) goto loop;
  }
  return sb.ToString();
}

对于特定的示例,这对我来说非常简单(这是您描述的解决方案之一):

string Join(string[] array, string delimiter) {
  var sb = new StringBuilder();
  foreach (string element in array) {
    sb.Append(element);
    sb.Append(delimiter);
  }
  if (sb.Length >= delimiter.Length) sb.Length -= delimiter.Length;
  return sb.ToString();
}

如果您想要实现功能,可以尝试使用这种折叠方法:

string Join(string[] array, string delimiter) {
  return array.Aggregate((left, right) => left + delimiter + right);
}

虽然它读起来非常好,但它没有使用StringBuilder,因此您可能希望滥用Aggregate一点来使用它:

string Join(string[] array, string delimiter) {
  var sb = new StringBuilder();
  array.Aggregate((left, right) => {
    sb.Append(left).Append(delimiter).Append(right);
    return "";
  });
  return sb.ToString();
}

或者你可以使用这个(从这里借用其他答案的想法):

string Join(string[] array, string delimiter) {
  return array.
    Skip(1).
    Aggregate(new StringBuilder(array.FirstOrDefault()),
      (acc, s) => acc.Append(delimiter).Append(s)).
    ToString();
}

答案 4 :(得分:4)

有时我会使用LINQ .First().Skip(1)来处理这个......这可以提供一个相对干净(且非常易读)的解决方案。

使用你的例子,

append(array.First());
foreach(var x in array.Skip(1))
{
  append(delimiter);
  append (x);
}

[这假设数组中至少有一个元素,如果要避免,则添加一个简单的测试。]

使用F#将是另一个建议: - )

答案 5 :(得分:2)

有些方法可以“解决”加倍的代码,但在大多数情况下,重复的代码比可能的解决方案更难看/更危险。你引用的“goto”解决方案对我来说似乎不是一种改进 - 我真的不认为你通过使用它获得了任何重要的(紧凑性,可读性或效率),同时增加了程序员出错的风险在代码生命周期的某个时刻。

总的来说,我倾向于采用这种方法:

  • 第一个(或最后一个)操作的特殊情况
  • 循环执行其他操作。

这消除了通过每次检查循环是否在第一次迭代中引入的低效率,并且非常容易理解。对于非平凡的情况,使用委托或帮助方法来应用操作可以最大限度地减少代码重复。

或者我有时使用的另一种方法,效率并不重要:

  • 循环,并测试字符串是否为空以确定是否需要分隔符。

这可以写成比goto方法更紧凑和可读,并且不需要任何额外的变量/存储/测试来检测“特殊情况”iteraiton。

但我认为Mark Byers的方法对于您的特定示例来说是一个很好的清洁解决方案。

答案 6 :(得分:0)

我更喜欢first变量方法。它可能不是最干净但最有效的方式。或者,您可以使用附加内容的Length并将其与零进行比较。适用于StringBuilder

答案 7 :(得分:0)

为什么不在循环外处理第一个元素?

StringBuilder sb = new StrindBuilder()
sb.append(array.first)
foreach (var elem in array.skip(1)) {
  sb.append(",")
  sb.append(elem)
}

答案 8 :(得分:0)

如果你想进入功能路线,你可以定义String.Join,就像可以跨类型重用的LINQ构造一样。

就个人而言,我几乎总是在代码清晰度上保留一些操作码执行。

EG:

namespace Play
{
    public static class LinqExtensions {
        public static U JoinElements<T, U>(this IEnumerable<T> list, Func<T, U> initializer, Func<U, T, U> joiner)
        {
            U joined = default(U);
            bool first = true;
            foreach (var item in list)
            {
                if (first)
                {
                    joined = initializer(item);
                    first = false;
                }
                else
                {
                    joined = joiner(joined, item);
                }
            }
            return joined;
        }
    }

    class Program
    {

        static void Main(string[] args)
        {
            List<int> nums = new List<int>() { 1, 2, 3 };
            var sum = nums.JoinElements(a => a, (a, b) => a + b);
            Console.WriteLine(sum); // outputs 6

            List<string> words = new List<string>() { "a", "b", "c" };
            var buffer = words.JoinElements(
                a => new StringBuilder(a), 
                (a, b) => a.Append(",").Append(b)
                );

            Console.WriteLine(buffer); // outputs "a,b,c"

            Console.ReadKey();
        }

    }
}