C#知多少 | 每个版本都更新了什么?

C# 小时候可胖了 24℃ 0评论

总所周知,.NET5.0马上就要来了,最后一个预览版RC2也已经发布了,在11月的时候,我们就正式的发布了,然后我们就可以迁移使用了,当然今天说的重点不是.NET,今天说的是伴随着.NET5一起到来的C#9.0,既然要了解9.0,肯定要对之前的版本也稍微了解一下,至少不会面试的时候出现:XXX这个功能用过么,哪个版本出来的知道么?一问三不知的尴尬情景。虽然官网都有,但是我相信有一小部分不看,所以还是发出来吧。

咱们就采用从小到大的顺序讲解吧,从6.0开始,5.0就太老了,基本不会问了,注意是C#,不是MVC,查看的都是官网的,想看更多,点击【阅读原文】。

C#6中新增的功能

  • get 只读属性

简洁的语法来创建不可变类型,仅有get访问器:

1 public string FirstName { get; }
2 public string LastName { get;  }

当然很多时候,我们使用的是私有化来设置set。

然后通过构造函数来赋值:

1 public Student(string firstName, string lastName)
2 {
3     if (IsNullOrWhiteSpace(lastName))
4         throw new ArgumentException(message: "Cannot be blank", paramName: nameof(lastName));
5     FirstName = firstName;
6     LastName = lastName;
7 }
  • get 属性初始化表达式

在属性声明中声明自动属性的初始值,

1 public ICollection<double> Grades { get; } 
2   = new List<double>();

声明处就可以直接被初始化。

  • Expression-bodied 函数成员

这适用于方法和只读属性。 例如,重写?

1
ToString()

?通常是理想之选:

1 public override string ToString() 
2   => $"{LastName}, {FirstName}";

也可以将此语法用于只读属性:

1 public string FullName => $"{FirstName} {LastName}";
  • using 静态命名空间

using static ?增强功能可用于导入单个类的静态方法。 指定要使用的类:

using static System.Math;

在 LINQ 查询中会经常看到这种情况。 可以通过导入 Enumerable 或 Queryable 来导入 LINQ 模式。

  • Null 条件运算符

Null 条件运算符使 null 检查更轻松、更流畅 ?。 将成员访问?

1
.

?替换为?

1
 .

1 var first = person .FirstName;

如果person为空,返回的值就是null,是string的默认值,如果FirstName是int类型,那返回的就是int的默认值0。

  • $ 字符串内插

新的字符串内插功能可以在字符串中嵌入表达式。 使用?

1
$

?作为字符串的开头,并使用?

1
{

?和?

1
}

?之间的表达式代替序号:

1 public string GetGradePointPercentage() =>
2     $"Name: {LastName}, {FirstName}. G.P.A: {Grades.Average():F2}";

上一行代码将?

1
Grades.Average()

?的值格式设置为具有两位小数的浮点数。

  • when?异常筛选器

“异常筛选器”是确定何时应该应用给定的catch子句的子句 。如果用于异常筛选器的表达式计算结果为

1
true

,则catch子句将对异常执行正常处理。 如果表达式计算结果为

1
false

,则将跳过

1
catch

子句。一种用途是检查有关异常的信息,以确定

1
catch

子句是否可以处理该异常:

 1 public static async Task<string> MakeRequest()
 2 {
 3     WebRequestHandler webRequestHandler = new WebRequestHandler();
 4     webRequestHandler.AllowAutoRedirect = false;
 5     using (HttpClient client = new HttpClient(webRequestHandler))
 6     {
 7         var stringTask = client.GetStringAsync("https://docs.microsoft.com/en-us/dotnet/about/");
 8         try
 9         {
10             var responseText = await stringTask;
11             return responseText;
12         }
13         catch (System.Net.Http.HttpRequestException e) when (e.Message.Contains("301"))
14         {
15             return "Site Moved";
16         }
17     }
18 }
  • nameof 表达式

nameof 表达式的计算结果为符号的名称。 每当需要变量、属性或成员字段的名称时,这是让工具正常运行的好办法,说白了就是更好的重构:

1 if (IsNullOrWhiteSpace(lastName))
2     throw new ArgumentException(message: "Cannot be blank", paramName: nameof(lastName));
  • Catch 和 Finally 块中的 Await

现在可以在?

1
catch

?或?

1
finally

?表达式中使用?

1
await

。 这通常用于日志记录方案:

 1 public static async Task<string> MakeRequestAndLogFailures()
 2 {
 3     await logMethodEntrance();
 4     var client = new System.Net.Http.HttpClient();
 5     var streamTask = client.GetStringAsync("https://localHost:10000");
 6     try {
 7         var responseText = await streamTask;
 8         return responseText;
 9     } catch (System.Net.Http.HttpRequestException e) when (e.Message.Contains("301"))
10     {
11         await logError("Recovered from redirect", e);
12         return "Site Moved";
13     }
14     finally
15     {
16         await logMethodExit();
17         client.Dispose();
18     }
19 }
  • 索引器初始化关联集合

可以将集合初始值设定项与 Dictionary<TKey,TValue> 集合和其他类型一起使用,在这种情况下,可访问的?

1
Add

?方法接受多个参数。 新语法支持使用索引分配到集合中:

private Dictionary<int, string> webErrors = new Dictionary<int, string>
{
    [404] = "Page not Found",
    [302] = "Page moved, but left a forwarding address.",
    [500] = "The web server can't come out to play today."
};

C#7.x 中新增的功能

  • out 变量

可以在方法调用的参数列表中声明?

1
out

?变量,而不是编写单独的声明语句:

1 if (int.TryParse(input, out int result))
2     Console.WriteLine(result);
3 else
4     Console.WriteLine("Could not parse input");

为清晰明了,可能需指定?

1
out

?变量的类型,如上所示。?但是,该语言支持使用隐式类型的局部变量:

1 if (int.TryParse(input, out var answer))
2     Console.WriteLine(answer);
3 else
4     Console.WriteLine("Could not parse input");
  • Tuple 元组

低于 C# 7.0 的版本中也提供元组,但它们效率低下且不具有语言支持。这意味着元组元素只能作为 Item1 和 Item2 等引用。

可以通过为每个成员赋值来创建元组,并可选择为元组的每个成员提供语义名称:

1 (string Alpha, string Beta) namedLetters = ("a", "b");
2 Console.WriteLine($"{namedLetters.Alpha}, {namedLetters.Beta}");

在进行元组赋值时,还可以指定赋值右侧的字段的名称:

1 var alphabetStart = (Alpha: "a", Beta: "b");
2 Console.WriteLine($"{alphabetStart.Alpha}, {alphabetStart.Beta}");

使用的时候,可以直接点出来:

1 alphabetStart.Alpha
2 alphabetStart.Beta
  • _ 弃元

C# 增添了对弃元的支持。?弃元是一个名为?

1
_

(下划线字符)的只写变量,可向单个变量赋予要放弃的所有值。?弃元类似于未赋值的变量;不可在代码中使用弃元(赋值语句除外)

 1 public class Example
 2 {
 3     public static void Main()
 4     {
 5         var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York City", 1960, 2010);
 6 
 7         Console.WriteLine($"Population change, 1960 to 2010: {pop2 - pop1:N0}");
 8     }
 9 
10     private static (string, double, int, int, int, int) QueryCityDataForYears(string name, int year1, int year2)
11     {
12         int population1 = 0, population2 = 0;
13         double area = 0;
14 
15         if (name == "New York City")
16         {
17             area = 468.48;
18             if (year1 == 1960)
19             {
20                 population1 = 7781984;
21             }
22             if (year2 == 2010)
23             {
24                 population2 = 8175133;
25             }
26             return (name, area, year1, population1, year2, population2);
27         }
28 
29         return ("", 0, 0, 0, 0, 0);
30     }
31 }
  • is 模式匹配

模式匹配支持?

1
is

?表达式和?

1
switch

?表达式。?每个表达式都允许检查对象及其属性以确定该对象是否满足所寻求的模式。?使用?

1
when

?关键字来指定模式的其他规则

 1 public static int SumPositiveNumbers(IEnumerable<object> sequence)
 2 {
 3     int sum = 0;
 4     foreach (var i in sequence)
 5     {
 6         switch (i)
 7         {
 8             case 0:
 9                 break;
10             case IEnumerable<int> childSequence:
11             {
12                 foreach(var item in childSequence)
13                     sum += (item > 0)   item : 0;
14                 break;
15             }
16             case int n when n > 0:
17                 sum += n;
18                 break;
19             case null:
20                 throw new NullReferenceException("Null found in sequence");
21             default:
22                 throw new InvalidOperationException("Unrecognized type");
23         }
24     }
25     return sum;
26 }
  • 1
    case 0:

    ?是常见的常量模式。

  • 1
    case IEnumerable&lt;int&gt; childSequence:

    ?是一种类型模式。

  • 1
    case int n when n &gt; 0:

    ?是具有附加?

    1
    when

    ?条件的类型模式。

  • 1
    case null:

    ?是 null 模式。

  • 1
    default:

    ?是常见的默认事例。

  • 本地函数(内部)

本地函数使你能够在另一个方法的上下文内声明方法。?本地函数使得类的阅读者更容易看到本地方法仅从声明它的上下文中调用。

 1 public static IEnumerable<char> AlphabetSubset3(char start, char end)
 2 {
 3     if (start < 'a' || start > 'z')
 4         throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter");
 5     if (end < 'a' || end > 'z')
 6         throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter");
 7 
 8     if (end <= start)
 9         throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}");
10 
11     return alphabetSubsetImplementation();
12 
13     IEnumerable<char> alphabetSubsetImplementation()
14     {
15         for (var c = start; c < end; c++)
16             yield return c;
17     }
18 }

注意上边

1
<span data-darkmode-bgimage-16028298275862="1" data-darkmode-color-16028298275862="#191919">alphabetSubsetImplementation</span>
1
<span data-darkmode-bgimage-16028298275862="1" data-darkmode-color-16028298275862="#191919">方法,是在内部定义的。</span>

同样可以使用异步:

 1 public Task<string> PerformLongRunningWork(string address, int index, string name)
 2 {
 3     if (string.IsNullOrWhiteSpace(address))
 4         throw new ArgumentException(message: "An address is required", paramName: nameof(address));
 5     if (index < 0)
 6         throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be non-negative");
 7     if (string.IsNullOrWhiteSpace(name))
 8         throw new ArgumentException(message: "You must supply a name", paramName: nameof(name));
 9 
10     return longRunningWorkImplementation();
11 
12     async Task<string> longRunningWorkImplementation()
13     {
14         var interimResult = await FirstWork(address);
15         var secondResult = await SecondStep(index, name);
16         return $"The results are {interimResult} and {secondResult}. Enjoy.";
17     }
18 }

当然也支持某些使用lambda表达式来完成。

  • 数字文本语法改进

C# 7.0 包括两项新功能,可用于以最可读的方式写入数字来用于预期用途:二进制文本和数字分隔符 。在创建位掩码时,或每当数字的二进制表示形式使代码最具可读性时,以二进制形式写入该数字:

1 public const int Sixteen =   0b0001_0000;
2 public const int ThirtyTwo = 0b0010_0000;
3 public const int SixtyFour = 0b0100_0000;
4 public const int OneHundredTwentyEight = 0b1000_0000;

常量开头的?

1
0b

?表示该数字以二进制数形式写入。?二进制数可能会很长,因此通过引入?

1
_

?作为数字分隔符通常更易于查看位模式,如上面二进制常量所示。?数字分隔符可以出现在常量的任何位置。?对于十进制数字,通常将其用作千位分隔符:

1 public const long BillionsAndBillions = 100_000_000_000;

C#8.0中新增的功能

“.NET Core 3.x”和“.NET Standard 2.1”支持 C# 8.0;

  • Readonly 成员

可将?

1
readonly

?修饰符应用于结构的成员。?它指示该成员不会修改状态。?这比将?

1
readonly

?修饰符应用于?

1
struct

?声明更精细。??请考虑以下可变结构:

1 public readonly double Distance => Math.Sqrt(X * X + Y * Y);
  • 默认接口方法

现在可以将成员添加到接口,并为这些成员提供实现。?借助此语言功能,API 作者可以将方法添加到以后版本的接口中,而不会破坏与该接口当前实现的源或二进制文件兼容性。?现有的实现继承默认实现。

1 public interface ICustomer
2 {
3     IEnumerable<IOrder> PreviousOrders { get; }
4 
5     DateTime DateJoined { get; }
6     DateTime  LastOrder { get; }
7     string Name { get; }
8     IDictionary<DateTime, string> Reminders { get; }
9 }
  • Switch 表达式升级

通常情况下,

1
switch

?语句在其每个?

1
case

?块中生成一个值。借助 Switch 表达式,可以使用更简洁的表达式语法。

 1 public static RGBColor FromRainbowClassic(Rainbow colorBand)
 2 {
 3     switch (colorBand)
 4     {
 5         case Rainbow.Red:
 6             return new RGBColor(0xFF, 0x00, 0x00);
 7         case Rainbow.Orange:
 8             return new RGBColor(0xFF, 0x7F, 0x00);
 9         case Rainbow.Yellow:
10             return new RGBColor(0xFF, 0xFF, 0x00);
11         case Rainbow.Green:
12             return new RGBColor(0x00, 0xFF, 0x00);
13         case Rainbow.Blue:
14             return new RGBColor(0x00, 0x00, 0xFF);
15         case Rainbow.Indigo:
16             return new RGBColor(0x4B, 0x00, 0x82);
17         case Rainbow.Violet:
18             return new RGBColor(0x94, 0x00, 0xD3);
19         default:
20             throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand));
21     };
22 }

这里有几个语法改进:

  • 变量位于?

    1
    switch

    ?关键字之前。?不同的顺序使得在视觉上可以很轻松地区分 switch 表达式和 switch 语句。

  • 将?

    1
    case

    ?和?

    1
    :

    ?元素替换为?

    1
    =&gt;

    。?它更简洁,更直观。

  • 将?

    1
    default

    ?事例替换为?

    1
    _

    ?弃元。

  • 正文是表达式,不是语句。

 1 public static RGBColor FromRainbow(Rainbow colorBand) =>
 2     colorBand switch
 3     {
 4         Rainbow.Red    => new RGBColor(0xFF, 0x00, 0x00),
 5         Rainbow.Orange => new RGBColor(0xFF, 0x7F, 0x00),
 6         Rainbow.Yellow => new RGBColor(0xFF, 0xFF, 0x00),
 7         Rainbow.Green  => new RGBColor(0x00, 0xFF, 0x00),
 8         Rainbow.Blue   => new RGBColor(0x00, 0x00, 0xFF),
 9         Rainbow.Indigo => new RGBColor(0x4B, 0x00, 0x82),
10         Rainbow.Violet => new RGBColor(0x94, 0x00, 0xD3),
11         _              => throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand)),
12     };
  • 属性模式

借助属性模式,可以匹配所检查的对象的属性。?请看一个电子商务网站的示例,该网站必须根据买家地址计算销售税。?这种计算不是?

1
Address

?类的核心职责。?它会随时间变化,可能比地址格式的更改更频繁。?销售税的金额取决于地址的?

1
State

?属性。?下面的方法使用属性模式从地址和价格计算销售税:

1 public static decimal ComputeSalesTax(Address location, decimal salePrice) =>
2     location switch
3     {
4         { State: "WA" } => salePrice * 0.06M,
5         { State: "MN" } => salePrice * 0.075M,
6         { State: "MI" } => salePrice * 0.05M,
7         // other cases removed for brevity...
8         _ => 0M
9     };

在 LINQ 查询中会经常看到这种情况。?可以通过导入 Enumerable 或 Queryable 来导入 LINQ 模式。

  • 元组模式

一些算法依赖于多个输入。?使用元组模式,可根据表示为元组的多个值进行切换。??以下代码显示了游戏“rock, paper, scissors(石头剪刀布)”的切换表达式::

 1 public static string RockPaperScissors(string first, string second)
 2     => (first, second) switch
 3     {
 4         ("rock", "paper") => "rock is covered by paper. Paper wins.",
 5         ("rock", "scissors") => "rock breaks scissors. Rock wins.",
 6         ("paper", "rock") => "paper covers rock. Paper wins.",
 7         ("paper", "scissors") => "paper is cut by scissors. Scissors wins.",
 8         ("scissors", "rock") => "scissors is broken by rock. Rock wins.",
 9         ("scissors", "paper") => "scissors cuts paper. Scissors wins.",
10         (_, _) => "tie"
11     };

如果person为空,返回的值就是null,是string的默认值,如果FirstName是int类型,那返回的就是int的默认值0。

  • using 声明

using 声明是前面带?

1
using

?关键字的变量声明。?它指示编译器声明的变量应在封闭范围的末尾进行处理。?以下面编写文本文件的代码为例:

 1 static int WriteLinesToFile(IEnumerable<string> lines)
 2 {
 3     using var file = new System.IO.StreamWriter("WriteLines2.txt");
 4     // Notice how we declare skippedLines after the using statement.
 5     int skippedLines = 0;
 6     foreach (string line in lines)
 7     {
 8         if (!line.Contains("Second"))
 9         {
10             file.WriteLine(line);
11         }
12         else
13         {
14             skippedLines++;
15         }
16     }
17     // Notice how skippedLines is in scope here.
18     return skippedLines;
19     // file is disposed here
20 }

前面的代码相当于下面使用经典 using 语句的代码:

static int WriteLinesToFile(IEnumerable<string> lines)
{
    // We must declare the variable outside of the using block
    // so that it is in scope to be returned.
    int skippedLines = 0;
    using (var file = new System.IO.StreamWriter("WriteLines2.txt"))
    {
        foreach (string line in lines)
        {
            if (!line.Contains("Second"))
            {
                file.WriteLine(line);
            }
            else
            {
                skippedLines++;
            }
        }
        return skippedLines;
    } // file is disposed here
}
  • Static 静态本地函数

现在可以向本地函数添加?

1
static

?修饰符,以确保本地函数不会从封闭范围捕获(引用)任何变量。

1 int M()
2 { 
3     int y = 5;
4     int x = 7;
5     return Add(x, y);
6     static int Add(int left, int right) => left + right;
7 }
  • async 异步流

从 C# 8.0 开始,可以创建并以异步方式使用流。返回异步流的方法有三个属性:

  1. 它是用?

    1
    async

    ?修饰符声明的。

  2. 它将返回 IAsyncEnumerable<T>。

  3. 该方法包含用于在异步流中返回连续元素的?

    1
    yield return

    ?语句。

 1 public static async System.Collections.Generic.IAsyncEnumerable<int> GenerateSequence()
 2 {
 3     for (int i = 0; i < 20; i++)
 4     {
 5         await Task.Delay(100);
 6         yield return i;
 7     }
 8 }
 9 
10 await foreach (var number in GenerateSequence())
11 {
12     Console.WriteLine(number);
13 }

异步可释放:

从 C# 8.0 开始,语言支持实现 System.IAsyncDisposable 接口的异步可释放类型。可使用?

1
await using

?语句来处理异步可释放对象。

  • 索引和范围

范围指定范围的开始和末尾 。 包括此范围的开始,但不包括此范围的末尾,这表示此范围包含开始但不包含末尾 。 范围?

1
[0..^0]

?表示整个范围,就像?

1
[0..sequence.Length]

?表示整个范围。

请看以下几个示例。 请考虑以下数组,用其顺数索引和倒数索引进行注释:

 1 var words = new string[]
 2 {
 3                 // index from start    index from end
 4     "The",      // 0                   ^9
 5     "quick",    // 1                   ^8
 6     "brown",    // 2                   ^7
 7     "fox",      // 3                   ^6
 8     "jumped",   // 4                   ^5
 9     "over",     // 5                   ^4
10     "the",      // 6                   ^3
11     "lazy",     // 7                   ^2
12     "dog"       // 8                   ^1
13 };              // 9 (or words.Length) ^0
14 
15 // 可以使用 ^1 索引检索最后一个词:
16 Console.WriteLine($"The last word is {words[^1]}");
17 // writes "dog"

以下代码创建了一个包含单词“quick”、“brown”和“fox”的子范围。?它包括?

1
words[1]

?到?

1
words[3]

。?元素?

1
words[4]

?不在该范围内。

1 var quickBrownFox = words[1..4];
2 
3 var allWords = words[..]; // contains "The" through "dog".
4 var firstPhrase = words[..4]; // contains "The" through "fox"
5 var lastPhrase = words[6..]; // contains "the", "lazy" and "dog"
  • null 合并赋值

C# 8.0 引入了 null 合并赋值运算符?

1
  =

。?仅当左操作数计算为?

1
null

?时,才能使用运算符?

1
  =

?将其右操作数的值分配给左操作数。

List<int> numbers = null;
int  i = null;

numbers   = new List<int>();
numbers.Add(i   = 17);
numbers.Add(i   = 20);

Console.WriteLine(string.Join(" ", numbers));  // output: 17 17
Console.WriteLine(i);  // output: 17

C#9.0 中新增的功能

.NET5支持C#9.0.

  • 记录类型

C# 9.0 引入了记录类型,这是一种引用类型,它提供合成方法来提供值语义,从而实现相等性。?默认情况下,记录是不可变的。

1 public record Person
2 {
3     public string LastName { get; }
4     public string FirstName { get; }
5 
6     public Person(string first, string last) 
7       => (FirstName, LastName) = (first, last);
8 }
  • Init 仅限的资源库

从 C# 9.0 开始,可为属性和索引器创建?

1
init

?访问器,而不是?

1
set

?访问器。?调用方可使用属性初始化表达式语法在创建表达式中设置这些值,但构造完成后,这些属性将变为只读。?仅限 init 的资源库提供了一个窗口用来更改状态。

 1 public struct WeatherObservation
 2 {
 3     public DateTime RecordedAt { get; init; }
 4     public decimal TemperatureInCelsius { get; init; }
 5     public decimal PressureInMillibars { get; init; }
 6 
 7     public override string ToString() =>
 8         $"At {RecordedAt:h:mm tt} on {RecordedAt:M/d/yyyy}: " +
 9         $"Temp = {TemperatureInCelsius}, with {PressureInMillibars} pressure";
10 }

调用方可使用属性初始化表达式语法来设置值,同时仍保留不变性:

1 var now = new WeatherObservation 
2 { 
3     RecordedAt = DateTime.Now, 
4     TemperatureInCelsius = 20, 
5     PressureInMillibars = 998.0m 
6 };
  • 顶级语句

顶级语句从许多应用程序中删除了不必要的流程。只有一行代码执行所有操作。?借助顶级语句,可使用?

1
using

?语句和执行操作的一行替换所有样本

1 using System;
2 
3 Console.WriteLine("Hello World!");

如果需要单行程序,可删除?

1
using

?指令,并使用完全限定的类型名称:

1 System.Console.WriteLine("Hello World!");
  • 模式匹配增强功能

C# 9 包括新的模式匹配改进:

  • 类型模式要求在变量是一种类型时匹配

  • 带圆括号的模式强制或强调模式组合的优先级

  • 联合?

    1
    and

    ?模式要求两个模式都匹配

  • 析取?

    1
    or

    ?模式要求任一模式匹配

  • 求反?

    1
    not

    ?模式要求模式不匹配

  • 关系模式要求输入小于、大于、小于等于或大于等于给定常数。

1 public static bool IsLetter(this char c) =>
2     c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';
3     
4 public static bool IsLetterOrSeparator(this char c) =>
5     c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '.' or ',';    
  • 调试和完成功能

在 C# 9.0 中,已知创建对象的类型时,可在?

1
new

?表达式中省略该类型。?最常见的用法是在字段声明中:

1 private List<WeatherObservation> _observations = new();

当需要创建新对象作为参数传递给方法时,也可使用目标类型?

1
new

。?请考虑使用以下签名的?

1
ForecastFor()

?方法:

1 public WeatherForecast ForecastFor(DateTime forecastDate, WeatherForecastOptions options)

可按如下所示调用该方法:

1 var forecast = station.ForecastFor(DateTime.Now.AddDays(2), new());

好啦,关于c#的更新呢,暂时就这么多了,看着很长,其实很多咱们平时都已经使用到了,当然还有一些不太常用的我没有去列举,更多的内容,左下角点击【阅读原文】吧。

转载请注明:九层楼 » C#知多少 | 每个版本都更新了什么?

喜欢 (0)
发表我的评论
取消评论
表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址