C# Magical Syntax
Introduction
This post is an update to an old one on my old blog and it was inspired by my latest posts on C# versus Java.
You may not have realized that some of the patterns that you’ve been using for ages in your C# programs are based on conventions, rather than on a specific API. What I mean by this is, some constructs in C# are based on some magical methods with well-defined names which are not defined in any base class or interface, but yet just work. Let’s see what they are.
Enumerating Collections
You are probably used to iterating through collections using the foreach statement. If you are, you may know that foreach actually wraps a call to a method called GetEnumerator, like the one that is defined by the IEnumerable and IEnumerable<T> interfaces. Thus, you might think, the magic occurs because the collection implements one or the two, but you would be wrong: it turns out that in order to iterate through a class using foreach all it takes is that the class exposes a public method called GetEnumerator that returns either a IEnumerator or a IEnumerator<T> instance. For example, this works:
public class Enumerable
{
public IEnumerator GetEnumerator()
{
yield return 1;
yield return 2;
yield return 3;
}
}
var e = new Enumerable();
foreach (int i in e) { /* 1, 2, 3 */ }
The specification can be found here.
Deconstruction to Tuples
Tuples were introduced in C# 7. In a nutshell, they provide a way for us to return multiple values from a method, for example:
(int x, int y) GetPosition() { return (x: 10, y: 20); }
Another option is to have a class deconstructed into a tuple. Say, for example, that we have a class like this:
public class Rectangle
{
public int Height { get; set; }
public int Width { get; set; }
}
We can have it deconstructed into a tuple, by providing one or more Deconstruct methods in this class:
public void Deconstruct(out int h, out int w)
{
h = this.Height;
w = this.Width;
}
Which allows you to write code like this:
var rectangle = new Rectangle { Height = 10, Width = 20 };
var (h, w) = rectangle;
You can implement multiple Deconstruct methods with different parameters, which must always be out. When you try to assign your class to a tuple, C# will try to find a Deconstruct method that matches the tuple’s declaration, or throw an exception if one cannot be found:
public void Deconstruct(out int perimeter, out int area, out bool square)
{
perimeter = this.Width * 2 + this.Height * 2;
area = this.Width * this.Height;
square = this.Width == this.Height;
}
var (perimeter, area, square) = rectangle;
Collection Initialization
Since C# 6, we have a more concise syntax for initializing collections:
var strings = new List<string> { "A", "B", "C" };
The syntax to follow is an enumeration of items whose type matches the collection’s item type, inside curly braces, each separated by a comma. This is possible because there is a public Add method that takes a parameter of the appropriate type. What happens behind the scene is that the Add method is called multiple times, one for each item inside the curly braces. Meaning, this works too:
public class Collection : IEnumerable
{
public IEnumerator GetEnumerator() => /* ... */
public void Add(string s) { /* ... */ }
}
Notice that this class does offer a public Add method but still needs to implement either IEnumerable or IEnumerable<T> , which, mind you, do not define an Add method.
Having this, we can write:
var col = new Collection { "A", "B", "C" };
The magical Add method can have multiple parameters, like for dictionaries (IDictionary, IDictionary<TKey, TValue> or any class implementing it), or any other method whose Add method takes two parameters:
var dict = new Dictionary<string, int> { { "A", 1 }, { "B", 2 }, { "C", 3 } };
Each parameter will need to go inside it’s own set of curly braces.
What’s even funnier is, you can mix different Add method overloads with different parameters, the right one will be called depending on the current value:
public void Add(int i) { /* ... */ }
public void Add(string s) { /* ... */ }
var col = new Collection { 1, 2, 3, "a", "b", "c" };
Dictionaries have its own special syntax too:
var dict = new Dictionary<string, int>
{
["A"] = 1,
["B"] = 2,
["C"] = 3
};
Whichever you prefer is really up to you, they do exactly the same, this one is probably neater for dictionaries.
Finalizers
This is an oldie and probably (hopefully) everybody knows about it. In .NET, all classes ultimately inherit from Object, which means all of them have a destructor method, which is called Finalize. Normally, in the (rare) cases where we need to override it - only in classes, not in structs -, it should be called ~MyClass. For example:
public class MyClass
{
~MyClass()
{
//do cleanup
}
}
We don't need to call the base Finalize method, as it's always done implicitly. To learn more, visit this page.
Operator Overloading
In C# it is possible to overload (I'd say override) some operators and to define cast (conversion) operators too. For example, you can make some class MyClass behave like a bool, or modify the equality, inequality operators.
Some examples:
public class MyClass
{
private bool _someInternalField;
public static explicit operator bool(MyClass mc)
{
return mc._someInternalField;
}
}
Cast operators can be explicit or implicit, meaning, you may need or not the cast syntax; in this example, it is explicit. For implicit operators, it's as simple as this:
MyClass c = new MyClass();
bool b = c; //if using implicit
Whereas for explicit:
MyClass c = new MyClass();
bool b = (bool) c;
The list of operators that can be overriden is (source):
Operators | Notes |
---|---|
+x, -x, !x, ~x, ++, --, true, false | The true and false operators must be overloaded together |
x + y, x - y, x * y, x / y, x % y, x & y, x | y, x ^ y, x << y, x >> y, x >>> y | |
x == y, x != y, x < y, x > y, x <= y, x >= y | Must be overloaded in pairs as follows: = and != , < and > , <= and >= |
For example, if we want to override the + operator:
public class Complex(int Real, int Imaginary)
{
public static Complex operator + (Complex c1, Complex c2) => new(c1.Real + c2.Real, c1.Imaginary + c2.Imaginary);
}
Complex c1 = new(10, 0);
Complex c2 = new(5, 5);
Complex sum = c1 + c2;
Beware, when you start overriding the standard operators you can end up with a very weird syntax, also, you should respect what the operator is supposed to do, e.g., the equality (==) and inequality (!=) operators should indeed check if the instancer are "equal" or "different", according to well defined rules.
For a complete list of overridable operators and examples, see this.
Custom Await
You're probably used to await on Task objects, but, in reality, you may be surprised to find that you can await on any class that offers an GetAwaiter method with the following characteristics:
- Implements the INotifyCompletion interface
- Have a public IsCompleted boolean property similar to Task.IsCompleted
- Contains a public GetResult method that takes no parameters similar to TaskAwaiter.GetResult for void return or TaskAwaiter<T>.GetResult for some return type (generic version)
An example:
public class CustomVoidAwaitable
{
public CustomVoidAwaiter GetAwaiter() => throw new NotImplementedException();
}
public class CustomVoidAwaiter : INotifyCompletion
{
public void OnCompleted(Action continuation) => throw new NotImplementedException();
public bool IsCompleted => throw new NotImplementedException();
public void GetResult() => throw new NotImplementedException();
}
public class CustomAwaitable<T>
{
public CustomAwaiter<T> GetAwaiter() => throw new NotImplementedException();
}
public class CustomAwaiter<T> : INotifyCompletion
{
public void OnCompleted(Action continuation) => throw new NotImplementedException();
public bool IsCompleted => throw new NotImplementedException();
public T GetResult() => throw new NotImplementedException();
}
Here's how to use them:
CustomVoidAwaitable ca = ...;
await ca;
CustomAwaitable<string> csa = ...;
var result = await csa;
Read more about this feature here. As an example, this is how the ValueTask<T> is implemented!
Custom Query Pattern
And did you know that you can use C# query syntax on any class? It's just a matter of implementing the standard operators:
- Cast
- GroupBy
- GroupJoin
- Join
- Order
- OrderBy
- OrderByDescending
- Select
- SelectMany
- ThenBy
- ThenByDescending
- Where
Note: .NET 9/C# 13 added a few more operations.
An example:
class CustomQueryable
{
public CustomQueryable<T> Cast<T>() => throw new NotImplementedException();
}
class CustomQueryable<T> : CustomQueryable
{
public CustomQueryable<T> Where(Func<T,bool> predicate) => throw new NotImplementedException();
public CustomQueryable<U> Select<U>(Func<T,U> selector) => throw new NotImplementedException();
public CustomQueryable<V> SelectMany<U,V>(Func<T,CustomQueryable<U>> selector, Func<T,U,V> resultSelector) => throw new NotImplementedException();
public CustomQueryable<V> Join<U,K,V>(CustomQueryable<U> inner, Func<T,K> outerKeySelector, Func<U,K> innerKeySelector, Func<T,U,V> resultSelector) => throw new NotImplementedException();
public CustomQueryable<V> GroupJoin<U,K,V>(CustomQueryable<U> inner, Func<T,K> outerKeySelector, Func<U,K> innerKeySelector, Func<T,CustomQueryable<U>,V> resultSelector) => throw new NotImplementedException();
public CustomOrderedQueryable<T> OrderBy<K>(Func<T,K> keySelector) => throw new NotImplementedException();
public CustomOrderedQueryable<T> OrderByDescending<K>(Func<T,K> keySelector) => throw new NotImplementedException();
public CustomQueryable<CustomGroup<K,T>> GroupBy<K>(Func<T,K> keySelector) => throw new NotImplementedException();
public CustomQueryable<CustomGroup<K,E>> GroupBy<K,E>(Func<T,K> keySelector, Func<T,E> elementSelector) => throw new NotImplementedException();
}
class CustomOrderedQueryable<T> : CustomQueryable<T>
{
public CustomOrderedQueryable<T> ThenBy<K>(Func<T,K> keySelector) => throw new NotImplementedException();
public CustomOrderedQueryable<T> ThenByDescending<K>(Func<T,K> keySelector) => throw new NotImplementedException();
}
class CustomGroup<K,T> : CustomQueryable<T>
{
public K Key { get; } => throw new NotImplementedException();
}
Now you can perform queries in the usual way, provided you implement all the methods that are returning NotImplementedException, of course:
CustomQueryable<T> Get() { ... }
var query = from c in myObj.Get<string>()
where c.Length > 0
orderby c
select c;
Have a look here for more info.
Conclusion
As you can see, C# offers some special syntax that should be known by all developers. It even allows us to write very bizarre code, so better know what we're doing! Stay up to date with the recent C# language changes!
Comments
Post a Comment