Chapter 1. Survival of the sharpest

This chapter covers

  • How C#’s rapid evolution has made developers more productive
  • Selecting minor versions of C# to use the latest features
  • Being able to run C# in more environments
  • Benefitting from an open and engaged community
  • The book’s focus on old and new C# versions

Choosing the most interesting aspects of C# to introduce here was difficult. Some are fascinating but are rarely used. Others are incredibly important but are now commonplace to C# developers. Features such as async/await are great in many ways but are hard to describe briefly. Without further ado, let’s look at how far C# has come over time.

1.1. An evolving language

In previous editions of this book, I provided a single example that showed the evolution of the language over the versions covered by that edition. That’s no longer feasible in a way that would be interesting to read. Although a large application may use almost all of the new features, any single piece of code that’s suitable for the printed page would use only a subset of them.

Instead, in this section I choose what I consider to be the most important themes of C# evolution and give brief examples of improvements. This is far from an exhaustive list of features. It’s also not intended to teach you the features; instead, it’s a reminder of how far features you already know about have improved the language and a tease for features you may not have seen yet.

If you think some of these features imitate other languages you’re familiar with, you’re almost certainly right. The C# team does not hesitate to take great ideas from other languages and reshape them to feel at home within C#. This is a great thing! F# is particularly worth mentioning as a source of inspiration for many C# features.

Note

It’s possible that F#’s greatest impact isn’t what it enables for F# developers but its influence on C#. This isn’t to underplay the value of F# as a language in its own right or to suggest that it shouldn’t be used directly. But currently, the C# community is significantly larger than the F# community, and the C# community owes a debt of gratitude to F# for inspiring the C# team.

Let’s start with one of the most important aspects of C#: its type system.

1.1.1. A helpful type system at large and small scales

C# has been a statically typed language from the start: your code specifies the types of variables, parameters, values returned from methods, and so on. The more precisely you can specify the shape of the data your code accepts and returns, the more the compiler can help you avoid mistakes.

That’s particularly true as the application you’re building grows. If you can see all the code for your whole program on one screen (or at least hold it all in your head at one time), a statically typed language doesn’t have much benefit. As the scale increases, it becomes increasingly important that your code concisely and effectively communicates what it does. You can do that through documentation, but static typing lets you communicate in a machine-readable way.

As C# has evolved, its type system has allowed more fine-grained descriptions. The most obvious example of this is generics. In C# 1, you might have had code like this:

public class Bookshelf
{
    public IEnumerable Books { get { ... } }
}

What type is each item in the Books sequence? The type system doesn’t tell you. With generics in C# 2, you can communicate more effectively:

public class Bookshelf
{
    public IEnumerable<Book> Books { get { ... } }
}

C# 2 also brought nullable value types, thereby allowing the absence of information to be expressed effectively without resorting to magic values such as –1 for a collection index or DateTime.MinValue for a date.

C# 7 gave us the ability to tell the compiler that a user-defined struct should be immutable using readonly struct declarations. The primary goal for this feature may have been to improve the efficiency of the code generated by the compiler, but it has additional benefits for communicating intent.

The plans for C# 8 include nullable reference types, which will allow even more communication. Up to this point, nothing in the language lets you express whether a reference (either as a return value, a parameter, or just a local variable) might be null. This leads to error-prone code if you’re not careful and boilerplate validation code if you are careful, neither of which is ideal. C# 8 will expect that anything not explicitly nullable is intended not to be nullable. For example, consider a method declaration like this:

string Method(string x, string? y)

The parameter types indicate that the argument corresponding to x shouldn’t be null but that the argument corresponding to y may be null. The return type indicates that the method won’t return null.

Other changes to the type system in C# are aimed at a smaller scale and focus on how one method might be implemented rather than how different components in a large system relate to each other. C# 3 introduced anonymous types and implicitly typed local variables (var). These help address the downside of some statically typed languages: verbosity. If you need a particular data shape within a single method but nowhere else, creating a whole extra type just for the sake of that method is overkill. Anonymous types allow that data shape to be expressed concisely without losing the benefits of static typing:

var book = new { Title = "Lost in the Snow", Author = "Holly Webb" };
string title = book.Title;                                             1
string author = book.Author;                                           1

  • 1 Name and type are still checked by the compiler

Anonymous types are primarily used within LINQ queries, but the principle of creating a type just for a single method doesn’t depend on LINQ.

Similarly, it seems redundant to explicitly specify the type of a variable that is initialized in the same statement by calling the constructor of that type. I know which of the following declarations I find cleaner:

Dictionary<string, string> map1 = new Dictionary<string, string>();   1

var map2 = new Dictionary<string, string>();                          2

  • 1 Explicit typing
  • 2 Implicit typing

Although implicit typing is necessary when working with anonymous types, I’ve found it increasingly useful when working with regular types, too. It’s important to distinguish between implicit typing and dynamic typing. The preceding map2 variable is still statically typed, but you didn’t have to write the type explicitly.

Anonymous types help only within a single block of code; for example, you can’t use them as method parameters or return types. C# 7 introduced tuples: value types that effectively act to collect variables together. The framework support for these tuples is relatively simple, but additional language support allows the elements of tuples to be named. For example, instead of the preceding anonymous type, you could use the following:

var book = (title: "Lost in the Snow", author: "Holly Webb");
Console.WriteLine(book.title);

Tuples can replace anonymous types in some cases but certainly not all. One of their benefits is that they can be used as method parameters and return types. At the moment, I advise that these be kept within the internal API of a program rather than exposed publicly, because tuples represent a simple composition of values rather than encapsulating them. That’s why I still regard them as contributing to simpler code at the implementation level rather than improving overall program design.

I should mention a feature that might come in C# 8: record types. I think of these as named anonymous types to some extent, at least in their simplest form. They’d provide the benefits of anonymous types in terms of removing boilerplate code but then allow those types to gain extra behavior just as regular classes do. Watch this space!

1.1.2. Ever more concise code

One of the recurring themes within new features of C# has been the ability to let you express your ideas in ways that are increasingly concise. The type system is part of this, as you’ve seen with anonymous types, but many other features also contribute to this. There are lots of words you might hear for this, especially in terms of what can be removed with the new features in place. C#’s features allow you to reduce ceremony, remove boilerplate code, and avoid cruft. These are just different ways of talking about the same effect. It’s not that any of the now-redundant code was wrong; it was just distracting and unnecessary. Let’s look at a few ways that C# has evolved in this respect.

Construction and initialization

First, we’ll consider how you create and initialize objects. Delegates have probably evolved the most and in multiple stages. In C# 1, you had to write a separate method for the delegate to refer to and then create the delegate itself in a long-winded way. For example, here’s what you’d write to subscribe a new event handler to a button’s Click event in C# 1:

button.Click += new EventHandler(HandleButtonClick);     1

  • 1 C# 1

C# 2 introduced method group conversions and anonymous methods. If you wanted to keep the HandleButtonClick method, method group conversions would allow you to change the preceding code to the following:

button.Click += HandleButtonClick;      1

  • 1 C# 2

If your click handler is simple, you might not want to bother with a separate method at all and instead use an anonymous method:

button.Click += delegate { MessageBox.Show("Clicked!"); };     1

  • 1 C# 2

Anonymous methods have the additional benefit of acting as closures: they can use local variables in the context within which they’re created. They’re not used often in modern C# code, however, because C# 3 provided us with lambda expressions, which have almost all the benefits of anonymous methods but shorter syntax:

button.Click += (sender, args) => MessageBox.Show("Clicked!");      1

  • 1 C# 3
Note

In this case, the lambda expression is longer than the anonymous method because the anonymous method uses the one feature that lambda expressions don’t have: the ability to ignore parameters by not providing a parameter list.

I used event handlers as an example for delegates because that was their main use in C# 1. In later versions of C#, delegates are used in more varied situations, particularly in LINQ.

LINQ also brought other benefits for initialization in the form of object initializers and collection initializers. These allow you to specify a set of properties to set on a new object or items to add to a new collection within a single expression. It’s simpler to show than describe, and I’ll borrow an example from chapter 3. Consider code that you might previously have written like this:

var customer = new Customer();
customer.Name = "Jon";
customer.Address = "UK";
var item1 = new OrderItem();
item1.ItemId = "abcd123";
item1.Quantity = 1;
var item2 = new OrderItem();
item2.ItemId = "fghi456";
item2.Quantity = 2;
var order = new Order();
order.OrderId = "xyz";
order.Customer = customer;
order.Items.Add(item1);
order.Items.Add(item2);

The object and collection initializers introduced in C# 3 make this so much clearer:

var order = new Order
{
    OrderId = "xyz",
    Customer = new Customer { Name = "Jon", Address = "UK" },
    Items =
    {
        new OrderItem { ItemId = "abcd123", Quantity = 1 },
        new OrderItem { ItemId = "fghi456", Quantity = 2 }
    }
};

I don’t suggest reading either of these examples in detail; what’s important is the simplicity of the second form over the first.

Method and property declarations

One of the most obvious examples of simplification is through automatically implemented properties. These were first introduced in C# 3 but have been further improved in later versions. Consider a property that would’ve been implemented in C# 1 like this:

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

Automatically implemented properties allow this to be written as a single line:

public string Name { get; set; }

Additionally, C# 6 introduced expression-bodied members that remove more ceremony. Suppose you’re writing a class that wraps an existing collection of strings, and you want to effectively delegate the Count and GetEnumerator() members of your class to that collection. Prior to C# 6, you would’ve had to write something like this:

public int Count { get { return list.Count; } }

public IEnumerator<string> GetEnumerator()
{
    return list.GetEnumerator();
}

This is a strong example of ceremony: a lot of syntax that the language used to require with little benefit. In C# 6, this is significantly cleaner. The => syntax (already used by lambda expressions) is used to indicate an expression-bodied member:

public int Count => list.Count;

public IEnumerator<string> GetEnumerator() => list.GetEnumerator();

Although the value of using expression-bodied members is a personal and subjective matter, I’ve been surprised by just how much difference they’ve made to the readability of my code. I love them! Another feature I hadn’t expected to use as much as I now do is string interpolation, which is one of the string-related improvements in C#.

String handling

String handling in C# has had three significant improvements:

  • C# 5 introduced caller information attributes, including the ability for the compiler to automatically populate method and filenames as parameter values. This is great for diagnostic purposes, whether in permanent logging or more temporary testing.
  • C# 6 introduced the nameof operator, which allows names of variables, types, methods, and other members to be represented in a refactoring-friendly form.
  • C# 6 also introduced interpolated string literals. This isn’t a new concept, but it makes constructing a string with dynamic values much simpler.

For the sake of brevity, I’ll demonstrate just the last point. It’s reasonably common to want to construct a string with variables, properties, the result of method calls, and so forth. This might be for logging purposes, user-oriented error messages (if localization isn’t required), exception messages, and so forth.

Here’s an example from my Noda Time project. Users can try to find a calendar system by its ID, and the code throws a KeyNotFoundException if that ID doesn’t exist. Prior to C# 6, the code might have looked like this:

throw new KeyNotFoundException(
    "No calendar system for ID "  + id + " exists");

Using explicit string formatting, it looks like this:

throw new KeyNotFoundException(
    string.Format("No calendar system for ID {0} exists", id);
Note

See section 1.4.2 for information about Noda Time. You don’t need to know about it to understand this example.

In C# 6, the code becomes just a little simpler with an interpolated string literal to include the value of id in the string directly:

throw new KeyNotFoundException($"No calendar system for ID {id} exists");

This doesn’t look like a big deal, but I’d hate to have to work without string interpolation now.

These are just the most prominent features that help improve the signal-to-noise ratio of your code. I could’ve shown using static directives and the null conditional operator in C# 6 as well as pattern matching, deconstruction, and out variables in C# 7. Rather than expand this chapter to mention every feature in every version, let’s move on to a feature that’s more revolutionary than evolutionary: LINQ.

1.1.3. Simple data access with LINQ

If you ask C# developers what they love about C#, they’ll likely mention LINQ. You’ve already seen some of the features that build up to LINQ, but the most radical is query expressions. Consider this code:

var offers =
    from product in db.Products
    where product.SalePrice <= product.Price / 2
    orderby product.SalePrice
    select new {
        product.Id, product.Description,
        product.SalePrice, product.Price
    };

That doesn’t look anything like old-school C#. Imagine traveling back to 2007 to show that code to a developer using C# 2 and then explaining that this has compile-time checking and IntelliSense support and that it results in an efficient database query. Oh, and that you can use the same syntax for regular collections as well.

Support for querying out-of-process data is provided via expression trees. These represent code as data, and a LINQ provider can analyze the code to convert it into SQL or other query languages. Although this is extremely cool, I rarely use it myself, because I don’t work with SQL databases often. I do work with in-memory collections, though, and I use LINQ all the time, whether through query expressions or method calls with lambda expressions.

LINQ didn’t just give C# developers new tools; it encouraged us to think about data transformations in a new way based on functional programming. This affects more than data access. LINQ provided the initial impetus to take on more functional ideas, but many C# developers have embraced those ideas and taken them further.

C# 4 made a radical change in terms of dynamic typing, but I don’t think that affected as many developers as LINQ. Then C# 5 came along and changed the game again, this time with respect to asynchrony.

1.1.4. Asynchrony

Asynchrony has been difficult in mainstream languages for a long time. More niche languages have been created with asynchrony in mind from the start, and some functional languages have made it relatively easy as just one of the things they handle neatly. But C# 5 brought a new level of clarity to programming asynchrony in a mainstream language with a feature usually referred to as async/await. The feature consists of two complementary parts around async methods:

  • Async methods produce a result representing an asynchronous operation with no effort on the part of the developer. This result type is usually Task or Task<T>.
  • Async methods use await expressions to consume asynchronous operations. If the method tries to await an operation that hasn’t completed yet, the method pauses asynchronously until the operation completes and then continues.
Note

More properly, I could call these asynchronous functions, because anonymous methods and lambda expressions can be asynchronous, too.

Exactly what’s meant by asynchronous operation and pausing asynchronously is where things become tricky, and I won’t attempt to explain this now. But the upshot is that you can write code that’s asynchronous but looks mostly like the synchronous code you’re more familiar with. It even allows for concurrency in a natural way. As an example, consider this asynchronous method that might be called from a Windows Forms event handler:

private async Task UpdateStatus()
{
    Task<Weather> weatherTask = GetWeatherAsync();        1
    Task<EmailStatus> emailTask = GetEmailStatusAsync();  1
    Weather weather = await weatherTask;                  2
    EmailStatus email = await emailTask;                  2

    weatherLabel.Text = weather.Description;              3
    inboxLabel.Text = email.InboxCount.ToString();        3
}

  • 1 Starts two operations concurrently
  • 2 Asynchronously waits for them to complete
  • 3 Updates the userinterface

In addition to starting two operations concurrently and then awaiting their results, this demonstrates how async/await is aware of synchronization contexts. You’re updating the user interface, which can be done only in a UI thread, despite also starting and waiting for long-running operations. Before async/await, this would’ve been complex and error prone.

I don’t claim that async/await is a silver bullet for asynchrony. It doesn’t magically remove all the complexity that naturally comes with the territory. Instead, it lets you focus on the inherently difficult aspects of asynchrony by taking away a lot of the boilerplate code that was previously required.

All of the features you’ve seen so far aim to make code simpler. The final aspect I want to mention is slightly different.

1.1.5. Balancing efficiency and complexity

I remember my first experiences with Java; it was entirely interpreted and painfully slow. After a while, optional just-in-time (JIT) compilers became available, and eventually it was taken almost for granted that any Java implementation would be JIT-compiled.

Making Java perform well took a lot of effort. This effort wouldn’t have happened if the language had been a flop. But developers saw the potential and already felt more productive than they had before. Speed of development and delivery can often be more important than application speed.

C# was in a slightly different situation. The Common Language Runtime (CLR) was pretty efficient right from the start. The language support for easy interop with native code and for performance-sensitive unsafe code with pointers helps, too. C# performance continues to improve over time. (I note with a wry smile that Microsoft is now introducing tiered JIT compilation broadly like the Java HotSpot JIT compiler.)

But different workloads have different performance demands. As you’ll see in section 1.2, C# is now in use across a surprising variety of platforms, including gaming and microservices, both of which can have difficult performance requirements.

Asynchrony helps address performance in some situations, but C# 7 is the most overtly performance-sensitive release. Read-only structs and a much larger surface area for ref features help to avoid redundant copying. The Span<T> feature present in modern frameworks and supported by ref-like struct types reduces unnecessary allocation and garbage collection. The hope is clearly that when used carefully, these techniques will cater to the requirements of specific developers.

I have a slight sense of unease around these features, as they still feel complex to me. I can’t reason about a method using an in parameter as clearly as I can about regular value parameters, and I’m sure it will take a while before I’m comfortable with what I can and can’t do with ref locals and ref returns.

My hope is that these features will be used in moderation. They’ll simplify code in situations that benefit from them, and they will no doubt be welcomed by the developers who maintain that code. I look forward to experimenting with these features in personal projects and becoming more comfortable with the balance between improved performance and increased code complexity.

I don’t want to sound this note of caution too loudly. I suspect the C# team made the right choice to include the new features regardless of how much or little I’ll use them in my work. I just want to point out that you don’t have to use a feature just because it’s there. Make your decision to opt into complexity a conscious one. Speaking of opting in, C# 7 brought a new meta-feature to the table: the use of minor version numbers for the first time since C# 1.

1.1.6. Evolution at speed: Using minor versions

The set of version numbers for C# is an odd one, and it is complicated by the fact that many developers get understandably confused between the framework and the language. (There’s no C# 3.5, for example. The .NET Framework version 3.0 shipped with C# 2, and .NET 3.5 shipped with C# 3.) C# 1 had two releases: C# 1.0 and C# 1.2. Between C# 2 and C# 6 inclusive, there were only major versions that were usually backed by a new version of Visual Studio.

C# 7 bucked that trend: there were releases of C# 7.0, C# 7.1, C# 7.2, and C# 7.3, which were all available in Visual Studio 2017. I consider it highly likely that this pattern will continue in C# 8. The aim is to allow new features to evolve quickly with user feedback. The majority of C# 7.1–7.3 features have been tweaks or extensions to the features introduced in C# 7.0.

Volatility in language features can be disconcerting, particularly in large organizations. A lot of infrastructure may need to be changed or upgraded to make sure the new language version is fully supported. A lot of developers may learn and adopt new features at different paces. If nothing else, it can be a little uncomfortable for the language to change more often than you’re used to.

For this reason, the C# compiler defaults to using the earliest minor version of the latest major version it supports. If you use a C# 7 compiler and don’t specify any language version, it will restrict you to C# 7.0 by default. If you want to use a later minor version, you need to specify that in your project file and opt into the new features. You can do this in two ways, although they have the same effect. You can edit your project file directly to add a <LangVersion> element in a <PropertyGroup>, like this:

<PropertyGroup>
  ...                                  1
  <LangVersion>latest</LangVersion>    2
</PropertyGroup>

  • 1 Other properties
  • 2 Specifies the language version of the project

If you don’t like editing project files directly, you can go to the project properties in Visual Studio, select the Build tab, and then click the Advanced button at the bottom right. The Advanced Build Settings dialog box, shown in figure 1.1, will open to allow you to select the language version you wish to use and other options.

Figure 1.1. Language version settings in Visual Studio

This option in the dialog box isn’t new, but you’re more likely to want to use it now than in previous versions. The values you can select are as follows:

  • default—The first release of the latest major version
  • latest—The latest version
  • A specific version number—For example, 7.0 or 7.3

This doesn’t change the version of the compiler you run; it changes the set of language features available to you. If you try to use something that isn’t available in the version you’re targeting, the compiler error message will usually explain which version is required for that feature. If you try to use a language feature that’s entirely unknown to the compiler (using C# 7 features with a C# 6 compiler, for example), the error message is usually less clear.

C# as a language has come a long way since its first release. What about the platform it runs on?

1.2. An evolving platform

The last few years have been exhilarating for .NET developers. A certain amount of frustration exists as well, as both Microsoft and the .NET community come to terms with the implications of a more open development model. But the overall result of the hard work by so many people is remarkable.

For many years, running C# code would almost always mean running on Windows. It would usually mean either a client-side app written in Windows Forms or Windows Presentation Foundation (WPF) or a server-side app written with ASP.NET and probably running behind Internet Information Server (IIS). Other options have been available for a long time, and the Mono project in particular has a rich history, but the mainstream of .NET development was still on Windows.

As I write this in June 2018, the .NET world is very different. The most prominent development is .NET Core, a runtime and framework that is portable and open source, is fully supported by Microsoft on multiple operating systems, and has streamlined development tooling. Only a few years ago, that would’ve been unthinkable. Add to that a portable and open source IDE in the form of Visual Studio Code, and you get a flourishing .NET ecosystem with developers working on all kinds of local platforms and then deploying to all kinds of server platforms.

It would be a mistake to focus too heavily on .NET Core and ignore the many other ways C# runs these days. Xamarin provides a rich multiplatform mobile experience. Its GUI framework (Xamarin Forms) allows developers to create user interfaces that are fairly uniform across different devices where that’s appropriate but that can take advantage of the underlying platform, too.

Unity is one of the most popular game-development platforms in the world. With a customized Mono runtime and ahead-of-time compilation, it can provide challenges to C# developers who are used to more-traditional runtime environments. But for many developers, this is their first or perhaps their only experience with the language.

These widely adopted platforms are far from the only ones making C#. I’ve recently been working with Try .NET and Blazor for very different forms of browser/C# interaction.

Try .NET allows users to write code in a browser, with autocompletion, and then build and run that code. It’s great for experimenting with C# with a barrier to entry that’s about as low as it can be.

Blazor is a platform for running Razor pages directly in a browser. These aren’t pages rendered by a server and then displayed in the browser; the user-interface code runs within the browser using a version of the Mono runtime converted into Web-Assembly. The idea of a whole runtime executing Intermediate Language (IL) via the JavaScript engine in a browser, not only on full computers but also on mobile phones, would’ve struck me as absurd just a few years ago. I’m glad other developers have more imagination. A lot of the innovation in this space has been made possible only by a more collaborative and open community than ever before.

1.3. An evolving community

I’ve been involved in the C# community since the C# 1.0 days, and I’ve never seen it as vibrant as it is today. When I started using C#, it was very much seen as an “enterprise” programming language, and there was relatively little sense of fun and exploration.[1] With that background, the open source C# ecosystem grew fairly slowly compared with other languages, including Java, which was also considered an enterprise language. Around the time of C# 3, the alt.NET community was looking beyond the mainstream of .NET development, and this was seen as being against Microsoft in some senses.

1

Don’t get me wrong; it was a pleasant community to be part of, and there have always been people experimenting with C# for fun.

In 2010, the NuGet (initially NuPack) package manager was launched, which made it much easier to produce and consume class libraries, whether commercial or open source. Even though the barrier of downloading a zip file, copying a DLL into somewhere appropriate, and then adding a reference to it doesn’t sound hugely significant, every point of friction can put developers off.

Note

Package managers other than NuGet were developed even earlier, and the OpenWrap project developed by Sebastien Lambla was particularly influential.

Fast-forward to 2014, and Microsoft announced that its Roslyn compiler platform was going to become open source under the umbrella of the new .NET Foundation. Then .NET Core was announced under the initial codename Project K; DNX came later, followed by the .NET Core tooling that’s now released and stable. Then came ASP.NET Core. And Entity Framework Core. And Visual Studio Code. The list of products that truly live and breathe on GitHub goes on.

The technology has been important, but the new embrace of open source by Microsoft has been equally vital for a healthy community. Third-party open source packages have blossomed, including innovative uses for Roslyn and integrations within .NET Core tooling that just feel right.

None of this has happened in a vacuum. The rise of cloud computing makes .NET Core even more important to the .NET ecosystem than it would’ve been otherwise; support for Linux isn’t optional. But because .NET Core is available, there’s now nothing special about packaging up an ASP.NET Core service in a Docker image, deploying it with Kubernetes, and using it as just one part of a larger application that could involve many languages. The cross-pollination of good ideas between many communities has always been present, but it is stronger than ever right now.

You can learn C# in a browser. You can run C# anywhere. You can ask questions about C# on Stack Overflow and myriad other sites. You can join in the discussion about the future of the language on the C# team’s GitHub repository. It’s not perfect; we still have collective work to do in order to make the C# community as welcoming as it possibly can be for everyone, but we’re in a great place already.

I’d like to think that C# in Depth has its own small place in the C# community, too. How has this book evolved?

1.4. An evolving book

You’re reading the fourth edition of C# in Depth. Although the book hasn’t evolved at the same pace as the language, platform, or community, it also has changed. This section will help you understand what is covered in this book.

1.4.1. Mixed-level coverage

The first edition of C# in Depth came out in April 2008, which was coincidentally the same time that I joined Google. Back then, I was aware that a lot of developers knew C# 1 fairly well, but they were picking up C# 2 and C# 3 as they went along without a firm grasp of how all the pieces fit together. I aimed to address that gap by diving into the language at a depth that would help readers understand not only what each feature did but why it was designed that way.

Over time, the needs of developers change. It seems to me that the community has absorbed a deeper understanding of the language almost by osmosis, at least for earlier versions. Attaining deeper understanding of the language won’t be a universal experience, but for the fourth edition, I wanted the emphasis to be on the newer versions. I still think it’s useful to understand the evolution of the language version by version, but there’s less need to look at every detail of the features in C# 2–4.

Note

Looking at the language one version at a time isn’t the best way to learn the language from scratch, but it’s useful if you want to understand it deeply. I wouldn’t use the same structure to write a book for C# beginners.

I’m also not keen on thick books. I don’t want C# in Depth to be intimidating, hard to hold, or hard to write in. Keeping 400 pages of coverage for C# 2–4 just didn’t feel right. For that reason, I’ve compressed my coverage of those versions. Every feature is mentioned, and I go into detail where I feel it’s appropriate, but there’s less depth than in the third edition. Use the coverage in the fourth edition as a review of topics you already know and to help you determine topics you want to read more about in the third edition. You can find a link to access an electronic copy of the third edition at www.manning.com/books/c-sharp-in-depth-fourth-edition. Versions 5–7 of the language are covered in more detail in this edition. Asynchrony is still a tough topic to understand, and the third edition obviously doesn’t cover C# 6 or 7 at all.

Writing, like software engineering, is often a balancing act. I hope the balance I’ve struck between detail and brevity works for you.

Tip

If you have a physical copy of this book, I strongly encourage you to write in it. Make note of places where you disagree or parts that are particularly useful. The act of doing this will reinforce the content in your memory, and the notes will serve as reminders later.

1.4.2. Examples using Noda Time

Most of the examples I provide in the book are standalone. But to make a more compelling case for some features, it’s useful to be able to point to where I use them in production code. Most of the time, I use Noda Time for this.

Noda Time is an open source project I started in 2009 to provide a better date and time library for .NET. It serves a secondary purpose, though: it’s a great sandbox project for me. It helps me hone my API design skills, learn more about performance and benchmarking, and test new C# features. All of this without breaking users, of course.

Every new version of C# has introduced features that I’ve been able to use in Noda Time, so I think it makes sense to use those as concrete examples in this book. All of the code is available on GitHub, which means you can clone it and experiment for yourself. The purpose of using Noda Time in examples isn’t to persuade you to use the library, but I’m not going to complain if that happens to be a side effect.

In the rest of the book, I’ll assume that you know what I’m talking about when I refer to Noda Time. In terms of making it suitable for examples, the important aspects of it are as follows:

  • The code needs to be as readable as possible. If a language feature lets me refactor for readability, I’ll jump at the chance.
  • Noda Time follows semantic versioning, and new major versions are rare. I pay attention to the backward-compatibility aspects of applying new language features.
  • I don’t have concrete performance goals, because Noda Time can be used in many contexts with different requirements. I do pay attention to performance and will embrace features that improve efficiency, so long as they don’t make the code much more complex.

To find out more about the project and check out its source code, visit https://nodatime.org or https://github.com/nodatime/nodatime.

1.4.3. Terminology choices

I’ve tried to follow the official C# terminology as closely as I can within the book, but occasionally I’ve allowed clarity to take precedence over precision. For example, when writing about asynchrony, I often refer to async methods when the same information also applies to asynchronous anonymous functions. Likewise, object initializers apply to accessible fields as well as properties, but it’s simpler to mention that once and then refer only to properties within the rest of the explanation.

Sometimes the terms within the specification are rarely used in the wider community. For example, the specification has the notion of a function member. That’s a method, property, event, indexer, user-defined operator, instance constructor, static constructor, or finalizer. It’s a term for any type member that can contain executable code, and it’s useful when describing language features. It’s not nearly as useful when you’re looking at your own code, which is why you may never have heard of it before. I’ve tried to use terms like this sparingly, but my view is that it’s worth becoming somewhat familiar with them in the spirit of getting closer to the language.

Finally, some concepts don’t have any official terminology but are still useful to refer to in a shorthand form. The one I’ll use most often is probably unspeakable names. This term, coined by Eric Lippert, refers to an identifier generated by the compiler to implement features such as iterator blocks or lambda expressions.[2] The identifier is valid for the CLR but not valid in C#; it’s a name that can’t be “spoken” within the language, so it’s guaranteed not to clash with your code.

2

We think it was Eric, anyway. Eric can’t remember for sure and thinks Anders Hejlsberg may have come up with the term first. I’ll always associate it with Eric, though, along with his classification for exceptions: fatal, boneheaded, vexing, or exogenous.

Summary

I love C#. It’s both comfortable and exciting, and I love seeing where it’s going next. I hope this chapter has passed on some of that excitement to you. But this has been only a taste. Let’s get onto the real business of the book without further delay.

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

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