Nine “black magic” in “C”

Time:2021-1-16

We know thatC#It’s a very advanced language, because it’s a visionary “grammar sugar.”. Sometimes these “grammar sugars” are too easy to use, leading some people to think it isC#There is no reason for compilers to write dead things – it’s a bit like “black magic”.

Then we can have a lookC#These high-level language functions, is the compiler to write dead things (“black magic”), or can be extended (SAO operation) “duck type”.

Let me list a directory first. You can try to judge whether it is “black magic” (compiler dead) or “duck type” (you can customize “operation”):

  1. LINQ operation, and IEnumerable < T > type;
  2. Async / await, and task / valuetask type;
  3. Expression tree, and expression < T > type;
  4. Interpolation string, and formattablestring type;
  5. Yield return, and IEnumerable < T > type;
  6. Foreach loop, and IEnumerable < T > type;
  7. Using keyword, interface with IDisposable;
  8. T? And nullable < T > types;
  9. Any type of index / range generic operation.

1. LINQOperation, andIEnumerable<T>type

It’s not “dark magic,” it’s “duck type.”.

LINQyesC# 3.0The new function of publishing, can operate data very conveniently. Now?12Years later, although some functions need to be enhanced, they are much more convenient than other languages.

As I mentioned in my last blog,LINQIt doesn’t have to be based onIEnumerable<T>Only one type needs to be defined to implement the requiredLINQExpression,LINQOfselectKeyword, will call.SelectMethods: the following “operation” can be used to achieve the effect of “grafting”


void Main()
{
 var query = 
  from i in new F()
  select 3;
  
 Console.WriteLine(string.Join(",", query)); // 0,1,2,3,4
}

class F
{
 public IEnumerable<int> Select<R>(Func<int, R> t)
 {
  for (var i = 0; i < 5; ++i)
  {
   yield return i;
  }
 }
}

2. async/await, andTask/ValueTasktype

It’s not “dark magic,” it’s “duck type.”.

async/awaitPublished onC# 5.0It is very convenient to do asynchronous programming, and its essence is state machine.

async/awaitThe essence is to look for the type of next nameGetAwaiter()Must return an interface inherited fromINotifyCompletionorICriticalNotifyCompletionClass, which also needs to be implementedGetResult()Methods andIsCompleteProperty.

  • First, call the t. getawaiter () method to get the wait a;
  • Call a. iscompleted to get boolean type B;
  • If B = true, a. getresult() is executed immediately to get the running result;
  • If B = false, it depends:

If a does not implement icriticalnotifycompletion, execute (a as inotifycompletion). Oncompleted (action)
If a implements icriticalnotifycompletion, execute (a as icriticalnotifycompletion). Oncompleted (action)
The execution is then suspended, and once oncompleted is completed, it returns to the state machine;

Interested can visitGithubSpecific specifications: https://github.com/dotnet/csharplang/blob/master/spec/expressions.md

normalTask.Delay()It’s based onThread pool timerCan use the following “Sao operation” to achieve a single threadTaskEx.Delay()

static Action Tick = null;

void Main()
{
 Start();
 while (true)
 {
  if (Tick != null) Tick();
  Thread.Sleep(1);
 }
}

async void Start()
{
 Console.WriteLine (the "commencement of execution");
 for (int i = 1; i <= 4; ++i)
 {
  Console.WriteLine ($"the {I} th time:{ DateTime.Now.ToString ("HH: mm: SS")} - thread number:{ Thread.CurrentThread.ManagedThreadId }");
  await TaskEx.Delay(1000);
 }
 Console.WriteLine (the "execution completed");
}

class TaskEx
{
 public static MyDelay Delay(int ms) => new MyDelay(ms);
}

class MyDelay : INotifyCompletion
{
 private readonly double _start;
 private readonly int _ms;
 
 public MyDelay(int ms)
 {
  _start = Util.ElapsedTime.TotalMilliseconds;
  _ms = ms;
 }
 
 internal MyDelay GetAwaiter() => this;
 
 public void OnCompleted(Action continuation)
 {
  Tick += Check;
  
  void Check()
  {
   if (Util.ElapsedTime.TotalMilliseconds - _start > _ms)
   {
    continuation();
    Tick -= Check;
   }
  }
 }

 public void GetResult() {}
 
 public bool IsCompleted => false;
}

The operation effect is as follows:

Execution begins
The first time, time: 17:38:03 – thread number: 1
The second time, time: 17:38:04 – thread number: 1
The third time, time: 17:38:05 – thread number: 1
The fourth time, time: 17:38:06 – thread number: 1
Execution complete

Note that it doesn’t have to be usedTaskCompletionSource<T>To create a definedasync/await

3. Expression tree, andExpression<T>type

It’s “black magic”, there’s no “operation space”, only if the type is “black magic”Expression<T>Is created as an expression tree.

Expression treeyesC# 3.0along withLINQIt’s a farsighted “dark magic” to publish together.

Such as the following code:


Expression<Func<int>> g3 = () => 3;

Will be translated by the compiler as:


Expression<Func<int>> g3 = Expression.Lambda<Func<int>>(
 Expression.Constant(3, typeof(int)), 
 Array.Empty<ParameterExpression>());

4. Interpolation string, andFormattableStringtype

It’s “black magic”. There’s no “operating space.”.

Interpolation stringPublished onC# 6.0Before that, many languages have provided similar functions.

Only if the type isFormattableString, will produce different compilation results, such as the following code:


FormattableString x1 = $"Hello {42}";
string x2 = $"Hello {42}";

The compiler generates the following results:


FormattableString x1 = FormattableStringFactory.Create("Hello {0}", 42);
string x2 = string.Format("Hello {0}", 42);

Notice that the essence of this is to callFormattableStringFactory.CreateTo create a type.

5. yield return, andIEnumerable<T>Type;

It’s “black magic,” but there’s a supplement.

yield returnExcept forIEnumerable<T>In addition, it can also be used forIEnumerableIEnumerator<T>IEnumerator

So if you want to useC#To simulateC++/JavaOfgenerator<T>It will be relatively simple:


var seq = GetNumbers();
seq.MoveNext();
Console.WriteLine(seq.Current); // 0
seq.MoveNext();
Console.WriteLine(seq.Current); // 1
seq.MoveNext();
Console.WriteLine(seq.Current); // 2
seq.MoveNext();
Console.WriteLine(seq.Current); // 3
seq.MoveNext();
Console.WriteLine(seq.Current); // 4

IEnumerator<int> GetNumbers()
{
 for (var i = 0; i < 5; ++i)
  yield return i;
}

yield return——Iterators published inC# 2.0

6. foreachCycle, andIEnumerable<T>type

It’s “duck type” and has “operation space”.

foreachIt doesn’t have to be used togetherIEnumerable<T>Type, as long as the object existsGetEnumerator()The method is as follows


void Main()
{
 foreach (var i in new F())
 {
  Console.Write(i + ", "); // 1, 2, 3, 4, 5, 
 }
}

class F
{
 public IEnumerator<int> GetEnumerator()
 {
  for (var i = 0; i < 5; ++i)
  {
   yield return i;
  }
 }
}

In addition, if the object implementsGetAsyncEnumerator(), or even the sameawait foreachAsynchronous loop:


async Task Main()
{
 await foreach (var i in new F())
 {
  Console.Write(i + ", "); // 1, 2, 3, 4, 5, 
 }
}

class F
{
 public async IAsyncEnumerator<int> GetAsyncEnumerator()
 {
  for (var i = 0; i < 5; ++i)
  {
   await Task.Delay(1);
   yield return i;
  }
 }
}

await foreachyesC# 8.0along withAsynchronous flowFor details, see “code demo C # new functions of various versions” I wrote before.

7. usingKeywords, andIDisposableInterface

Yes, and No.

reference typeAnd normalValue typeuseusingKeyword, must be based onIDisposableInterface.

butref structandIAsyncDisposableIt’s another story, becauseref structIt is not allowed to move freely, but the reference type, managed heap, will allow memory to move, soref structAnd not allowedreference typeIf there is any relationship, it includes inheritanceInterface——BecauseInterfaceYesreference type

But the need to release resources still exists. What should we do? When the “duck type” comes, you can write oneDispose()Method without inheriting any interfaces:

void S1Demo()
{
 using S1 s1 = new S1();
}

ref struct S1
{
 public void Dispose()
 {
  Console.WriteLine ("normal release");
 }
}

The same thing, if you use itIAsyncDisposableInterface:

async Task S2Demo()
{
 await using S2 s2 = new S2();
}

struct S2 : IAsyncDisposable
{
 public async ValueTask DisposeAsync()
 {
  await Task.Delay(1);
  Console.WriteLine ("async release");
 }
}

8. T?, andNullable<T>type

It’s “black magic,” onlyNullable<T>To acceptT?Nullable<T>As aValue typeIt can also accept directlynullValue (normal)Value typeNot allowed to acceptnullValue).

The sample code is as follows:


int? t1 = null;
Nullable<int> t2 = null;
int t3 = null; // Error CS0037: Cannot convert null to 'int' because it is a non-nullable value type

The generated code is as follows(int?AndNullable<int>Exactly the same, skipping the compilation failure code)


IL_0000: nop
IL_0001: ldloca.s 0
IL_0003: initobj valuetype [System.Runtime]System.Nullable`1<int32>
IL_0009: ldloca.s 1
IL_000b: initobj valuetype [System.Runtime]System.Nullable`1<int32>
IL_0011: ret

9. Any type ofIndex/RangeGeneric operation

There are “black magic” and “duck type” – there is operating space.

Index/RangePublished onC# 8.0It can be likePythonSo it is convenient to operate the index position and get the corresponding value. Previously calledSubstringAnd other complex operations, now very simple.


string url = "https://www.super-cool.com/product/7705a33a-4d2c-455d-a42c-c95e6ac8ee99/summary";
string productId = url[35..url.LastIndexOf("/")];
Console.WriteLine(productId);

The generated code is as follows:


string url = "https://www.super-cool.com/product/7705a33a-4d2c-455d-a42c-c95e6ac8ee99/amd-r7-3800x";
int num = 35;
int length = url.LastIndexOf("/") - num;
string productId = url.Substring(num, length);
Console.WriteLine(productId); // 7705a33a-4d2c-455d-a42c-c95e6ac8ee99

soC#Compiler ignoredIndex/RangeTo callSubstringIt’s too late.

But arrays are different


var range = new[] { 1, 2, 3, 4, 5 }[1..3];
Console.WriteLine(string.Join(", ", range)); // 2, 3

The generated code is as follows:


int[] range = RuntimeHelpers.GetSubArray<int>(new int[5]
{
 1,
 2,
 3,
 4,
 5
}, new Range(1, 3));
Console.WriteLine(string.Join<int>(", ", range));

So it’s actually createdRangeType, and then called.RuntimeHelpers.GetSubArray<int>It’s all black magic.

But it’s also a duck type, as long as it’s implemented in the codeLengthAttributes andSlice(int, int)MethodIndex/Range


var range2 = new F()[2..];
Console.WriteLine(range2); // 2 -> -2

class F
{
 public int Length { get; set; }
 public IEnumerable<int> Slice(int start, int end)
 {
  yield return start;
  yield return end;
 }
}

The generated code is as follows:


F f = new F();
int length2 = f.Length;
length = 2;
num = length2 - length;
string range2 = f.Slice(length, num);
Console.WriteLine(range2);

summary

As you can see,C#There are a lot of “black magic”, but there are also many “duck types”, and the “operation space” of “Sao operation” is very large.

It is said thatC# 9.0The ancestor of duck type will be added——Type ClassesBy that time, the “operation space” will be bigger than now. I’m looking forward to it!

This article about the nine “black magic” in c#is introduced here. For more related c#black magic content, please search the previous articles of developer or continue to browse the following related articles. I hope you can support developer more in the future!