The Magical Methods in C#
There’s a certain set of special method signatures in C# which have particular support on the language level.
Methods with those signatures allow for using a special syntax which has several benefits. For example, we can use them to simplify our code or create DSL
to express a solution to our domain-specific problem in a much cleaner way. I came across those methods in different places, so I decided to create a blog post to summarize all my discoveries on this subject.
Collection initialization syntax 🔗︎
Collection initializer is a quite old feature, as it exists in the language since version 3 (released in late 2007). Just as a reminder, Collection initializer
allows for pre-populating a list by providing elements inside the block statement:
var list = new List<int> { 1, 2, 3};
This translates simply to the following list of statements:
var list = new List<int>();
list.Add(1);
list.Add(2);
list.Add(3);
Collection initializer
is not characteristic only for arrays and collection types from BCL
but can be used with any type meeting the following conditions:
- Implements
IEnumerable
interface - Declares method with
void Add(T item)
signature
public class CustomList<T>: IEnumerable
{
public IEnumerator GetEnumerator() => throw new NotImplementedException();
public void Add(T item) => throw new NotImplementedException();
}
We can add support for Collection initializer
to existing types by defining the Add
method as an extension method:
public static class ExistingTypeExtensions
{
public static void Add<T>(ExistingType @this, T item) => throw new NotImplementedException();
}
This syntax can be also used to insert elements into the collection field without accessible setter in initialization block:
class CustomType
{
public List<string> CollectionField { get; private set; } = new List<string>();
}
class Program
{
static void Main(string[] args)
{
var obj = new CustomType
{
CollectionField =
{
"item1",
"item2"
}
};
}
}
Add
method can have more than one parameter:
public class CustomList<T>: IEnumerable
{
public IEnumerator GetEnumerator() => throw new NotImplementedException();
public void Add(T item, string extraParam1, int extraParam1) => throw new NotImplementedException();
}
To use this overload inside the initialization block we need to wrap all parameters in extra pair of curly braces:
var obj = new CustomType
{
CollectionField =
{
{"item1", "extraParamVal1", 2 },
{"item2", "extraParamVal2", 3 }
}
};
Collection initializer
is quite often used to initialize collection with a well known number of items, but we can utilize it to set up collection with a dynamic number of elements. In both cases the syntax is identical:
var obj = new CustomType
{
CollectionField =
{
existingItems
}
};
This is possible for types which meet the following conditions:
- Implements
IEnumerable
interface - Declares method with
void Add(IEnumerable<T> items)
signature
public class CustomList<T>: IEnumerable
{
public IEnumerator GetEnumerator() => throw new NotImplementedException();
public void Add(IEnumerable<T> items) => throw new NotImplementedException();
}
Unfortunately, array type and collections from BCL
don’t implement void Add(IEnumerable<T> items)
method, but we could easily change that by defining an extension method for the existing collection types:
public static class ListExtensions
{
public static void Add<T>(this List<T> @this, IEnumerable<T> items) => @this.AddRange(items);
}
Thanks to this extension method it’s now possible to write code which looks as follows:
var obj = new CustomType
{
CollectionField =
{
existingItems.Where(x => /*Filter items*/) .Select(x => /*Map items*/)
}
};
or even compose the resulting collection from the mix of individual elements and results of multiple enumerables
:
var obj = new CustomType
{
CollectionField =
{
individualElement1,
individualElement2,
list1.Where(x => /*Filter items*/) .Select(x => /*Map items*/),
list2.Where(x => /*Filter items*/) .Select(x => /*Map items*/)
}
};
Without this syntax, it would be very hard to achieve a similar result inside the initialization block.
I’ve discovered this language feature by accident while working with mappings for types with collection fields generated from protobuf
contracts. For those of you who are not familiar with protobuf
, if you are using grpctools to generate dotnet types from proto
files, all collection fields are generated as follows:
[DebuggerNonUserCode]
public RepeatableField<ItemType> SomeCollectionField
{
get
{
return this.someCollectionField_;
}
}
As you can see, collection fields in generated code don’t have a setter, but a blessing in disguise, RepeatableField
/// <summary>
/// Adds all of the specified values into this collection. This method is present to
/// allow repeated fields to be constructed from queries within collection initializers.
/// Within non-collection-initializer code, consider using the equivalent <see cref="AddRange"/>
/// method instead for clarity.
/// </summary>
/// <param name="values">The values to add to this collection.</param>
public void Add(IEnumerable<T> values)
{
AddRange(values);
}
Dictionary initialization syntax 🔗︎
One of the cool features introduced in C# 6 was Index initializers which simplified syntax for dictionary initialization. Thanks to that we can write dictionary init code in a much more readable way:
var errorCodes = new Dictionary<int, string>
{
[404] = "Page not Found",
[302] = "Page moved, but left a forwarding address.",
[500] = "The web server can't come out to play today."
};
This code is translated to:
var errorCodes = new Dictionary<int, string>();
errorCodes[404] = "Page not Found";
errorCodes[302] = "Page moved, but left a forwarding address.";
errorCodes[500] = "The web server can't come out to play today.";
It’s not much but it definitely results with a better experience for writing and reading the code.
The best thing about Index initializer
is that it is not limited only to Dictionary<>
class, it can be used with any type which defines an indexer
:
class HttpHeaders
{
public string this[string key]
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
}
class Program
{
static void Main(string[] args)
{
var headers = new HttpHeaders
{
["access-control-allow-origin"] = "*",
["cache-control"] = "max-age=315360000, public, immutable"
};
}
}
Deconstructors 🔗︎
In C# 7.0, together with tuples, a deconstructor mechanism has been introduced. Deconstructors allow for “decomposing” a tuple into a set of individual variables as follows:
var point = (5, 7);
// decomposing tuple into separated variables
var (x, y) = point;
which is equivalent to:
ValueTuple<int, int> point = new ValueTuple<int, int>(1, 4);
int x = point.Item1;
int y = point.Item2;
This syntax allows also for switching values of two variables without the need for an explicit declaration of the third variable:
int x = 5, y = 7;
//switch
(x, y) = (y,x);
… or for a more succinct way of member initialization:
class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
}
Deconstructors can be used not only with tuples but with custom types too. To allow for deconstructing custom type, it needs to implement a method that obeys the following rules:
- It’s named
Deconstruct
- Returns void
- Every parameter has to be defined with
out
modifier
For our type Point
we can define deconstructor in the following way:
class Point
{
public int X {get;}
public int Y {get;}
public Point(int x, int y) => (X, Y) = (x, y);
public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}
and sample usage can look as follows:
var point = new Point(2,4);
var (x, y)= point;
which under the hood is translated to:
int x;
int y;
new Point(2, 4).Deconstruct(out x, out y);
Deconstructors can be added to types declared outside the source code by defining them as an extension method:
public static class PointExtensions
{
public static void Deconstruct(this Point @this, out int x, out int y) => (x, y) = (@this.X, @this.Y);
}
One of the most useful examples of deconstructors is the one for KeyValuePair<TKey,TValue>
, which allows for easy access to key and value while iterating over a dictionary:
foreach(var (key, value) in new Dictionary<int, string> { [1] = "val1", [2] = "val2" })
{
//TODO: Do something
}
KeyValuePair<TKey,TValue>.Deconstruct(TKey, TValue)
is available only from netstandard2.1
. For previous netstandard
versions we need to add it manually using the approach with the extension method.
Custom awaitable types 🔗︎
C# 5 (released together with Visual Studio 2012) introduced an async/await
mechanism which was a real game-changer in the area of asynchronous programming. Before that, handling invocation of asynchronous methods resulted very often in quite a messy code, especially when there was more than one asynchronous invocation:
void DoSomething()
{
DoSomethingAsync().ContinueWith((task1) => {
if (task1.IsCompletedSuccessfully)
{
DoSomethingElse1Async(task1.Result).ContinueWith((task2) => {
if (task2.IsCompletedSuccessfully)
{
DoSomethingElse2Async(task2.Result).ContinueWith((task3) => {
//TODO: Do something
});
}
});
}
});
}
private Task<int> DoSomethingAsync() => throw new NotImplementedException();
private Task<int> DoSomethingElse1Async(int i) => throw new NotImplementedException();
private Task<int> DoSomethingElse2Async(int i) => throw new NotImplementedException();
With async/await
syntax this can be written in a much cleaner way:
async Task DoSomething()
{
var res1 = await DoSomethingAsync();
var res2 = await DoSomethingElse1Async(res1);
await DoSomethingElse2Async(res2);
}
This might be surprising, but the await
keyword is not reserved to work only with the Task
type. It can be used with any type which contains a method called GetAwaiter
and returns type that meets the following requirement:
- Implements the
System.Runtime.CompilerServices.INotifyCompletion
interface withvoid OnCompleted(Action continuation)
method. - Contains the
IsCompleted
boolean property. - Contains the parameter-less
GetResult
method .
To add support of await
keyword to the custom type we need to define GetAwaiter
method that returns an instance of TaskAwaiter<TResult>
or a custom type that meets aforementioned conditions:
class CustomAwaitable
{
public CustomAwaiter GetAwaiter() => throw new NotImplementedException();
}
class CustomAwaiter: INotifyCompletion
{
public void OnCompleted(Action continuation) => throw new NotImplementedException();
public bool IsCompleted => => throw new NotImplementedException();
public void GetResult() => throw new NotImplementedException();
}
You may wonder what could be a possible scenario of using await
syntax with custom awaitable type. If that’s the case, I highly recommend reading the article from Stephen Toub entitled “await anything” which provides plenty of interesting examples.
The query expression pattern 🔗︎
The best invention of C# 3.0 was definitely Language-Integrated Query
, known as LINQ
, which allows for collection manipulation using SQL-like syntax.LINQ
comes in two variations: SQL-like syntax and Extension method syntax. I prefer the second one because in my opinion it is more readable, but it’s probably because I’m accustomed to it. An interesting fact about the SQL-like syntax is that it is translated into the Extension method syntax during the compilation because it’s a C#, not CLR
feature. LINQ
was invented in the first place to work with IEnumerable
, IEnumerable<T>
and IQueryable<T>
types but it’s not limited to them, we can use it with any type that meets requirements of query expression pattern. The complete set of method signatures used by LINQ
looks as follows:
class C
{
public C<T> Cast<T>();
}
class C<T> : C
{
public C<T> Where(Func<T,bool> predicate);
public C<U> Select<U>(Func<T,U> selector);
public C<V> SelectMany<U,V>(Func<T,C<U>> selector, Func<T,U,V> resultSelector);
public C<V> Join<U,K,V>(C<U> inner, Func<T,K> outerKeySelector, Func<U,K> innerKeySelector, Func<T,U,V> resultSelector);
public C<V> GroupJoin<U,K,V>(C<U> inner, Func<T,K> outerKeySelector, Func<U,K> innerKeySelector, Func<T,C<U>,V> resultSelector);
public O<T> OrderBy<K>(Func<T,K> keySelector);
public O<T> OrderByDescending<K>(Func<T,K> keySelector);
public C<G<K,T>> GroupBy<K>(Func<T,K> keySelector);
public C<G<K,E>> GroupBy<K,E>(Func<T,K> keySelector, Func<T,E> elementSelector);
}
class O<T> : C<T>
{
public O<T> ThenBy<K>(Func<T,K> keySelector);
public O<T> ThenByDescending<K>(Func<T,K> keySelector);
}
class G<K,T> : C<T>
{
public K Key { get; }
}
Of course, we don’t need to implement all of those methods to use LINQ
syntax with our custom type. The list of LINQ
operators and methods required for them can be found here. LINQ
syntax with custom types is very often used to implement monads. A really good explanation how to do it can be found in the article Understand monads with LINQ by Miłosz Piechocki
Enumerate everything (UPDATE: 2022-01-25) 🔗︎
In C# 9, the foreach
statement was extended to lookup for GetEnumerator()
method also among the extension methods. More details about this feature can be found on MSDN Extension GetEnumerator support for foreach loops.. Thanks to that, we can create an extension method that expands on the fly any type into a collection which can be easily enumerated with foreach
. Here’s an interesting example from Oleg Kyrylchuk tweet
public static IEnumerator<int> GetEnumerator(this Range range)
{
if (range.Start.IsFromEnd)
{
for (var i = range.Start.Value; i >= range.End.Value; i--)
{
yield return i;
}
}
else
{
for (var i = range.Start.Value; i <= range.End.Value; i++)
{
yield return i;
}
}
}
and sample usage can looks as follows:
foreach(var i in 4..6)
{
Console.WriteLine(i);
}
Another interesting example could be enumerator for tuple of DateOnly
structs that allows us to enumerate through the date range:
public static IEnumerator<DateOnly> GetEnumerator(this (DateOnly from, DateOnly to) range)
{
var currentDate = range.from;
while(currentDate <= range.to)
{
yield return currentDate;
currentDate = currentDate.AddDays(1);
}
}
// Usage
foreach(var date in (from: new DateOnly(2022,6,1), to: new DateOnly(2022,6,7)))
{
}
Summary 🔗︎
The purpose of this article was not to encourage you to abuse those syntax tricks but rather to demystify them. On the other hand, they should not be completely avoided. They were invented to be used, and sometimes they can make your code much cleaner. If you are afraid that the resulting code might not be so obvious to your teammates, you should find a way to share your knowledge or at least link to this article ;) I’m not sure if it’s a complete set of those “magical methods” - if you know any others, I would appreciate you sharing that in the comment section below.