O'Reilly logo

C# 7.0 in a Nutshell by Ben Albahari, Joseph Albahari

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Chapter 1. Introducing C# and the .NET Framework

C# is a general-purpose, type-safe, object-oriented programming language. The goal of the language is programmer productivity. To this end, C# balances simplicity, expressiveness, and performance. The chief architect of the language since its first version is Anders Hejlsberg (creator of Turbo Pascal and architect of Delphi). The C# language is platform-neutral and works with a range of platform-specific compilers and frameworks, most notably the Microsoft .NET Framework for Windows.

Object Orientation

C# is a rich implementation of the object-orientation paradigm, which includes encapsulation, inheritance, and polymorphism. Encapsulation means creating a boundary around an object, to separate its external (public) behavior from its internal (private) implementation details. The distinctive features of C# from an object-oriented perspective are:

Unified type system

The fundamental building block in C# is an encapsulated unit of data and functions called a type. C# has a unified type system, where all types ultimately share a common base type. This means that all types, whether they represent business objects or are primitive types such as numbers, share the same basic functionality. For example, an instance of any type can be converted to a string by calling its ToString method.

Classes and interfaces

In a traditional object-oriented paradigm, the only kind of type is a class. In C#, there are several other kinds of types, one of which is an interface. An interface is like a class, except that it only describes members. The implementation for those members comes from types that implement the interface. Interfaces are particularly useful in scenarios where multiple inheritance is required (unlike languages such as C++ and Eiffel, C# does not support multiple inheritance of classes).

Properties, methods, and events

In the pure object-oriented paradigm, all functions are methods (this is the case in Smalltalk). In C#, methods are only one kind of function member, which also includes properties and events (there are others, too). Properties are function members that encapsulate a piece of an object’s state, such as a button’s color or a label’s text. Events are function members that simplify acting on object state changes.

While C# is primarily an object-oriented language, it also borrows from the functional programming paradigm. Specifically:

Functions can be treated as values

Through the use of delegates, C# allows functions to be passed as values to and from other functions.

C# supports patterns for purity

Core to functional programming is avoiding the use of variables whose values change, in favor of declarative patterns. C# has key features to help with those patterns, including the ability to write unnamed functions on the fly that “capture” variables (lambda expressions), and the ability to perform list or reactive programming via query expressions. C# also makes it easy to define read-only fields and properties for writing immutable (read-only) types.

Type Safety

C# is primarily a type-safe language, meaning that instances of types can interact only through protocols they define, thereby ensuring each type’s internal consistency. For instance, C# prevents you from interacting with a string type as though it were an integer type.

More specifically, C# supports static typing, meaning that the language enforces type safety at compile time. This is in addition to type safety being enforced at runtime.

Static typing eliminates a large class of errors before a program is even run. It shifts the burden away from runtime unit tests onto the compiler to verify that all the types in a program fit together correctly. This makes large programs much easier to manage, more predictable, and more robust. Furthermore, static typing allows tools such as IntelliSense in Visual Studio to help you write a program, since it knows for a given variable what type it is, and hence what methods you can call on that variable.

Note

C# also allows parts of your code to be dynamically typed via the dynamic keyword. However, C# remains a predominantly statically typed language.

C# is also called a strongly typed language because its type rules (whether enforced statically or at runtime) are very strict. For instance, you cannot call a function that’s designed to accept an integer with a floating-point number, unless you first explicitly convert the floating-point number to an integer. This helps prevent mistakes.

Strong typing also plays a role in enabling C# code to run in a sandbox—an environment where every aspect of security is controlled by the host. In a sandbox, it is important that you cannot arbitrarily corrupt the state of an object by bypassing its type rules.

Memory Management

C# relies on the runtime to perform automatic memory management. The Common Language Runtime has a garbage collector that executes as part of your program, reclaiming memory for objects that are no longer referenced. This frees programmers from explicitly deallocating the memory for an object, eliminating the problem of incorrect pointers encountered in languages such as C++.

C# does not eliminate pointers: it merely makes them unnecessary for most programming tasks. For performance-critical hotspots and interoperability, pointers and explicit memory allocation is permitted in blocks that are marked unsafe.

Platform Support

Historically, C# was used almost entirely for writing code to run on Windows platforms. Recently, however, Microsoft and other companies have invested in other platforms, including Linux, macOS, iOS, and Android. Xamarin™ allows cross-platform C# development for mobile applications, and Portable Class Libraries are becoming increasingly widespread. Microsoft’s ASP.NET Core is a cross-platform lightweight web hosting framework that can run either on the .NET Framework or on .NET Core, an open source cross-platform runtime.

C# and the CLR

C# depends on a runtime equipped with a host of features such as automatic memory management and exception handling. At the core of the Microsoft .NET Framework is the Common Language Runtime (CLR), which provides these runtime features. (The .NET Core and Xamarin frameworks provide similar runtimes.) The CLR is language-neutral, allowing developers to build applications in multiple languages (e.g., C#, F#, Visual Basic .NET, and Managed C++).

C# is one of several managed languages that get compiled into managed code. Managed code is represented in Intermediate Language or IL. The CLR converts the IL into the native code of the machine, such as X86 or X64, usually just prior to execution. This is referred to as Just-In-Time (JIT) compilation. Ahead-of-time compilation is also available to improve startup time with large assemblies or resource-constrained devices (and to satisfy iOS app store rules when developing with Xamarin).

The container for managed code is called an assembly or portable executable. An assembly can be an executable file (.exe) or a library (.dll), and contains not only IL, but type information (metadata). The presence of metadata allows assemblies to reference types in other assemblies without needing additional files.

Note

You can examine and disassemble the contents of an IL assembly with Microsoft’s ildasm tool. And with tools such as ILSpy, dotPeek (JetBrains), or Reflector (Red Gate), you can go further and decompile the IL to C#. Because IL is higher-level than native machine code, the decompiler can do quite a good job of reconstructing the original C#.

A program can query its own metadata (reflection), and even generate new IL at runtime (reflection.emit).

The CLR and .NET Framework

The .NET Framework consists of the CLR plus a vast set of libraries. The libraries consist of core libraries (which this book is concerned with) and applied libraries, which depend on the core libraries. Figure 1-1 is a visual overview of those libraries (and also serves as a navigational aid to the book).

Topics covered in this book and the chapters in which they are found. Topics not covered are shown outside the large circle.
Figure 1-1. Topics covered in this book and the chapters in which they are found. Topics not covered are shown outside the large circle.
Note

The core libraries are sometimes collectively called the Base Class Library (BCL). The entire framework is called the Framework Class Library (FCL).

Other Frameworks

The Microsoft .NET Framework is the most expansive and mature framework, but runs only on Microsoft Windows (desktop/server). Over the years, other frameworks have emerged to support other platforms. There are currently three major players besides the .NET Framework, all of which are currently owned by Microsoft:

Universal Windows Platform (UWP)

For writing Windows 10 Store Apps and for targeting Windows 10 devices (mobile, XBox, Surface Hub, Hololens). Your app runs in a sandbox to lessen the threat of malware, prohibiting operations such as reading or writing arbitrary files.

.NET Core with ASP.NET Core

An open source framework (originally based on a cut-down version of the .NET Framework) for writing easily deployable Internet apps and micro-services that run on Windows, macOS, and Linux. Unlike the .NET Framework, .NET Core can be packaged with the web application and xcopy-deployed (self-contained deployment).

Xamarin

For writing mobile apps that target iOS, Android, and Windows Mobile. The Xamarin company was purchased by Microsoft in 2016.

Table 1-1 compares the current platform support for each of the major frameworks.

The four major frameworks differ in the platforms they support, the libraries that sit on top, and their intended uses. However, it’s fair to say that as of the release of .NET Core 2.0, they all expose a similar core framework (BCL), which is the main focus of this book. It is even possible to directly leverage this commonality by writing class libraries that work across all four frameworks (see “.NET Standard 2.0” in Chapter 5).

Note

A nuance not shown in Table 1-1 is that UWP uses .NET Core under the covers, so technically .NET Core does run on Windows 10 devices (albeit not for the purpose of providing a framework for ASP.NET Core). It’s likely that we’ll see more uses for .NET Core 2 in the future.

Legacy and Niche Frameworks

The following frameworks are still available to support older platforms:

  • Windows Runtime for Windows 8/8.1 (now UWP)

  • Windows Phone 7/8 (now UWP)

  • Microsoft XNA for game development (now UWP)

  • Silverlight (no longer actively developed since the rise of HTML5 and JavaScript)

  • .NET Core 1.x (the predecessor to .NET Core 2.0, with significantly reduced functionality)

There are also a couple of niche frameworks worth mentioning:

  • The .NET Micro Framework is for running .NET code on highly resource-constrained embedded devices (under 1 MB).

  • Mono, the open source framework upon which Xamarin sits, also has libraries to develop cross-platform desktop applications on Linux, macOS, and Windows. Not all features are supported, or work fully.

It’s also possible to run managed code inside SQL Server. With SQL Server CLR integration, you can write custom functions, stored procedures, and aggregations in C# and then call them from SQL. This works in conjunction with the standard .NET Framework, but with a special “hosted” CLR that enforces a sandbox to protect the integrity of the SQL Server process.

Windows Runtime

C# also interoperates with Windows Runtime (WinRT) technology. WinRT means two things:

  • A language-neutral object-oriented execution interface supported in Windows 8 and above

  • A set of libraries baked into Windows 8 and above that support the preceding interface

Note

Somewhat confusingly, the term “WinRT” has historically been used to mean two more things:

  • The predecessor to UWP, i.e., the development platform for writing Store apps for Windows 8/8.1, sometimes called “Metro” or “Modern”

  • The defunct mobile operating system for RISC-based tablets (“Windows RT”) that Microsoft released in 2011

By execution interface, we mean a protocol for calling code that’s (potentially) written in another language. Microsoft Windows has historically provided a primitive execution interface in the form of low-level C-style function calls comprising the Win32 API.

WinRT is much richer. In part, it is an enhanced version of COM (Component Object Model) that supports .NET, C++, and JavaScript. Unlike Win32, it’s object-oriented and has a relatively rich type system. This means that referencing a WinRT library from C# feels much like referencing a .NET library—you may not even be aware that you’re using WinRT.

The WinRT libraries in Windows 10 form an essential part of the UWP platform (UWP relies on both WinRT and .NET Core libraries). If you’re targeting the standard .NET Framework platform, referencing the Windows 10 WinRT libraries is optional, and can be useful if you need to access Windows 10–specific features not otherwise covered in the .NET Framework.

The WinRT libraries in Windows 10 support the UWP user interface for writing immersive touch-first applications. They also support mobile device-specific features such as sensors, text messaging, and so on (the new functionality of Window 8, 8.1, and 10 has been exposed through WinRT rather than Win32). WinRT libraries also provide file I/O tailored to work well within the UWP sandbox.

What distinguishes WinRT from ordinary COM is that WinRT projects its libraries into a multitude of languages, namely C#, VB, C++, and JavaScript, so that each language sees WinRT types (almost) as though they were written especially for it. For example, WinRT will adapt capitalization rules to suit the standards of the target language, and will even remap some functions and interfaces. WinRT assemblies also ship with rich metadata in .winmd files, which have the same format as .NET assembly files, allowing transparent consumption without special ritual; this is why you might be unaware that you’re using WinRT rather than .NET types, aside from namespace differences. Another clue is that WinRT types are subject to COM-style restrictions; for instance, they offer limited support for inheritance and generics.

In C#, you can not only consume WinRT libraries, but also write your own (and call them from a JavaScript application).

A Brief History of C#

The following is a reverse chronology of the new features in each C# version, for the benefit of readers already familiar with an older version of the language.

What’s New in C# 7.0

(C# 7.0 ships with Visual Studio 2017.)

Numeric literal improvements

Numeric literals in C# 7 can include underscores to improve readability. These are called digit separators and are ignored by the compiler:

int million = 1_000_000;

Binary literals can be specified with the 0b prefix:

var b = 0b1010_1011_1100_1101_1110_1111;

Out variables and discards

C# 7 makes it easier to call methods that contain out parameters. First, you can now declare out variables on the fly:

bool successful = int.TryParse ("123", out int result);
Console.WriteLine (result);

And when calling a method with multiple out parameters, you can discard ones you’re uninterested in with the underscore character:

SomeBigMethod (out _, out _, out _, out int x, out _, out _, out _);
Console.WriteLine (x);

Patterns

You can also introduce variables on the fly with the is operator. These are called pattern variables (see “The is operator and pattern variables (C# 7)” in Chapter 3):

void Foo (object x)
{
  if (x is string s)
    Console.WriteLine (s.Length);
}

The switch statement also supports patterns, so you can switch on type as well as constants (see “The switch statement with patterns (C# 7)” in Chapter 2). You can specify conditions with a when clause, and also switch on the null value:

switch (x)
{
  case int i:
    Console.WriteLine ("It's an int!");
    break;
  case string s:
    Console.WriteLine (s.Length);   // We can use the s variable
    break;
  case bool b when b == true:        // Matches only when b is true
    Console.WriteLine ("True");
    break;
  case null:
    Console.WriteLine ("Nothing");
    break;
}

Local methods

A local method is a method declared inside another function (see “Local methods (C# 7)” in Chapter 3):

void WriteCubes()
{
  Console.WriteLine (Cube (3));
  Console.WriteLine (Cube (4));
  Console.WriteLine (Cube (5));

  int Cube (int value) => value * value * value;
}

Local methods are visible only to the containing function, and can capture local variables in the same way that lambda expressions do.

More expression-bodied members

C# 6 introduced the expression-bodied “fat-arrow” syntax for methods, read-only properties, operators, and indexers. C# 7 extends this to constructors, read/write properties, and finalizers:

public class Person
{
  string name;

  public Person (string name) => Name = name;

  public string Name
  {
    get => name;
    set => name = value ?? "";
  }

  ~Person () => Console.WriteLine ("finalize");
}

Deconstructors

C# 7 introduces the deconstructor pattern. Whereas a constructor typically takes a set of values (as parameters) and assigns them to fields, a deconstructor does the reverse and assigns fields back to a set of variables. We could write a deconstructor for the Person class in the preceding example as follows (exception-handling aside):

public void Deconstruct (out string firstName, out string lastName)
{
  int spacePos = name.IndexOf (' ');
  firstName = name.Substring (0, spacePos);
  lastName = name.Substring (spacePos + 1);
}

Deconstructors are called with the following special syntax:

var joe = new Person ("Joe Bloggs");
var (first, last) = joe;          // Deconstruction
Console.WriteLine (first);        // Joe
Console.WriteLine (last);         // Bloggs

Tuples

Perhaps the most notable improvement to C# 7 is explicit tuple support (see “Tuples (C# 7)” in Chapter 4). Tuples provide a simple way to store a set of related values:

var bob = ("Bob", 23);
Console.WriteLine (bob.Item1);   // Bob
Console.WriteLine (bob.Item2);   // 23

C#’s new tuples are syntactic sugar for using the System.ValueTuple<...> generic structs. But thanks to compiler magic, tuple elements can be named:

var tuple = (Name:"Bob", Age:23);
Console.WriteLine (tuple.Name);     // Bob
Console.WriteLine (tuple.Age);      // 23

With tuples, functions can return multiple values without resorting to out parameters:

static (int row, int column) GetFilePosition() => (3, 10);

static void Main()
{
  var pos = GetFilePosition();
  Console.WriteLine (pos.row);      // 3
  Console.WriteLine (pos.column);   // 10
}

Tuples implicitly support the deconstruction pattern, so they can easily be deconstructed into individual variables. We can rewrite the preceding Main method so that the tuple returned by GetFilePosition is instead assigned to two local variables, row and column:

static void Main()
{
  (int row, int column) = GetFilePosition();   // Creates 2 local variables
  Console.WriteLine (row);      // 3 
  Console.WriteLine (column);   // 10
}

throw expressions

Prior to C# 7, throw was always a statement. Now it can also appear as an expression in expression-bodied functions:

public string Foo() => throw new NotImplementedException();

A throw expression can also appear in a ternary conditional expression:

string Capitalize (string value) =>
  value == null ? throw new ArgumentException ("value") :
  value == "" ? "" :
  char.ToUpper (value[0]) + value.Substring (1);

Other improvements

C# 7 also includes a couple of features for specialized micro-optimization scenarios (see “Ref Locals (C# 7)” and “Ref Returns (C# 7)” in Chapter 2), and the ability to declare asynchronous methods with return types other than Task / Task<T>.

What’s New in C# 6.0

C# 6.0, which shipped with Visual Studio 2015, features a new-generation compiler, completely written in C#. Known as project “Roslyn,” the new compiler exposes the entire compilation pipeline via libraries, allowing you to perform code analysis on arbitrary source code (see Chapter 27). The compiler itself is open source, and the source code is available at github.com/dotnet/roslyn.

In addition, C# 6.0 features a number of minor, but significant enhancements, aimed primarily at reducing code clutter.

The null-conditional (“Elvis”) operator (see “Null Operators”, Chapter 2) avoids having to explicitly check for null before calling a method or accessing a type member. In the following example, result evaluates to null instead of throwing a NullReferenceException:

System.Text.StringBuilder sb = null;
string result = sb?.ToString();      // result is null

Expression-bodied functions (see “Methods”, Chapter 3) allow methods, properties, operators, and indexers that comprise a single expression to be written more tersely, in the style of a lambda expression:

public int TimesTwo (int x) => x * 2;
public string SomeProperty => "Property value";

Property initializers (Chapter 3) let you assign an initial value to an automatic property:

public DateTime TimeCreated { get; set; } = DateTime.Now;

Initialized properties can also be read-only:

public DateTime TimeCreated { get; } = DateTime.Now;

Read-only properties can also be set in the constructor, making it easier to create immutable (read-only) types.

Index initializers (Chapter 4) allow single-step initialization of any type that exposes an indexer:

var dict = new Dictionary<int,string>()
{
  [3] = "three",
  [10] = "ten"
};

String interpolation (see “String Type”, Chapter 2) offers a succinct alternative to string.Format:

string s = $"It is {DateTime.Now.DayOfWeek} today";

Exception filters (see “try Statements and Exceptions”, Chapter 4) let you apply a condition to a catch block:

string html;
try
{
  html = new WebClient().DownloadString ("http://asef");
}
catch (WebException ex) when (ex.Status == WebExceptionStatus.Timeout)
{
  ...
}

The using static (see “Namespaces”, Chapter 2) directive lets you import all the static members of a type, so that you can use those members unqualified:

using static System.Console;
...
WriteLine ("Hello, world");  // WriteLine instead of Console.WriteLine

The nameof (Chapter 3) operator returns the name of a variable, type, or other symbol as a string. This avoids breaking code when you rename a symbol in Visual Studio:

int capacity = 123;
string x = nameof (capacity);   // x is "capacity"
string y = nameof (Uri.Host);   // y is "Host"

And finally, you’re now allowed to await inside catch and finally blocks.

What’s New in C# 5.0

C# 5.0’s big new feature was support for asynchronous functions via two new keywords, async and await. Asynchronous functions enable asynchronous continuations, which make it easier to write responsive and thread-safe rich-client applications. They also make it easy to write highly concurrent and efficient I/O-bound applications that don’t tie up a thread resource per operation.

We cover asynchronous functions in detail in Chapter 14.

What’s New in C# 4.0

The features new to C# 4.0 were:

  • Dynamic binding

  • Optional parameters and named arguments

  • Type variance with generic interfaces and delegates

  • COM interoperability improvements

Dynamic binding (Chapters 4 and 20) defers binding—the process of resolving types and members—from compile time to runtime and is useful in scenarios that would otherwise require complicated reflection code. Dynamic binding is also useful when interoperating with dynamic languages and COM components.

Optional parameters (Chapter 2) allow functions to specify default parameter values so that callers can omit arguments and named arguments allow a function caller to identify an argument by name rather than position.

Type variance rules were relaxed in C# 4.0 (Chapters 3 and 4), such that type parameters in generic interfaces and generic delegates can be marked as covariant or contravariant, allowing more natural type conversions.

COM interoperability (Chapter 25) was enhanced in C# 4.0 in three ways. First, arguments can be passed by reference without the ref keyword (particularly useful in conjunction with optional parameters). Second, assemblies that contain COM interop types can be linked rather than referenced. Linked interop types support type equivalence, avoiding the need for Primary Interop Assemblies and putting an end to versioning and deployment headaches. Third, functions that return COM-Variant types from linked interop types are mapped to dynamic rather than object, eliminating the need for casting.

What’s New in C# 3.0

The features added to C# 3.0 were mostly centered on Language Integrated Query capabilities or LINQ for short. LINQ enables queries to be written directly within a C# program and checked statically for correctness, and query both local collections (such as lists or XML documents) or remote data sources (such as a database). The C# 3.0 features added to support LINQ comprised implicitly typed local variables, anonymous types, object initializers, lambda expressions, extension methods, query expressions, and expression trees.

Implicitly typed local variables (var keyword, Chapter 2) let you omit the variable type in a declaration statement, allowing the compiler to infer it. This reduces clutter as well as allowing anonymous types (Chapter 4), which are simple classes created on the fly that are commonly used in the final output of LINQ queries. Arrays can also be implicitly typed (Chapter 2).

Object initializers (Chapter 3) simplify object construction by allowing properties to be set inline after the constructor call. Object initializers work with both named and anonymous types.

Lambda expressions (Chapter 4) are miniature functions created by the compiler on the fly, and are particularly useful in “fluent” LINQ queries (Chapter 8).

Extension methods (Chapter 4) extend an existing type with new methods (without altering the type’s definition), making static methods feel like instance methods. LINQ’s query operators are implemented as extension methods.

Query expressions (Chapter 8) provide a higher-level syntax for writing LINQ queries that can be substantially simpler when working with multiple sequences or range variables.

Expression trees (Chapter 8) are miniature code DOMs (Document Object Models) that describe lambda expressions assigned to the special type Expression<TDelegate>. Expression trees make it possible for LINQ queries to execute remotely (e.g., on a database server) because they can be introspected and translated at runtime (e.g., into a SQL statement).

C# 3.0 also added automatic properties and partial methods.

Automatic properties (Chapter 3) cut the work in writing properties that simply get/set a private backing field by having the compiler do that work automatically. Partial methods (Chapter 3) let an auto-generated partial class provide customizable hooks for manual authoring that “melt away” if unused.

What’s New in C# 2.0

The big new features in C# 2 were generics (Chapter 3), nullable types (Chapter 4), iterators (Chapter 4), and anonymous methods (the predecessor to lambda expressions). These features paved the way for the introduction of LINQ in C# 3.

C# 2 also added support for partial classes, static classes, and a host of minor and miscellaneous features such as the namespace alias qualifier, friend assemblies, and fixed-size buffers.

The introduction of generics required a new CLR (CLR 2.0), because generics maintain full type fidelity at runtime.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required