一. 顶级语句
1. 直接在C#文件中直接编写入口方法的代码,不用类,不用Main.同时经典写法仍然支持,反编译一下了解真相.
2. 同一个项目中只能有一个文件具有顶级语句
3. 顶级语句中可以直接使用await语法,也可以声明函数
代码分享:
Console.WriteLine("测试写入文件哦");
await File.WriteAllTextAsync("1.txt", "hello");
Console.WriteLine("写入完成");
Console.ReadKey();
反编译代码:
二. global using
1. 说明
将 global 修饰符添加到 using 前,这个命名空间就应用到整个项目,不再需要重复每个文件使用using了。
2. 常用套路
通常创建一个专门用来编写全局using代码的C#文件。
比如新建一个GlobalUsing.cs类,里面使用global using 导入需要使用的命名空间,那么在该项目中的其它文件使用这些命名空间下的文件方法的时候,不再需要using了
3. 补充
如果csproj中启用了<ImplicitUsings>enable</ImplicitUsings>,编译器会自动隐式增加对于System、System.Linq等常用命名空间的引入,不同各类型项目引入的命名空间也不一样。
三. 文件范围的声明空间
比如新建1个类,它都是在 namespace xxx { 类xxx},现在可以省略最外层的{},详见 utils/TestHelp2
旧写法:
namespace _02_基础总结.utils
{
internal class TestHelp2
{
}
}
新写法:
namespace _02_基础总结.utils;
internal class TestHelp2
{
}
四. using管理
1. 旧写法
实现了IDisposible接口的对象可以用using进行管理, 形如 using(var xxx= new xxx){}, 其中 {} 执行完毕,using中的内容dispose释放。
string connStr = "Data Source=.;Initial Catalog=demo1;Integrated Security=True";
using (var conn = new SqlConnection(connStr))
{
conn.Open();
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "select * from T_Articles";
using (SqlDataReader reader = cmd.ExecuteReader())
{
while (reader.Read())
{
}
}
}
}
自己实现一个案例测试:
using (MyFile myFile = new MyFile())
{
Console.WriteLine("执行完毕");
}
运行结果:
2. 新写法(C#8.0)
在实现了Idisposable/IAsyncDisposable接口的类型的变量声明前加上using,当代码执行离开变量的作用域时(比如这些代码在一个函数里,那么就是离开这个函数),对象就会被释放。
形如:using SqlConnection conn = new SqlConnection("xxx") ; 当离开conn的作用域的时候,conn将被释放 如下案例
详细测试:using后面变量离开作用域释放问题
前置代码:MyFile类
public class MyFile : IDisposable
{
public void Dispose()
{
Console.WriteLine("MyFile被释放了");
}
}
(1). 案例1--using作用域为所在的函数
myFile的作用域为TestDispose所在的函数,当函数结束,myFile被释放,所以运行结果如下:
// 测试1--using作用域为所在的函数
{
Console.WriteLine("start");
TestDispose();
Console.WriteLine("end");
static void TestDispose()
{
using MyFile myfile = new MyFile();
Console.WriteLine("TestDispose执行完毕");
}
}
(2). 案例2--using作用域是为每次for循环结束
myFile的作用域为所在的for循环每次结束的时候释放,所以运行结果如下
// 测试2--using作用域为每次for循环结束
{
Console.WriteLine("start");
TestDispose();
Console.WriteLine("end");
static void TestDispose()
{
for (int i = 0; i < 2; i++)
{
using MyFile myfile = new();
Console.WriteLine($"i={i}");
}
Console.WriteLine("TestDispose执行完毕");
}
}
3. using声明陷阱
如下例子,使用新的using写法,导致上面写文件的using还没释放,就又要去读文件,所以会报错
System.IO.IOException: The process cannot access the file 'e:\1.txt' because it is being used by another process.
//using陷阱
/*
System.IO.IOException: The process cannot access the file 'e:\1.txt' because it is being used by another process.
*/
{
//写文件
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);
}
解决方案:把上面写的using释放到即可
方案1:using包裹的模式
方案2:给using新写法加1层作用域
//方案1-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);
}
//方案2-给using新写法加1层作用域
{
{
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);
}
五. 可空引用类型
1. 背景
C#数据类型分为值类型和引用类型两种,值类型的变量不可以为空,而引用类型变量可以为空。
问题:如果不注意检查引用类型变量是否可空,就有可能造成程序中出现NullReferenceException异常。
2. 实操
(1). csproj中<Nullable>enable</Nullable>启用可空引用类型检查
(2). 在引用类型后添加“?”修饰符来声明这个类型是可空的。对于没有添加“?”修饰符的引用类型的变量,如果编译器发现存在为这个变量赋值null的可能性的时候,编译器会给出警告信息
如:Nullable/student类中的name 和 array 是引用类型,需要加个?, 否则报警告。
internal class Student
{
public int Id { get; set; }
public string? Name { get; set; }
public DateTime CreatedDate { get; set; }
public string[]? array { get; set; }
}
(3). 如果程序员确认被访问的变量、成员确实不会出现为空的情况,也可以在访问可空的变量、成员的时候加上!来抑制编译器的警告。
Console.WriteLine(s1.PhoneNumber!.ToLower());
(4). 个人倾向:不喜欢开启Nullable
六. record新类型
1. 背景
C#中的==运算符 和 equal方法 默认是判断两个变量指向的是否是同一个对象,即使两个对象内容完全一样,也不相等。可以通过重写Equals方法、重写==运算符等来解决这个问题,不过需要开发人员编写非常多的额外代码。
代码分享:
public class Person1
{
public int Id { get; set; }
public string Name { get; set; }
public Person1(int Id,string Name)
{
this.Id = Id;
this.Name = Name;
}
}
测试:
Person1 p1 = new Person1(1, "ypf1");
Person1 p2 = new Person1(1, "ypf1");
Person1 p3 = p1;
Console.WriteLine(p1 == p2); //false
Console.WriteLine(p1.Equals(p2)); //false
//下面指向同一个地址
Console.WriteLine(p1 == p3); //true
Console.WriteLine(p1.Equals(p3)); //true
2. record
在C#9.0中增加了记录(record)类型的语法,编译器会为我们自动生成Equals、GetHashcode等方法。
编译器会根据Person类型中的属性定义,自动为Person类型生成包含全部属性的构造方法。注意,默认情况下,编译器会生成一个包含所有属性的构造方法,
因此,我们编写new Person()、new Person(“ypf”)这两种写法都是不可以的。也会生成ToString方法和Equals等方法
对于record类型,如果两个两个对象内容完全一样,使用equal或==相比较,返回的是true。如Person2相关案例
代码分享
public record Person2(int Id,string Name);
测试
{
Person2 p1 = new(1, "ypf1");
Person2 p2 = new(1, "ypf1");
Console.WriteLine(p1 == p2); //true
Console.WriteLine(p1.Equals(p2)); //true
}
3. 补充
默认生成的构造方法的行为不能修改,我们可以为类型提供多个构造方法,然后其他构造方法通过this调用默认的构造方法。
如下Person3案例
4. 对象的拷贝
record也是普通类,变量的赋值是引用的传递。
生成一个对象的副本,这个对象的其他属性值与原对象的相同,只有一个或者少数几个属性改变。麻烦的做法:User u2 = new User(u1.UserName, "test@example", u1.Age);
简单的方法:用with关键字简化:User u2 = u1 with { Email= “test@example” }; 创建的也是拷贝。
代码分享:
public record Person3(int Id, string Email, string Name)
{
public Person3(int Id, string Name) : this(Id, null, Name)
{
}
}
测试:
{
Person3 p1 = new(1, "ypf1");
Person3 p2 = new(1, "ypf@qq.com", "ypf2");
Person3 p3 = p1 with { };
Console.WriteLine(p1 == p3); //true
Console.WriteLine(Object.ReferenceEquals(p1, p3)); //比较两个对象是否指向同一个地址, false
Person3 p4 = p1 with { Email = "lmr@qq.com" };
Console.WriteLine(p1 == p4); //false
Console.WriteLine(Object.ReferenceEquals(p1, p4)); //比较两个对象是否指向同一个地址, false
}
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。