22

まず、gotoステートメントは、最新のプログラミング言語の高レベルの構造によってほとんど無関係になり、適切な代替が利用できる場合は使用しないことに同意します。

私は最近、スティーブマコネルのコードコンプリートのオリジナル版を読み直していて、一般的なコーディングの問題についての彼の提案を忘れていました。何年も前に最初に読んだことがありますが、レシピがどれほど役立つかはわかりませんでした。コーディングの問題は次のとおりです。ループを実行する場合、状態を初期化するためにループの一部を実行してから、他のロジックでループを実行し、同じ初期化ロジックで各ループを終了する必要があります。具体的な例は、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つ減らし、主要部分の基本ブロックサイズを増やします。その結果、各ループで条件を実行して、区切り文字を追加する必要があるかどうかを判断します。

私はまた、この一般的なループの問題に対処するための他の見解を見て使用しました。最初にループの外側で最初の要素コードを実行してから、2番目の要素から最後までループを実行できます。ロジックを変更して、常に要素と区切り文字を追加することもできます。ループが完了したら、最後に追加した区切り文字を削除するだけです。

後者の解決策は、コードを複製しないという理由だけで私が好む解決策になる傾向があります。初期化シーケンスのロジックが変更された場合でも、2か所で修正することを覚えておく必要はありません。ただし、何かを実行してから元に戻すには余分な「作業」が必要であり、少なくとも余分な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#では機能しないので、次善の策は何ですか?

*編集* いくつかの議論のためにパフォーマンス基準を変更しました。ここでは通常、パフォーマンスが制限要因ではないため、より正確な目標は、不合理にならないようにすること、これまでで最速にならないようにすることです。

私が提案する代替の実装が嫌いな理由は、コードを複製して一方の部分を変更する余地を残しているため、または私が一般的に選択する実装では、操作を「元に戻す」必要があり、その操作を元に戻すには余分な思考と時間が必要になるためです。あなたがやったこと。特に文字列操作では、これにより通常、1つのエラーが発生したり、空の配列の説明に失敗したり、発生しなかった何かを元に戻そうとしたりすることができます。

4

9 に答える 9

18

個人的にはMarkByerのオプションが好きですが、これにはいつでも独自のジェネリックメソッドを作成できます。

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

編集:あなたのコメントはこれのパフォーマンスに関係していたので、この回答で私のコメントを繰り返します:この一般的な問題はかなり一般的ですが、マイクロ最適化する価値があるほどのパフォーマンスのボトルネックになることは一般的ではありません。確かに、ループ機械がボトルネックになった状況に出くわしたことは今まで覚えていません。私はそれが起こると確信していますが、それ「一般的」ではありません。私がそれに遭遇した場合、私はその特定のコードを特別なケースに入れます、そして最良の解決策はコードが何をする必要があるかによって正確に異なります。

ただし、一般的に、私は読みやすさと再利用性をマイクロ最適化よりもはるかに重視しています。

于 2010-08-29T20:07:04.210 に答える
12

あなたの特定の例のために、標準的な解決策があります:string.Join。これにより、区切り文字の追加が正しく処理されるため、ループを自分で作成する必要がありません。

あなたが本当にこれを自分で書きたいのなら、あなたが使うことができるアプローチは次のとおりです:

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

これはかなり効率的であるはずであり、読むのは合理的だと思います。定数文字列"、"はインターンされるため、反復ごとに新しい文字列が作成されることはありません。もちろん、パフォーマンスがアプリケーションにとって重要である場合は、推測するのではなくベンチマークを行う必要があります。

于 2010-08-29T20:00:13.353 に答える
7

あなたはすでにforeachをあきらめることをいとわない。したがって、これは適切なはずです。

        using (var enumerator = array.GetEnumerator()) {
            if (enumerator.MoveNext()) {
                for (;;) {
                    append(enumerator.Current);
                    if (!enumerator.MoveNext()) break;
                    append(delimiter);
                }
            }
        }
于 2010-08-29T20:23:10.753 に答える
6

あなたは確かgotoにC#でソリューションを作成することができます(注:私は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();
}

あなたの特定の例では、これは私にはかなり単純に見えます(そしてそれはあなたが説明した解決策の1つです):

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();
}
于 2010-08-29T21:54:33.690 に答える
4

時々私はLINQを使用.First().Skip(1)てこれを処理します...これは比較的クリーンな(そして非常に読みやすい)ソリューションを提供することができます。

あなたの例を使用して、

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

[これは、配列に少なくとも1つの要素があることを前提としています。これは、回避する必要がある場合に追加する簡単なテストです。]

F#を使用することは別の提案です:-)

于 2010-08-29T20:31:20.530 に答える
2

二重化されたコードを「回避」する方法はいくつかありますが、ほとんどの場合、複製されたコードは、考えられる解決策よりも醜い/危険性がはるかに低くなります。あなたが引用する「goto」ソリューションは、私には改善されていないようです-プログラマーが何かを間違えるリスクを高めながら、それを使用することによって実際に重要なもの(コンパクトさ、読みやすさ、効率)を得るとは思いませんコードの存続期間のある時点で。

一般的に、私はこのアプローチを採用する傾向があります。

  • 最初(または最後)のアクションの特殊なケース
  • 他のアクションのループ。

これにより、ループが毎回最初の反復にあるかどうかをチェックすることによって生じる非効率性が取り除かれ、非常に理解しやすくなります。重要なケースでは、デリゲートまたはヘルパーメソッドを使用してアクションを適用すると、コードの重複を最小限に抑えることができます。

または、効率が重要ではない場合に私が時々使用する別のアプローチ:

  • ループし、文字列が空かどうかをテストして、区切り文字が必要かどうかを判断します。

これは、gotoアプローチよりもコンパクトで読みやすいように記述でき、「特殊なケース」の反復を検出するために追加の変数/ストレージ/テストを必要としません。

しかし、Mark Byersのアプローチは、あなたの特定の例にとって良いクリーンな解決策だと思います。

于 2010-08-29T20:34:45.557 に答える
0

first私は可変メソッドを好みます。それはおそらく最もクリーンではありませんが、最も効率的な方法です。または、追加するものを使用Lengthして、ゼロと比較することもできます。でうまく機能しStringBuilderます。

于 2010-08-29T20:06:12.447 に答える
0

ループの外側の最初の要素を処理するように移動しないのはなぜですか?

StringBuilder sb = new StrindBuilder()
sb.append(array.first)
foreach (var elem in array.skip(1)) {
  sb.append(",")
  sb.append(elem)
}
于 2010-08-29T20:13:23.023 に答える
0

機能的なルートをたどりたい場合は、タイプ間で再利用可能なLINQコンストラクトのようにString.Joinを定義できます。

個人的には、ほとんどの場合、いくつかのオペコードの実行を保存するよりも、コードを明確にするために行きます。

例えば:

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

    }
}
于 2010-08-30T02:01:57.067 に答える