.NET是什么?为什么要选择.NET?
Span<T>、硬件内部函数、可空引用类型便是典型代表。后续文章:
在深入细节之前,有必要先聊聊.NET 的应用现状。全球数百万开发者都在使用它,在多种操作系统与芯片架构上开发云应用、客户端应用及各类其他程序。
.NET 设计要点
.NET 平台四大核心价值:开发高效、性能强劲、安全可靠、运行稳定。.NET 在这四大价值之间实现的均衡取舍,正是其独特吸引力所在。
.NET 的核心设计思路可以概括为:无论在安全托管域(主打高效开发),还是非安全原生域(提供强大底层能力),都兼顾易用性与运行效率。
我们可以对这套核心设计思路展开细说:
- 覆盖全技术栈,运行时、类库、编程语言与配套工具协同优化开发者使用体验。
- 安全代码作为主流计算模型,非安全代码则用于实现额外的手动性能优化。
- 同时支持静态代码与动态代码,可适配各类差异化开发场景。
- 原生代码互操作、硬件内部函数开销极低,且高度保真(可直接调用原生 API 与 CPU 指令)。
- 代码具备跨平台可移植性(支持不同操作系统、芯片架构),同时可针对特定平台做定制适配与深度优化。
- 依托通用编程模型的专用实现方案,可适配云服务、客户端、游戏等各类开发领域。
- 优先采用 OpenTelemetry、gRPC 等行业标准,而非自研专属方案。
.NET 技术栈的核心支柱
运行时(CLR, Common Language Runtime)、基础类库(FCL,Framework Class Library)与编程语言(C#/F#/VB.NET等)构成了 .NET 技术栈的根基。像.NET 命令行工具、ASP.NET Core 应用框架等上层组件,均搭建在这三大支柱之上。三者属于共生依存关系,由同一批开发者(微软员工与开源社区)协同设计、联合开发,开发人员往往同时参与多个组件的研发,并相互同步设计思路。
C# 是面向对象语言,运行时原生支持面向对象特性;C# 依赖垃圾回收机制,而运行时内置了跟踪式垃圾回收器。事实上,完整形态的 C# 无法移植到不具备垃圾回收能力的系统中。基础类库(以及各类应用框架)将底层能力封装为易懂的抽象概念与对象模型,让开发者能够以直观的流程高效编写业务逻辑。
C# 是一种现代、安全且通用的编程语言,从高级特性(如面向数据的record类型)到低级特性(如函数指针)都有涵盖。它提供了静态类型检查和类型及内存安全性的基本功能,这同时提高了开发人员的生产力和代码安全性。C# 编译器也是可扩展的,支持插件模型,使开发人员能够通过额外的诊断和编译时代码生成来增强系统。
众多 C# 语言特性与业界前沿编程语言相互借鉴、彼此影响。例如,C# 是第一个引入async和await异步语法的主流语言。同时,C# 借用了在其他编程语言中首次引入的概念,例如模式匹配、主构造函数等函数式编程特性。
核心类库提供数千种内置类型,其中大量类型与 C# 语言深度联动、互为支撑。比如 C# 的 foreach 可遍历任意集合,搭配基于模式的优化机制,让 List<T> 这类集合能够简洁且高效地遍历处理。资源释放可交由垃圾回收自动处理,也能通过 IDisposable 接口配合语言原生 using 语法实现资源即时清理。
C# 字符串插内插语法兼具可读性与高性能,依托 string、StringBuilder、Span<T> 等核心库底层类型实现;语言集成查询(LINQ)则基于类库中数百个序列处理方法(Where、Select、GroupBy 等)实现,整体设计可扩展,同时支持内存数据与远程数据源查询。
类似的深度融合案例不胜枚举,语言内置特性仅仅是 .NET 核心类库能力的冰山一角:从压缩、加密到正则表达式,类库均提供完整实现。网络功能自成一套完备体系,底层覆盖套接字到 HTTP/3 协议;除此之外,类库还支持 JSON、XML、tar 等海量文件格式解析处理。
.NET 运行时最初被称为“公共语言运行时 (CLR)”。它继续支持多种语言,有些由微软维护(例如 C#、F#、Visual Basic、C++/CLI 和 PowerShell),有些则由其他组织维护(例如 Cobol、Java、PHP、Python、Scheme)。大量运行时优化与语言无关,所有托管语言都能同步受益。
下文我们将介绍三大支柱协同打造的各项平台核心特性。我们固然可以分开详述每个组件,但你很快就能发现:三者紧密协作,共同落地 .NET 的核心设计理念。我们先从类型系统开始讲起。
类型系统
List<T> 是开放泛型类;实例化后的 List<string>、List<int> 无需像早年 ArrayList 那样,单独编写 ListOfString、ListOfInt 专用容器,也不用依赖 object 基类配合强制类型转换。借助泛型还能跨不同数据类型构建通用工具体系(大幅减少重复代码),泛型数学库便是典型应用。app.MapGet("/Product/{id}", async (int id) =>
{
if (await IsProductIdValid(id))
{
return await GetProductDetails(id);
}
return Products.InvalidProduct;
});
List<T> 这类泛型容器存储值类型时,内存布局为连续平铺结构,不存在额外内存损耗。除此之外,当泛型参数替换为值类型时,.NET 会生成专属编译代码,这条泛型执行路径能够规避高昂的垃圾回收开销。byte magicSequence = 0b1000_0001;
Span<byte> data = stackalloc byte[128];
DuplicateSequence(data[0..4], magicSequence);
Span<byte> 是安全且功能更完善的 byte*(字节指针)替代方案,自带长度信息(配套边界越界检查),还提供便捷的切片截取能力。Span<T> 便属于此类。这套模型并非通用编程模型,存在诸多严格限制,目的是保障内存安全。internal readonly ref T _reference;
通过 ref 这种写法,只会复制指向底层存储的指针,而非复制指针所引用的完整数据。值类型默认采用「值拷贝」机制,而 ref 能实现「引用拷贝」,可大幅提升程序性能。
自动内存管理
指针 bump 分配机制
分代回收
堆压缩
并行回收
服务器 GC 会创建多个独立堆供程序分配内存,支持更高内存分配速率,非常适合追求吞吐的业务。
并发回收
固定(Pinning)
可替换独立 GC
完备诊断能力
安全性
编程安全是过去十年中的热门话题之一。同时也是 .NET 这类托管运行环境与生俱来的底层设计能力。
安全体系分为三类
- 类型安全 — 一个任意类型不能替代另一个类型,避免未定义行为。
- 内存安全 — 仅使用分配的内存,例如变量要么引用一个活动对象,要么是
null。 - 并发或线程安全性 — 共享数据不能以会导致未定义行为的方式被访问。
补充说明: 美国联邦政府最近发布了关于内存安全重要性的指导。
.NET 在立项之初就将安全作为核心设计目标。它最初的定位是支撑新一代 Web 服务,而 Web 服务天然需要在互联网这种风险最高的计算环境中处理不可信外部输入。如今行业已形成共识:Web 程序应当采用具备内存安全特性的语言开发。
类型安全由编程语言与运行时协同强制校验。编译器负责静态约束校验,例如将字符串赋值给流对象这类类型不匹配操作,会直接报编译错误;
运行时负责动态约束校验,例如跨不兼容类型强制转换,运行时会抛出 InvalidCastException。
内存安全主要依靠即时编译器(JIT)与垃圾回收器配合实现:变量只会指向存活对象、空值或已脱离作用域;内存默认自动初始化,新创建对象不会读取未初始化脏内存。数组边界校验机制可拦截非法下标访问(这类错误大多源于差一错误),不会放任程序读取未知内存,而是抛出 IndexOutOfRangeException。
空值处理是内存安全的细分分支。可空引用类型 是 C# 语言与编译器配套特性,能在编译期静态识别未妥善处理空值的代码:
若你对可能为 null 的变量做解引用操作,编译器会主动给出警告;你也可以限制变量禁止赋值 null,一旦将可能为空的值赋给该变量,编译器同样会告警。运行时配套动态校验逻辑,一旦访问空引用会立即抛出 NullReferenceException 阻断危险操作。
该特性依赖类库中配套的可空特性注解;同时基础类库、各类应用框架都全面完整标注了这套注解,才能让静态分析工具为业务代码给出精准可靠的空安全检测结果。
string? SomeMethod() => null;
string value = SomeMethod() ?? "default string";
?? 空合并运算符起到关键作用。变量 value 永远不会为空,与其声明的类型约束保持一致。
错误处理
异常是 .NET 中主要的错误处理模型。异常的优势在于:无需将错误信息体现在方法签名中,也不必让每一层方法都处理错误。
下方代码展示了一种典型使用范式:
try
{
var lines = await File.ReadAllLinesAsync(file);
Console.WriteLine($"The {file} has {lines.Length} lines.");
}
catch (Exception e) when (e is FileNotFoundException or DirectoryNotFoundException)
{
Console.WriteLine($"{file} doesn't exist.");
}
规范的异常处理对应用稳定性至关重要,可预期的异常需要在业务代码中主动捕获处理,否则程序会直接崩溃。相比出现未知未定义行为的程序,直接崩溃的程序更便于定位问题、排查故障。
异常会在发生错误的位置抛出,并自动收集程序运行时的各类诊断信息,这些信息可用于交互式调试、应用可观测性监控以及事后崩溃分析。上述每一种排查手段,都依赖详尽的错误信息与程序现场状态来定位故障根因。
异常机制设计用于处理罕见异常场景,一部分原因是异常抛出的性能开销相对较高。即便偶尔有人用异常做流程分支控制,这也并非其设计初衷。
异常机制也承担取消任务的能力:一旦检测到取消请求,可快速终止当前执行流程,并逐层释放调用栈中正在执行的任务。
try
{
await source.CopyToAsync(destination, cancellationToken);
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation was canceled");
}
int.TryParse 会返回布尔值,解析成功时通过输出参数存放转换后的合法整型数值;Dictionary<TKey, TValue>.TryGetValue 采用同类设计,查找命中时通过输出参数返回对应的有效值。dotnet-monitor 内置面向诊断的 Web 服务,可将应用运行时数据向外推送至监控接收端。
并发性
Thread是最底层抽象,直接封装操作系统线程,开发者可手动创建线程并等待其执行完毕。- 线程池(
ThreadPool)构建在线程之上,开发者只需关注待执行任务,任务会异步调度至池内线程;线程的扩容、回收、任务分发全部交由运行时自动管理。 Task统一抽象所有异步操作,支持多种创建与等待方式:Task.Run将委托调度至线程池执行并返回任务对象标识执行结果;Socket.ReceiveAsync返回Task<int>(或ValueTask<int>),代表从套接字读取数据的异步 IO 操作。
Parallel.ForEach、Parallel.ForEachAsync 可便捷并行遍历处理数据集。async、await 关键字,开发者能够轻松编写、组合异步逻辑,同时完整复用语言原生各类流程控制语法。
反射
foreach (Type type in typeof(Program).Assembly.DefinedTypes)
{
if (type.IsAssignableTo(typeof(IStory)) &&
!type.IsInterface)
{
IStory? story = (IStory?)Activator.CreateInstance(type);
if (story is not null)
{
var text = story.TellMeAStory();
Console.WriteLine(text);
}
}
}
interface IStory
{
string TellMeAStory();
}
class BedTimeStore : IStory
{
public string TellMeAStory() => "Once upon a time, there was an orphan learning magic ...";
}
class HorrorStory : IStory
{
public string TellMeAStory() => "On a dark and stormy night, I heard a strange voice in the cellar ...";
}
List<IStory>。如果这段逻辑需要从插件目录加载任意未知程序集,就更适合采用这种晚期绑定方案。当程序集与类型在编译阶段无法预知时,反射是这类场景的常用方案。
编译的二进制格式
- 二进制字节码(IL)
格式精简规整,经 C# 这类高级语言编译器编译后,程序运行时无需再解析文本源码。
- 元数据(Metadata)
记录所有导入、导出类型信息,同时标注每个方法对应的字节码存储位置。
在开发阶段,各类开发工具可高效读取元数据,快速获取类库对外暴露的全部类型、以及哪些类型实现了指定接口等信息。该机制大幅提升编译速度,同时让集成开发环境(IDE)等工具能够精准展示当前上下文下可用的类型与成员列表。
对于运行时,依托元数据,类库支持延迟加载,方法体更是可以做到按需延迟解析。前文介绍的反射,就是运行时访问元数据与 IL 字节码的标准 API;除此之外还有更适配开发工具的专用接口。
代码生成
互操作
LibraryImport 源码生成器带来的高效互操作能力(这套源码生成能力是在 .NET 初代就已存在的 DllImport 基础上做的增强封装)。// Using a function pointer avoids a delegate allocation.
// Equivalent to `void (*fptr)(int) = &RegisterCallback;` in C
delegate* unmanaged<int, void> fptr = &RegisterCallback;
RegisterCallback(fptr);
[UnmanagedCallersOnly]
static void Callback(int a) => Console.WriteLine($"Callback: {a}");
[LibraryImport("...", EntryPoint = "RegisterCallback")]
static partial void RegisterCallback(delegate* unmanaged<int, void> fptr);
二进制分发
总结
这篇文章由 Jan Kotas、Rich Lander、Maoni Stephens和 Stephen Toub撰写,感谢 .NET 团队各位同事提供思路并参与审稿。
本文翻译自:https://devblogs.microsoft.com/dotnet/why-dotnet/
原文更新于:2023年2月14日
非常推荐阅读《CLR via C#(第4版)》这本书,书中对.NET的技术底座进行了详细介绍。
美中不足的是这本书是基于.NET Framework 4.5.2进行讲解的,对于今天的.NET Core平台来说,确实会存一些内容滞后,但是该书底层 CLR 基础原理依然通用,只是缺少新版.NET的一些功能特性而已。

浙公网安备 33010602011771号