This feature is a mainstay of C# and has had such an impact on the industry that it's found its way in to other mainstream languages. There are countless tutorials and books that go deep in to this feature; but this post is just meant as a quick reference guide, so I will only summarize it here.
Async/await allows methods to use asynchrony but be written in a synchronous manner:
// async keyword tells the compiler we're writing an async method
// Task<int> is a 'Future'/Promise that will eventually yield a value of type int
async Task<int> GetUserAgeFromDatabase(string username) {
// await keyword tells the compiler to convert this method in to a state machine at this point
// Method will return the Task<int> immediately at this point (assuming GetUserDetails() does not complete immediately and synchronously)
// A continuation for the remainder of the method will be scheduled either on this thread (via captured context) or on task pool thread (by default) to be executed once GetUserDetails()'s Task has completed
var userDetails = await _databaseAccessLayer.GetUserDetails(username);
// Once we're here, we're executing the continuation
return userDetails.Age;
}
View Code Fullscreen • "Async/Await"
This feature involves three attributes that can be applied to optional method parameters. The compiler will then fill in the details; these are useful mostly for logging:
static void Log(string message, [CallerMemberName] string callerMemberName = null, [CallerFilePath] string callerFilePath = null, [CallerLineNumber] int callerLineNumber) {
Console.WriteLine($"{message} (called from {callerMemberName} on line {callerLineNumber} in file {callerFilePath})");
}
static void Test() {
Log("My message"); // Will print something like "My message (called from Test() on line 15 in file C:\...\Example.cs)"
}
View Code Fullscreen • "Caller Info Attributes"
This feature allows usage of static methods on a class without using the class name:
using static System.Console;
static void Test() {
WriteLine("hello"); // No Console. prefix required
}
View Code Fullscreen • "Static Imports"
Exception filters allow catching exceptions only when certain parameters are met:
static void Test() {
try {
SomeOperation();
}
catch (Exception e) when (e.InnerException is OperationCanceledException oce) {
Console.WriteLine($"Operation was cancelled: {oce}");
}
}
View Code Fullscreen • "Exception Filters"
This feature allows omitting the setter from an auto-property to make it immutable:
class MyClass {
public string Name { get; }
public MyClass(string name) {
Name = name; // Can be initialized in constructor
}
}
View Code Fullscreen • "Immutable Auto-Properties"
Name can not be set from any place other than the constructor (or inline, see next feature).
This allows setting of an initial value for a property inline at its declaration point:
class MyClass {
public string Name { get; } = "Ben";
public int Age { get; set; } = 30;
}
View Code Fullscreen • "Auto-Property Initializers"
This feature allows writing certain function bodies as single-line expressions:
class MyClass {
// Age is a read-only property; the code to the right of the '=>' is evaluated every time the property is invoked and the result of the expression is returned
public int Age => (int) (DateTime.Now - new DateTime(1990, 01, 19)).TotalYears;
// PrintAge is a method, the code to the right of the '=>' is executed when the function is invoked
public void PrintAge() => Console.WriteLine(Age);
}
View Code Fullscreen • "Expression-Bodied Members"
Some further support was added in C# 7.0:
class MyClass {
int _age;
// Property getter and setter
public int Age {
get => _age;
set => _age = value >= 0 ?? value : throw new ArgumentOutOfRangeException(nameof(value));
// Constructor
public MyClass(int age) => Age = age;
// Finalizer
~MyClass() => ResourceManager.NotifyFinalizedMyClass(this);
}
View Code Fullscreen • "More Expression-Bodied Members"
This operator allows you to access members of an object if that object is not null; or simply fold all values to null otherwise:
static void PrintUserName(UserDetails? user) {
Console.WriteLine($"Name: {user?.Name ?? "No name"}, Age: {user?.Age.ToString() ?? "No age"}");
}
static void Test() {
PrintUserName(new UserDetails("Ben", 30)); // Prints "Name: Ben, Age: 30"
PrintUserName(null); // Prints "Name: No name, Age: No age"
}
View Code Fullscreen • "Null-Conditional Operator"
When chaining multiple property/method/field invocations together (i.e.
var x = a?.B?.C()?.D) the entire expression will return null if any individual element is null in the chain.
This is a feature I've been using already in various examples so far. String interpolation allows for a more natural way of embedding variables in to strings:
static void Test() {
var name = "Ben";
Console.WriteLine($"My name is {name}"); // The $ sign before the opening quotemark indicates this is an interpolated string
}
View Code Fullscreen • "Basic String Interpolation"
The way values are converted to string can be specified via format suffix. The following example shows one way to specify a number of decimal places when converting a floating-point value to a string:
static void Test() {
var percentageComplete = 12.345d;
Console.WriteLine($"Percentage complete: {percentageComplete:F0}%"); // Prints "Percentage complete: 12%"
Console.WriteLine($"Percentage complete: {percentageComplete:F2}%"); // Prints "Percentage complete: 12.34%"
}
View Code Fullscreen • "Formatted String Interpolation"
It's also possible to specify an alignment; useful for printing ASCII tables:
static void Test() {
var names = new[] { "Ben", "Javier", "Chris" };
var favoriteFoods = new[] { "Ramen", "Something Vegetarian", "No idea" };
for (var i = 0; i < 3; ++i) {
Console.WriteLine($"Name: {names[i],10} | Food: {favoriteFoods[i]}"); // Notice the ,10 that right-aligns names to a 10-column width
}
}
/* Prints:
* Name: Ben | Food: Ramen
* Name: Javier | Food: Something Vegetarian
* Name: Chris | Food: No idea
*/
static void Test() {
var names = new[] { "Ben", "Javier", "Chris" };
var favoriteFoods = new[] { "Ramen", "Something Vegetarian", "No idea" };
for (var i = 0; i < 3; ++i) {
Console.WriteLine($"Name: {names[i],-10} | Food: {favoriteFoods[i]}"); // Notice the ,-10 that left-aligns names to a 10-column width
}
}
/* Prints:
* Name: Ben | Food: Ramen
* Name: Javier | Food: Something Vegetarian
* Name: Chris | Food: No idea
*/
View Code Fullscreen • "Aligned String Interpolation"
The default formatting for objects uses the thread-local culture as a format provider. Sometimes this isn't what we want. Therefore, we can manually specify a format provider by explicitly creating a
FormattableString and then converting it to a string after:
static void Test() {
var percentageComplete = 12.345d;
FormattableString str = $"Percentage complete: {percentageComplete:F2}%";
Console.WriteLine(str.ToString(CultureInfo.GetCultureInfo("de-DE"))); // Prints "Percentage complete: 12,35%" (German-style number formatting)
}
View Code Fullscreen • "FormattableString"
This small feature allows you to convert the name of a token in code to a string. It's useful because it avoids issues with manual writing out of member/type names when those types are renamed:
class User {
public string Name { get; }
public User(string name) {
if (name == null) throw new ArgumentNullException(nameof(name)); // If we rename name later this will not compile (which is good)
Name = name;
}
}
View Code Fullscreen • "Nameof Operator"
This is a small feature. It allows a cleaner syntax for initializing associative collections (i.e. mostly dictionaries). The following two initializations are identical:
class User {
static void Test() {
var oldWay = new Dictionary<int, string> {
{ 1, "One" },
{ 2, "Two" },
{ 3, "Three" },
{ 4, "Four" }
};
var newWay = new Dictionary<int, string> {
[1] = "One",
[2] = "Two",
[3] = "Three",
[4] = "Four"
};
}
}
View Code Fullscreen • "Old vs New Dictionary Initialization"
The dictionary keys and values can be of any type.
Let's say a collection type (must implement
IEnumerable<>) is defined in a library you're using; but the method for adding elements isn't named
Add(T item). This is a requirement for collection initializers to work.
Here's an example using an imaginary type named
UserDatabase that implements
IEnumerable<User> from an imaginary third-party library:
static void Test() {
// Won't compile
// Doesn't work becuase UserDatabase calls its add method AddUser(), so we have to use the second approach below
var database = new UserDatabase {
new User("Ben", 30),
new User("Seb", 27),
new User("Rob", 33)
};
// Will compile but less pretty
var database = new UserDatabase();
database.AddUser(new User("Ben", 30));
database.AddUser(new User("Seb", 27));
database.AddUser(new User("Rob", 33));
}
View Code Fullscreen • "No Add Method when Trying to Use Collection Initializer"
In this scenario, starting with C# 6.0, we can specify an
Add(T item) extension method to enable collection initializers:
static class UserDatabaseExtensions {
public static void Add(this UserDatabase @this, User u) => @this.AddUser(u);
}
// ...
static void Test() {
// Hooray, this works now!
var database = new UserDatabase {
new User("Ben", 30),
new User("Seb", 27),
new User("Rob", 33)
};
}
View Code Fullscreen • "Add Extension Method when Trying to Use Collection Initializer"