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的一些主要变更都总结完成了,有一些特性变更不是很方便整合到一个实例中去,但又比较重要,就单拉出来说了

posted @ 2021-05-02 03:41  TanSea  阅读(296)  评论(0编辑  收藏  举报