33

背景:データベースから取得した文字列がたくさんあるので、それらを返したいと思います。従来は、次のようになります。

public List<string> GetStuff(string connectionString)
{
    List<string> categoryList = new List<string>();
    using (SqlConnection sqlConnection = new SqlConnection(connectionString))
    {
        string commandText = "GetStuff";
        using (SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection))
        {
            sqlCommand.CommandType = CommandType.StoredProcedure;

            sqlConnection.Open();
            SqlDataReader sqlDataReader = sqlCommand.ExecuteReader();
            while (sqlDataReader.Read())
            {
                categoryList.Add(sqlDataReader["myImportantColumn"].ToString());
            }
        }
    }
    return categoryList;
}

しかし、消費者はアイテムを繰り返し処理したいと考えており、他のことはあまり気にしないと思います。リスト自体にはボックスに入れないようにしたいので、IEnumerableを返すとすべてがうまくいきます。 /フレキシブル。だから私はこれを処理するために「イールドリターン」タイプのデザインを使用できると思っていました...このようなもの:

public IEnumerable<string> GetStuff(string connectionString)
{
    using (SqlConnection sqlConnection = new SqlConnection(connectionString))
    {
        string commandText = "GetStuff";
        using (SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection))
        {
            sqlCommand.CommandType = CommandType.StoredProcedure;

            sqlConnection.Open();
            SqlDataReader sqlDataReader = sqlCommand.ExecuteReader();
            while (sqlDataReader.Read())
            {
                yield return sqlDataReader["myImportantColumn"].ToString();
            }
        }
    }
}

しかし、今私は歩留まりについてもう少し読んでいます(このようなサイトでは... msdnはこれについて言及していないようです)、それは明らかに怠惰な評価者であり、誰かが尋ねることを見越して、人口の状態を維持します次の値を取得し、次の値が返されるまで実行します。

これはほとんどの場合問題ないように見えますが、DB呼び出しでは、これは少し厄介に聞こえます。やや不自然な例として、誰かが私がDB呼び出しから入力しているIEnumerableを要求した場合、その半分を通過し、ループでスタックします...私のDB接続が進行していることがわかる限り永遠に開いたままにします。

イテレータが終了しない場合は、トラブルを要求するように聞こえます...何かが足りないのですか?

4

11 に答える 11

44

これはバランスをとる行為です。接続を解放できるようにすべてのデータをすぐにメモリに強制しますか、それとも接続を常に拘束することを犠牲にしてデータをストリーミングすることで利益を得たいですか?

私の見方では、その決定は、彼らが何をしたいのかについてもっと知っている発信者次第である可能性があります。イテレータブロックを使用してコードを記述した場合、呼び出し元はそのストリーミングフォームを完全にバッファリングされたフォームに非常に簡単に変換できます。

List<string> stuff = new List<string>(GetStuff(connectionString));

一方、自分でバッファリングを行う場合、発信者がストリーミングモデルに戻る方法はありません。

したがって、私はおそらくストリーミングモデルを使用し、それが何をするのかをドキュメントで明示的に述べ、発信者に適切に決定するようにアドバイスします。基本的にストリーミングバージョンを呼び出してリストに変換するヘルパーメソッドを提供することもできます。

もちろん、発信者が適切な決定を下すのを信頼せず、発信者が実際にデータをストリーミングしたくないと信じる十分な理由がある場合(たとえば、データがあまり返されない場合)、リストに移動しますアプローチ。いずれにせよ、それを文書化してください-それは戻り値がどのように使用されるかに非常によく影響する可能性があります。

もちろん、大量のデータを処理するためのもう1つのオプションは、バッチを使用することです。これは、元の質問とは少し異なる考え方ですが、ストリーミングが通常魅力的な状況で検討するのは別のアプローチです。

于 2009-04-29T19:36:29.850 に答える
10

IEnumerableで常に安全であるとは限りません。GetEnumeratorフレームワークの呼び出し(ほとんどの人が行うことです)を離れれば、安全です。基本的に、メソッドを使用するコードの注意と同じくらい安全です。

class Program
{
    static void Main(string[] args)
    {
        // safe
        var firstOnly = GetList().First();

        // safe
        foreach (var item in GetList())
        {
            if(item == "2")
                break;
        }

        // safe
        using (var enumerator = GetList().GetEnumerator())
        {
            for (int i = 0; i < 2; i++)
            {
                enumerator.MoveNext();
            }
        }

        // unsafe
        var enumerator2 = GetList().GetEnumerator();

        for (int i = 0; i < 2; i++)
        {
            enumerator2.MoveNext();
        }
    }

    static IEnumerable<string> GetList()
    {
        using (new Test())
        {
            yield return "1";
            yield return "2";
            yield return "3";
        }
    }

}

class Test : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("dispose called");
    }
}

データベース接続を開いたままにしておくことができるかどうかは、アーキテクチャにも依存します。発信者がトランザクションに参加している場合(そして接続が自動的に参加している場合)、接続はとにかくフレームワークによって開いたままになります。

もう1つの利点は、yield(サーバー側カーソルを使用する場合)、コンシューマーがループから早く抜け出したい場合(例:後10番目のアイテム)。これにより、データのクエリを高速化できます。特に、サーバー側カーソルがデータを取得する一般的な方法であるOracle環境では。

于 2009-04-29T19:48:33.143 に答える
8

あなたは何も見逃していません。サンプルは、yieldreturnを使用しない方法を示しています。アイテムをリストに追加し、接続を閉じて、リストを返します。メソッドシグネチャは引き続きIEnumerableを返すことができます。

編集:そうは言っても、Jonにはポイントがあります(とても驚いています!):パフォーマンスの観点から、ストリーミングが実際に最善の方法であるというまれな機会があります。結局のところ、ここで話しているのが100,000(1,000,000?10,000,000?)行の場合、最初にすべてをメモリにロードする必要はありません。

于 2009-04-29T19:28:20.540 に答える
6

余談ですが、このIEnumerable<T>アプローチは基本的に、LINQプロバイダー(LINQ-to-SQL、LINQ-to-Entities)が生活のために行うことであることに注意してください。ジョンが言うように、このアプローチには利点があります。ただし、明確な問題もあります。特に(私にとっては)分離(の組み合わせ)に関しては問題があります。抽象化。

ここで私が意味するのは、次のとおりです。

  • MVCシナリオ(たとえば)では、「データの取得」ステップで実際にデータを取得する必要があります。これにより、ビューではなくコントローラーで機能することをテストできます(呼び出すことを忘れないでください) 。.ToList()
  • 別のDAL実装がデータをストリーミングできることを保証することはできません(たとえば、POX / WSE / SOAP呼び出しは通常レコードをストリーミングできません)。また、動作を紛らわしいほど異なるものにする必要はありません(つまり、ある実装での反復中に接続が開いたままで、別の実装では閉じられます)。

これは、ここでの私の考えと少し結びついています:実用的なLINQ

しかし、強調する必要があります。ストリーミングが非常に望ましい場合は確かにあります。それは単純な「常に対決して」ではありません...

于 2009-04-30T04:17:08.367 に答える
3

イテレータの評価を強制するもう少し簡潔な方法:

using System.Linq;

//...

var stuff = GetStuff(connectionString).ToList();
于 2010-09-23T22:19:09.303 に答える
1

いいえ、あなたは正しい道を進んでいます...歩留まりはリーダーをロックします...IEnumerableを呼び出している間に別のデータベース呼び出しを実行してそれをテストできます

于 2009-04-29T19:29:00.913 に答える
1

これが問題を引き起こす唯一の方法は、呼び出し元がのプロトコルを悪用した場合ですIEnumerable<T>。それを使用する正しい方法はDispose、それがもはや必要とされなくなったときにそれを呼び出すことです。

によって生成された実装は、開いているブロックを実行するためのシグナルとして呼び出しをyield return受け取ります。この例では、ステートメントで作成したオブジェクトを呼び出します。DisposefinallyDisposeusing

正しくforeach使用するのを非常に簡単にする多くの言語機能(特に)があります。IEnumerable<T>

于 2009-04-29T21:19:31.350 に答える
0

私はこの壁に数回ぶつかりました。SQLデータベースクエリは、ファイルのように簡単にストリーミングすることはできません。代わりに、必要と思われる量だけクエリを実行し、必要なコンテナ(、、など)として返しIList<>ますDataTableIEnumerableここでは役に立ちません。

于 2009-04-29T19:37:14.543 に答える
0

いつでも別のスレッドを使用してデータを(おそらくキューに)バッファリングすると同時に、データを返すためにyeildを実行することができます。ユーザーがデータを要求すると(yeildを介して返される)、アイテムはキューから削除されます。データは、別のスレッドを介してキューに継続的に追加されています。そうすれば、ユーザーが十分な速さでデータを要求した場合、キューがいっぱいになることはなく、メモリの問題について心配する必要はありません。そうでない場合、キューはいっぱいになりますが、それほど悪くはないかもしれません。メモリに課したい何らかの制限がある場合は、最大キューサイズを適用できます(この時点で、他のスレッドはアイテムが削除されるのを待ってから、キューに追加します)。当然、2つのスレッド間でリソース(つまり、キュー)を正しく処理することを確認する必要があります。

別の方法として、データをバッファリングする必要があるかどうかを示すために、ユーザーにブール値を渡すように強制することもできます。trueの場合、データはバッファリングされ、接続はできるだけ早く閉じられます。falseの場合、データはバッファリングされず、ユーザーが必要とする限りデータベース接続は開いたままになります。ブールパラメータを使用すると、ユーザーは選択を強制され、問題について確実に知ることができます。

于 2009-05-21T20:21:14.747 に答える
-1

代わりにSqlDataAdapterを使用して、DataTableに入力することができます。このようなもの:

public IEnumerable<string> GetStuff(string connectionString)
{
    DataTable table = new DataTable();
    using (SqlConnection sqlConnection = new SqlConnection(connectionString))
    {
        string commandText = "GetStuff";
        using (SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection))
        {
            sqlCommand.CommandType = CommandType.StoredProcedure;
            SqlDataAdapter dataAdapter = new SqlDataAdapter(sqlCommand);
            dataAdapter.Fill(table);
        }

    }
    foreach(DataRow row in table.Rows)
    {
        yield return row["myImportantColumn"].ToString();
    }
}

このように、すべてを1回のショットでクエリし、接続をすぐに閉じますが、それでも結果を怠惰に繰り返します。さらに、このメソッドの呼び出し元は、結果をリストにキャストして、実行してはいけないことを実行することはできません。

于 2009-04-29T19:39:20.357 に答える
-2

ここではyieldを使用しないでください。サンプルは問題ありません。

于 2009-04-29T19:29:26.840 に答える