Details in C#that you don't know

Details in C#that you don't know

There is a thing called a duck type. The so-called duck type means that as long as a thing behaves like a duck, then it can be launched. It is a duck.

最后更新 3/29/2022 8:23 AM
hez2010
预计阅读 8 分钟
分类
.NET
标签
.NET C#

preface

There is a thing called a duck type. The so-called duck type means that as long as a thing behaves like a duck, then it can be launched. It is a duck.

There are actually many duck-type things hidden in C#, but many developers don't know about them, so they can't make good use of these things. So today I will count these details hidden in the compiler in detail.

Task and ValueTask are not the only ones that can await

When writing asynchronous code in C#, we often choose to include the asynchronous code in a Task or ValueTask, so that callers can implement asynchronous calls in an await manner.

Sikasi, Task and ValueTask are not the only ones who can wait. Tasks and ValueTasks are clearly scheduled by the thread pool, but why is C#async/await called coroutine?

Because what you want is not necessarily Task/ValueTask, in C#, as long as your class contains the GetAwaiter () method and the bool IsCompleted attribute, and the thing returned by GetAwaiter () contains a GetResult () method, a bool IsCompleted attribute, and implements INotifyCompletion, then the object of this class is awayable.

Therefore, when encapsulating I/O operations, we can implement an Awaiter ourselves, which is based on the underlying epoll/IOCP implementation, so that when waiting, no threads will be created and no thread scheduling will occur, but directly relinquish control. After completing the I/O call, the OS notifies the user to complete the asynchronous call through CompletionPort (Windows), etc., and at this time, the context is restored to continue executing the remaining logic. This is actually a real stackless coroutine.

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

public class MyAwaiter<T> : 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<int>();
        await obj;
    }
}

In fact, the asynchronous API related to I/O in. NET Core does do this. There will be no thread allocation waiting results during the I/O operation. It is a coroutine operation: directly after the I/O operation starts, relinquish control until the I/O operation is completed. The reason why you sometimes find that threads change before and after await is just because the Task itself is scheduled.

The IAsyncAction/IAsyncOperation used in UWP development comes from the underlying encapsulation, which has nothing to do with Task but is awaitable. If you develop UWP in C ++/WinRT, the methods to return these interfaces are also co_awaitable.

Not only IEnumerable and IEnumerator can be foreached

Often we would write the following code:

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

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

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

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

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

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

Not only IAsyncEnumerable and IAsyncEnumerator are the only ones that can be awaited foreach

Same as above, but this time the requirements have changed, GetEnumerator () and MoveNext () have changed to GetAsyncEnumerator () and MoveNextAsync ().

The thing returned by MoveNextAsync () should be an Awaitable. As for what this Awaitable is, it can be Task/ValueTask, it can be something else or something you implement yourself.

class MyAsyncEnumerator<T>
{
    public T Current { get; private set; }
    public MyTask<bool> MoveNextAsync()
    {
        throw new NotImplementedException();
    }
}

class MyAsyncEnumerable<T>
{
    public MyAsyncEnumerator<T> GetAsyncEnumerator()
    {
        throw new NotImplementedException();
    }
}

class Program
{
    public static async Task Main()
    {
        var x = new MyAsyncEnumerable<int>();
        await foreach (var i in x)
        {
            // ......
        }
    }
}

How to implement IDisposable ref struct

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

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

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

Slicing is not the only way to use Range

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

You can use this feature as long as your class can be counted (with the Length or Count attribute) and sliced (with a Slice (int, int) method).

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 one that can use indexes

C#8 introduces Indexes for indexing, such as using ^1 to index the penultimate element, but you don't have to provide an indexer that accepts parameters of type Index to use this feature.

You can use this feature as long as your class can be counted (with the Length or Count attribute) and indexed (with an indexer that accepts the int parameter).

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];
    }
}

Deconstructing types

How to deconstruct a type? In fact, you only need to write a method named Deconstruct (), and all parameters are 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;
    }
}

LINQ is not the only way you can use IEnumerable

LINQ is an integrated query language commonly used in C#that allows 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 have to implement IEnumerable. In fact, as long as there is an extension method with the corresponding name, it is enough. For example, if you have a method called Select, you can use select, and if you have a method called Where, you can use where.

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

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

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

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

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

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

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

        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;
    }
}
Keep Exploring

延伸阅读

更多文章
同分类 / 同标签 4/22/2026

Support for. NET by operating system versions (250707 update)

Use virtual machines and test machines to test the support of each version of the operating system for. NET. After installing the operating system, it is passed by measuring the corresponding running time of the installation and being able to run the Stardust Agent.

继续阅读
同分类 / 同标签 2/7/2026

Summary of experience in using AOT

From the very beginning of project creation, you should develop a good habit of conducting AOT release testing in a timely manner whenever new features are added or newer syntax is used.

继续阅读