Ben Bowen's Blog • Home / Blog • • About • • Subscribe •

Two Decades of C#: A Reference - C# 2, 3 and 4 

Since C# was introduced in 2000 the language has grown immensely in size and I'm not sure it's possible for any one person to have intimate knowledge of every language feature in their head at all times. Therefore I wanted to write a series of quick reference posts summarizing all the major new language features ever since C# 2.0. I won't go in to detail with any of them, but instead I want this series to act as a reference for myself (and hopefully you too!) that I can go back to from time-to-time to remember what tools I have in the toolbox. :)

One small note before we start: I'm going to skip some of the more fundamental stuff (for example C# 2.0 introduced generics, but they're so widespread in usage that they're not really worth including); and I may also 'glue' a few features together for the sake of making this more terse. This series isn't meant to be a definitive or historical record of the language; instead it's meant as more of a "cheat-sheet" of important language features that might come in handy. You may find it useful to skim through the table-of-contents on the left to search for any features you don't recognise or need a quick reminder on.

C# 2.0 

Nullable Value Types 

These allow you to specify null as a potential value on any struct variable (where null would otherwise be invalid):

class MyClass {
	public int MyInt { get; }
	public int? MyNullableInt { get; } // This property can be null even though it's of type 'int'
	
	public MyClass(int? input) { // input can be null
		MyInt = input != null ? input.Value : 0; // .Value throws an exception when accessed if input is null
		MyNullableInt = input;
	}
}

// ..

static void Test() {
	var mc = new MyClass(null);
	if (mc.MyNullableInt == null) Console.WriteLine("Was null!");
}
View Code Fullscreen • "Nullable Value Types"
Technically the type of any nullable value object is Nullable<T> where T is the actual encompassed type (i.e. int? is the same as Nullable<int>). Nullable<T> is itself a struct, so in theory it does not make sense to check instances of that type for null, but a combination of the compiler and overridden Equals() implementation on Nullable<T> allow us to treat instances the type as though it does make sense. You can also determine whether a Nullable<T> instance is "not null" by using its HasValue property.

Partial Types 

This feature allows spreading the implementation of a large type (class, interface, or struct) across multiple files.

// File: ExampleClass.Alpha.cs

public partial class ExampleClass {
	public void DoAlphaOne() { ... }
	
	public void DoAlphaTwo() { ... }
}





// File: ExampleClass.Beta.cs

public partial class ExampleClass {
	public void DoBetaOne() { ... }
	
	public void DoBetaTwo() { ... }
}




// Elsewhere

static void Test() {
	var ec = new ExampleClass();
	
	ec.DoAlphaOne();
	ec.DoBetaTwo();
	// etc
}
View Code Fullscreen • "Partial Classes"

Null Coalescing 

This feature allows creating expressions that evaluate to the first non-null value in a chain:

var neverNullString = _stringField ?? stringParameter ?? "Default";
View Code Fullscreen • "Null Coalesceing"
This code will set neverNullString to _stringField, unless _stringField is null; in which case it will set it to stringParameter, unless stringParameter is also null, in which case it will set it to the literal value "Default".

Iterator Generators (Yield) 

This functionality allows you to create an IEnumerable<T> or IEnumerator<T> by 'yielding' elements in the enumerable. The following example demonstrates how we can create a sequence of three or six integers:

public IEnumerable<int> GetOneTwoThree(bool includeNegative = false) {
	yield return 1;
	yield return 2;
	yield return 3;
	if (!includeNegative) yield break;
	yield return -1;
	yield return -2;
	yield return -3;
}

// ..

Console.WriteLine(String.Join(",", GetOneTwoThree())); // Prints "1,2,3" on console
Console.WriteLine(String.Join(",", GetOneTwoThree(true))); // Prints "1,2,3,-1,-2,-3" on console
View Code Fullscreen • "Yield Return and Break"

C# 3.0 

Extension Methods 

This feature allows defining new methods on pre-existing types. This is useful for adding functionality to types you don't control. The following example shows
how to add an overload of ToString() to the double type:

public static class DoubleExtensions {
	public static string ToString(this double @this, int numDecimalPlaces) {
		return @this.ToString("N" + numDecimalPlaces.ToString(CultureInfo.InvariantCulture), CultureInfo.InvariantCulture); 
	}
}

// ... Elsewhere ...

// Will write something like "3.5"; assuming YearsWorkedAtCompany is a double:
Console.WriteLine(user.YearsWorkedAtCompany.ToString(1)); 
View Code Fullscreen • "Double ToString Extension"

Collection Initializers 

This feature allows instantiation of various collection types while at the same time adding initial values to them:

var myArray = new[] { 1, 2, 3, 4, 5 }; // myArray is an int[] of length 5
var myList = new List<int> { 1, 2, 3, 4, 5 }; // myList is a List<int> with 5 elements
var myDict = new Dictionary<int, int> { { 1, 100 }, { 2, 200 } }; // myDict is a Dictionary with 2 key-value pairs
View Code Fullscreen • "Collection Initializers"

Object Initializers 

This feature allows setting an object's properties inline at its instantiation-point:

class Person {
	public string Name { get; set; }
	public int Age { get; set; }
}

static void Test() {
	var person = new Person {
		Name = "Ben",
		Age = 30
	};
}
View Code Fullscreen • "Object Initializers"

Partial Methods 

Like partial classes, this allows you to write two or more parts of a method in different files. There is no guarantee on the order of execution of parts. The method must have a void return type, and must be private.

// File: ExampleClass.Alpha.cs

public partial class ExampleClass {
	public void Print() => DoThing();

	partial void DoThing() {
		Console.WriteLine("AAA");
	}
}





// File: ExampleClass.Beta.cs

public partial class ExampleClass {
	partial void DoThing() {
		Console.WriteLine("BBB");
	}
}




// Elsewhere

static void Test() {
	var ec = new ExampleClass();
	
	ec.Print(); // Prints AAA and BBB to console (order unspecified).
}
View Code Fullscreen • "Partial Methods"
Auto-generated code can use empty partial method declarations to allow users to manually insert custom logic if desired:

// File: ExampleClass.AutoGenerated.cs

public partial class ExampleClass {
	public void SomeAutoGeneratedMethod() {
		DoSomethingOnAutoGenMethodCall();
		
		// do other stuff
	}

	partial void DoSomethingOnAutoGenMethodCall(); // If user does not supply implementation in ExampleClass.User.cs this method will not even be compiled and calls to it will be removed
}





// File: ExampleClass.User.cs

public partial class ExampleClass {
	partial void DoSomethingOnAutoGenMethodCall() {
		Console.WriteLine("SomeAutoGeneratedMethod Invoked");
	}
}
View Code Fullscreen • "Partial Methods for Auto-Generated Code"

C# 4.0 

Dynamic / Late-Bound Typing 

Dynamic typing was introduced to allow "late bound" type resolution. One thing I use dynamic for occasionally is as a more succinct form of reflection:

class GenericClass<T1, T2> {
	public T1 SomeComplexMethod<T3>(T2 inputA, T3 inputB) {
		// ...
	}
}

class Program {
	static void Main() {
		var gc = new GenericClass<int, string>();
		var resultA = InvokeComplexMethodReflectively(gc, "Hi", 3f);
		var resultB = InvokeComplexMethodDynamically(gc, "Hi", 3f);

		Console.WriteLine(resultA);
		Console.WriteLine(resultB);
	}

	static object InvokeComplexMethodReflectively(object genericClassInstance, string inputA, float inputB) {
		var openMethodDefinition = genericClassInstance.GetType().GetMethod("SomeComplexMethod");
		var genericMethodDefinition = openMethodDefinition.MakeGenericMethod(typeof(float));
		return genericMethodDefinition.Invoke(genericClassInstance, new object[] { inputA, inputB });
	}

	static object InvokeComplexMethodDynamically(object genericClassInstance, string inputA, float inputB) {
		return ((dynamic) genericClassInstance).SomeComplexMethod(inputA, inputB);
	}
}
View Code Fullscreen • "Dynamic as an Alternative to Reflection"
A lot of people have reservations about using dynamic at all, but the late-binding is resolved internally using reflection anyway, so it can really be thought of as a syntax sugar for reflection. Additionally, due to caching of binding information dynamic can actually often outperfom the same approach using 'pure' reflection.

C# also added a new type called ExpandoObject which is like a dictionary but the keys are members added dynamically:

static void Test() {
	dynamic user = new ExpandoObject();
	user.Name = "Ben";
	user.Age = 30;
	
	// Prints "User Name is Ben" and "User Age is 30"
	foreach (var kvp in (IDictionary<String, Object>) user) {
		Console.WriteLine($"User {kvp.Key} is {kvp.Value}");
	}
}
View Code Fullscreen • "ExpandoObject"

Optional Arguments 

Optional arguments are parameters of a method that have default values specified, and therefore do not need to be specified by the caller:

// 'nickname' is optional here
public static void MethodWithOptionalArgs(string name, int age, string nickname = null) {
	// ...
}

static void Test() {
	MethodWithOptionalArgs("Ben", 30); // No nickname specified, 'null' will be passed in
}
View Code Fullscreen • "Optional Arguments"
Optional arguments must always come last in the parameter list.

Named Arguments 

Named arguments in C# 4.0 allow specifying specific optional arugments when there are more than one:

public static void MethodWithOptionalArgs(string name, int age, string nickname = null, bool married = false, string address = null) {
	// ...
}

static void Test() {
	MethodWithOptionalArgs("Ben", 30, address: "Noveria"); // 'nickname' and 'married' are left unspecified
}
View Code Fullscreen • "Optional Arguments"
As of C# 7.2, arguments can also be named even when they're not optional. I use this occasionally when specifying a boolean parameter that is otherwise cryptic to understand:

static void Test() {
	CreateTables(true); // What is true?
	
	CreateTables(deletePreviousData: true); // Ahh... Much better.
}
View Code Fullscreen • "Optional Arguments"

Covariance and Contravariance for Generic Type Parameters 

When creating interfaces or delegate types with generic type parameters, we can specify that those type parameters are covariant or contravariant:

interface ICovariant<out T> { }
interface IContravariant<in T> { }

static void Test() {
	ICovariant<object> covariantObj;
	ICovariant<int> covariantInt = GetCovariant<int>();
	
	covariantObj = covariantInt; // "out T" in ICovariant allows this
	
	
	
	IContravariant<object> contravariantObj = GetContravariant<object>();
	IContravariant<int> contravariantInt;
	
	contravariantInt = contravariantObj; // "in T" in IContravariant allows this
}
View Code Fullscreen • "Covariant and Contravariant Generic Parameters in Interfaces"
delegate T Covariant<out T>();
delegate void Contravariant<in T>(T value);

static void Test() {
	Covariant<object> covariantObj;
	Covariant<int> covariantInt = () => 3;
	
	covariantObj = covariantInt;
	


	Contravariant<object> contravariantObj = myObj => Console.WriteLine(myObj);
	Contravariant<int> contravariantInt;
	
	contravariantInt = contravariantObj;
}
View Code Fullscreen • "Covariant and Contravariant Generic Parameters in Delegates"

Covariance is useful when specifying a type of object that will generally be an output from the interface; because we generally don't mind if the actual output type is a child of the specified type.

Contravariance is useful when specifying a type of object that will generally be an input to the interface; because we generally don't mind if the expected input type is a parent of the given type.

Rather than remembering the difference between "contra" and "co" variance, I find it useful to just remember that in is for inputs and out is for outputs (generally, anyway).