要素数を提供することによるToArrayおよびToListメソッドの最適化

ToArrayおよびToList拡張メソッドは、列挙されたシーケンス(Linqクエリなど)を配列またはリストにすばやく変換する便利な方法です。 ただし、気になる点があります。これらのメソッドはどちらも、シーケンス内の要素の数がわからない場合は非常に非効率的です(ほとんどの場合、Linqクエリで使用すると発生します)。 最初にToArrayメソッドを見てみましょう( ToListにはいくつかの違いがありますが、原理はほとんど同じです)。



基本的にToArrayはシーケンスを取り、元のシーケンスのすべての要素を含む配列を返します。 シーケンスがICollection <T>インターフェイスを実装する場合、 ToArrayCountプロパティを使用して正しいサイズの配列をメモリに配置し、シーケンスの要素をそこにコピーします。 たとえば、ユーザーのリストを配列に変換します。



List<User> users = GetUsers(); User[] array = users.ToArray();
      
      





この場合、 ToArrayは非常に効率的です。 ユーザーのリストから名前のみが抽出され、配列に変換されるようにコードを変更しましょう。

 List<User> users = GetUsers(); string[] array = users.Select(u => u.Name).ToArray();
      
      







現在、 ToArrayの引数は、 Selectメソッドによって返されるIEnumerable <User>型です。 IEnumerable <User>ICollection <User>インターフェイスを実装しないため、 ToArrayは要素の数を認識せず、適切なサイズの配列をメモリに配置できません。 彼は次のことを行います。



  1. 最初に、小さなサイズの配列がメモリに配置されます( 現在の実装では4つの要素)
  2. 要素は、いっぱいになるまで配列にコピーされます。
  3. ソースに要素がもうない場合は、手順7に進みます
  4. それ以外の場合、新しい配列がメモリに配置され、前の配列の2倍になります
  5. 前の配列の要素が新しい配列にコピーされます
  6. ステップ2に進み、すべてが繰り返されます
  7. その結果、配列が要素の数よりも大きい場合、切り捨てられます。正しいサイズの新しい配列がメモリに配置され、要素がそこにコピーされます
  8. 結果の配列はメソッドによって返されます


ソースに少数の要素しかない場合、このアルゴリズムは非常に簡単に実行できますが、非常に多くの要素の場合、多数のメモリ割り当てと要素のコピーのために非常に非効率になります。

ほとんどの場合、要素の初期数がわかっているのは面倒です! 上記の例では、シーケンス内の要素の数を変更しないSelectを使用していますが、同じ数が残っていることがわかります。 しかし、 ToArrayはこの情報を知らないため、 ToArrayはそれについて知りません。 これを自分で提供する方法はありますか...

実際、これは非常に簡単です。要素の数をパラメーターとして受け取る拡張メソッドを作成するだけです。 次のようになります。



 public static TSource[] ToArray<TSource>(this IEnumerable<TSource> source, int count) { if (source == null) throw new ArgumentNullException("source"); if (count < 0) throw new ArgumentOutOfRangeException("count"); var array = new TSource[count]; int i = 0; foreach (var item in source) { array[i++] = item; } //   RouR     ,  source.Count < count,  //     if(i != count) throw new ArgumentOutOfRangeException(); return array; }
      
      





翻訳者からの注記: PsyHaSTeは、明示的にcountを指定せずに別の方法を提案しました。

 public static TResult[] SelectToArray<T, TResult>(this ICollection<T> source, Func<T, TResult> func) { if (source == null) throw new ArgumentNullException("source"); TResult[] result = new TResult[source.Count]; int i = 0; foreach (var value in source) { result[i++] = func(value); } return result; }
      
      







これで、この方法で例を最適化できます。



 List<User> users = GetUsers(); string[] array = users.Select(u => u.Name).ToArray(users.Count);
      
      







実際に取得するよりも少ない要素を渡すと、 IndexOutOfRangeExceptionが発生することに注意してください。 メソッドに正しい番号を指定する必要があります。

では、今どのようなパフォーマンスが得られますか? 私のテストでは、ToArrayのこのバージョンは、大きなシーケンスの標準バージョンの約2倍の速さです(テストでは1,000,000アイテムを実行しました)。 いいね!



リストの初期容量を設定できるList <T>クラスのコンストラクターを使用して、同様の方法でToListメソッド改善できることに注意してください。



 public static List<TSource> ToList<TSource>(this IEnumerable<TSource> source, int count) { if (source == null) throw new ArgumentNullException("source"); if (count < 0) throw new ArgumentOutOfRangeException("count"); var list = new List<TSource>(count); foreach (var item in source) { list.Add(item); } return list; }
      
      







この場合、おそらくリストを切り捨てる必要がないため、パフォーマンスの向上はToArrayの場合ほど大きくありません(50%ではなく約25%)。

ディクショナリ<TKey、TValue>クラスは、ディクショナリの初期容量を持つコンストラクタも提供するため、明らかに、同じ最適化をToDictionaryメソッドに適用できます。

ToArrayおよびToListメソッドの改良バージョンは、 Linq.Extrasライブラリで利用できます。 このライブラリは、シーケンスおよびコレクションを操作するための他の多くの便利なメソッドも提供します。



All Articles