May 07, 2009

Using List.ForEach() and lambda expressions

In my previous post, I refactored a small bit of the BuildCatalog() method, removing the need for the Catalog to know how to filter out the published posts and pages, and the approved comments.

What we were left with was a pretty simple method, but I think it could be made even more readable with a few tweaks. Here’s the code as it is after my changes:
private static void BuildCatalog()

{

OnIndexBuilding();

 

bool commentsEnabled = BlogSettings.Instance.EnableCommentSearch;

 

lock (_SyncRoot)

{

_Catalog.Clear();

 

foreach (Post post in Post.PublishedPosts)

{

AddItem(post);

if (commentsEnabled)

{

foreach (Comment comment in post.ApprovedComments)

{

AddItem(comment);

}

}

}

 

foreach (Page page in Page.PublishedPages)

{

AddItem(page);

}

}

 

OnIndexBuild();

}

There are a three foreach() blocks working on List<> objects, and within each block there’s a single line of code. That kind of code is just begging to be made a bit more elegant.

The .NET Framework 2.0 added the List.ForEach() method, which allowed for something like:

post.ApprovedComments.ForEach(delegate(Comment comment)

{

AddItem(comment);

}

Using a delegate, this syntax isn’t substantially better nor easier to read. However, toss in a lambda expression instead (introduced by .NET Framework 3.0), and ForEach() suddenly becomes juicily delicious:
post.ApprovedComments.ForEach(comment => AddItem(comment));

Yummy. All that blocky wordiness condensed to a simple, readable, one-line statement. Once we sprinkle those into our BuildCatalog() method, we may end up with something like:

private static void BuildCatalog()

{

OnIndexBuilding();

 

lock (_SyncRoot)

{

_Catalog.Clear();

 

if (BlogSettings.Instance.EnableCommentSearch)

{

Post.PublishedPosts.ForEach(post => AddPostAndComments(post));

}

else

{

Post.PublishedPosts.ForEach(post => AddItem(post));

}

Page.PublishedPages.ForEach(page => AddItem(page));

}

 

OnIndexBuild();

}

Much cleaner code, I think. My final version of the Catalog class, after extracting it from the Search.cs file and adding a few more refactorings (like an extension method or two), looks like this:

public static class Catalog

{

private static List<Entry> entries = new List<Entry>();

private readonly static object syncRoot = new object();

 

internal static List<Entry> Entries

{

get { return entries; }

}

 

internal static void AddItem(IPublishable item)

{

item.AddToCatalog();

}

 

internal static void Build()

{

OnIndexBuilding();

lock (syncRoot)

{

entries.Clear();

Page.PublishedPages.ForEach(page => page.AddToCatalog());

if (BlogSettings.Instance.EnableCommentSearch)

{

Post.PublishedPosts.ForEach(post => post.AddSelfAndCommentsToCatalog());

}

else

{

Post.PublishedPosts.ForEach(post => post.AddToCatalog());

}

}        

OnIndexBuilt();

}

 

private static void AddSelfAndCommentsToCatalog(this Post post)

{

post.AddToCatalog();

post.ApprovedComments.ForEach(c => c.AddToCatalog());

}

 

private static void AddToCatalog(this IPublishable item)

{

var entry = new Entry

{

Item = item,

Title = TextAgent.CleanContent(item.Title, false),

Content = HttpUtility.HtmlDecode(TextAgent.CleanContent(item.Content, true))

};

 

if (item is Comment)

{

entry.Content += HttpUtility.HtmlDecode(TextAgent.CleanContent(item.Author, false));

}

entries.Add(entry);

}

 

public static event EventHandler<EventArgs> IndexBuilding;

private static void OnIndexBuilding()

{

if (IndexBuilding != null)

{

IndexBuilding(null, EventArgs.Empty);

}

}

 

public static event EventHandler<EventArgs> IndexBuilt;

private static void OnIndexBuilt()

{

if (IndexBuilt != null)

{

IndexBuilt(null, EventArgs.Empty);

}

}

}