惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Google DeepMind News
Google DeepMind News
Stack Overflow Blog
Stack Overflow Blog
Hugging Face - Blog
Hugging Face - Blog
博客园_首页
T
The Blog of Author Tim Ferriss
博客园 - 叶小钗
N
Netflix TechBlog - Medium
腾讯CDC
C
Check Point Blog
P
Proofpoint News Feed
Engineering at Meta
Engineering at Meta
GbyAI
GbyAI
S
SegmentFault 最新的问题
F
Fortinet All Blogs
美团技术团队
U
Unit 42
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 司徒正美
F
Full Disclosure
Recorded Future
Recorded Future
D
DataBreaches.Net
博客园 - 【当耐特】
Martin Fowler
Martin Fowler
J
Java Code Geeks
I
InfoQ
Y
Y Combinator Blog
A
About on SuperTechFans
AI
AI
爱范儿
爱范儿
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Forbes - Security
Forbes - Security
W
WeLiveSecurity
M
MIT News - Artificial intelligence
雷峰网
雷峰网
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Simon Willison's Weblog
Simon Willison's Weblog
Schneier on Security
Schneier on Security
The GitHub Blog
The GitHub Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
aimingoo的专栏
aimingoo的专栏
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
G
GRAHAM CLULEY
Know Your Adversary
Know Your Adversary
Latest news
Latest news
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
D
Docker
Recent Commits to openclaw:main
Recent Commits to openclaw:main
量子位
V2EX - 技术
V2EX - 技术
Project Zero
Project Zero

博客园 - TanSea

VMware虚拟机下Linux系统(Ubuntu桌面版)微服务环境搭建 - 构建篇 VMware虚拟机下Linux系统(Ubuntu桌面版)微服务环境搭建 - Gogs篇 VMware虚拟机下Linux系统(Ubuntu桌面版)微服务环境搭建 - Docker篇 VMware虚拟机下Linux系统(Ubuntu桌面版)微服务环境搭建 - Jenkins篇 VMware虚拟机下Linux系统(Ubuntu桌面版)微服务环境搭建 - 准备篇 Sql Server 分批复制数据 Sql Server 查询数据库表结构 MAUI Blazor+MASA开发安卓应用学习笔记 - 设置图标和初始屏幕 MAUI Blazor+MASA开发安卓应用学习笔记 - 设置APP格式、名称、版本信息 MAUI Blazor+MASA开发安卓应用学习笔记 - 新建项目和发布 一道SQL面试题的解题思路 Windows10设置默认简体美式键盘输入法 SQL Server Report Builder RDLC按记录数分页 一次Exchange邮箱接口的开发经历 SQL Server分页查询进化史 一次.NET项目反编译的实战经验(WinForm) 点石成金-访客至上的网站设计秘笈 读书笔记 Windows7使用无线网卡建立WiFi热点 程序员的职业素养 读书笔记 - 第14章 辅导、学徒期与技艺
C# 历史版本特性变更(更新到C# 11)
TanSea · 2021-05-02 · via 博客园 - TanSea

官方的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为空时,实例化,上面的代码和下面的代码意思是一样的

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();

现在,因为实例化的时候已经确定了类型,在后面就不用再接类型了

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