質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

LINQ

LINQとはLanguage INtegrated Queryの略で、「統合言語クエリ」という意味です。C#やVisual Basicといった言語のコード内に記述することができるクエリです。

Q&A

解決済

3回答

11748閲覧

LinqとToList()の動作について

haru853

総合スコア38

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

LINQ

LINQとはLanguage INtegrated Queryの略で、「統合言語クエリ」という意味です。C#やVisual Basicといった言語のコード内に記述することができるクエリです。

0グッド

1クリップ

投稿2017/06/23 08:58

以下の処理をおこなう為、以下のコードを作成しました。
・日付ごとにDepositの合計を求める(ただし、Idが1と2の場合は除外)
・Idが1と2以外の場合、Idを0にする

c#

1public class Transaction 2{ 3 public int Id { get; set; } 4 5 public DateTime Date { get; set; } 6 7 public int Deposit { get; set; } 8} 9 10public static void Run() 11{ 12 var transactions = new List<Transaction> 13 { 14 new Transaction { Id = 1, Date = new DateTime(2017, 1, 1), Deposit = 11 }, 15 new Transaction { Id = 2, Date = new DateTime(2017, 2, 1), Deposit = 12 }, 16 new Transaction { Id = 3, Date = new DateTime(2017, 3, 1), Deposit = 13 }, 17 new Transaction { Id = 4, Date = new DateTime(2017, 4, 1), Deposit = 14 }, 18 new Transaction { Id = 5, Date = new DateTime(2017, 1, 1), Deposit = 15 }, 19 new Transaction { Id = 6, Date = new DateTime(2017, 2, 1), Deposit = 16 }, 20 new Transaction { Id = 7, Date = new DateTime(2017, 3, 1), Deposit = 17 }, 21 new Transaction { Id = 8, Date = new DateTime(2017, 4, 1), Deposit = 18 } 22 }; 23 24 var result = BundleTransactions(transactions); 25 26 DebugPrintTransactions(result); 27 Console.WriteLine(" "); 28 DebugPrintTransactions(result.ToList()); 29} 30 31public static IEnumerable<Transaction> BundleTransactions(IEnumerable<Transaction> transactions) 32{ 33 var notBundleLoanIds = new List<int> { 1, 2 }; 34 var groupByDate = transactions.GroupBy(a => a.Date).OrderBy(a => a.Key); 35 var bundleTransaction = new Transaction { Id = 0 }; 36 37 foreach (var group in groupByDate) 38 { 39 bundleTransaction.Date = group.Key; 40 bundleTransaction.Deposit = 0; 41 42 foreach (var transaction in group) 43 { 44 if (notBundleLoanIds.Any(a => a == transaction.Id)) 45 yield return transaction; 46 else 47 bundleTransaction.Deposit += transaction.Deposit; 48 } 49 yield return bundleTransaction; 50 } 51} 52 53public static void DebugPrintTransactions(IEnumerable<Transaction> transactions) 54{ 55 foreach (var transaction in transactions) 56 { 57 Console.WriteLine($"ID:{transaction.Id}, 日付:{transaction.Date:d}, 入金:{transaction.Deposit}"); 58 } 59}

DebugPrintTransactions(result);では以下の結果になります。
(期待どおりの結果です)

ID:1, 日付:2017/01/01, 入金:11
ID:0, 日付:2017/01/01, 入金:15
ID:2, 日付:2017/02/01, 入金:12
ID:0, 日付:2017/02/01, 入金:16
ID:0, 日付:2017/03/01, 入金:30
ID:0, 日付:2017/04/01, 入金:32

しかし、DebugPrintTransactions(result.ToList());では以下の結果になります。
なぜ、こうなるのでしょうか?

ID:1, 日付:2017/01/01, 入金:11
ID:0, 日付:2017/04/01, 入金:32
ID:2, 日付:2017/02/01, 入金:12
ID:0, 日付:2017/04/01, 入金:32
ID:0, 日付:2017/04/01, 入金:32
ID:0, 日付:2017/04/01, 入金:32

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答3

0

原因は他の方がおっしゃるとおりと思いますが、少しだけ一般化した言い方をすれば「LINQ(パイプライン機能)にからむ計算が副作用を持っているから」という表現がよく使われると思います。最初に列挙した値が後続の要素の列挙の間に「変化してしまう」ので。

一般的な対策(というか原則)は「LINQで用いる論理では極力副作用を持たない演算に限定する」ことだと思います。

haru666さんが対処として挙げておられる方法と本質的には差がないのですが、bundleTransactionのような自前での繰り返し制御を書く代わりに、LINQに用意されている単純機能の組み合わせに置き換える書き方もあると思います。

C#

1int[] notBundleLoanIds = { 1, 2 }; 2Func<int, int> groupingId = (x) => notBundleLoanIds.Any(id => id == x) ? x : 0; 3var result = transactions 4 // 1,2以外のIdを0にマッピングした上で日付とIdでグルーピング 5 .GroupBy(t => { 6 var Id = groupingId(t.Id); 7 return new { t.Date, Id }; 8 }) 9 // Id=0についてだけ集約 10 .SelectMany(g => { 11 if (g.Key.Id == 0) { 12 var total = g.Sum(t => t.Deposit); 13 return new Transaction[] { 14 new Transaction { Id = g.Key.Id, Date = g.Key.Date, Deposit = total } 15 }; 16 } else { 17 // Id=1 or 2で同一日付のレコードが複数あるという前提でSelectManyを使っています。 18 // 複数ないならSelectに書き直してもよいと思います。 19 return g as IEnumerable<Transaction>; 20 } 21 }); 22 // この例ではソートしていない

こうした書き方では累積などの計算を自分で書かなくて済むので副作用のある処理が自然に出てこなくなる気がします。

投稿2017/06/23 13:20

KSwordOfHaste

総合スコア18394

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

haru853

2017/06/24 00:46

ありがとうございました。 恥ずかしながら、私のレベルでは最初コードを見ても何をしているのかさっぱりわかりませんでした。 Func<>やLinqの中でのreturnなど知らない使い方でしたので、とても勉強になりました。
KSwordOfHaste

2017/06/24 00:47

そうでしたか。逆に自分はyield returnの方が凝った書き方に見えましたw;
guest

0

イテレータによる評価タイミングとクラスが参照型であることとToListメソッドによるものが原因ではないでしょうか。
下記のようにやれば上手くいくと思います。

C#

1public static IEnumerable<Transaction> BundleTransactions(IEnumerable<Transaction> transactions) 2{ 3 var notBundleLoanIds = new List<int> { 1, 2 }; 4 var groupByDate = transactions.GroupBy(a => a.Date).OrderBy(a => a.Key); 5 6 foreach (var group in groupByDate) 7 { 8 int deposit = 0; 9 10 foreach (var transaction in group) 11 { 12 if (notBundleLoanIds.Any(a => a == transaction.Id)) 13 yield return transaction; 14 else 15 deposit += transaction.Deposit; 16 } 17 yield return new Transaction { Id = 0, Date=group.Key, Deposit = deposit }; 18 } 19}

投稿2017/06/23 09:33

編集2017/06/23 09:37
workaholist

総合スコア559

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

haru853

2017/06/24 00:42

ありがとうございました。newで返してしまえば良かったのですね。
guest

0

ベストアンサー

ToListにより一旦処理されているかされていないかの差です。

BundleTransactionsでは内部で1つのTransactionインスタンスを使いまわしていますね。
直接DebugPrintTransactionsに値を渡した場合はループの途中途中で出力がされるため問題になりませんが、ToList()を使うとyield return bundleTransactionで戻された値は全て同一インスタンスですから、その値は全て同じになってしまいます。

インスタンスの識別はIDがあるので簡単ですね。
ID:1
ID:0
ID:2
ID:0
ID:0
ID:0

この通りにListにはインスタンスが設定されているので、リストを逐次出力すると
bundleTransaction(ID:0)の最終状態が何度も出力されることになってしまいます。

#追記

解決策を書いていませんでした。
ToList()が必要な場合は、bundleTransactionを返す時にクローンを作って戻すことです。
これには2通りの方法があります。

1.クローンメソッドをTransactionクラスに作成して使う
今回はシャローコピーで十分ですから、MemberwiseCloneメソッドが使えます。
シャローコピーで十分な条件というのは中身のデータが全てイミュータブルであるということです。

C#

1public class Transaction 2{ 3 public int Id { get; set; } 4 5 public DateTime Date { get; set; } 6 7 public int Deposit { get; set; } 8 9 /// <summary> 10 /// 同一の情報を持つ新しい Transaction のインスタンスを生成して返します。 11 /// </summary> 12 public Transaction CreateClone() 13 { 14 return (Transaction)MemberwiseClone(); 15 } 16}

後はyield return bundleTransactionyield return bundleTransaction.CreateClone()に書き換えましょう。

2.Transactionをクラスではなく構造体にしてしまう
16バイト以下ですし、後からデータの修正が起きないのであれば構造体にしてしまうという方法もあります。
単純にclassをstructに変えるだけです。
方法としては単純ですが、構造体の適切な取り扱いが分からない場合は1番を使った方が良いです。

投稿2017/06/23 09:16

編集2017/06/23 09:30
haru666

総合スコア1591

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

haru853

2017/06/24 00:39

ありがとうございました。理解できました。 MemberwiseCloneメソッドの事も知らなかった為、勉強になりました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問