Chapter 13. Elegant code in the new era

This chapter covers

  • Reasons for language evolution

  • Changes of emphasis for C# 3

  • Readability: “what” over “how”

  • Effects of parallel computing

You’ve now seen all the features that C# 3 has to offer, and you’ve had a taste of some of the flavors of LINQ available now and in the near future. Hopefully I’ve given you a feeling for the directions C#3 might guide you in when coding, and this chapter puts those directions into the context of software development in general.

There’s a certain amount of speculation in this chapter. Take everything with a grain of salt—I don’t have a crystal ball, after all, and technology is notoriously difficult to predict. However, the themes are fairly common ones and I am confident that they’ll broadly hit the mark, even if the details are completely off.

Life is all about learning from our mistakes—and occasionally failing to do so. The software industry has been both innovative and shockingly backward at times. There are elegant new technologies such as C# 3 and LINQ, frameworks that do more than we might have dreamed about ten years ago, and tools that hold our hands throughout the development processes... and yet we know that a large proportion of software projects fail. Often this is due to management failures or even customer failures, but sometimes developers need to take at least some of the blame.

Many, many books have been written about why this is the case, and I won’t pretend to be an expert, but I believe that ultimately it comes down to human nature. The vast majority of us are sloppy—and I certainly include myself in that category. Even when we know that best practices such as unit testing and layered designs will help us in the long run, we sometimes go for quick fixes that eventually come back to haunt us.

There’s only so much a language or a platform can do to counter this. The only way to appeal to laziness is to make the right thing to do also the easiest one. Some areas make that difficult—it will always seem easier in some ways to not write unit tests than to write them. Quite often breaking our design layers (“just for this one little thing, honest”) really is easier than doing the job properly—temporarily.

On the bright side, C# 3 and LINQ allow many ideas and goals to be expressed much more easily than before, improving readability while simultaneously speeding up development. If you have the opportunity to use C#3 for pleasure before putting it in a business context, you may well find yourself being frustrated at the shackles imposed when you have to go back to C# 2 (or, heaven forbid, C# 1). There are so many shortcuts that you may often find yourself surprised at just how easy it is to achieve what might previously have been a time-consuming goal.

Some of the improvements are simply obvious: automatic properties replace several lines of code with a single one, at no cost. There’s no need to change the way you think or how you approach design and development—it’s just a common scenario that is now more streamlined.

What I find more interesting are the features that do ask us to take a step back. They suggest to us that while we haven’t been doing things “wrong,” there may be a better way of looking at the world. In a few years’ time, we may look back at old code and be amazed at the way we used to develop. Whenever a language evolves, it’s worth asking what the changes mean in this larger sense. I’ll try to answer that question now, for C#3.

The changing nature of language preferences

The changes in C# 3 haven’t just added more features. They’ve altered the idiom of the language, the natural way of expressing certain ideas and implementing behavior. These shifts in emphasis aren’t limited to C#, however—they’re part of what’s happening within our industry as a whole.

A more functional emphasis

It would be hard to deny that C# has become more functional in the move from version 2 to version 3. Delegates have been part of C# 1 since the first version, but they have become increasingly convenient to specify and increasingly widely used in the framework libraries.

The most extreme example of this is LINQ, of course, which has delegates at its very core. While LINQ queries can be written quite readably without using query expressions, if you take away lambda expressions and extension methods they become frankly hideous. Even a simple query expression requires extra methods to be written so that they can be used as delegate actions. The creation of those delegates is ugly, and the way that the calls are chained together is unintuitive. Consider this fairly simple query expression:

from user in SampleData.AllUsers
where user.UserType == UserType.Developer
orderby user.Name
select user.Name.ToUpper();

That is translated into the equally reasonable set of extension method calls:

SampleData.AllUsers
          .Where(user => user.UserType == UserType.Developer)
          .OrderBy(user => user.Name)
          .Select(user => user.Name.ToUpper());

It’s not quite as pretty, but it’s still clear. To express that in a single expression without any extra local variables and without using any C# 2 or 3 features beyond generics requires something along these lines:

Enumerable.Select
   (Enumerable.OrderBy
      (Enumerable.Where(SampleData.AllUsers,
                        new Func<User,bool>(AcceptDevelopers)),
      new Func<User, string>(OrderByName)),
    new Func<User, string>(ProjectToUpperName));

Oh, and the AcceptDevelopers, OrderByName, and ProjectToUpperName methods all need to be defined, of course. It’s an abomination. LINQ is just not designed to be useful without a concise way of specifying delegates. Where previously functional languages have been relatively obscure in the business world, some of their benefits are now being reaped in C#.

At the same time as mainstream languages are becoming more functional, functional languages are becoming more mainstream. The Microsoft Research “F#” language[1] is in the ML family, but executing on the CLR: it’s gained enough interest to now have a dedicated team within the nonresearch side of Microsoft bringing it into production so that it can be a truly integrated language in the .NET family.

The differences aren’t just about being more functional, though. Is C# becoming a dynamic language?

Static, dynamic, implicit, explicit, or a mixture?

As I’ve emphasized a number of times in this book, C#3 is still a statically typed language. It has no truly dynamic aspects to it. However, many of the features in C#2 and 3 are those associated with dynamic languages. In particular, the implicitly typed local variables and arrays, extra type inference capabilities for generic methods, extension methods, and better initialization structures are all things that in some ways look like they belong in dynamic languages.

While C# itself is currently statically typed, the Dynamic Language Runtime (DLR) will bring dynamic languages to .NET. Integration between static languages and dynamic ones such as IronRuby and IronPython should therefore be relatively straightforward—this will allow projects to pick which areas they want to write dynamically, and which are better kept statically typed.

Should C# become dynamic in the future? Given recent blog posts from the C# team, it seems likely that C# 4 will allow dynamic lookup in clearly marked sections of code. Calling code dynamically isn’t the same as responding to calls dynamically, however—and it’s possible that C# will remain statically typed at that level. That doesn’t mean there can’t be a language that is like C# in many ways but dynamic, in the same way that Groovy is like Java in many ways but with some extra features and dynamic execution. It should be noted that Visual Basic already allows for optionally dynamic lookups, just by turning Option Strict on and off. In the meantime, we should be grateful for the influence of dynamic languages in making C#3 a lot more expressive, allowing us to state our intentions without as much fluff surrounding the really useful bits of code.

The changes to C# don’t just affect how our source code looks in plain text terms, however. They should also make us reconsider the structure of our programs, allowing designs to make much greater use of delegates without fear of forcing thousands of one-line methods on users.

Delegation as the new inheritance

There are many situations where inheritance is currently used to alter the behavior of a component in just one or two ways—and they’re often ways that aren’t so much inherent in the component itself as in how it interacts with the world around it.

Take a data grid, for example. A grid may use inheritance (possibly of a type related to a specific row or column) to determine how data should be formatted. In many ways, this is absolutely right—you can build up a flexible design that allows for all kinds of different values to be displayed, possibly including images, buttons, embedded tables, and the like. The vast majority of read-only data is likely to consist of some plain text, however. Now, we could have a TextDataColumn type with an abstract FormatData method, and derive from that in order to format dates, plain strings, numbers, and all kinds of other data in whatever way we want.

Alternatively, we could allow the user to specify the formatting by way of a delegate, which simply converts the appropriate data type to a string. With C#3’s lambda expressions, this makes it easy to provide a custom display of the data. Of course, you may well want to provide easy ways of handling common cases—but delegates are immutable in .NET, so simple “constant” delegates for frequently used types can fill this need neatly.

This works well when a single, isolated aspect of the component needs to be specialized. It’s certainly not a complete replacement of inheritance, nor would I want it to be (the title of this section notwithstanding)—but it allows a more direct approach to be used in many situations. Using interfaces with a small set of methods has often been another way of providing custom behavior, and delegates can be regarded as an extreme case of this approach.

Of course, this is similar to the point made earlier about a more functional bias, but it’s applied to the specific area of inheritance and interface implementation. It’s not entirely new to C#3, either: List<T> made a start in .NET 2.0 even when only C#2 was available, with methods such as Sort and FindAll. Sort allows both an interface-based comparison (with IComparer) and a delegate-based comparison (with Comparison), whereas FindAll is purely delegate based. Anonymous methods made these calls relatively simple and lambda expressions add even more readability.

In short, when a type or method needs a single aspect of specialized behavior, it’s worth at least considering the ability to specify that behavior in terms of a delegate instead of via inheritance or an interface.

All of this contributes to our next big goal: readable code.

Readability of results over implementation

The word readability is bandied around quite casually as if it can only mean one thing and can somehow be measured objectively. In real life, different developers find different things readable, and in different ways. There are two kinds of readability I’d like to separate—while acknowledging that many more categorizations are possible.

First, there is the ease with which a reader can understand exactly what your code is doing at every step. For instance, making every conversion explicit even if there’s an implicit one available makes it clear that a conversion is indeed taking place. This sort of detail can be useful if you’re maintaining code and have already isolated the problem to a few lines of code. However, it tends to be longwinded, making it harder to browse large sections of source. I think of this as “readability of implementation.”

When it comes to getting the broad sweep of code, what is required is “readability of results”—I want to know what the code does, but I don’t care how it does it right now. Much of this has traditionally been down to refactoring, careful naming, and other best practices. For example, a method that needs to perform several steps can often be refactored into a method that simply calls other (reasonably short) methods to do the actual work. Declarative languages tend to emphasize readability of results.

C#3 and LINQ combine to improve readability of results quite significantly—at the cost of readability of implementation. Almost all the cleverness shown by the C# 3 compiler adds to this: extension methods make the intention of the code clearer, but at the cost of the visibility of the extra static class involved, for example.

This isn’t just a language issue, though; it’s also part of the framework support. Consider how you might have implemented our earlier user query in .NET 1.1. The essential ingredients are filtering, sorting, and projecting:

ArrayList filteredUsers = new ArrayList();
foreach (User user in SampleData.AllUsers)
{
   if (user.UserType==UserType.Developer)
   {
      filteredUsers.Add(user);
   }
}

filteredUsers.Sort(new UserNameComparer());

ArrayList upperCasedNames = new ArrayList();
foreach (User user in filteredUsers)
{
   upperCasedNames.Add(user.Name.ToUpper());
}

Each step is clear, but it’s relatively hard to understand exactly what’s going on! The version we saw earlier with the explicit calls to Enumerable was shorter, but the evaluation order still made it difficult to read. C#3 hides exactly how and where the filtering, sorting, and projection is taking place—even after translating the query expression into method calls—but the overall purpose of the code is much more obvious.

Usually this type of readability is a good thing, but it does mean you need to keep your wits about you. For instance, capturing local variables makes it a lot easier to write query expressions—but you need to understand that if you change the values of those local variables after creating the query expression, those changes will apply when you execute the query expression.

One of the aims of this book has been to make you sufficiently comfortable with the mechanics of C# 3 that you can make use of the magic without finding it hard to understand what’s going on when you need to dig into it—as well as warning you of some of the potential hazards you might run into.

So far these have all been somewhat inward-looking aspects of development—changes that could have happened at any time. The next point is very much due to what a biologist might call an “external stimulus.”

Life in a parallel universe

In chapter 12 we looked briefly at Parallel LINQ, and I mentioned that it is part of a wider project called Parallel Extensions. This is Microsoft’s next attempt to make concurrency easier. I don’t expect it to be the final word on such a daunting topic, but it’s exciting nonetheless.

As I write this, most computers still have just a few cores. Some servers have eight or possibly even 16 (within the x86/x64 space—other architectures already support far more than this). Given how everything in the industry is progressing, it may not be long before that looks like small fry, with genuine massively parallel chips becoming part of everyday life. Concurrency is at the tipping point between “nice to have” and “must have” as a developer skill.

We’ve already seen how the functional aspects of C#3 and LINQ enable some concurrency scenarios—parallelism is often a matter of breaking down a big task into lots of smaller ones that can run at the same time, after all, and delegates are nice building blocks for that. The support for delegates in the form of lambda expressions—and even expression trees to express logic in a more data-like manner—will certainly help parallelization efforts in the future.

There will be more advances to come. Some improvements may come through new frameworks such as Parallel Extensions, while others may come through future language features. Some of the frameworks may use existing language features in novel ways, just as the Concurrency and Coordination Runtime uses iterator blocks as we saw in chapter 6.

One area we may well see becoming more prominent is provability. Concurrency is a murky area full of hidden pitfalls, and it’s also very hard to test properly. Testing every possibility is effectively impossible—but in some cases source code can be analyzed for concurrency correctness automatically. Making this applicable to business software at a level that is usable by “normal” developers such as ourselves is likely to be challenging, but we may see progress as it becomes increasingly important to use the large number of cores becoming available to us.

There are clearly dozens of areas I could have picked that could become crucial in the next decade—mobile computing, service-oriented architectures (SOA), human computer interfaces, rich Internet applications, system interoperability, and so forth. These are all likely to be transformed significantly—but parallel computing is likely to be at the heart of many of them. If you don’t know much about threading, I strongly advise you to start learning right now.

Farewell

So, that’s C#—for now. I doubt that it will stay at version 3 forever, although I would personally like Microsoft to give us at least a few years of exploring and becoming comfortable with C#3 before moving the world on again. I don’t know about you, but I could do with a bit of time to use what we’ve got instead of learning the next version. If we need a bit more variety and spice, there are always other languages to be studied...

In the meantime, there will certainly be new libraries and architectures to come to grips with. Developers can never afford to stand still—but hopefully this book has given you a rock-solid foundation in C#, enabling you to learn new technologies without worrying about what the language is doing.

There’s more to life than learning about the new tools available, and while you may have bought this book purely out of intellectual curiosity, it’s more likely that you just want to get the most out of C#3. After all, there’s relatively little point in acquiring a skill if you’re not going to use it. C#3 is a wonderful language, and .NET 3.5 is a great platform—but on their own they mean very little. They need to be used to provide value.

I’ve tried to give you a thorough understanding of C#3, but that doesn’t mean that you’ve seen all that it can do, any more than playing each note on a piano in turn means you’ve heard every possible tune. I’ve put the features in context and given some examples of where you might find them helpful. I can’t tell you exactly what ground-breaking use you might find for C#3—but I wish you the very best of luck.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.36.38