代码之家  ›  专栏  ›  技术社区  ›  Peter Oehlert

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

  •  22
  • Peter Oehlert  · 技术社区  · 14 年前

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

    我想每个人对这个问题的第一反应就是这个。假设定义了append方法将参数添加到返回值中。

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

    注意:稍微优化一下,就是去掉else并把它放在循环的末尾。一种赋值,通常是一条指令,相当于一条else指令,使基本块的数目减少1,并使主要部分的基本块的大小增加。结果是在每个循环中执行一个条件,以确定是否应该添加分隔符。

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

    后一种解决方案往往是我更喜欢的解决方案,因为它不会复制任何代码。如果初始化序列的逻辑发生了变化,您不必记住在两个地方修复它。然而,它确实需要额外的“工作”来做某事,然后撤销它,至少会导致额外的cpu周期,在许多情况下,比如我们的系统字符串。连接这个例子也需要额外的内存。

    我当时读到这篇文章很兴奋

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

    这里的好处是您不会得到重复的代码,也不会得到额外的工作。在执行第一个循环的一半时间内开始循环,这就是初始化。您仅限于使用do while构造来模拟其他循环,但是翻译很容易,阅读起来也不难。

    更具体的要求是:

    • 不复制代码
    • 不要做不必要的工作
    • 不要比其他代码慢2到3倍

    我认为可读性是唯一的事情,可能会受到我说的食谱。然而,它在C语言中不起作用,那么下一个最好的方法是什么呢?

    *编辑* 因为一些讨论,我改变了我的表现标准。在这里,性能通常不是一个限制因素,因此更正确的目标应该是不合理,而不是有史以来最快的速度。

    我不喜欢我建议的替代实现的原因是,它们要么是重复的代码,留下了更改一部分而不是另一部分的空间,要么是对于我通常选择的部分,它需要“撤消”操作,这需要额外的思考和时间来撤消您刚才所做的操作。特别是对于字符串操作,这通常会导致一个错误或无法解释空数组并试图撤消未发生的操作。

    9 回复  |  直到 13 年前
        1
  •  12
  •   Mark Byers    14 年前

    对于您的具体示例,有一个标准解决方案: string.Join

    如果您真的想自己写这篇文章,您可以使用以下方法:

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

        2
  •  18
  •   Jon Skeet    14 年前

    就我个人而言,我喜欢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);
        }
    }
    

    编辑:由于您的评论关注的是这个问题的表现,我将在下面的回答中重申我的评论:虽然这个普遍的问题是相当常见的,但它是 确切地 代码需要做什么。

    不过,总的来说,我重视可读性和可重用性 许多的

        3
  •  7
  •   Hans Passant    14 年前

            using (var enumerator = array.GetEnumerator()) {
                if (enumerator.MoveNext()) {
                    for (;;) {
                        append(enumerator.Current);
                        if (!enumerator.MoveNext()) break;
                        append(delimiter);
                    }
                }
            }
    
        4
  •  6
  •   Jordão    14 年前

    你当然可以创造一个 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();
    }
    
        5
  •  4
  •   Ian Mercer    14 年前

    .First() .Skip(1) 为了处理这个。。。这可以提供一个相对干净(并且非常可读)的解决方案。

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

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

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

        6
  •  2
  •   Jason Williams    14 年前

    有很多方法可以绕过双重代码,但是在大多数情况下,重复的代码比可能的解决方案要简单得多。你引用的“goto”解决方案在我看来并不是一种改进——我真的认为你并没有通过使用它获得任何显著的好处(紧凑性、可读性或效率),同时你增加了程序员在代码生命周期的某个时候出错的风险。

    • 第一次(或最后一次)行动的特例
    • 循环其他操作。

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

    或者我有时在效率不重要的情况下使用的另一种方法:

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

    但我认为马克·拜尔斯的方法对于你的例子来说是一个很好的解决方案。

        7
  •  0
  •   Ilia G    14 年前

    我更喜欢 first Length 把它和零比较。适用于 StringBuilder .

        8
  •  0
  •   Bart    14 年前

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

    StringBuilder sb = new StrindBuilder()
    sb.append(array.first)
    foreach (var elem in array.skip(1)) {
      sb.append(",")
      sb.append(elem)
    }
    
        9
  •  0
  •   Sam Saffron James Allen    14 年前

    如果你想走功能路线,你可以定义字符串。连接就像LINQ构造一样,它可以跨类型重用。

    就我个人而言,我几乎总是追求代码的清晰性,而不是节省一些操作码执行。

    如:

    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();
            }
    
        }
    }