次の方法で共有


Generic Collections III

So far I've gone over the basics of generic collections and moved on to looking at List<T> and Predicate<T>.  As mentioned yesterday, this post will deal with subclassing and the other System delegate types.

The "Other" System.Delegates

We've already looked at System.Predicate<T>.  Now we can turn our attention to the rest of the System deleagte types:

  • Action<T>
  • Comparison<T>
  • Converter<TInput, TOutput>

As a reminder, here's the signature of Predicate<T>:

    public delegate bool Predicate<T>(T obj); 

Now here's Action<T>:

    public delegate void Action<T>(T obj); 

The only difference from Predicate<T> is that Action<T> returns a void (nothing).  One way to think of this is "just do this".  The only place it's used (currently) is with List<T>.ForEach().  (Well, one other place, but that comes later.)  For each?  Yes, it's pretty much the same.  Let's say you have a List<int> that you want to sum up:

 public void SumUpMeaningDemo()
{
   List<int> magicNumbers = new List<int>(new int[] { 7, 13, 1000, 42, 9 });
   int total = 0;

   // The traditional way:
   // foreach (int number in magicNumbers)
   // {
   //    total += number;
   // }

   // A new way:
   magicNumbers.ForEach(delegate(int number){ total += number; });
}

The syntax is a little different, but not much visible difference.  So what to use then?  What's the not-so-visible difference, if any?  Well this surprised me, but the List<T>.ForEach() is twice as fast (in this example, done a million times) as using foreach on the List<T>.  Why is that?  I'm not terribly great at IL (Intermediate Language) so I can't give you the final word on this, but when you do a foreach in code you actually end up with the equivalent of:

 // For reference, this is the code in Enumerator<int>.MoveNext():
public bool MoveNext()
{
   if (this.version != this.list._version)
   {
      // throw;
   }
   if (this.index < this.list._size)
   {
      this.current = this.list._items[this.index];)
      this.index++;
      return true;
   }
   this.index = this.list._size + 1;
   this.current = default(int);
   return false;
}

public void SumUpMeaningDemo()
{
   List<int> magicNumbers = new List<int>(new int[] { 7, 13, 1000, 42, 9 });
   int total = 0;

   // What you write:
   // foreach (int number in magicNumbers)
   // {
   //    total += number;
   // }

   // What you get:
   List<int>.Enumerator enumerator = (List<int>.Enumerator)magicNumbers.GetEnumerator() ;
   try
   {
      while(enumerator.MoveNext() )
      {
         int temp = enumerator.Current;
         total += temp;
      }
      finally
      {
         enumerator.Dispose() ;
      }
   }
}

There are four different calls in the above code that are marked in bold.  Here's the equivalent of what you end up with if you use the List<T>.ForEach():

 private void CompilerGeneratedAnonymousDelegateClass()
{
   public int total;

   private void CompilerGeneratedDelegateMethod(int number)
   {
      this.total += number;
   }
}

// For reference, this is the code in List<int>.ForEach():
public void ForEach(Action action)
{
   if (action == null)
   {
      // throw;
   }
   for (int i = 0; i < this._size; ++i)
   {
      action(this._items[i];)
   }
}

public void SumUpMeaningDemo()
{
   List<int> magicNumbers = new List<int>(new int[] { 7, 13, 1000, 42, 9 });
   int total = 0;

   // What you write:
   // int total = 0;
   // magicNumbers.ForEach(delegate(int number){ total += number; });   //    total += number;

   // What you get (plus the compiler generated class above):
   CompilerGeneratedAnonymousDelegateClass tempClass = new CompilerGeneratedAnonymousDelegateClass();
   tempClass.total = 0;
   magicNumbers.ForEach((Action<int> new Action<int>(tempClass.CompilerGeneratedDelegateMethod));
}

There are two calls (in bold, again) and two classes being constructed (the action delegate and the compiler generated class).  The easiest way to differentiate the two methods is to look at the size of the resulting code.  If you tear into the IL you'll see that in the loop for your .ForEach you have 53 IL bytes and one call.  The standard foreach174 IL bytes and two calls.  We could go further into it, but that should make it more than clear.

Ugh, sorry about all that.  That took way more 'splaining than I expected at first.  But I couldn't leave an open-ended question now, could I? ;)

Ok, where was I?  Oh yes, the last two types.  Here is the signature of Comparison<T>:

    public delegate int Comparison<T>(T x, T y); 

Its used (no suprises here) on the Sort<T>() call.  The one thing you need to know that isn't obvious is that your method must return the following:

  • x < y (return less than zero)
  • x == y (return zero)
  • x > y (return greater than zero)

And now the last delegate type.  Here is the signature of Converter<T, U>:

    public delegate TOutput Converter<TInput, TOutput>(TInput i); 

Its used with the ConvertAll<TOutput>() call.  In the case of List<T>, this method returns a new List<TOutput>.  There is nothing to say that TInput and TOutput can't be the same type.  This could be useful for doing something like uppercasing a List<string>. (Why not use ForEach?  Just like foreach you're working on a copy.)  Here's the line of code to upper case a List<string>:

    myStringList = myStringList.ConvertAll<string>(delegate(string item) { return item.ToUpper(); };

That gets us through the System delegate types.  That leads us to subclassing.  And me running out of time (sorry, disassembling the foreach example took longer than I expected).  So, until next time...

Comments

  • Anonymous
    July 24, 2005
    Blog link of the week 29
  • Anonymous
    May 24, 2007
    In C# 2.0, generics and generic collections are notable and very useful features to pay attention to: