C# Corner

Reduce Lines of Code With C# 3.0

You already know what those new features in C# 3.0 are doing because you do these same things in C# 2.0. Learn how C# 3.0 can reduce coding lines and improve readability by walking through a reverse migration.

Technology Toolbox: C#

I could refer to many of the C# 3.0 features as syntactic sugar, but that term has too much of a negative connotation.

It shouldn't be that way. Clearer ways of expressing the constructs we already use—syntactic sugar—are among our favorite features in any new language. That's also true for C# 3.0. There is nothing new you can do with the new C# 3.0 syntax that you couldn't already do with C# 2.0. But, as this article will show you, that doesn't diminish the importance of these enhancements. By showing you how these new features are natural extensions of the language you already use, you'll gain a better understanding of how they work, and you'll be able to adopt C# 3.0 much more quickly.

You can see this at work by running through a somewhat unusual exercise: a reverse migration. That is, you start with a simple application that makes use of all the C# 3.0 features to display some statistics about current running processes, then you change the application so it contains the same functionality, but uses version 2.0 of the framework. Along the way, I'll explain how the 3.0 features are implemented in terms of techniques you already know. At the end, you'll see how much more concise the 3.0 version is.

The C# 3.0 version of the console program displays a few statistics about running processes. It displays the name, peak working set, and base priority (Listing 1). It filters output by displaying this information only for processes with more than one thread, showing the top 25 processes, sorted first by priority and second by peak working set.

Running through this exercise will highlight several important advantages conveyed by C# 3.0. For example, the C# 3.0 version of the application is roughly one third the size of what you get in C# 2.0. That saves you work when creating your applications, and it also saves time for anyone doing maintenance development because there's only one third the code to understand before making changes.

The C# 3.0 version is also more declarative, compared to the imperative version you would have created in C# 2.0. That also improves readability and understanding. The 3.0 version is much simpler to read and understand. The underlying steps and processes are much more at the forefront in the 2.0 version.

Replace the New Keywords
You begin the reverse migration by replacing the C# 3.0 keywords (from, select, where) with the equivalent methods, or extension methods (Listing 2). The keyword defines an IEnumerable<T> sequence to use as the source for the algorithm. In this instance, the from keyword defines the starting sequence as the return from System.Diagnostices.GetProcesses(). Next, you replace the where clause with a call to System.Linq.Sequence.Where<T>(). You pass it a predicate that evaluates the condition. Now alter the listing so you call System.Linq.Sequence.OrderByDescending. You need to call this twice to order the result by the base priority, and then by the size of the working set. Note that the order of conditions on OrderBy is reversed from the C# 3.0 version. To get the order you want, you must call OrderBy on the secondary condition, followed by the primary condition. That's counterintuitive for anyone reading the code, but that's how you get the right answer in C# 2.0.

The penultimate method call is to System.Linq.Sequence.Select, which is where you create the output for the query. Like the C# 3.0 version, the output in the C# 2.0 version contains a sequence of elements containing some interesting properties of the Process class. Finally, call Take() to limit the size of the output collection to the first twenty five elements.

The next step in the reverse migration is to remove the anonymous type used for the return value, as well as all its supporting syntax. This is a several step process because anonymous types require a fair amount of supporting language features. You should remove them one at a time, starting with the anonymous type. Here's the cleanest (although only valid in C# 3.0) definition for the anonymous class:

public class ProcessStatistics
{
   public string Name { get; set; }
   public int Priority { get; set; }
   public long MaxMemory { get; set; }
}

Anonymous types are immutable, which means that their observable state cannot change. The ProcessStatistics class allows other code to change the values of its properties. Fixing that requires more code, but I wanted to show you this important C# 3.0 feature: implicit properties. The Name, Priority, and MaxMemory properties are implemented by the compiler. The compiler creates a backing field to use as the storage for the publicly accessible property. The compiler creates code equivalent to this hand-coded version of ProcessStatistics:

public class ProcessStatistics
{
   private string name;
   public string Name
   {
      get { return name; }
      set { name = value; }
   }

   private int priority;
   public int Priority
   {
      get { return priority; }
      set { priority = value; }
   }

   private long maxMemory;
   public long MaxMemory
   {
      get { return maxMemory; }
      set { maxMemory = value; }
   }
}

Converting to C# 2.0 Carries Costs
In order for the hand-coded class to have the closest functionality to the anonymous class, you need to remove the property setters and replace them with a single constructor that sets all the fields (see the ProcessStatistics class in Listing 3). But, this change isn't without cost. The C# 2.0 compatible version of ProcessStatistics doesn't allow you to use the object initializer syntax, so you need to replace that with an explicit constructor call in the Select method parameter list. After creating your own hand-coded version of the result, you can replace the var keyword with IEnumerable<ProcessStatistics> and ProcessStatistics, respectively. You see, var is nothing more than a substitute for the type on the right hand side of the assignment. The reason for using var in the C# 3.0 sample is that when you use anonymous types, you can't rely on the name of the anonymous type. The compiler generates the name, and you have no control over what it will be called. Listing 3 shows the result of these changes, where var has been replaced by a hand-coded type.

Extension methods are nothing more than a cleaner syntax to call static methods. The compiler behaves as though the extension method is a member method of the type of the first argument to the method call. You can change the calling structure for the extension methods and replace it with the static call syntax:

IEnumerable<ProcessStatistics> processes =
Enumerable.Take(
Enumerable.Select(
Enumerable.OrderByDescending(
Enumerable.OrderByDescending(
Enumerable.Where(
System.Diagnostics.Process.GetProcesses(),
process => process.Threads.Count > 1),
process => process.PeakWorkingSet64),
process => process.BasePriority),
process => new ProcessStatistics(
process.ProcessName,
process.BasePriority,
process.PeakWorkingSet64
)),
25);

foreach (ProcessStatistics p in processes)
   Console.WriteLine(
   "Name: {0,25}, MaxMemory: {1,15}, Priority: {2,5}",
   p.Name, p.MaxMemory, p.Priority);

You probably find this code somewhat impenetrable; I do! You can break up that single statement by creating a series of statements, each storing the intermediate results, which helps you see each step required to construct the query more clearly (Listing 4).

I'll admit to a bit of cheating here: This version (and all subsequent versions) uses the System.Linq.Enumerable class members. But now, you're explicitly calling those members, rather than relying on the extension method syntax to find the right method, as you did in the C# 3.0 version of the code. For one example, you define where with this signature:

public static IEnumerable<TSource> 
   Where<TSource>(this IEnumerable<TSource> source, 
   Func<TSource, bool> predicate);

Note that the first parameter is prefixed with the "this" keyword, which indicates an extension method. It is this that gives you the ability to call Where as though it were a member of any type that implements IEnumerable<T>. Of course, importing extension methods into the current scope does introduce the possibility of name collisions. In most cases, this won't be an issue because extension methods are the last choice when searching for suitable methods. If you implement your own version of Where on your class, it will be chosen over the standard extension methods.

C# 3.0 Favors Declarative Programming
At this point, you should also see that you've made quite a style change: This version of the algorithm is much more imperative than previous versions. Each step is specified in rather gory detail. It's much more about how you create the answer than what the right answer is. That sums up precisely one of the most significant benefits of the C# 3.0 syntax: You can write programs in a much more declarative style. They are more concise and much more readable because you write significantly fewer lines of code to express the same result.

You're almost done. The only remaining visible C# 3.0 feature to remove is lambda expressions. Lambdas are nothing more than bits of code that are passed to other methods. You can write them just as easily using anonymous delegates. Where uses a delegate that tests for a condition. Its input is the query type, and its output is bool. OrderByDescending uses a delegate that takes the query type and returns the field or property that should be used as the sort field. Select uses a delegate that constructs the return type from the query type. Converting to C# 2.0 requires utilizing the same algorithm after removing the lambda expressions and replacing them with the equivalent anonymous delegate (see Listing 5). At this point, you've removed all the 3.0 language features and replaced them with the equivalent functionality using the C# 2.0 language features.

The C# 3.0 language features let you concentrate on the problems you're trying to solve, rather than replicating work that the compiler can do for you. Look again at the definition for the Process-Statistics class. There's no real intelligence there. It's simply a placeholder for some elements that you want to extract from the Process type. Using C# 3.0, you don't need to perform as many rote tasks.

I recommend taking a close look at the differences between Listing 1 and the subsequent versions. For example, Listing 1 is much clearer about what it does and how it does it, in far fewer lines of code. The fact is, if you aren't taking advantage of C# 3.0's syntactic sugar you'll just spend more time typing C# 2.0 compatible code.

About the Author

Bill Wagner, author of Effective C#, has been a commercial software developer for the past 20 years. He is a Microsoft Regional Director and a Visual C# MVP. His interests include the C# language, the .NET Framework and software design. Reach Bill at [email protected].

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube