C#の「ループ初期化」に対処する他の方法
-
02-10-2019 - |
質問
まず、GOTOの声明は、最新のプログラミング言語のより高いレベルの構成要素によってほとんど無関係になり、適切な代替品が利用できる場合は使用すべきではないことに同意します。
私は最近、Steve McConnellのコードのオリジナル版を再読していましたが、共通のコーディングの問題に対する彼の提案を忘れていました。私は何年も前に最初に始めたときにそれを読んでいましたが、レシピがどれほど役に立つか気づいたとは思いませんでした。コーディングの問題は次のとおりです。ループを実行する場合、ループの一部を実行して状態を初期化し、他のロジックでループを実行し、同じ初期化ロジックで各ループを終了する必要があります。具体的な例は、string.join(delimiter、array)メソッドを実装することです。
誰もが問題について最初に考えたと思います。付録メソッドが定義されていると仮定して、返品値に引数を追加します。
bool isFirst = true;
foreach (var element in array)
{
if (!isFirst)
{
append(delimiter);
}
else
{
isFirst = false;
}
append(element);
}
注:これに対するわずかな最適化は、他のものを削除し、ループの最後に置くことです。通常、割り当ては単一の命令であり、他の命令に相当し、基本ブロックの数を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で他のループをシミュレートすることに限定されていますが、翻訳は簡単で、読むことは難しくありません。
だから、今質問。私はこれを私が取り組んでいたいくつかのコードにこれを追加しようと喜んで行きましたが、うまくいかないことがわかりました。 C、C ++、Basicでうまく機能しますが、C#では、親の範囲ではない異なる語彙スコープ内のラベルにジャンプすることはできません。私はとてもがっかりしました。それで、私は疑問に思っていました、この非常に一般的なコーディングの問題に対処するための最良の方法は何ですか(私はそれが主に文字列生成で見ています)。
おそらく要件をより具体的にするために:
- コードを複製しないでください
- 不必要な仕事をしないでください
- 他のコードよりも2倍または3倍遅くなることはありません
- 読みやすい
読みやすさは、私が述べたレシピで間違いなく苦しむかもしれない唯一のものだと思います。ただし、C#では機能しないので、次の最高のものは何ですか?
* 編集 *いくつかの議論のために、パフォーマンス基準を変更しました。パフォーマンスは一般にここでは制限要因ではないため、目標はより正確には、これまでで最速ではないことではなく、不合理ではないことです。
私が提案する代替の実装が嫌いな理由は、それらが1つの部分を変更する余地を残し、他方の部品を変更するための余地を残すか、または一般的に選択したものには、物事を元に戻すために余分な思考と時間を必要とする操作を「元に戻す」必要があるためです。あなたがちょうどしたこと。特に文字列の操作により、通常、1つのエラーによってオフになり、空の配列を説明できず、発生しなかったことを元に戻そうとします。
解決
特定の例には、標準的なソリューションがあります。 string.Join
. 。これにより、デリミッターを正しく追加して、ループを自分で書く必要がないように処理できます。
あなたが本当にこれを自分で書きたいなら、あなたが使用できるアプローチは次のとおりです。
string delimiter = "";
foreach (var element in array)
{
append(delimiter);
append(element);
delimiter = ",";
}
これは合理的に効率的であるはずであり、読むのは合理的だと思います。定数文字列 "、"はインターンされているため、各反復で新しい文字列が作成されません。もちろん、アプリケーションにとってパフォーマンスが重要な場合は、推測するのではなく、ベンチマークする必要があります。
他のヒント
個人的にはマーク・バイヤーのオプションが好きですが、これについてはいつでも独自の一般的な方法を書くことができます。
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);
}
}
編集:あなたのコメントがこれのパフォーマンスに関係していたので、私はこの答えで私のコメントを繰り返します:この一般的な問題は合理的に一般的ですが、それはそれです いいえ それが非常にパフォーマンスのボトルネックであるため、周りに微小最適化する価値があります。確かに、ループ機械がボトルネックになった状況に出くわしたことを思い出すことはできません。私はそれが起こると確信していますが、 それ 「一般的」ではありません。私がそれに遭遇した場合、私はその特定のコードを特別にケースします、そして最良の解決策は依存します まさに コードが必要なこと。
しかし、一般的に、私は読みやすさと再利用性を大切にしています 多くの 微小最適化以上のもの。
あなたはすでにforeachをあきらめることをいとわない。したがって、これは適切なはずです:
using (var enumerator = array.GetEnumerator()) {
if (enumerator.MoveNext()) {
for (;;) {
append(enumerator.Current);
if (!enumerator.MoveNext()) break;
append(delimiter);
}
}
}
確かに作成できます 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();
}
時々私はlinqを使用します .First()
と .Skip(1)
これを処理するために...これにより、比較的きれいな(そして非常に読みやすい)ソリューションが得られます。
あなたの例を使用して、
append(array.First());
foreach(var x in array.Skip(1))
{
append(delimiter);
append (x);
}
これは、アレイに少なくとも1つの要素があると仮定しており、それが回避されるかどうかを追加する簡単なテストです。
F#を使用することは別の提案です:-)
2倍のコードを「回避する」方法がありますが、ほとんどの場合、重複したコードは、可能なソリューションよりもはるかにugい/危険です。あなたが引用した「goto」ソリューションは私にとって改善のようには見えません - それを使用して、あなたがそれを使用することで本当に重要なもの(コンパクトさ、読みやすさ、または効率)を得ることは本当にないと思いますが、プログラマーが何か間違っているリスクを高めますコードの生涯のある時点で。
一般的に、私はアプローチに行く傾向があります:
- 最初の(または最後の)アクションのための特別なケース
- 他のアクションのループ。
これにより、ループが毎回最初の反復であるかどうかを確認することで導入された非効率性が削除され、本当に理解しやすいです。自明でない場合、代表者またはヘルパーメソッドを使用してアクションを適用すると、コードの複製を最小限に抑えることができます。
または、効率が重要でない場合に使用する別のアプローチ:
- ループ、および区切り文字が必要かどうかを判断するために文字列が空であるかどうかをテストします。
これは、GOTOアプローチよりもコンパクトで読みやすくなるように書くことができ、「特別なケース」Iteraitonを検出するための追加の変数/ストレージ/テストは必要ありません。
しかし、Mark Byersのアプローチは、特定の例に適したきれいなソリューションだと思います。
私は好きです first
可変メソッド。おそらく最もクリーンではありませんが、最も効率的な方法です。または、使用することもできます Length
ゼロと比較するために追加するものの。うまく機能します StringBuilder
.
ループの外側の最初の要素を扱って動かないのはなぜですか?
StringBuilder sb = new StrindBuilder()
sb.append(array.first)
foreach (var elem in array.skip(1)) {
sb.append(",")
sb.append(elem)
}
機能的なルートに移動したい場合は、タイプ間で再利用可能なstring.join lik linq constructを定義できます。
個人的には、私はほとんど常に、いくつかのオペコード実行を保存することについてコードの明確さを求めています。
例えば:
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();
}
}
}