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

推荐订阅源

T
Tailwind CSS Blog
月光博客
月光博客
爱范儿
爱范儿
罗磊的独立博客
博客园 - 叶小钗
Apple Machine Learning Research
Apple Machine Learning Research
IT之家
IT之家
aimingoo的专栏
aimingoo的专栏
D
DataBreaches.Net
F
Full Disclosure
博客园 - 司徒正美
小众软件
小众软件
D
Docker
大猫的无限游戏
大猫的无限游戏
O
OpenAI News
T
Threatpost
Engineering at Meta
Engineering at Meta
Cisco Talos Blog
Cisco Talos Blog
Google DeepMind News
Google DeepMind News
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Y
Y Combinator Blog
H
Help Net Security
C
Cyber Attacks, Cyber Crime and Cyber Security
C
Cisco Blogs
The GitHub Blog
The GitHub Blog
S
SegmentFault 最新的问题
博客园 - 聂微东
A
Arctic Wolf
T
Threat Research - Cisco Blogs
U
Unit 42
NISL@THU
NISL@THU
H
Hackread – Cybersecurity News, Data Breaches, AI and More
博客园 - 【当耐特】
T
Troy Hunt's Blog
PCI Perspectives
PCI Perspectives
Webroot Blog
Webroot Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
AWS News Blog
AWS News Blog
The Last Watchdog
The Last Watchdog
Last Week in AI
Last Week in AI
V
Vulnerabilities – Threatpost
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
P
Proofpoint News Feed
腾讯CDC
V
V2EX
A
About on SuperTechFans
Know Your Adversary
Know Your Adversary
S
Security Affairs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角 .NET Core 原生 DI 扩展之基于名称的注入实现 - 元视角
从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角
飞鸿踏雪 · 2021-02-02 · via 元视角

C# 版本历史记录

从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览

说明:因为Markdown下维护这样复杂的表格有一点麻烦,故,这里以图片形式展示出来,如后续内容有更新,请点击 这里 访问原始笔记链接。为知笔记 的表格渲染在移动端表现不佳,为了获得更好的阅读体验,请在电脑端访问查看。

C# 版本特性说明

现在是 2021 年,相信 C# 7.0 以前的版本大家都应该没有什么问题,因为像博主这样的 90 后“中年”男人,接触的都是这个版本的 C#。所以,在这里我们主要讲解大家C# 7.0、8.0 以及 9.0 的语法特性。考虑到文章篇幅有限,这里选取的都是博主个人比较喜欢的语法特性,如果这里没有你喜欢的特性,请参考文章末尾的参考链接。如果这里的特性你都不喜欢,请你马上关掉这个网页,愿这个世界:Love & Peace。可能你会感觉到我说话变得小心翼翼起来,因为这个世界上有种叫做“杠精”的生物,当它从我的只言片语里读出那些挫败感的时候,终于有了嘲笑我们这批步入30岁行列的90后的底气,没错,我在最近的博客评论中被读者“嘲讽”了,让暴风雨来得更猛烈一些吧!

C# 7.0

在 C# 7.0 中,我个人比较喜欢的特性主要有以下几个:元组和弃元更多的 expression-bodied 成员out 变量异步 Main 方法模式匹配引发表达式

元组和弃元

这个概念乍听起来可能会有一点陌生,其实,按我的理解,这就是增强的元组语法,终于可以摆脱Item1Item2……啦:

 //示例1
(string Alpha, string Beta) namedLetters = ("a", "b");
Console.WriteLine($"{namedLetters.Alpha}, {namedLetters.Beta}");
 //示例2
var alphabetStart = (Alpha: "a", Beta: "b");
Console.WriteLine($"{alphabetStart.Alpha}, {alphabetStart.Beta}");
//示例3
int count = 5;
string label = "Colors used in the map";
var pair = (count, label);
Console.WriteLine(pair);

有一段时间,前端同事总和我吹嘘 ES6 里面的解构多么多么好用!对此,我想说,C# 一样可以解构,假设我们现在有下面的一个方法:

static (string, double, double) GetLocation() 
 {
    var city = "西安市";
    var lat = 33.42d;
    var lon = 107.40d;
    return (city, lon, lat);
}

这就是简化后的元组的用法,如果是以前,我们还需要返回一个Tuple<string, double, double>。此时,如果我们需要解析城市名称及其经纬度,可以这样做:

//示例4
(string city, double lon, double lat) = GetLocation();
Console.WriteLine($"{city},({lon},{lat})");

OK,那么什么又是弃元呢?继续以上面的代码为例,如果我不关心经纬度,只需要城市名称又该怎么办呢?人家的方法返回的是一个3元的结果,而我们只需要其中的1元,此时,就有了所谓弃元的概念:

(string city, _, _) = GetLocation();
Console.WriteLine($"{city}");

在 C# 中可以使用下划线_来表示要舍弃的元,是为弃元,怎么样?你学会了吗?

更多的 expression-bodied 成员

这部分同样是经过强化的 Lambda 表达式,之前我们可以在成员函数和 只读属性上使用 Lambda 表达式,而现在,我们可以将其运用在构造函数终结器以及 getset 访问器:

// Expression-bodied constructor
public ExpressionMembersExample(string label) => this.Label = label;

// Expression-bodied finalizer
~ExpressionMembersExample() => Console.Error.WriteLine("Finalized!");

private string label;

// Expression-bodied get / set accessors.
public string Label
{
    get => label;
    set => this.label = value ?? "Default label";
}

out变量

个人认为,这是一个非常不错的改进,终于不用再单独声明out变量啦:

if (int.TryParse(input, out int result))
    Console.WriteLine(result);
else
    Console.WriteLine("Could not parse input");

异步 Main 方法

顾名思义,Main 方法现在可以支持 async 关键字啦:

static async Task<int> Main()
{
    // This could also be replaced with the body
    // DoAsyncWork, including its await expressions:
    return await DoAsyncWork();
}

在没有返回值的情况下,可以考虑返回Task:

static async Task Main()
{
    await SomeAsyncMethod();
}

模式匹配

主要是针对 isswitch 语句提供了增强的语法。在这里,对于前者来说,我们可以将判断和赋值两个步骤合二为一:

public static double ComputeAreaModernIs(object shape)
{
    if (shape is Square s)
        return s.Side * s.Side;
    else if (shape is Circle c)
        return c.Radius * c.Radius * Math.PI;
    else if (shape is Rectangle r)
        return r.Height * r.Length;
    // elided
    throw new ArgumentException(
        message: "shape is not a recognized shape",
        paramName: nameof(shape));
}

而对于后者来说,主要打破了传统 switch 语句的常量模式:

public static double ComputeArea_Version3(object shape)
{
    switch (shape)
    {
        case Square s when s.Side == 0:
        case Circle c when c.Radius == 0:
            return 0;

        case Square s:
            return s.Side * s.Side;
        case Circle c:
            return c.Radius * c.Radius * Math.PI;
        default:
            throw new ArgumentException(
                message: "shape is not a recognized shape",
                paramName: nameof(shape));
    }
}

引发表达式

这个主要是针对 throw 关键字的增强,当我看到微软的文档的时候,我突然意识到,这个语法其实我用了很久啦!

//场景A:条件运算符
string arg = args.Length >= 1 ? args[0] :
    throw new ArgumentException("You must supply an argument");
//场景B:Null合并运算符
public string Name
{
    get => name;
    set => name = value ??
        throw new ArgumentNullException(
          paramName: nameof(value), 
          message: "Name cannot be null");
}
//场景C:Lambda表达式
DateTime ToDateTime(IFormatProvider provider) =>
    throw new InvalidCastException("Conversion to a DateTime is not supported.");

以上,就是 C# 7.0 中我个人比较喜欢的语法特性。需要了解所有 C# 7.0 语法特性的小伙伴们,则可以参考这里:C# 7.0 - C# 7.3 中的新增功能

C# 8.0

在 C# 8.0 中,我个人比较喜欢的特性主要有以下几个:默认接口方法异步流索引和范围

默认接口方法

关于这个,我觉得有点多此一举,如果一定要有一个默认行为,那你用继承来实现不就好啦,接口本来就是用来实现的啊摔!

public class ChineseSayHello : ISayHello
{
    public string Who { get; set; }
}

public interface ISayHello
{
    private const string DefaultPersopn = "Anumouse";
    string Who { get; set; }
    void SayHello()
    {
        Who = DefaultPersopn;
         Console.WriteLine($"Hello, {Who}");
    }
 }

在上面这个例子里,ChineseSayHello没有实现SayHello()方法不影响编译,因为ISayHello有默认实现,可正因为如此,SayHello()方法属于ISayHello,不属于ChineseSayHello

//正确,可以编译
var sayHello = new ChineseSayHello() as ISayHello;
sayHello.SayHello();
//错误,无法编译
 var sayHello = new ChineseSayHello();
sayHello.SayHello();

异步流

该特性可以看作是IEnumerable<T>的一个延伸,即IAsyncEnumerable<T>,主要有下面三个属性:

  • 它是用 async 修饰符声明的。
  • 它将返回 IAsyncEnumerable。
  • 该方法包含用于在异步流中返回连续元素的 yield return 语句。

下面是一个来自微软官方的基本示例:

//生成异步流
public static async System.Collections.Generic.IAsyncEnumerable<int> GenerateSequence()
{
    for (int i = 0; i < 20; i++)
    {
        await Task.Delay(100);
        yield return i;
    }
}
//枚举异步流
await foreach (var number in GenerateSequence())
{
    Console.WriteLine(number);
}

和异步流相关的一个概念是:异步可释放,即 System.IAsyncDisposable,这个可以参考:实现 DisposeAsync 方法

索引和范围

关于这个,我们换一种说法,可能大家就能接受啦!是什么呢?答案是:切片。切片语法博主经常在 Python 中使用,想不到有生之年居然可以在 C# 里用到这个语法。不过,这个语法糖怎么看都不甜啊,因为没那味儿!

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
};  
//取最后一个元素
Console.WriteLine($"The last word is {words[^1]}");
//获取第一个元素到第三个元素
var quickBrownFox = words[1..4];
//获取倒数第一个元素到倒数第二个元素
var lazyDog = words[^2..^0];
//获取全部元素
var all = words[..];
//获取开始到第三个元素
var firstPhrase = words[..4];
//获取结束到倒数第二个元素
var lastPhrase = words[6..];

看起来这些东西在 Python 里都有啊,到底是哪里除了问题呢?我觉得更多的是符号上的不同吧, ^ 这个符号除了表示指数的意思以外,还有按位进行异或运算的意思,所以,这个语法糖加进来以后就会显得相当混乱,而 .. 这个符号显然没有 : 写起来方便啊,所以,虽然 C# 从 C# 8.0 开始有了切片语法,可这不是我想要的切片语法啊!

以上,就是 C# 8.0 中我个人比较喜欢的语法特性。需要了解所有 C# 8.0 语法特性的小伙伴们,则可以参考这里:C# 8.0 中的新增功能

C# 9.0

在 C# 9.0 中,我个人比较喜欢的特性主要有以下几个:Record顶级语句模式匹配增强

Record

record 是 C# 9.0 中提供的一个新的关键字,地位上等同于 classstruct,中文翻译为:记录类型。这是一种引用类型,它提供合成方法来提供值语义,从而实现相等性。 默认情况下,记录是不可变的。简而言之,record 是不可变的引用类型。你可能会说,我们为什么要搞这么一个类型出来呢?难道 class 不香吗?

我觉得如果要回答这个问题,可以借鉴 DDD 中的实体值对象这两个概念。实体 通常都有一个唯一的标识并且在整个生命周期中具有连续性,这一类角色通过 class 来实现一直都工作得很好。例如,每一个 User 都会有一个唯一的UserId ,我们使用 UserId 来判断其相等性。而 值对象 则是指那些没有唯一的标识、不可变的、通过属性来判断相等性。例如,我们有一个地址 Address,它由省、市、区、县和详细地址组成,那么,问题来了,如果两个 Address 的省、市、区、县和详细地址都相同,这两个 Address 是不是同一个地址呢?常识告诉我们:不会,因为它们是不同的实例。

这就是 record 出现的原因,对于上面的这个问题,我们可以来解决:

record Address
{
    public string Province { get; set; }
    public string City { get; set; }
    public string District { get; set; }
    public string County { get; set; }
}

var addr1 = new Address() { Province = "陕西省", City = "西安市", District = "雁塔区" };
var addr2 = new Address() { Province = "陕西省", City = "西安市", District = "雁塔区" };
Console.WriteLine($"addr1 == addr2:{addr1 == addr2}");

想想以前我们是怎么做的呢?是不是要写类似下面这样的代码:

if (addr1.Province == addr2.Province && addr1.City == addr2.City) {
    //属性太多啦,我就不一个一个地比较啦,懂得都懂
}

所以,这就是 record 存在的意义。除此之外呢,这个关键字更多的是语法层面上的,实际上从编译出来的 IL 来看,它本质上依然是一个类,并且它是不可变的。定义记录类型时,编译器会合成其他几种方法:

  • 基于值的相等性比较方法
  • 替代 GetHashCode()
  • 复制和克隆成员
  • PrintMembers 和 ToString()

那么,你可能还会有疑问,假如我定义了两个不同的记录类型,它们都拥有相同的属性成员,如果按值相等来判断的话,岂不是这两个不同的记录类型变成相同的了?这么重要的问题,微软怎么可能没有想到呢?编译器会合成一个 EqualityContract 属性,该属性返回与记录类型匹配的 Type 对象。在这里,微软再一次发挥了元组的威力,对于上面定义的地址,我们可以继续使用解构语法:

(province, city, district, county) = addr1;

当然,我相信哪怕到2090年,这个世界上依然会有“杠精”:你说这玩意儿不能变?我就想变怎么办?答案是使用with语法:

public record Person
{
    public string LastName { get; }
    public string FirstName { get; }

    public Person(string first, string last) => (FirstName, LastName) = (first, last);
}

var person = new Person("Bill", "Wagner");
Person brother = person with { FirstName = "Paul" }; // 修改FirstName的副本
Person clone = person with { }; // 空集副本

好了,关于记录类型就先为大家介绍到这里,更详细的说明可以参考这里:使用记录类型

顶级语句

顶级语句,这个又是一个听起来非常模糊的概念对不对? 大家可以看一下这篇文章:26 种不同的编程语言的 “Hello World” 程序。怎么样,在众多解释型的语言中,C#、Java 甚至 C++ 的 “Hello World” 是不是都看起来有一点臃肿?

好了,现在可以梦想成真啦!

using System;

Console.WriteLine("Hello World!");

如果觉得这样还显得臃肿,可以省略 using 部分:

System.Console.WriteLine("Hello World!");

当然啦,一个项目里显然只能有一个文件可以使用顶级语句,你可以理解为这些代码运行在一个看不见的Main()方法中,而Main()方法显然只能有一个,相比下来,Python 就自由多啦,不过if __name__ == '__main__'的老梗就不再这里展开啦!

模式匹配增强

感觉微软在模式匹配的道路上越走越远啊,说好的语法糖呢?这简直是毒药,7.0 里面眼花缭乱的switch都还没学会呢!

public static bool IsLetter(this char c) =>
    c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';

public static bool IsLetterOrSeparator(this char c) =>
    c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') or '.' or ',';

if (e is not null)
{
    // ...
}

以上,就是 C# 9.0 中我个人比较喜欢的语法特性。需要了解所有 C# 9.0 语法特性的小伙伴们,则可以参考这里:C# 9.0 中的新增功能

参考链接