web-dev-qa-db-ja.com

データを2回評価せずにループする

私はしばしば次のパターンに遭遇します。

while(GetValue(i) != null)
{
    DoSomethingWith(GetValue(i++));
}

ここでGetValueは2回実行されます。 GetValueの結果を評価して保存できるパターンを使用するほうがはるかに便利です。場合によっては(ストリーム)、値を2回取得することが不可能です(回避策については、以下の代替案を参照してください)。使用できるパターンまたはループ構造はありますか?

私が自分の欠点で考えたいくつかの代替案。

代替1

// Variable outside of loop scope, extra if
var value = null;
do
{
    value = GetValue(i++);
    if(value != null) { DoSomethingWith(value); }
} while(value != null);

代替2

// Two get values, variable outside of loop
var value = GetValue(i);
while(value != null)
{
  DoSometingWith(value);
  value = GetValue(++i);
}

代替

// Out only works on reference types, most enumerables do not have
// a TryGet like method so we need to create our own wrapper
Object value;
while(TryGetValue(i++, out value))
{
    DoSomethingWith(value);
}

理想的な世界のシナリオ(無効なC#)

while((var value = GetValue(i++)) != null)
{
    DoSomethingWith(value);
}
6
Roy T.

「理想的な世界」に近いソリューション

以下は有効なC#です

public static void Main()
{
    var i = 0;
    string value;
    while ((value = GetValue(i++)) != null)
    {
        DoSomethingWith(value);
    }
}

private static string GetValue(int input)
{
    if (input > 20)
    {
        return null;
    }
    return input.ToString();
}

private static void DoSomethingWith(string value)
{
    Console.WriteLine(value);
}

「理想的な世界のシナリオ」と比較してください。

var i = 0;
while((var value = GetValue(i++)) != null)
{
    DoSomethingWith(value);
}

私がしなければならなかったのは、valueをループの外に引っ張ることだけでした。提示されたすべての代替案について、これを喜んで行うようです。したがって、私はこれがあまりにも長すぎるとは思わない。


「無限」ながら代替

私は代替案で遊んでいます...ここに別のものがあります:

var i = 0;
while (true)
{
    var value = GetValue(i++);
    if (value == null)
    {
        break;
    }
    DoSomethingWith(value);
}

この場合、valueをループ内で宣言して(varを使用して)、nullを2回確認する必要はありません。同じ入力でGetValueを2回呼び出す必要はなく、作成する必要もありません。 outパラメーターを持つラッパー。


Forを使用した代替

forループを使用して同じことを表現しようとする場合があります。ただし、素朴なアプローチは機能しません。

// I repeat, this does not work:
var i = 0;
for (string value = null; value != null; value = GetValue(i++))
{
    DoSomethingWith(value);
}

このバージョンの問題は、valuenullであることで始まり、終了基準を満たしているため、反復処理が行われないことです。


JimmyJamesが指摘 のように、次のように記述できます。

var i = 0;
for(string value; (value = GetValue(i++)) != null;)
{
    DoSomethingWith(value);
}

私が目にする唯一の欠点は、型を記述する必要があることです(varは使用できません)。


補遺:これは別のバリアントです Maliafoによって提案されました

var i = 0;
for (var value = GetValue(i); value != null; value = GetValue(++i))
{
    DoSomethingWith(value);
}

このバージョンではGetValueを書き込む必要がありますが、同じ値で2回呼び出すことはありません。


スコープでもiを宣言しますか?これを見てください:

for (var i = 0; ; i++)
{
    var value = GetValue(i);
    if (value == null)
    {
        break;
    }
    DoSomethingWith(value);
}

これは、私が「無限」で見たループと同じですが、私が上に投稿したソリューションです。それでも、しばらくの間条件がなかったので... forに変更してiをインクリメントするのはなぜですか?


Addendum:C#7.0機能の興味深い使用法については NetMageの回答 も参照してください。

13
Theraot

C#7.0では、次のように(ab)使用できます。

while (GetValue(i++) is var value && value != null)
    DoSomethingWith(value);

別のC#7.0機能を使用して、拡張メソッドを作成できます。

public static T As<T>(this T val, out T newvar) => newvar = val;

そして、outを犠牲にして、変数を作成し、値をテストします。

while (GetValue(i++).As(out var value) != null) {
    DoSomethingWith(value);

または、理想的な世界シナリオをエミュレートするバリエーションを使用します。

public static T Let<T>(out T newvar, T val) => newvar = val;

while (Let(out var value, GetValue(i++)) != null) {
    DoSomethingWith(value);
5
NetMage

高次関数を使用する

関数型プログラミングの脚本から一枚を取り出します。コードの構造に繰り返しパターンがある場合は、同じままの部分を含む関数を作成し、変化する部分を関数パラメーター(デリゲート、 C#)。そうすれば、それがどれほど醜いかは関係ありません。一度だけ書いて、どこかの図書館でそれを隠す必要があります。

(あなたが私にJavaここに落ちることを許す必要があります;私のC#スキルは少し錆びています)

public static <T> void untilNull (Function<Integer,T> supplier, int i, Action<T>  action) {
    T v;
    while ((v = supplier.apply(i++)) != null)
        action.perform(v);
}

....
untilNull (this::getValue, 1, this::doSomethingWithValue);

更新-C#バージョン:

public static <T> void UntilNull (Func<int,T> supplier, int i, Action<T> action) {
    T v;
    while ((v = supplier(i++)) != null) action(v);
}

....
UntilNull (this.GetValue, 1, this.DoSomethingWithValue);
4
Jules

シンプルなソリューション

この解決策は、問題を解決するために最小限のコードが必要な場合、代替案1および2よりも確実に優れています。

while(true){
    var value = GetValue(i++);
    if(value != null) { 
        DoSomethingWith(value); 
    }
    else{
        break;
    }
}

DRYイテレーターベースのForeach互換ソリューション

私の意見では、最もクリーンで一般的なソリューションの場合、このイテレータ function parameter ベースのソリューションを使用するのが最適です。 3番目のオプションによって提供される特定のラッパーと重複コードをクラス化する必要はなく、このようなすべての問題に対して機能します(そのため、短いインラインのwhileループ実装よりも優れていると思います)。さらに良いことに、とにかくイテレータを使用する場合は、イテレータ自体にnullcheckを入れてみませんか?これにより、位置を取得してnullを返すことができるほぼすべての関数にこれを使用できます。

public static System.Collections.Generic.IEnumerable<T>  
    FunctionSequence(Func<int,T> f)  
{  
    int i = 0;
    while(true){
        value = f(i++);
        if(value != null) { 
            yield return value; 
        }
        else{
            break;
        }
    }
}  

foreach(GetValueType value in FunctionSequence<GetValueType>(GetValue)){
    DoSomethingWith(value);
}

すべての関数にボイラープレートコードが必要ないことに注意してくださいFunctionSequenceイテレータ、最初のnull要素まで反復します。これを書くのは一度だけで、これは


イテレーターベースのソリューションのさらなる一般化

0から始めたくない場合は、常に関数のシグネチャを次のように変更できます。

public static System.Collections.Generic.IEnumerable<T>  
    FunctionSequence(Func<int,T> f, int start = 0)  
{  
    int i = start;
    ...

また、チェックが何であるかを変更することもできます。それがnullであるか、特定のオブジェクトが返されたかです。

public static System.Collections.Generic.IEnumerable<T>  
    FunctionSequence(Func<int,T> f, int start = 0, T terminator = null)  
{  
    int i = start;
    while(true){
        value = f(i++);
        if(value != terminator) { 
        ...

これはあなたの場合に最適なソリューションだと思います。

3
whn

私はC#ではなく、ほとんどがSwiftです。ほとんどの場合、私は書くでしょう

for value in <some collection> { ... }

これは構文糖

while let value = iterator.next() { }

(let value = expressionはexpressionを評価し、nilかどうかをチェックします。nilが存在する場合、nilでない場合は、オプションではない値をvalueに格納します。イテレータのnext()はオプションの値を返します。イテレータが終了した場合はnil )。

0
gnasher729

私は代替3を使用します。参照型で使用できる理由がわかりません。まず、参照型を使用していない場合は、nullを確認する必要はありません。 「これはenumに適切な値がありますか?」などの他の検証が必要な場合そのチェックをwhileで使用するだけで、適切な場合にのみ実行されます(または、宣言時に変数を初期化することを意味しますが、outの代わりにrefを使用できます)

0
Zalomon

代替案はどうですか4:

GetValueをIEnumerable/IEnumeratorにラップします。その後、理想的なシナリオを可能にする通常のforeach構文を使用できます。動的なコンテンツがある場合は、本格的なCollectionクラスを実装する必要はありません。yieldステートメントの周りのいくつかの関数でうまくいきます。

データでいくつかのLinq手品を使用することもできるはずです。

0
Newtopian