Expression Tree Practice: Representing Five Types of Operators in C#

2019年12月15日 66点热度 0人点赞 1条评论
内容目录

Expression Tree Practice: C# Operators

[TOC]

In C#, the arithmetic operators can be categorized into the following types:

  • Arithmetic Operators
  • Relational Operators
  • Logical Operators
  • Bitwise Operators
  • Assignment Operators
  • Other Operators

These operators can be further classified as unary operators, binary operators, and ternary operators based on the number of operands. This article will demonstrate how to operate using expression trees concerning these operators.

The subtypes of Expression for unary and binary operators are as follows:

UnaryExpression; // Unary operation expression
BinaryExpression; // Binary operation expression

1. Arithmetic Operators

Operator Description
+ Adds two operands
- Subtracts the second operand from the first
* Multiplies two operands
/ Divides the numerator by the denominator
% Modulus operator, the remainder after division
++ Increment operator, increases integer value by 1
-- Decrement operator, decreases integer value by 1

+ and Add()

Normal Code

            int a;
            int b;
            a = 100;
            b = 200;
            var ab = a + b;
            Console.WriteLine(ab);

Constructing with Expression Tree

            ParameterExpression a = Expression.Parameter(typeof(int), "a");
            ParameterExpression b = Expression.Parameter(typeof(int), "b");
        // ab = a + b
        BinaryExpression ab = Expression.Add(a, b);

        // Print the value of a + b
        MethodCallExpression method = Expression.Call(null, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(int) }), ab);

        Expression<Action<int, int>> lambda = Expression.Lambda<Action<int, int>>(method, a, b);
        lambda.Compile()(100, 200);

        Console.ReadKey();</code></pre>

If you want to make it a bit more complex, execute it using a block:

            ParameterExpression a = Expression.Parameter(typeof(int), "a");
            ParameterExpression b = Expression.Parameter(typeof(int), "b");
        // Don't forget the assignments
        BinaryExpression aa = Expression.Assign(a, Expression.Constant(100, typeof(int)));
        BinaryExpression bb = Expression.Assign(b, Expression.Constant(200, typeof(int)));

        // ab = a + b
        BinaryExpression ab = Expression.Add(a, b);

        // Print the value of a + b
        MethodCallExpression method = Expression.Call(null, typeof(Console).GetMethod(&quot;WriteLine&quot;, new Type[] { typeof(int) }), ab);

        // Execute the code in block form, equivalent to { }
        // Don't get too concerned about this part, it will be explained in detail later; focus on the above
        var call = Expression.Block(new ParameterExpression[] { a, b }, aa, bb, method);
        Expression&lt;Action&gt; lambda = Expression.Lambda&lt;Action&gt;(call);
        lambda.Compile()();</code></pre>

In the above two examples, the results are computed using expression trees, and the outputs are also printed using expression trees.

The former relies on external values being passed in to assign to a and b, while the latter uses expression trees completely for assignments and operations.

So, how do we execute calculations via expression trees and obtain the results?

            ParameterExpression a = Expression.Parameter(typeof(int), "a");
            ParameterExpression b = Expression.Parameter(typeof(int), "b");
        // ab = a + b
        BinaryExpression ab = Expression.Add(a, b);

        Expression&lt;Func&lt;int, int, int&gt;&gt; lambda = Expression.Lambda&lt;Func&lt;int, int, int&gt;&gt;(ab, a, b);
        int result = lambda.Compile()(100, 200);

        Console.WriteLine(result);
        Console.ReadKey();</code></pre>

The difference lies in how you write Expression.Lambda().

Additionally, using AddChecked() can check for overflow during operations.

- and Subtract()

This corresponds with addition and is not elaborated further here; SubtractChecked() can also check for overflow.

a - b results in 100.

            ParameterExpression a = Expression.Parameter(typeof(int), "a");
            ParameterExpression b = Expression.Parameter(typeof(int), "b");
        // ab = a - b
        BinaryExpression ab = Expression.Subtract(a, b);

        Expression&lt;Func&lt;int, int, int&gt;&gt; lambda = Expression.Lambda&lt;Func&lt;int, int, int&gt;&gt;(ab, a, b);
        int result = lambda.Compile()(200, 100);

        Console.WriteLine(result);</code></pre>

Multiplication, Division, and Modulus

Multiplication

            // ab = a * b
            BinaryExpression ab = Expression.Multiply(a, b);
// ab = 20000

Division

            // ab = a / b
            BinaryExpression ab = Expression.Divide(a, b);
// ab = 2

Modulus (%)

            ParameterExpression a = Expression.Parameter(typeof(int), "a");
            ParameterExpression b = Expression.Parameter(typeof(int), "b");
        // ab = a % b
        BinaryExpression ab = Expression.Modulo(a, b);

        Expression&lt;Func&lt;int, int, int&gt;&gt; lambda = Expression.Lambda&lt;Func&lt;int, int, int&gt;&gt;(ab, a, b);
        int result = lambda.Compile()(200, 150);

// ab = 50
Console.WriteLine(result);
Console.ReadKey();

Increment and Decrement

Increments and decrements come in two models: one is x++ or x--, and the other is ++x or --x.

They both belong to the UnaryExpression type.

Arithmetic Operator Expression Tree Description
x++ Expression.PostIncrementAssign() Postfix
x-- Expression.PostDecrementAssign() Postfix
++x Expression.PreIncrementAssign() Prefix
--x Expression.PreDecrementAssign() Prefix

Easy to remember: Post is postfix, Pre is prefix; Increment means add, Decrement means subtract; Assign relates to assignment (will be discussed later);

Usage of x++ and x--

            int a = 10;
            int b = 10;
            a++;
            b--;
            Console.WriteLine(a);
            Console.WriteLine(b);
            // int a,b;
            ParameterExpression a = Expression.Parameter(typeof(int), "a");
            ParameterExpression b = Expression.Parameter(typeof(int), "b");
        // a = 10, b = 10;
        BinaryExpression setA = Expression.Assign(a, Expression.Constant(10));
        BinaryExpression setB = Expression.Assign(b, Expression.Constant(10));

        // a++
        UnaryExpression aa = Expression.PostIncrementAssign(a);

        // b--
        UnaryExpression bb = Expression.PostDecrementAssign(b);

        //Console.WriteLine(a);
        //Console.WriteLine(b);
        MethodCallExpression callA = Expression.Call(null, typeof(Console).GetMethod(&quot;WriteLine&quot;, new Type[] { typeof(int) }), a);
        MethodCallExpression callB = Expression.Call(null, typeof(Console).GetMethod(&quot;WriteLine&quot;, new Type[] { typeof(int) }), b);

        BlockExpression block = Expression.Block(
            new ParameterExpression[] { a, b },
            setA,
            setB,
            aa,
            bb,
            callA,
            callB
            );

        Expression&lt;Action&gt; lambda = Expression.Lambda&lt;Action&gt;(block);
        lambda.Compile()();

        Console.ReadKey();</code></pre>

If you want to pass parameters from outside, set a and b:

            // int a,b;
            ParameterExpression a = Expression.Variable(typeof(int), "a");
            ParameterExpression b = Expression.Variable(typeof(int), "b");
        // a++
        UnaryExpression aa = Expression.PostIncrementAssign(a);

        // b--
        UnaryExpression bb = Expression.PostDecrementAssign(b);

        //Console.WriteLine(a);
        //Console.WriteLine(b);
        MethodCallExpression callA = Expression.Call(null, typeof(Console).GetMethod(&quot;WriteLine&quot;, new Type[] { typeof(int) }), a);
        MethodCallExpression callB = Expression.Call(null, typeof(Console).GetMethod(&quot;WriteLine&quot;, new Type[] { typeof(int) }), b);

        BlockExpression block = Expression.Block(
            aa,
            bb,
            callA,
            callB
            );

        Expression&lt;Action&lt;int, int&gt;&gt; lambda = Expression.Lambda&lt;Action&lt;int, int&gt;&gt;(block, a, b);
        lambda.Compile()(10, 10);
        Console.ReadKey();</code></pre>

The resulting expression tree is as follows:

.Lambda #Lambda1<System.Action`2[System.Int32,System.Int32]>(
    System.Int32 $a,
    System.Int32 $b) {
    .Block() {
        $a++;
        $b--;
        .Call System.Console.WriteLine($a);
        .Call System.Console.WriteLine($b)
    }
}

To understand Expression.Block(), you can learn here (more will be discussed about Block() later).

            // int a,b;
            ParameterExpression a = Expression.Parameter(typeof(int), "a");
            ParameterExpression b = Expression.Parameter(typeof(int), "b");
            ParameterExpression c = Expression.Variable(typeof(int), "c");
        BinaryExpression SetA = Expression.Assign(a, c);
        BinaryExpression SetB = Expression.Assign(b, c);
        // a++
        UnaryExpression aa = Expression.PostIncrementAssign(a);

        // b--
        UnaryExpression bb = Expression.PostDecrementAssign(b);

        //Console.WriteLine(a);
        //Console.WriteLine(b);
        MethodCallExpression callA = Expression.Call(null, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(int) }), a);
        MethodCallExpression callB = Expression.Call(null, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(int) }), b);

        BlockExpression block = Expression.Block(
            new ParameterExpression[] { a, b },
            SetA,
            SetB,
            aa,
            bb,
            callA,
            callB
            );

        Expression<Action<int>> lambda = Expression.Lambda<Action<int>>(block, c);
        lambda.Compile()(10);

        Console.ReadKey();</code></pre>

Why do we have an extra variable c here? Let's take a look at the generated expression tree

.Lambda #Lambda1	(System.Int32 $c) {
    .Block(
        System.Int32 $a,
        System.Int32 $b) {
        $a = $c;
        $b = $c;
        $a++;
        $b--;
        .Call System.Console.WriteLine($a);
        .Call System.Console.WriteLine($b)
    }
}

Now let's observe the expression tree generated by the following code

            // int a,b;
            ParameterExpression a = Expression.Parameter(typeof(int), "a");
            ParameterExpression b = Expression.Parameter(typeof(int), "b");
        // a++
        UnaryExpression aa = Expression.PostIncrementAssign(a);

        // b--
        UnaryExpression bb = Expression.PostDecrementAssign(b);

        //Console.WriteLine(a);
        //Console.WriteLine(b);
        MethodCallExpression callA = Expression.Call(null, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(int) }), a);
        MethodCallExpression callB = Expression.Call(null, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(int) }), b);

        BlockExpression block = Expression.Block(
            new ParameterExpression[] { a, b },
            aa,
            bb,
            callA,
            callB
            );

        Expression<Action<int, int>> lambda = Expression.Lambda<Action<int, int>>(block, a, b);
        lambda.Compile()(10, 10);
        Console.ReadKey();</code></pre>
.Lambda #Lambda1(
    System.Int32 $a,
    System.Int32 $b) {
    .Block(
        System.Int32 $a,
        System.Int32 $b) {
        $a++;
        $b--;
        .Call System.Console.WriteLine($a);
        .Call System.Console.WriteLine($b)
    }
}

When it comes to prefix increment/decrement, simply follow the example above. However, note that ++x and --x mean "calculate first, then increment/decrement".

II. Relational Operators

==, !=, >, <, >=, <=

The relational operators in C# are as follows:

Operator Description
== Checks if the values of two operands are equal; if so, the condition is true.
!= Checks if the values of two operands are not equal; if not, the condition is true.
> Checks if the value of the left operand is greater than the value of the right operand; if so, the condition is true.
< Checks if the value of the left operand is less than the value of the right operand; if so, the condition is true.
>= Checks if the value of the left operand is greater than or equal to the value of the right operand; if so, the condition is true.
<= Checks if the value of the left operand is less than or equal to the value of the right operand; if so, the condition is true.

== indicates equality comparison; if they are value types or strings, it compares whether the values are the same; if they are reference types, it compares whether the references are equal.

Other relational operators only compare the sizes of value types.

Example Code

            int a = 21;
            int b = 10;
            Console.Write("a == b:");
            Console.WriteLine(a == b);
        Console.Write(&quot;a &lt; b :&quot;);
        Console.WriteLine(a &lt; b);

        Console.Write(&quot;a &gt; b :&quot;);
        Console.WriteLine(a &gt; b);

        // Change the values of a and b 
        a = 5;
        b = 20;

        Console.Write(&quot;a &lt;= b:&quot;);
        Console.WriteLine(a &lt;= b);

        Console.Write(&quot;a &gt;= b:&quot;);
        Console.WriteLine(b &gt;= a);

        Console.ReadKey();</code></pre>

Implementation using Expression Trees

            // int a,b;
            ParameterExpression a = Expression.Parameter(typeof(int), "a");
            ParameterExpression b = Expression.Parameter(typeof(int), "b");
        // a = 21, b = 10;
        BinaryExpression setA = Expression.Assign(a, Expression.Constant(21));
        BinaryExpression setB = Expression.Assign(b, Expression.Constant(20));

        // Console.Write(&quot;a == b:&quot;);
        // Console.WriteLine(a == b);
        MethodCallExpression call1 = Expression.Call(null,
            typeof(Console).GetMethod(&quot;Write&quot;, new Type[] { typeof(string) }),
            Expression.Constant(&quot;a == b:&quot;));
        MethodCallExpression call11 = Expression.Call(null,
            typeof(Console).GetMethod(&quot;WriteLine&quot;, new Type[] { typeof(bool) }),
            Expression.Equal(a, b));

        // Console.Write(&quot;a &lt; b :&quot;);
        // Console.WriteLine(a &lt; b);
        MethodCallExpression call2 = Expression.Call(null,
            typeof(Console).GetMethod(&quot;Write&quot;, new Type[] { typeof(string) }),
            Expression.Constant(&quot;a &lt; b :&quot;));
        MethodCallExpression call22 = Expression.Call(null,
            typeof(Console).GetMethod(&quot;WriteLine&quot;, new Type[] { typeof(bool) }),
            Expression.LessThan(a, b));

        // Console.Write(&quot;a &gt; b :&quot;);
        // Console.WriteLine(a &gt; b);
        MethodCallExpression call3 = Expression.Call(null,
            typeof(Console).GetMethod(&quot;Write&quot;, new Type[] { typeof(string) }),
            Expression.Constant(&quot;a &gt; b :&quot;));
        MethodCallExpression call33 = Expression.Call(null,
            typeof(Console).GetMethod(&quot;WriteLine&quot;, new Type[] { typeof(bool) }),
            Expression.GreaterThan(a, b));

        // Change the values of a and b 
        // a = 5;
        // b = 20;
        BinaryExpression setAa = Expression.Assign(a, Expression.Constant(5));
        BinaryExpression setBb = Expression.Assign(b, Expression.Constant(20));

        // Console.Write(&quot;a &lt;= b:&quot;);
        // Console.WriteLine(a &lt;= b);
        MethodCallExpression call4 = Expression.Call(null,
            typeof(Console).GetMethod(&quot;Write&quot;, new Type[] { typeof(string) }),
            Expression.Constant(&quot;a &lt;= b:&quot;));
        MethodCallExpression call44 = Expression.Call(null,
            typeof(Console).GetMethod(&quot;WriteLine&quot;, new Type[] { typeof(bool) }),
            Expression.LessThanOrEqual(a, b));

        // Console.Write(&quot;a &gt;= b:&quot;);
        // Console.WriteLine(b &gt;= a);
        MethodCallExpression call5 = Expression.Call(null,
            typeof(Console).GetMethod(&quot;Write&quot;, new Type[] { typeof(string) }),
            Expression.Constant(&quot;a &gt;= b:&quot;));
        MethodCallExpression call55 = Expression.Call(null,
            typeof(Console).GetMethod(&quot;WriteLine&quot;, new Type[] { typeof(bool) }),
            Expression.GreaterThanOrEqual(a, b));

        BlockExpression block = Expression.Block(new ParameterExpression[] { a, b },
            setA,
            setB,
            call1,
            call11,
            call2,
            call22,
            call3,
            call33,
            setAa,
            setBb,
            call4,
            call44,
            call5,
            call55
            );

        Expression&lt;Action&gt; lambda = Expression.Lambda&lt;Action&gt;(block);
        lambda.Compile()();
        Console.ReadKey();</code></pre>

The generated expression tree is as follows

.Lambda #Lambda1<System.Action>() {
    .Block(
        System.Int32 $a,
        System.Int32 $b) {
        $a = 21;
        $b = 20;
        .Call System.Console.Write("a == b:");
        .Call System.Console.WriteLine($a == $b);
        .Call System.Console.Write("a < b :");
        .Call System.Console.WriteLine($a < $b);
        .Call System.Console.Write("a > b :");
        .Call System.Console.WriteLine($a > $b);
        $a = 5;
        $b = 20;
        .Call System.Console.Write("a <= b:");
        .Call System.Console.WriteLine($a <= $b);
        .Call System.Console.Write("a >= b:");
        .Call System.Console.WriteLine($a >= $b)
    }
}

Three, Logical Operators

&&, ||, !

Operator Description
&& Called the logical AND operator. The condition is true if both operands are non-zero.
|| Called the logical OR operator. The condition is true if at least one of the operands is non-zero.
! Called the logical NOT operator. It is used to reverse the logical state of the operand. If the condition is true, the logical NOT operator will make it false.

Logical operators evaluate to true or false.

.

Logical Operators Expression Tree
&& Expression.AndAlso()
|| Expression.OrElse()
Expression.Not()
            int a = 10;
            int b = 11;
        Console.Write(&quot;[a == b &amp;&amp; a &gt; b]:&quot;);
        Console.WriteLine(a == b &amp;&amp; a &gt; b);

        Console.Write(&quot;[a &gt; b || a == b]:&quot;);
        Console.WriteLine(a &gt; b || a == b);

        Console.Write(&quot;[!(a == b)]:&quot;);
        Console.WriteLine(!(a == b));
        Console.ReadKey();</code></pre>

Using Expression Trees

            //int a = 10;
            //int b = 11;
            ParameterExpression a = Expression.Parameter(typeof(int), "a");
            ParameterExpression b = Expression.Parameter(typeof(int), "b");
            BinaryExpression setA = Expression.Assign(a, Expression.Constant(10));
            BinaryExpression setB = Expression.Assign(b, Expression.Constant(11));
        //Console.Write(&quot;[a == b &amp;&amp; a &gt; b]:&quot;);
        //Console.WriteLine(a == b &amp;&amp; a &gt; b);
        MethodCallExpression call1 = Expression.Call(null, typeof(Console).GetMethod(&quot;Write&quot;, new Type[] { typeof(string) }), Expression.Constant(&quot;[a == b &amp;&amp; a &gt; b]:&quot;));

        MethodCallExpression call2 = Expression.Call(
            null,
            typeof(Console).GetMethod(&quot;WriteLine&quot;, new Type[] { typeof(bool) }),
             Expression.AndAlso(Expression.Equal(a, b), Expression.GreaterThan(a, b))
            );

        //Console.Write(&quot;[a &gt; b || a == b]:&quot;);
        //Console.WriteLine(a &gt; b || a == b);
        MethodCallExpression call3 = Expression.Call(null, typeof(Console).GetMethod(&quot;Write&quot;, new Type[] { typeof(string) }), Expression.Constant(&quot;[a &gt; b || a == b]:&quot;));
        MethodCallExpression call4 = Expression.Call(
            null,
            typeof(Console).GetMethod(&quot;WriteLine&quot;, new Type[] { typeof(bool) }),
            Expression.OrElse(Expression.Equal(a, b), Expression.GreaterThan(a, b))
            );

        //Console.Write(&quot;[!(a == b)]:&quot;);
        //Console.WriteLine(!(a == b));
        MethodCallExpression call5 = Expression.Call(null, typeof(Console).GetMethod(&quot;Write&quot;, new Type[] { typeof(string) }), Expression.Constant(&quot;[!(a == b)]:&quot;));
        MethodCallExpression call6 = Expression.Call(
            null,
            typeof(Console).GetMethod(&quot;WriteLine&quot;, new Type[] { typeof(bool) }),
            Expression.Not(Expression.Equal(a, b))
            );
        BlockExpression block = Expression.Block(
            new ParameterExpression[] { a, b },
            setA,
            setB,
            call1,
            call2,
            call3,
            call4,
            call5,
            call6
            );

        Expression&lt;Action&gt; lambda = Expression.Lambda&lt;Action&gt;(block);
        lambda.Compile()();
        Console.ReadKey();</code></pre>

The generated expression tree is as follows:

.Lambda #Lambda1<System.Action>() {
    .Block(
        System.Int32 $a,
        System.Int32 $b) {
        $a = 10;
        $b = 11;
        .Call System.Console.Write("[a == b && a > b]:");
        .Call System.Console.WriteLine($a == $b && $a > $b);
        .Call System.Console.Write("[a > b || a == b]:");
        .Call System.Console.WriteLine($a == $b || $a > $b);
        .Call System.Console.Write("[!(a == b)]:");
        .Call System.Console.WriteLine(!($a == $b))
    }
}

4. Bitwise Operators

&, |, ^, ~, <<, >>

Operator Description Example
& The binary AND operator copies a bit to the result if it exists in both operands. (A & B) will yield 12, which is 0000 1100
| The binary OR operator copies a bit to the result if it exists in either operand. (A | B) will yield 61, which is 0011 1101
^ The binary XOR operator copies a bit to the result if it exists in one operand but not both. (A ^ B) will yield 49, which is 0011 0001
~ The bitwise NOT operator is a unary operator that "flips" bits, meaning 0 becomes 1 and 1 becomes 0, including the sign bit. (~A) will yield -61, which is 1100 0011 in the two's complement form of a signed binary number.
<< The binary left shift operator shifts the value of the left operand to the left by the number of bits specified by the right operand. A << 2 will yield 240, which is 1111 0000
>> The binary right shift operator shifts the value of the left operand to the right by the number of bits specified by the right operand. A >> 2 will yield 15, which is 0000 1111

Due to space limitations, only examples are provided.

Bitwise Operator Expression Tree
& Expression.Add(Expression left, Expression right)
| Expression.Or(Expression left, Expression right)
^ Expression.ExclusiveOr(Expression expression)
~ Expression.OnesComplement(Expression expression)
<< Expression.LeftShift(Expression left, Expression right)
>> Expression.RightShift(Expression left, Expression right)

5. Assignment Operators

Operator Description Example
= Simple assignment operator that assigns the value of the right operand to the left operand. C = A + B assigns the value of A + B to C.
+= Add and assign operator that assigns the result of adding the right operand to the left operand back to the left operand. C += A is equivalent to C = C + A.
-= Subtract and assign operator that assigns the result of subtracting the right operand from the left operand back to the left operand. C -= A is equivalent to C = C - A.
*= Multiply and assign operator that assigns the result of multiplying the right operand by the left operand back to the left operand. C *= A is equivalent to C = C * A.
/= Divide and assign operator that assigns the result of dividing the left operand by the right operand back to the left operand. C /= A is equivalent to C = C / A.
%= Modulus and assign operator assigns the modulus of the two operands back to the left operand. C %= A is equivalent to C = C % A.
<<= Left shift and assign operator. C <<= 2 is equivalent to C = C << 2.
>>= Right shift and assign operator. C >>= 2 is equivalent to C = C >> 2.
&= Bitwise AND and assign operator. C &= 2 is equivalent to C = C & 2.
^= Bitwise XOR and assign operator. C ^= 2 is equivalent to C = C ^ 2.
|= Bitwise OR and assign operator. C |= 2 is equivalent to C = C | 2.

Due to space limitations, please appreciate... ...

Operator Expression Tree
= Expression.Assign
+= Expression.AddAssign
-= Expression.SubtractAssign
*= Expression.MultiplyAssign
/= Expression.DivideAssign
%= Expression.ModuloAssign
<<= Expression.LeftShiftAssign
>>= Expression.RightShiftAssign
&= Expression.AndAssign
^= Expression.ExclusiveOrAssign
|= Expression.OrAssign

^= , note that it has two meanings: one is the bitwise operator for XOR (ExclusiveOrAssign), and the other is the arithmetic operator for PowerAssign.

6. Other Operators

Operator Description Example
sizeof() Returns the size of the data type. sizeof(int) will return 4.
typeof() Returns the type of a class.

. typeof(StreamReader);
& Returns the address of a variable. &a; will obtain the actual address of the variable.
* Pointer to a variable. *a; will point to a variable.
? : Conditional expression If the condition is true ? then X : otherwise Y
is Determines if an object is of a certain type. If( Ford is Car) // Check if Ford is an object of class Car.
as Type casting that does not throw an exception even if the cast fails. Object obj = new StringReader("Hello"); StringReader r = obj as StringReader;

I haven't found how to write these operators in expression trees. If you find them, feel free to let me know...

痴者工良

高级程序员劝退师

文章评论