顶级语句(C#9.0)

1、直接在 C#文件中直接编写入口方法的代码,不用类,不用 Main。经典写法仍然支持。反编译一下了解真相。
2、同一个项目中只能有一个文件具有顶级语句。
3、顶级语句中可以直接使用 await 语法,也可以声明函数

全局 using 指令(C#10)

1、将 global 修饰符添加到 using 前,这个命名空间就应用到整个项目,不用重复 using。
2、通常创建一个专门用来编写全局 using 代码的 C#文件。
3、如果 csproj 中启用了enable,编译器会自动隐式增加对于 System、System.Linq 等常用命名空间的引入,不同各类型项目引入的命名空间也不一样。

Using 声明(C#8)

在实现了 Idisposable/IAsyncDisposable 接口的类型的变量声明前加上 using,当代码执行离开变量的作用域时,对象就会被释放。

using SqlConnection conn = new SqlConnection("Data Source=.;Initial Catalog=db1;Integrated Security=True") ;
conn.Open();
using SqlCommand cmd = conn.CreateCommand() ;
cmd.CommandText = "select * from T_Articles";
using SqlDataReader reader = cmd.ExecuteReader() ;
while (reader.Read())
{
string title = reader.GetString(reader.GetOrdinal("Title"));
Console.WriteLine(title);
}

Using 声明的陷阱

using var outStream = File.OpenWrite("e:/1.txt");
using var writer = new StreamWriter(outStream);
writer.WriteLine("hello");
string s = File.ReadAllText("e:/1.txt");
Console.WriteLine(s);

抛异常 System.IO.IOException;outStream 和 writer 在方法执行结束后才被释放资源。

文件范围的命名空间声明(C#10)

在之前版本的 C#中,类型必须定义在 namespace 中。C#10.0 开始允许编写独立的 namespace 代码行声明命名空间,文件中所有类型都是这个命名空间下的成员。

namespace TMS.Admin;
class Teacher
{
public int Id { get; set; }
public string Name { get; set; }
}

可空的引用类型(C#8)

C#数据类型分为值类型和引用类型两种,值类型的变量不可以为空,而引用类型变量可以为空。
问题:如果不注意检查引用类型变量是否可空,就有可能造成程序中出现 NullReferenceException 异常。

1、csproj 中enable启用可空引用类型检查。
2、在引用类型后添加“?”修饰符来声明这个类型是可空的。对于没有添加“?”修饰符的引用类型的变量,如果编译器发现存在为这个变量赋值 null 的可能性的时候,编译器会给出警告信息。

使用!抑制警告

1、如果程序员确认被访问的变量、成员确实不会出现为空的情况,也可以在访问可空的变量、成员的时候加上!来抑制编译器的警告。
Student s1 = GetData();
Console.WriteLine(s1.Name.ToLower());
Console.WriteLine(s1.PhoneNumber!.ToLower());
2、不要滥用。

记录(record)类型(C#9)

1、C#中的==运算符默认是判断两个变量指向的是否是同一个对象,即使两个对象内容完全一样,也不相等。可以通过重写 Equals 方法、重写==运算符等来解决这个问题,不过需要开发人员编写非常多的额外代码。
2、在 C#9.0 中增加了记录(record)类型的语法,编译器会为我们自动生成 Equals、GetHashcode 等方法。

public record Person(string FirstName, string LastName);
Person p1 = new Person("Yang", "Zack");
Person p2 = new Person("Yang","Zack");
Person p3 = new Person("Gates", "Bill");
Console.WriteLine(p1); // Person { FirstName = Yang,LastName = Zack}
Console.WriteLine(p1==p2);// true
Console.WriteLine(p1==p3);// false
Console.WriteLine(p1.FirstName);

record 原理

1、编译器会根据 Person 类型中的属性定义,自动为 Person 类型生成包含全部属性的构造方法。注意,默认情况下,编译器会生成一个包含所有属性的构造方法,因此,我们编写 new Person()、new Person(“Yang”)这两种写法都是不可以的。也会生成 ToString 方法和 Equals 等方法。
2、通过反编译看背后原理。避免反编译器的优化,需要把反编译器生成的代码改成 C#8.0 的语法。结论:record 就是普通的一个类。
3、record 数据类型为我们提供了为所有属性赋值的构造方法,所有属性都是只读的,而且对象可以进行值相等性比较,并且提供了可读性强的 ToString()返回值。在需要编写一些不可变类并且需要进行对象值比较的对象时候,record 可以帮我们把代码的编写难度大大降低。

record 深入

1、可以实现部分属性是只读的、而部分属性是可以读写。
2、默认生成的构造方法的行为不能修改,我们可以为类型提供多个构造方法,然后其他构造方法通过 this 调用默认的构造方法。
3、也推荐使用只读属性的类型。这样的所有属性都为只读的类型叫做“不可变类型”,可以让程序逻辑简单,减少并发访问、状态管理等的麻烦。

对象的副本

1、record 也是普通类,变量的赋值是引用的传递。这是和结构体不同之处。
2、生成一个对象的副本,这个对象的其他属性值与原对象的相同,只有一个或者少数几个属性改变。麻烦的做法:User u2 = new User(u1.UserName, “test@example”, u1.Age);
3、用 with 关键字简化:User u2 = u1 with { Email= “test@example” }; 创建的也是拷贝。

yield 关键字用法

yield return:

先看下面的代码,通过 yield return 实现了类似用 foreach 遍历数组的功能,说明 yield return 也是用来实现迭代器的功能:

using static System.Console;
using System.Collections.Generic;

class Program
{
//一个返回类型为IEnumerable<int>,其中包含三个yield return
public static IEnumerable<int> enumerableFuc()
{
yield return 1;
yield return 2;
yield return 3;
}

static void Main(string[] args)
{
//通过foreach循环迭代此函数
foreach(int item in enumerableFuc())
{
WriteLine(item);
}
ReadKey();
}
}

输出结果:
1
2
3

yield break

只输出了 1,2,没有输出 3,说明这个迭代器被 yield break 停掉了,所以 yield break 是用来终止迭代的。

using static System.Console;
using System.Collections.Generic;
class Program
{
//一个返回类型为IEnumerable<int>,其中包含三个yield return
public static IEnumerable<int> enumerableFuc()
{
yield return 1;
yield return 2;
yield break;
yield return 3;
}

static void Main(string[] args)
{
//通过foreach循环迭代此函数
foreach(int item in enumerableFuc())
{
WriteLine(item);
}
ReadKey();
}
}

输出结果:
1
2

只能使用在返回类型必须为 IEnumerable、IEnumerable、IEnumerator 或 IEnumerator的方法、运算符、get 访问器中。

为什么要用 yield return

对于函数:

public static List<int> Unique(IEnumerable<int> nums)
{
List<int> uniqueVals = new List<int>();

foreach(int num in nums)
{
if(!uniqueVals.ContainsKey(num))
{
uniqueVals.Add(num);
Console.WriteLine(num);
}
}
return uniqueVals;
}

不难理解,传进来一个数组,比如说是一个 list,返回一个去重的 list 出去,简单明了,事实上,这个函数如果不需要复用的话,没有任何问题。但是,作为程序员,我们遇到不需要复用的情形实际上是非常少的。

比方说,现在有一个要求,让你把一个去重的 list 乘方,你会怎么做,我们可能很自然的想到,把这个 Unique 函数的输出结果当做输入传到另一个乘方函数里面,然后再遍历一遍即可解决。

可是,这样的问题是,我们完全可以把去重和乘方放在一次遍历里面一起解决啊,你这样多遍历了一次,增加了时间复杂度。

又有同学可能要说,我们直接去改 Unque 函数不就行了吗,再里面添加一段乘方逻辑。好,这样的话我再问你,比方说现在又有一个需求,让你把一个 list 去重以后开方,你怎么办?你是要再写一个新的函数么?这样的话,你这两个函数里面都有 list 去重这段逻辑,造成了代码冗余,这是软件工程的禁忌。
那怎么办?yield return 可以完美的解决这个问题,以上面的去重乘方需求为例,我们可以把 Unique 函数改写为:

public static IEnumerable<int> Unique2(IEnumerable<int> nums) {
List<int> uniqueVals = new List<int>();
foreach(int num in nums) {
if (!uniqueVals.ContainsKey(num)) {
uniqueVals.Add(num);
yield return num;
}
}
}

接着,我们写乘方函数,同样使用 yield return:

public static IEnumerable<int> Square(IEnumerable<int> nums) {
foreach (var num in nums) yield return num * num;
}

使用时直接进行函数嵌套即可,通过一个 for 循环查看一下结果:

var l = new List<int> { 0, 3, 4, 5, 7, 3, 2, 7, 8, 0 ,3, 1};
foreach (int num in Square(Unique2(l))) Console.WriteLine($"{num}");

yield 关键字的实现原理

用 while 循环代替 foreach 循环,发现我们虽然没有实现 GetEnumerator(),也没有实现对应的 IEnumerator 的 MoveNext(),和 Current 属性,但是我们仍然能正常使用这些函数。

至于为什么会出现这种情况,我们可以用 ILSpy 对生成的 exe 进行反编译来找到原因。
由于直接反编译成 C#会变为原样

每次调用 MoveNext()函数都会将 state 加 1,一共进行了 4 次迭代,前三次返回 true,最后一次返回 false,代表迭代结束。这四次迭代对应被 3 个 yield return 语句分成 4 部分的 enumberableFuc()中的语句。

用 enumberableFuc()来进行迭代的真实流程就是: 1.运行 enumberableFuc()函数,获取代码自动生成的类的实例。 2.接着调用 GetEnumberator()函数,将获取的类自己作为迭代器开始迭代。 3.每次运行 MoveNext(),state 增加 1,通过 switch 语句可以让每次调用 MoveNext()的时候执行不同部分的代码。
4。MoveNext()返回 false,结束。
这也能说明 yield 关键字其实是一种语法糖,最终还是通过实现 IEnumberable、IEnumberable、IEnumberator和 IEnumberator 接口实现的迭代功能。

参考书籍、链接

《ASP.NET Core 技术内幕与项目实战 杨中科》
https://www.cnblogs.com/blueberryzzz/p/8678700.html
https://zhuanlan.zhihu.com/p/103850999