C# 历史版本特性变更(更新到C# 11)
官方的C#历史版本特性变更是按版本排序的,知识点有些乱,我就产生了一个想法,以一个数据库操作模块对这些知识做一个整合
注意事项:
1、不一定会整合所有的特性变更,但是努力整合
2、由于目的是在介绍历史版本特性,代码的实现不一定是最优方案
废话不多说,先上一段实体类代码
定义实体类
public class BaseEntity { private Guid _id; public Guid Id { get { return _id; } set { _id = value; } } public DateTime CreateTime { get; set; } public Nullable<DateTime> UpdateTime { get; set; } public bool IsDelete { get; set; } = false; } public partial class Member : BaseEntity { private string _firstName; public string FirstName { get => _firstName; set => _firstName = (!string.IsNullOrWhiteSpace(value)) ? value : throw new ArgumentException("FirstName不能为空"); } public string LastName { get; set; } public DateTime? BirthOfDay { get; set; } } public partial class Member { public string FullName => $"{FirstName} {LastName}"; public int? Age { get; init; } public override string ToString() => $"{FirstName} {LastName}"; }
分部类型(Partial types)【C# 2.0】
拆分一个类、一个结构、一个接口或一个方法的定义到两个或更多的文件中
public partial class Member : BaseEntity { private string _firstName; public string FirstName { get => _firstName; set => _firstName = value; } public string LastName { get; set; } public DateTime? BirthOfDay { get; set; } } public partial class Member { public string FullName => $"{FirstName} {LastName}"; public int? Age { get; init; } public override string ToString() => $"{FirstName} {LastName}"; }
以上实体类的定义,BaseEntity类用来定义数据库表的公共字段,Member类用partial定义成分部类。个人习惯一个分部定义映射属性,一个分部定义扩展属性
可为空的值类型(Nullable value types)【C# 2.0】
建立数据映射时,有些属性可能是没有数据的,例如:UpdateTime、BirthOfDay
public Nullable<DateTime> UpdateTime { get; set; } public DateTime? BirthOfDay { get; set; }
Nullable<T>用来定义可空值类型,也可以简写成T?,T为不能为空的值类型。官方推荐使用T?
既然提到了可空类型,那么就把相关的变更也一起说一下
Null传播器(Null propagator)【C# 6.0】
从字面上理解,就是将Null传播到下一级属性
Guid id = member.Id; // id是Guid类型,但member为空时会报异常 Guid? id = member?.Id; // id是Guid?类型,member可以为空;当member为空时,id也为空;当member不为空时,id = member.Id
Null传播器的出现,使得我们不需要写大量的代码来判断对象是否为空
默认文本表达式(Default literal expressions)【C# 7.1】
在大多数情况下,我们并不喜欢对可空类型进行操作,更希望给他一个默认值
过去,可以这样给一个变量赋默认值
Guid id = member?.Id ?? default(Guid);
现在,可以把后面的类型省略掉
Guid id = member?.Id ?? default;
Null合并赋值(Null-coalescing assignment)【C# 8.0】
member ??= new Member();
当member为空时,实例化,上面的代码和下面的代码意思是一样的
if (member == null) member = new Member();
自动实现的属性(Auto-Implemented properties)【C# 3.0】
过去,定义属性代码量有些多
private Guid _id; public Guid Id { get { return _id; } set { _id = value; } }
现在,一行代码就可以了
public DateTime CreateTime { get; set; }
自动属性初始化表达式(Auto property initializers)【C# 6.0】
过去,给属性初始化需要在构造方法中实现,现在,实现属性的时候可以直接初始化了
public bool IsDelete { get; set; } = false;
表达式主体定义(Expression body definition)【C# 6.0 - 7.0】
表达式主体使用 member => expression 来定义,在C# 6.0支持方法、运算符和只读属性,在C# 7.0支持构造函数、终结器、属性和索引器访问器
public override string ToString() => $"{FirstName} {LastName}"; // 方法
public string FullName => $"{FirstName} {LastName}"; // 属性
字符串内插(string interpolation)【C# 6.0】
过去,字符串的拼接是这样的
string fullName = string.Format("{0} {1}", firstName, lastName);
现在,使用$将变量直接写到花括号内
string fullName = $"{firstName} {lastName}";
可使用 const 内插字符串(Allow const interpolated strings)【C# 10.0】
const string name = "TanSea"; const string greeting = $"Hello, {name}.";
字符串内插中的换行符(Newlines in string interpolations)【C# 11.0】
为增加代码的可读性,字符串内插支持换行了
string season = $"The season is {month switch { 1 or 2 or 12 => "winter", > 2 and < 6 => "spring", > 5 and < 9 => "summer", > 8 and < 12 => "autumn", _ => "unknown. Wrong month number", }}.";
原始字符串(Raw string literals)【C# 11.0】
C# 11的版本引入了原始字符串,允许包含任意文本而不转义,格式是用至少3个双引号
string longMessage = """ This is a long message. It has several lines. Some are indented more than others. Some should start at the first column. Some have "quoted text" in them. """;
这段文本会按他的原始格式输出,那么问题来了,如果在文本里面有3个双引号怎么办呢?这个问题的解决办法就是在最外层再补一个双引号,变成4个双引号。始终保持多一个双引号就行了。
那么问题又来了,如果在文本里面有花刮号呢?我像上面那样内插字符串也是一样的套路,比原来多一个,花刮号多一个,$也要多一个
var location = $$""" You are at {{{Longitude}}, {{Latitude}}} """;
2个$说明2个花刮号里面的是内插字符串,其他的都是原始的花刮号字符
仅限 Init 的资源库(Init only setters)【C# 9.0】
init访问器定义的属性仅在构造时可以赋值,实例化之后属性就变成只读了
public int? Age { get; init; }
年龄属性是根据生日属性计算出来了,适合使用init访问器
记录类型(Record types)【C# 9.0】
record 和 class 类似,区别在于属性完全相同的两个对象,class 会认为是两个对象,record 会认为是同一个对象
User user1 = new() { Name = "TanSea", Email = "TanSea_CN@MSN.com", Age = 18 }; User user2 = new() { Name = "TanSea", Email = "TanSea_CN@MSN.com", Age = 18 }; Console.WriteLine(user1 == user2); // False UserRecord userRecord1 = new() { Name = "TanSea", Email = "TanSea_CN@MSN.com", Age = 18 }; UserRecord userRecord2 = new() { Name = "TanSea", Email = "TanSea_CN@MSN.com", Age = 18 }; Console.WriteLine(userRecord1 == userRecord2); // True
微软推荐实体类用 record 类型
记录结构(Record structs)【C# 10.0】
都说 class 和 struct 很像,有记录类型了当然也要有记录结构,同 class 一样,属性完全相同的对象被认为是同一个对象
with 表达式(with expression)【C# 9.0 - 10.0】
对Record类型修改特定属性和字段并生成副本,在后续的10版本能支持到结构类型和匿名类型,要注意的是 with 不支持类
UserRecord userRecord = new() { Name = "TanSea", Email = "TanSea_CN@MSN.com", Age = 18, Sex = "男" }; UserRecord userRecord1 = userRecord with { Name = "TT" };
必需的成员(Required members)【C# 11.0】
新的属性修饰符 required ,在初始化对象时,必需给他赋值 ,未赋值就会报异常
var member = new Member(); // 错误 var member = new Memeber() { FirstName = "Tan", LastName = "Sea" }; // 正确 public class Member { public required string FirstName { get; set; } public required string LastName { get; set; } }
用 SetsRequiredMembers 来修饰构造方法,可能对required修饰的属性赋类型的默认值
var member1 = new Member(); // FirstName="", LastName="", Age=0 Member member2 = new() { FirstName = "Tan", LastName = "Sea" }; // FirstName="Tan", LastName="Sea" Age=0 public class Member { [SetsRequiredMembers] public Member() { } public required string FirstName { get; set; } public required string LastName { get; set; } public required int Age { get; set; } }
那么问题来了,如果我定义成 public required int? Age { get; set; },给一个可空类型加一个 required 修饰会是什么情况呢?
添加记录
public bool Insert(Member member) { return _dbHelper.Insert(member); }
一个简单的Insert方法,通过数据库帮助类把实体类添加进数据库。这里就有一个问题,数据库表肯定不只一个,那么我们就需要定义很多这些类似的方法。不管是手撸还是用生成器代码量还是很多
泛型(Generics)和泛型约束(Constraints)【C# 2.0】
延时指定类型,也就是在调用方法时再指定参数的类型。泛型的命名通常是以大写T开头
public bool Insert<TEntity>(TEntity entity) { return _dbHelper.Insert<TEntity>(entity); }
泛型可以指定任何类型,但在这里我们要求只能是实体类,所以这里要用到泛型约束
public bool Insert<TEntity>(TEntity entity) where TEntity: class { return _dbHelper.Insert<TEntity>(entity); }
用where来指定泛型只能是类,但是这仍然还不够,我们要求的是和数据库有映射关系的实体类,所以要对泛型进一步约束
public bool Insert<TEntity>(TEntity entity) where TEntity: BaseEntity { return _dbHelper.Insert<TEntity>(entity); }
这里指定泛型只能是BaseEntity及继承自BaseEntity的类
对象和集合初始值设定项(Object and collection initializers)【C# 3.0】
过去,初始化一个实体类要么在构造方法中实现,要么先实例化之后再一个个的赋值
Member member = new Member(); member.Id = Guid.NewGuid(); member.FirstName = "Tan"; member.LastName = "Sea"; member.CreateTime = DateTime.Now; bool isSuccess = Insert<Member>(member);
现在,可以用以下的方式来初始化
Member member = new Member { Id = Guid.NewGuid(), FirstName = "Tan", LastName = "Sea", CreateTime = DateTime.Now }; bool isSuccess = Insert<Member>(member);
目标类型的 new 表达式(Target-typed new expressions)【C# 9.0】
过去,实例化类型时 new 后面是要接类型的
Member member = new Member();
现在,因为实例化的时候已经确定了类型,在后面就不用再接类型了
Member member = new();
异步方法(async/await)【C# 5.0】
异步方法可以避免 UI线程卡顿,提高系统吞吐率
过去,用Thread来实现异步方法,Thread功能很强大但相对的非常难用好。后来ThreadPool针对Thread进行了一次封装,只要将线程提交给它即可,其他的什么也做不了
现在,用async/await定义异步方法,异步方法命名通常用Async结尾
public async Task<bool> InsertAsync<TEntity>(TEntity entity) where TEntity: BaseEntity { return await _dbHelper.InsertAsync<TEntity>(entity); }
async/await的本质是状态机,如果想了解更多可以查看官方文档
查询记录
public async Task<IEnumerable<TEntity>> GetEntitiesAsync<TEntity>(Expression<Func<TEntity, bool>> whereExpression) where TEntity: BaseEntity { return await _dbHelper.GetEntitiesAsync<TEntity>(whereExpression); }
IEnumerable<Member> adultMembers = await GetEntitiesAsync<Member>(member => member.Age >= 18 && member.IsDelete == false);
建议在判断布尔值为假时,使用 member.IsDelete == false 而不是 !member.IsDelete,前者的可读性更高一些
匿名方法(Anonymous methods)【C# 2.0】
Func<Member, bool> func = delegate (Member member) { return member.Age >= 18 && member.IsDelete == false; };
拉姆达表达式(Lambda expressions)【C# 3.0】
在C# 3.0版本之后,都是用拉姆达表达式来定义匿名方法了,拉姆达表达式格式分两种:
(input-parameters) => expression
Func<Member, bool> func = member => member.Age >= 18 && member.IsDelete == false;
(input-parameters) => { <sequence-of-statements> }
Func<Member, bool> func = member => { return member.Age >= 18 && member.IsDelete == false; };
拉姆达表达式的自然类型(Natural type of a lambda expression)【C# 10.0】
之前的拉姆达表达式需要显式的声明委托类型,现在支持自然类型,由拉姆达表达式去推断委托类型
var func = member => member.Age >= 18 && member.IsDelete == false; var func = member => { return member.Age >= 18 && member.IsDelete == false; };
显式返回类型(Explicit return type)【C# 10.0】
现在没有显式的声明委托类型,拉姆达表达式不知道返回什么值,这时候就要显式的给他声明一个返回类型
var func = bool () => member.Age >= 18 && member.IsDelete == false; var func = bool () => { return member.Age >= 18 && member.IsDelete == false; };
本地函数(Local functions)【C# 7.0】
说到匿名方法了,可以再说一下本地函数。在方法内定义,和匿名方法不同的是,本地函数可以定义在调用之后
public void Method() { LocalMethod(member); bool LocalMethod(Member member) { return member.Age >= 18 && member.IsDelete == false; } }
静态本地函数(Static local functions)【C# 8.0】
静态本地函数和本地函数的作用域都是一样的,区别在于静态本地函数不访问封闭范围中的任何变量
public void Method() { int age = 18; LocalMethod(member); static bool LocalMethod(Member member) { return member.Age >= age && member.IsDelete == false; // 错误,这里使用了封闭范围中的变量 } }
表达式树(Expression Trees)【C# 3.0】
使用表达式类构造一段代码,再通过对这段代码的解释来完成特定的需求。微软的Entity Framework就是将表达式树解释成SQL语句来操作数据库的
Expression<Func<Member, bool>> expr = member => member.Age >= 18 && member.IsDelete == false;
那么怎么把查询传入的参数构造成一段表达式代码并解释成SQL语句,这个要看更深入的了解表达式树,这里就不再展开了
查询分页记录
public IEnumerable<TEntity> GetEntitiesPage<TEntity>(Expression<Func<TEntity, bool>> whereExpression, out int totalRecord, int pageIndex = 1, int pageSize = 10) where TEntity : BaseEntity { return _dbHelper.GetEntitiesPage<TEntity>(whereExpression, out totalRecord, pageIndex, pageSize); }
int totalRecord = 0; IEnumerable<Member> members = GetEntitiesPage<Member>(member => member.IsDelete == false, out totalRecord, 2, 20); var result = new { Members = members, TotalRecord = totalRecord };
可以看出来这个方法的定义没有使用异步方法,因为异步方法不能使用out关键字
out变量(out variables)【C# 7.0】
现在,使用out变量的时候不用先定义了,可以直接在out变量后面定义
IEnumerable<Member> members = GetEntitiesPage<Member>(member => member.IsDelete == false, out int totalRecord, 2, 20);
命名参数和可选参数(Named and optional arguments)【C# 4.0】
在定义方法时,在pageIndex和pageSize之后添加了一个默认值,在调用方法时,可以不用传参
IEnumerable<Member> members = GetEntitiesPage<Member>(member => member.IsDelete == false, out totalRecord);
也可以给指定参数赋值
IEnumerable<Member> members = GetEntitiesPage<Member>(member => member.IsDelete == false, out totalRecord, pageSize: 20);
隐式类型本地变量(Implicitly typed local variables)【C# 3.0】
使用var来在方法内定义隐式类型,隐式类型也是属于强类型,编译器会根据初始化时的值来决定是什么类型
var i = 5; // i is int
var str = "name" // str is string
动态类型(Dynamic Type)【C# 4.0】
dynamic i = 5; dynamic str = "name"
dynamic和var一样都是定义隐式类型。不同的是var是属于强类型,而dynamic是弱类型,它会绕过编译时类型检查
dynamic和object的行为类似,任何非空的表达式都可以转换为dynamic
匿名类型(Anonymous Types)【C# 3.0】
通常,我们不需要返回实体类的所有属性,可以定义一个匿名类型
var result = new { Members = members, TotalRecord = totalRecord };
在new后面不指定类型就是匿名类型,由于类型无法确定,result变量只能定义成隐式类型
var是配合匿名类型一起使用的,在可以显式的定义类型时不推荐使用var
查询表达式(Query expressions)【C# 3.0】
一种和SQL很像的表达式,现在用得很少了,简单的介绍一下,如果要深入了解可以去查看官方文档
var result = from m in members
select new { Members = members, TotalRecord = totalRecord };
元组(Tuples)【C# 7.0】
要使用异步方法,就不能用out参数来返回值,定义成对象返回结果是一种解决方案
public async Task<Response<TEntity>> GetEntitiesPageAsync<TEntity>(Expression<Func<TEntity, bool>> whereExpression, int pageIndex = 1, int pageSize = 10) where TEntity : BaseEntity { return await _dbHelper.GetEntitiesPageAsync<TEntity>(whereExpression, pageIndex, pageSize); } public class Response<T> { public IEnumerable<T> Entities { get; set; } public int TotalRecord { get; set; } }
Response<Member> response = await GetEntitiesPageAsync<Member>(member => member.IsDelete == false, 2, 20);
而另一种解决方案就是使用元组来返回多个值
public async Task<(IEnumerable<TEntity>, int)> GetEntitiesPageAsync<TEntity>(Expression<Func<TEntity, bool>> whereExpression, int pageIndex = 1, int pageSize = 10) where TEntity : BaseEntity { return await _dbHelper.GetEntitiesPageAsync<TEntity>(whereExpression, pageIndex, pageSize); }
(IEnumerable<Member> members, int totalRecord) = await GetEntitiesPageAsync<Member>(member => member.IsDelete == false, 2, 20);
弃元(Discards)【C# 7.0】
当然,有时候可能不需要元组中的某一些参数
(IEnumerable<Member> members, _) = await GetEntitiesPageAsync<Member>(member => member.IsDelete == false, 2, 20);
下划线变量是一个只写不读的变量,同样的微软也推荐在任何不需要返回值的时候显式的用弃元
_ = Insert<Member>(member);
元组解析(Deconstruction)【C# 7.0】
public void Deconstruct(out string firstName, out string lastName) => (firstName, lastName) = (FirstName, LastName);
在类中定义了Deconstruct方法之后,我们可以用元组来提取类中的各个字段
Member member = new Member { Id = Guid.NewGuid(), FirstName = "Tan", LastName = "Sea" }; (string FirstName, string LastName) = member;
同一个解构中的赋值和声明(Assignment and declaration in same deconstruction)【C# 10.0】
之前的元组解析只能同时赋值,同时声明,现在解构就可以混搭了,感觉意义不是太大:)
string FirstName = "XX"; (FirstName, string LastName) = member;
修改实体类
现在,业务拓展了,需要记录会员的消费次数和最后的消费时间,通过这2个属性来得到会员的状态
public partial class Member : BaseEntity { // 代码略 public int ConsumeTimes { get; set; } public DateTime LastConsumeTime { get; set; } } public partial class Member { // 代码略 public string ConsumeState { get; set; } }
public IEnumerable<Member> GetMembersWithConsumeState() { IEnumerable<Member> members = _dbHelper.GetEntities<Member>(member => member.IsDelete == false); foreach (var member in members) { member.ConsumeState = member.GetConsumeState(); yield return member; } }
扩展方法(Extension methods)【C# 3.0】
给已知类型添加新的方法,GetConsumeState就是给Member类型添加的扩展方法
public static class ExtensionMethod { public static string GetConsumeState(this Member member) { var days = DateTime.Now.Subtract(member.LastConsumeTime).Days; if (member.ConsumeTimes == 1) { return "新客户"; } else if (member.ConsumeTimes >= 10) { return "老客户"; } else { if (days <= 30) { return "活跃客户"; } else if (days <= 60) { return "非活跃客户"; } else { return "静默客户"; } } } }
扩展方法的类和方法都要是静态方法,this就是给哪个类型添加方法
模式匹配(Pattern matching)【C# 7.0 - 11.0】
匹配一个类型,如果成功给变量赋值,如果不成功变量为默认值
模式匹配从7.0开始,每个版本都有增强,7.0支持is和switch。8.0支持switch表达式、属性模式、元组模式、位置模式。9.0支持and、or、not模式
简单的is模式匹配,当input是int时,赋值给count并参与求和。当input不是int时,count赋默认值。
var input = "你好"; var sum = 0; if (input is int count) sum += count;
元组模式匹配,重写上面的GetConsumeState方法
public static string GetConsumeState(this Member member) { var days = DateTime.Now.Subtract(member.LastConsumeTime).Days; return (member.ConsumeTimes, days) switch { (1, _) => "新客户", ( >= 10, _) => "老客户", ( > 1 and < 10, <= 30) => "活跃客户", ( > 1 and < 10, <= 60) => "非活跃客户", _ => "静默客户" }; }
模式匹配还能用来判断对象的值是否合法
string message = user switch { { Name: "" } => "Name cannot be empty", { Email: "" } => "Email cannot be empty", { Sex: not ("男" or "女") } => "Sex can only be male or female", { Age: < 0 or > 100 } => "Age cannot be less than 0 or greater than 100", _ => "" };
当然小伙伴们肯定会想到,如果我要判断姓名的长度怎么办?邮箱地址是不是合法?长度的判断在C#10是支持的,但是邮箱地址合法性判断还不支持。换句话说,属性是支持的,但是方法不支持。
string message = user switch { { Name.Length: < 6 } => "Name length at least greater than 6", // 支持 { ValidateEmail(Email): true } => "Email format is incorrect", // 不支持 _ => "" };
更多的模式匹配用法请查看官方文档,这里就不再介绍了
迭代器(Iterators)【C# 2.0】
当返回类型为IEnumerable、IEnumerable<T>、IEnumerator 或 IEnumerator<T>时,可以使用yield return来逐个返回元素。迭代器会保留状态并在下次进入方法时继续执行
public IEnumerable<Member> GetMembersWithConsumeState() { IEnumerable<Member> members = _dbHelper.GetEntities<Member>(member => member.IsDelete == false); foreach (var member in members) { member.ConsumeState = member.GetConsumeState(); yield return member; } }
如果members里面有10个元素,则yield return会返回10次,每次返回1个元素
异步流(Asynchronous streams)【C# 8.0】
迭代器的异步版本,可以看出来这中间经过了很多版本,直到出了IAsyncEnumerable才解决了迭代器异步的问题
public async IAsyncEnumerable<Member> GetMembersWithConsumeStateAsync() { IEnumerable<Member> members = await _dbHelper.GetEntitiesAsync<Member>(member => member.IsDelete == false); foreach (var member in members) { member.ConsumeState = member.GetConsumeState(); yield return member; } }
可以看出来,这个异步流的写法和之前异步方法Task<IEnumerable<Member>>不一样
其他变更
默认接口方法(Default interface methods)【C# 8.0】
现在可以将成员添加到接口,并为这些成员提供实现
void Main()
{
ILogger foo = new Logger();
foo.Log (new Exception ("test"));
}
class Logger : ILogger
{
public void Log (string message) => Console.WriteLine (message);
}
interface ILogger
{
void Log (string message);
// Adding a new member to an interface need not break implementors:
public void Log (Exception ex) => Log (ExceptionHeader + ex.Message);
// The static modifier (and other modifiers) are now allowed:
static string ExceptionHeader = "Exception: ";
}
静态引用(Using Static)【C# 6.0】
过去,我们只能对命名空间进行引用
using System;
Math.Round(3.1415926); // Math是静态类
现在,我们可以对静态的类进行引用
using static System.Math;
Round(3.1415926);
全局 using 指令(Global using directives)【C# 10.0】
在以前,一些经常用到的东西在每个页面都要重新引用一遍,非常不方便,也不整洁,现在可以只引用一次,哪哪都能用了,很方便
global using System; global using System.Collections.Generic; global using System.Linq; global using System.Threading.Tasks;
using 声明(Using declarations)【C# 8.0】
过去,我们对继承了IDisposable接口的对象是这么使用的,代码会在花括号结束时自动释放资源
using (var file = new System.IO.StreamWriter("WriteLines.txt"))
{
代码略
}
现在,使用using关键字声明的变量在封闭范围的末尾释放资源,两个用法差别不大(可以脑补一下他们的区别),但是代码的整洁度后者更好
using var file = new System.IO.StreamWriter("WriteLines.txt");
文件范围的命名空间声明(File-scoped namespace declaration)【C# 10.0】
using省去了一个花括号的缩进,那么命名空间也要
namespace Namespace { 代码略 }
现在,同一个文件下都属于这个命名空间,更进一步的提高了代码整洁度
namespace CSharpHistoryFeature;
nameof 表达式(nameof operator)【C# 6.0】
nameof 表达式可生成变量、类型或成员的名称作为字符串常量
Console.WriteLine(nameof(System.Collections.Generic)); // output: Generic
Console.WriteLine(nameof(List<int>)); // output: List
Console.WriteLine(nameof(List<int>.Count)); // output: Count
Console.WriteLine(nameof(List<int>.Add)); // output: Add
var numbers = new List<int> { 1, 2, 3 };
Console.WriteLine(nameof(numbers)); // output: numbers
Console.WriteLine(nameof(numbers.Count)); // output: Count
Console.WriteLine(nameof(numbers.Add)); // output: Add
异常筛选器(Exception filters)【C# 6.0】
对指定条件进行catch,其他条件不catch
try
{
......
}
catch (Exception e) when (e.Message.Contains("404"))
{
return;
}
索引和范围(Indices and ranges)
类似于Python的切片,例子很清晰,就不多解释了
var words = new string[]
{
// index from start index from end
"The", // 0 ^9
"quick", // 1 ^8
"brown", // 2 ^7
"fox", // 3 ^6
"jumped", // 4 ^5
"over", // 5 ^4
"the", // 6 ^3
"lazy", // 7 ^2
"dog" // 8 ^1
}; // 9 (or words.Length) ^0
var quickBrownFox = words[1..4]; // 从索引1到索引4,但不包括索引4
var lazyDog = words[^2..^0]; // 从索引^2到索引^0,但不包括索引^0
var allWords = words[..]; // 从开始到结束,从The到dog
var firstPhrase = words[..4]; // 从开始到索引4,但不包括索引4。从The到fox
var lastPhrase = words[6..]; // 从索引6到结束,从the到dog
数字文本语法改进(Numeric literal syntax improvements)【C# 7.0】
增加数字的可读性,给数字添加分隔符
public const int Sixteen = 0b0001_0000;
public const int ThirtyTwo = 0b0010_0000;
public const int SixtyFour = 0b0100_0000;
public const int OneHundredTwentyEight = 0b1000_0000;
public const long BillionsAndBillions = 100_000_000_000;
public const double AvogadroConstant = 6.022_140_857_747_474e23; // 阿伏伽德罗常量
public const decimal GoldenRatio = 1.618_033_988_749_894_848_204_586_834_365_638_117_720_309_179M; // 黄金比例
调用方信息(Determine caller information)【C# 5.0】
可以获取调用方的一些信息方便调试,比如方法名,方法所在源文件路径,方法所在源文件的行数等
public void TraceMessage(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
System.Diagnostics.Trace.WriteLine("message:" + message);
System.Diagnostics.Trace.WriteLine("member name:" + memberName);
System.Diagnostics.Trace.WriteLine("source file path:" + sourceFilePath);
System.Diagnostics.Trace.WriteLine("source line number:" + sourceLineNumber);
}
结语
C# 2.0到10.0的一些主要变更都总结完成了,有一些特性变更不是很方便整合到一个实例中去,但又比较重要,就单拉出来说了