Unicorn in C#

Unicorn in C#

C # has a lot of unicorn stuff hidden away, but many developers don’t know it, so they can’t make good use of these things. So today I will count the details hidden in the compiler.

Not only Task and ValueTask can await

When writing asynchronous code in C #, we often choose to include asynchronous code in a Task or ValueTask so that the caller can use wait to implement asynchronous calls.

not only Task and ValueTask can await. Task and ValueTask are scheduled by the thread pool, but why is async / await called a coroutine in C #?

Since what you are awaiting is not necessarily Task / ValueTask, in C # if your class contains a GetAwaiter () method and a bool IsCompleted property, and what GetAwaiter () returns contains a GetResult () method, the IsCompleted property is of type bool and implementing INotifyCompletion, you can await objects of that class.

Therefore, when encapsulating I / O operations, we can implement Awaiter ourselves, which is based on the underlying epoll / IOCP implementation, so that await will not create a thread and no thread scheduling will take place. take control directly. Upon completion of the I / O call, the OS notifies the user mode of the completion of the asynchronous call via CompletionPort (Windows), etc. At this time, the context is restored to continue executing the remaining logic, which is a real coroutine without a stack.

public class MyTask
{
    public MyAwaiter GetAwaiter()
    {
        return new MyAwaiter();
    }
}

public class MyAwaiter : INotifyCompletion
{
    public bool IsCompleted { get; private set; }
    public T GetResult()
    {
        throw new NotImplementedException();
    }
    public void OnCompleted(Action continuation)
    {
        throw new NotImplementedException();
    }
}

public class Program
{
    static async Task Main(string[] args)
    {
        var obj = new MyTask();
        await obj;
    }
}

The asynchronous API related to I/O in .NET Core does this. During the I/O operation, there will not be any thread allocation waiting for the result. They are all coroutine operations: after the I/O operation starts. Give up control directly until the I/O operation is completed. And the reason why sometimes you find that the thread changes before and after await is just because the Task itself is scheduled.

The IAsyncAction/IAsyncOperation used in UWP development comes from the underlying package, which has nothing to do with Task but can be awaited, and if UWP is developed with C++/WinRT, the methods that return these interfaces can also be co_awaited.

Not only IEnumerable and IEnumerator can be foreach

Often we will write the following code:

foreach (var i in list)
{
// ......
}

Then when you ask why you can foreach, most of them will reply because this list implements IEnumerable or IEnumerator.

But in fact, if you want an object to be foreach, you only need to provide a GetEnumerator() method, and the object returned by GetEnumerator() contains a bool MoveNext() method plus a Current property.

class MyEnumerator
{
    public T Current { get; private set; }
    public bool MoveNext()
    {
        throw new NotImplementedException();
    }
}

class MyEnumerable
{
    public MyEnumerator GetEnumerator()
    {
        throw new NotImplementedException();
    }
}

class Program
{
    public static void Main()
    {
        var x = new MyEnumerable();
        foreach (var i in x)
        {
            // ......
        }
    }
}

How do ref struct implement IDisposable

As we all know, ref struct must be on the stack and cannot be boxed, so it cannot implement the interface, but if you have a void Dispose() in your ref struct, you can use the using syntax to achieve automatic destruction of the object.

ref struct MyDisposable
{
    public void Dispose() => throw new NotImplementedException();
}

class Program
{
    public static void Main()
    {
        using var y = new MyDisposable();
        // ......
    }
}

Not only Range can use Slice

C# 8 introduced Ranges, allowing slicing operations, but in fact, it is not necessary to provide an indexer that receives Range type parameters to use this feature.

As long as your class can be counted (has a Length or Count property), and can be sliced (has a Slice(int, int) method), then you can use this feature.

class MyRange
{
    public int Count { get; private set; }
    public object Slice(int x, int y) => throw new NotImplementedException();
}

class Program
{
    public static void Main()
    {
        var x = new MyRange();
        var y = x[1..];
    }
}

Index is not the only way to use index

C# 8 introduced Indexes for indexing, such as using ^1 to index the last element, but it is not necessary to provide an indexer that receives an Index type parameter to use this feature.

As long as your class can be counted (have Length or Count properties) and indexed (have an indexer that accepts an int parameter), then you can use this feature.

class MyIndex
{
    public int Count { get; private set; }
    public object this[int index]
    {
        get => throw new NotImplementedException();
    }
}

class Program
{
    public static void Main()
    {
        var x = new MyIndex();
        var y = x[^1];
    }
}

Deconstruct the type

How to deconstruct a type? In fact, you only need to write a method named Deconstruct(), and the parameters are all out.

class MyDeconstruct
{
    private int A => 1;
    private int B => 2;
    public void Deconstruct(out int a, out int b)
    {
        a = A;
        b = B;
    }
}

class Program
{
    public static void Main()
    {
        var x = new MyDeconstruct();
        var (o, u) = x;
    }
}

It’s not only IEnumerable that can use LINQ

LINQ is an integrated query language commonly used in C#, allowing you to write code like this:

from c in list where c.Id > 5 select c;

However, the type of list in the above code does not necessarily have to implement IEnumerable. As long as there is an extension method with the corresponding name, it is enough. For example, if there is a method called Select, you can use select, and if you have a method called Where, you can use where.

class Just : Maybe
{
    private readonly T value;
    public Just(T value) { this.value = value; }

    public override Maybe Select(Func<T, Maybe> f) => f(value);
    public override string ToString() => $"Just {value}";
}

class Nothing : Maybe
{
    public override Maybe Select(Func<T, Maybe> _) => new Nothing();
    public override string ToString() => "Nothing";
}

abstract class Maybe
{
    public abstract Maybe Select(Func<T, Maybe> f);

    public Maybe SelectMany<U, V>(Func<T, Maybe> k, Func<T, U, V> s)
        => Select(x => k(x).Select(y => new Just(s(x, y))));

    public Maybe Where(Func<Maybe, bool> f) => f(this) ? this : new Nothing();
}

class Program
{
    public static void Main()
    {
        var x = new Just(3);
        var y = new Just(7);
        var z = new Nothing();

        var u = from x0 in x from y0 in y select x0 + y0;
        var v = from x0 in x from z0 in z select x0 + z0;
        var just = from c in x where true select c;
        var nothing = from c in x where false select c;
    }
}