顶级语句(c#9.0)
- 程序入口处直接写代码,不用类,不用main方法,即:
Console.WriteLine("Hello, World!");
经典写法仍然支持
namespace A
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
}
- 同一个项目中只能有一个文件有顶级语句
如果我们在项目中再添加一个类并且使用顶级语句,会报错
- 顶级语句中可以用await语法
之前我们在main方法中如果用await 调用异步方法,main方法前必须加上asyn,返回值也必须为Task,如下所示
static async Task Main(string[] args)
但是在顶级语句中,可以直接调用异步方法
string s = await File.ReadAllTextAsync("d/:1.txt");
- 也可以声明函数
Console.WriteLine(Add(3, 4));// 调用方法
Console.ReadKey();
int Add(int a, int b) // 方法前不能有任何访问修饰符,否则报错
{
return a + b;
}
运行结果:
这里面有个需要注意的点: add 方法前面不能有任何访问修饰符,否则会报错。
2 全局using指令(c#10)
- 将global添加到using前,这个命名空间就能被整个项目引用,不用重复添加
假如有这样一个case,animal 类在linq学习命名空间下,但是我想在位于新语法学习命名空间下的program类和dog类中用,那么我必须在两个类中都引入 using linq学习 才可以使用,代码实例如下:
// animal类
namespace linq学习
{
public class Animal
{
}
}
// program类
using linq学习;
Animal animal = new Animal(); // 调用位于linq学习命名空间下的animal类
//dog 类
using linq学习;
namespace 新语法学习
{
public class Dog
{
public Dog()
{
Animal animal = new Animal(); //调用位于linq学习命名空间下的animal类
}
}
}
现在我将global加到 program类的using linq学习 前,则dog类就不必再次引入了。
- 通常新增一个专门用来写全局using指令的代码文件
新加一个类,将using全部放到这个类里面即可。
- 如果csproj文件中启用了<ImplicitUsings>enable</ImplicitUsings>,编译器会隐式增加对于system,system.linq等常用命名空间的引入。不同各类型项目引入的命名空间也不同。
项目文件中,默认是ennable
3 USING声明(c#8)
实现了IDisposable接口的对象可以用using来管理,但如果一个类里面有很多非托管资源,就会出现多个using嵌套的情况,如:
using (var con = new SqlConnection())
{
con.Open();
using (var cmd = new SqlCommand())
{
cmd.CommandText = "select username from user";
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
}
}
}
}
从c#8开始引入了using声明。在实现了IDisposable/IAsynDIsposable接口的类型变量前加上using,当代码执行到离开变量的作用域时,对象就会被释放。上面的代码就可以写成:
using var con = new SqlConnection();
con.Open();
using var cmd = new SqlCommand();
cmd.CommandText = "select username from user";
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
}
我们来看个列子,能更好的理解变量作用域的含义
void TestUsing()
{
for (int i = 0; i < 3; i++)
{
using MyClass myClass = new MyClass();
Console.WriteLine($"i={i}");
}
Console.WriteLine("TestUsing end");
}
public class MyClass : IDisposable
{
public void Dispose()
{
Console.WriteLine("myclass disposed.");
}
}
例子中myclass的作用域就是for循环的花括号,所以每次打印过i的值后就会dispose。
4 文件范围的命名空间声明(c#10)
之前c#类的样子,类必须在namespace里面,如下所示:
namespace A
{
public class B
{
}
}
现在可以这样写
namespace A;
public class B
{
}
class和namespace是平级的,但是这个类仍然属于这个命名空间。
5 可空引用类型(c#8)
c#中数据类型分为值类型和引用类型。值类型不可为null,引用类型可为null。如果不注意检查引用类型是否为null,就有可能引起NullReferenceException。为了规避这种异常,引入了可空引用类型。
- csproj文件中默认启用<Nullable>enable</Nullable>可空引用类型检查
- 在引用类型后添加"?“修饰符来声明这个类型是可空的。对于没有添加”?"修饰符的变量,如果赋值为null,编译器会给出警告信息。
namespace 新语法学习
{
public class Person
{
// name addressa 都不可以为null,但是我只给name赋值,address存在为null的可能
public Person(string name)
{
this.Name = name;
}
public string Name { get; set; }
public string Address { get; set; }
}
}
编译器会给出警告信息
6 Init 语法
如果类中的属性只能被当前类赋值,那么我们可以将它的属性设置成 private set,这样在类外就不能给它赋值了。
public class Person
{
public Person(string name, string address)
{
this.Name = name;
this.Address = address;
}
public string Name { get; private set; } // privare set 说明是只能在当前类中赋值
public string Address { get; set; }
void Test()
{
this.Name = "hahha"; // 在当前类中是可以给name赋值的
}
}
// 在program 类中给name赋值 会报错
Person p1 = new Person("a", "aa");
p1.Address = "new";
p1.Name = "ddd"; // name是private set 不能在它所在的类外给它赋值
如果只想让属性在初始化时被赋值,那么就可以用init
public class Person
{
public Person(string name, string address, string hobby)
{
this.Name = name;
this.Address = address;
this.Hobby = hobby;
}
public string Name { get; private set; } // privare set 说明是只能在当前类中赋值
public string Address { get; set; }
public string Hobby { get; init; } // init说明只能在初始化时被赋值
void Test1()
{
this.Name = "hahha"; // 在当前类中是可以给name赋值的
}
void Test2()
{
this.Hobby = "uuuu"; // 在当前类中是不可以给hobby赋值的,只能在初始化时给它赋值
}
}
在person类中增加一个属性hobby,它是init的,然后尝试在person类中写个方法给他赋值,会报错。在program中给它赋值也会报同样的错。
7 record 类型(c#9)
c#中==默认比较的是两个对象是否指向同一个引用,即使两个对象内容完全相同也不想等。可以通过重写Equal方法来解决,但是需要额外写很多代码。
例:
Person p1 = new Person("a", "aa", "dd");
Person p2 = new Person("a", "aa", "dd");
Console.WriteLine(p1 == p2); // false p1 p2 虽然内容完全相同,但2个对象,指向不同的引用
Console.WriteLine(p1.ToString());// 默认的tostring方法打印的是 命名空间.类名
在c#9中,新增了record类型的语法,编译器会自动帮我们生成Equals, ToString, GetHashcode等方法。
语法: recode className(datatype property);
新增一个 record 类型的cat类
using System;
namespace 新语法学习
{
public record Cat(string name, string color);
}
// program 中调用
Cat cat1 = new Cat("a", "blue");
Cat cat2 = new Cat("a", "blue");
Console.WriteLine(cat1 == cat2);
Console.WriteLine(cat1.ToString());
结果可知,它比较的是对象的内容,并且重写了tostring方法。
原理探究
编译器会根据cat类中的属性定义,自动为cat生成包含所有属性的构造函数。所以我们写 new cat(), new cat(“a”)是不对的。也会自动生成tostring equals等方法。
用反编译器ILSpy去看一下,其实record就是一个普通的类。
深入理解
- 可以实现部分属性只读而部分属性可读写。代码示例如下:
namespace 新语法学习
{
public record Tiger(string name, string color) // name color只能通过构造函数赋值
{
public int Age { get; set; } // age可以在外部赋值
}
}
// program 中调用
Tiger t1 = new Tiger("t", "e");
t1.Age = 2;
Tiger t2 = new Tiger("t", "e");
t2.Age = 3;
Console.WriteLine(t1 == t2);
t2.Age = 2;
Console.WriteLine(t1 == t2);
Console.WriteLine(t1.ToString());
2. 默认生成的构造函数行为不能更改,但是我们可以为类提供多个构造函数,用this调用默认构造函数
namespace 新语法学习
{
public record Tiger(string name, string color) // name color只能通过构造函数赋值
{
public int Age { get; set; } // age可以在外部赋值
public Tiger(string name, string color, int age) : this(name, color) // 可以提供多个构造函数
{
this.Age = age;
}
}
}
- 推荐使用只读类型的属性。所有属性都为只读的类型叫做不可变类型,可以让程序逻辑简单,减少并发访问,状态管理等麻烦。
- record是普通类,变量的传递是引用传递
// record 是引用传递
Tiger t1 = new Tiger("t", "e");
Tiger t2 = t1;
Console.WriteLine(object.ReferenceEquals(t1, t2)); // 看是不是引用同一个实例
with 创建对象副本
生成一个对象的副本
// 创建对象副本
Tiger t1 = new Tiger("t", "e");
Tiger t2 = new Tiger(t1.name, t1.color); // 麻烦的写法
Console.WriteLine(t1 == t2);
Console.WriteLine(object.ReferenceEquals(t1, t2)); // 看是不是引用同一个实例
with写法:
Tiger t1 = new Tiger("t", "e");
//Tiger t2 = new Tiger(t1.name, t1.color); // 麻烦的写法
Tiger t2 = t1 with { }; // with 写法
Console.WriteLine(t1 == t2);
Console.WriteLine(object.ReferenceEquals(t1, t2)); // 看是不是引用同一个实例
// 创建对象副本
Tiger t1 = new Tiger("t", "e");
Tiger t3 = t1 with { name = "t3" };// 只有name不同
Console.WriteLine(t3.ToString());