C#8.0,9.0,10.0中增加了很多新的语法,这些语法中有一些对开发人员帮助很大,这里做一点整理。
目录
1.全局using指令
C# 10.0中增加了“全局using指令”语法,我们可以将global修饰符添加到任何using关键字前,这样通过using语句引入的命名空间就可以应用到这个项目的所以源代码中,因此同一个项目中的C#代码就不需要再去重复引入这个命名空间了。在实践中,通常专门创建一个来编写全局using代码的文件,然后把项目中经常用到的命名空间声明到这个C#文件。
例如你的项目中很多类文件都要引用 Microsoft.Data.Sqlite、System.Text.Json这两个命名空间,,那么可以创建一个Using.cs文件(文件名随意):
global using Microsfot.Data.Sqlite;
global using System.Text.Json;
2.using 声明
C#中的using关键字可以简化非托管资源的释放,当变量离开using作用的范围后,会自动调用对象的Dispose方法,从而完成非托管资源的释放。但是一段代码中如果有很多非托管资源需要被释放,代码就存在多个嵌套using语句。如下例子:
using(var conn=new SqlConnection(connStr))
{
conn.Open();
using(var cmd=conn.CreateCommand())
{
cmd.CommandText="Select * from Students";
using(SqlDataReader reader=cmd.ExecuteReader())
{
...
}
}
}
上面三层嵌套大大加重了程序的深度。C#8.0后简化了使用,可以避免嵌套。在声明变量时,如果类型实现了IDisposable或IAsyncDisposable接口,那么可以在变量声明前加上using关键字,这样当代码执行离开被using修饰的变量作用域时,变量只想的对象的Dispose方法就会被调用。
using var conn=new SqlConnection(connStr)
conn.Open();
using var cmd=conn.CreateCommand()
cmd.CommandText="Select * from Students";
using SqlDataReader reader=cmd.ExecuteReader()
...
虽然上述办法很完美,但是也会遇到一些问题:
看下面的例子:
using var outStream = File.OpenWrite("e:/1.txt");
using var writter = new StreamWriter(outStream);
writter.WriteLine("HelloWorld");
string s = File.ReadAllText("e:/1.txt");
Console.WriteLine(s);
执行时会报错误:System.IO.IOException:“设备未就绪。 : 'e:\1.txt'”
原因是outStream和writter两个变量在方法执行结束后才被释放资源,程序执行到第4行时,文件仍然被占用,因此第4行抛出了异常。所以解决办法是:手动添加括号,将要提前释放的资源放到单独的作用域中:
{
using var outStream = File.OpenWrite("e:/1.txt");
using var writter = new StreamWriter(outStream);
writter.WriteLine("HelloWorld");
}
string s = File.ReadAllText("e:/1.txt");
Console.WriteLine(s);
3.文件范围的命名空间声明
从C#10.0开始,C#允许编写独立的namespace代码声明命名空间,文件中所有类型都是这个命名空间下的成员,这种语法能够减少C#源代码文件的嵌套层次。
namespace TMS.Admin;
class Teacher
{
pulic int Id {get;set;}
public string Name {get;set;}
}
4.记录类型
C#中的==运算符默认判断两个变量是否只想同一个对象,如果两个对象是同一种类型,并且所有属性完全相等,但是他们是两个完全不同的对象,导致==运算符的结果为false,当然你可以通过重写Equals方法,重写==运算符等来解决这个问题,不过这会增加不少的额外代码。
C#9.0中增加了记录 record类型,编译器会自动生成Equals、GetHashCode等方法。
定义一个record类型很简单:
public record Person(string FirstName, string LastName);
接下来我们声明一些person对象并进行判断:
Person p1 = new Person("无忌", "张");
Person p2 = new Person("翠山", "张");
Person p3 = new Person("无忌", "张");
Console.WriteLine(p1);
Console.WriteLine(p2==p1);
Console.WriteLine(p3==p1);
Console.WriteLine(p1.FirstName);
运行结果如下:
Person { FirstName = 无忌, LastName = 张 }
False
True
无忌
编译器会根据定义自动生成一个包含所有属性的构造方法,因此对于Person类型:下面两种写法都是不允许的:
var p4 = new Person();
var p5 = new Person("haha");
利用反编译起编译person类型,你会发现Person本质还是一个类,只是编译器帮你做了很多工作,record类型提供了为所有属性赋值的构造方法,所有属性都是只读的,对象之间可以进行值得相等比较,并且编译器为类型提供了可读性更强的ToString方法。在编写不可变类型,并且需要进行对象值比较是,使用record类型可以把编写代码的难度大大降低。
Record类型还以灵活定义:
public record Student(string Name)
{
public string? Address { get; set; }
public void SayHi() => Console.WriteLine($"Hi, 我是{Name}");
}
这样Address属性就是可写的,而Name是只读的,所以可以这样构造Student对象:
Student s1 = new Student("张麻子");
Student s2 = new Student("张麻子");
Console.WriteLine(s1 == s2);
s1.Address = "广州";
s1.SayHi();
Console.WriteLine(s1==s2);
运行结果:
True
Hi, 我是张麻子
False
可见属性值不一样,两个record就不相等,无论属性是否为只读。
当然我们也可以额外增加构造函数:
public record Student(string Name)
{
public string? Address { get; set; }
public void SayHi() => Console.WriteLine($"Hi, 我是{Name}");
public Student(string name,string? address):this(name)
{
Address=address;
}
}
虽然上面也是可行的,但是这会让record类型变得复杂,有点背离record类型的初衷,我们还是建议将reco类型的属性设置为只读的,也就是默认形式!
由于record类型是只读的,所以当我们想生成一个副本时,以及对生成的副本略作更改时,可以使用with关键字:
Person p4 = p1 with { LastName = "谢" };
var p5 = p4;
Console.WriteLine(p4);
Console.WriteLine(p5);