






























.NET 自我们启动快速发展的 .NET 开源和跨平台项目以来已经发生了很大变化。我们重新思考并优化了平台,添加了新的低级功能,这些功能旨在提高性能和安全性,同时伴随着更高层次的专注于生产力的特性。Span<T>,硬件特性,和 可空引用类型 就是例子。我们启动了一个新的“.NET 设计点”博客系列,以探讨定义当今 .NET 平台的基本原理和设计选择,以及它们如何影响你现在编写的代码。
该系列的第一篇帖子提供了该平台支柱和设计点的广泛概述。它描述了在选择 .NET 时的基础“内容”,并旨在成为一个充分且以事实为重点的框架,供您用来向他人描述该平台。后续帖子将对这些相同主题进行更详细的介绍,因为本文无法充分展示这些功能。本文未描述工具(如 Visual Studio)或像 ASP.NET 提供的高级库和应用程序模型。
跟进帖子:
在深入讨论之前,值得谈谈 .NET 的使用情况。它被数百万开发者使用,在多个操作系统和芯片架构上创建云、客户端和其他应用程序。它还在一些知名地方运行,比如Azure、StackOverflow和Unity。在各种规模的公司中,常见到 .NET 被使用,尤其是大型公司。在许多地方,了解 .NET 是获得工作的良好技术。
.NET 平台代表了 生产力、性能、安全性和 可靠性。.NET 在这些价值之间的平衡是使其具有吸引力的原因。
.NET 设计点可以归结为在安全域(一切皆高效)和不安全域(存在大量功能)中都有效且高效。.NET 可能是内置功能最多的托管环境,同时提供与外部世界进行互操作的最低成本,并且在这两者之间没有权衡。事实上,许多功能利用了这种无缝划分,在操作系统和CPU的原始能力和功能上构建安全的托管API。
我们可以更详细地讨论一下设计点:
运行时、库和语言是 .NET 堆栈的支柱。高级组件,如 .NET 工具和 ASP.NET Core 等应用堆栈,建立在这些支柱之上。这些支柱之间存在共生关系,它们由一个团队(微软员工和开源社区)共同设计和构建,个人在多个这些组件上工作并提供信息。
C# 是面向对象的,运行时支持面向对象编程。C# 需要垃圾回收,运行时提供了一个跟踪垃圾回收器。事实上,不可能将 C#(其完整形式)移植到没有垃圾回收的系统上。库(以及应用堆栈)将这些功能塑造成概念和对象模型,使开发人员能够以直观的工作流程编写高效的算法。
C# 是一种现代、安全且通用的编程语言,从高级特性(如面向数据的记录)到低级特性(如函数指针)都有涵盖。它提供了静态类型检查和类型及内存安全性的基本功能,这同时提高了开发人员的生产力和代码安全性。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 设计点方面进行合作。让我们从类型系统开始。
.NET 类型系统提供了相当广泛的类型,大致平衡地服务于安全性、描述性、动态性和原生互操作性。
首先,类型系统支持面向对象编程模型。它包括类型、(单一基类)继承、接口(包括默认方法实现)和虚方法调度,以提供面向对象所允许的所有类型层次结构的合理行为。
泛型 是一个普遍的特性,允许将类 specializing 到一个或多个类型。例如,List<T> 是一个开放的泛型类,而像List<string> 和List<int> 这样的实例避免了需要单独的ListOfString 和ListOfInt 类或依赖于object 和强制转换,就像在ArrayList 中的情况一样。泛型还使得在不同的类型之间创建有用的系统成为可能(并减少了大量代码的需求),比如泛型数学。
委托和lambda使得可以将方法作为数据传递,这使得在另一个系统拥有的操作流中集成外部代码变得容易。它们是一种“粘合代码”,其签名通常是通用的,以实现广泛的用途。
app.MapGet("/Product/{id}", async (int id) =>
{
if (await IsProductIdValid(id))
{
return await GetProductDetails(id);
}
return Products.InvalidProduct;
});
这种使用 lambdas 的方式是 ASP.NET Core Minimal APIs的一部分。它使能够将端点实现直接提供给路由系统。在更 recent 的版本中,ASP.NET Core 更广泛地使用了类型系统。
值类型和堆栈分配的内存块提供了对数据和原生平台互操作的更直接、低级控制,与 .NET 的 GC 管理类型形成对比。.NET 中的大多数基本类型,如整数类型,都是值类型,用户可以定义具有类似语义的自定义类型。
值类型通过 .NET 的泛型系统得到了完全支持,这意味着像 List<T> 这样的泛型可以为值类型集合提供扁平、无开销的内存表示。此外,当值类型被替换时,.NET 泛型提供了专门的编译代码,这意味着这些泛型代码路径可以避免昂贵的 GC 开销。
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提供了“按引用复制”的行为,这可以提供显著的性能优势。
.NET 运行时通过垃圾回收器(GC)提供自动内存管理。对于任何语言,其内存管理模型可能是其最显著的特征。这对于 .NET 语言也是如此。
堆损坏错误调试起来非常困难。工程师们常常需要花费数周甚至数月的时间去追踪这些错误。许多语言使用垃圾回收器来消除这些错误,因为垃圾回收器可以确保对象的正确生命周期。通常,垃圾回收器批量释放内存以高效运行。这会导致暂停,如果你有非常严格的延迟要求,这可能不合适,并且内存使用量会更高。垃圾回收器往往具有更好的内存局部性,并且有些垃圾回收器能够对堆进行压缩,使其不易出现内存碎片。
.NET 有一个自调优的,跟踪式垃圾回收器。它旨在在一般情况下实现“无需干预”的操作,同时为更极端的工作负载提供配置选项。垃圾回收器是多年投资、改进和从各种类型的工作负载中学习的结果。
增长指针分配 — 对象通过增加分配指针的大小所需来分配(而不是在隔离的空闲块中找到空间),因此一起分配的对象往往会保持在一起。由于它们经常一起访问,这使得内存局部性更好,这对于性能很重要。
代际集合 — 对象的生命周期通常遵循代际假设,即一个对象要么存活很长时间,要么很快就会死亡。因此,垃圾回收器(GC)在运行时大部分时间只回收由短暂对象占用的内存(称为短暂GC),而不是每次运行时都回收整个堆(称为完全GC)。
compaction — 在较大较少的块中相同的空闲空间比在较小更多的块中更有用。在 compaction GC 期间,幸存的对象被一起移动,以便可以形成更大的空闲空间。这比非移动 GC 更难实现,因为它需要更新这些移动对象的引用。.NET GC 动态调整,仅在它确定回收的内存值得 GC 成本时才进行 compaction。这意味着短暂的垃圾回收通常是 compaction 的。
并行 — GC工作可以在单个线程或多个线程上运行。工作站版本的GC工作在一个线程上进行,而服务器版本的GC工作在多个GC线程上,因此可以更快完成。服务器GC还可以容纳更高的分配率,因为应用程序可以分配在多个堆上,而不是只有一个,因此非常适合吞吐量。
并发 — 在用户线程暂停时执行垃圾回收工作 — 称为停止世界 — 使实现更简单,但这些暂停的长度可能是不可接受的。 .NET 提供了并发版本 来缓解该问题。
固定 — .NET GC 支持对象固定,这使得与原生代码的零拷贝互操作成为可能。此功能使高性能和高保真的原生互操作成为可能,并且对 GC 的开销有限。
独立的GC — 可以使用具有不同实现的独立GC(通过配置指定并满足接口要求)。这使得调查和尝试新功能变得更加容易。
诊断 — GC 提供了关于内存和垃圾回收的丰富信息,这些信息以一种允许您将数据与系统其余部分相关联的方式结构化。例如,您可以通过捕获 GC 事件并将其与 IO 等其他事件相关联,来评估 您的尾部延迟的 GC 影响,这样您就可以将努力集中在正确的组件上。
编程安全是过去十年中的热门话题之一。它是像.NET这样的管理环境的固有组成部分。
安全形式:
null。注意: 美国联邦政府最近发布了关于内存安全重要性的指导。
.NET从一开始就设计为一个安全的平台。特别是,它旨在启用新一代的网络服务器,这些服务器在世界上最恶劣的计算环境中(互联网)需要接受不信任的输入。现在普遍认为网络程序应该用安全的语言编写。
类型安全由语言和运行时的组合来确保。编译器验证静态不变量,例如分配不同类型的值——例如,将string分配给Stream——这将产生编译器错误。运行时验证动态不变量,例如在不同类型之间进行类型转换,这将产生一个InvalidCastException。
内存安全主要由代码生成器(如JIT)和垃圾回收器之间的合作提供。变量要么引用活动对象,要么null,要么超出作用域。内存默认自动初始化,以确保新对象不使用未初始化的内存。边界检查确保访问具有无效索引的元素不会允许读取未定义的内存——通常是由越界错误引起的——而是会引发IndexOutOfRangeException。
null 处理是特定形式的内存安全。 可空引用类型 是 C# 语言和编译器功能,静态识别不安全处理的代码 null。特别地,如果解引用可能为 null 的变量,编译器会警告你。你还可以禁止 null 赋值,这样如果给变量赋值的值可能为 null,编译器会警告你。运行时有一个匹配的动态验证功能,通过抛出 null 引发异常来防止 引用被访问,NullReferenceException。
该功能依赖于可空属性在库中的应用。它还依赖于在库和应用堆栈中的全面应用,以便用户代码可以从静态分析工具中获得准确的结果。
string? SomeMethod() => null;
string value = SomeMethod() ?? "default string";
由于 null使用被声明和处理,部分由 ??完成,C# 编译器认为此代码是安全的。空合并运算符。 value变量将始终为非空,符合其声明。
.NET 没有内置的并发安全性。相反,开发人员需要遵循模式和约定来避免未定义行为。.NET 生态系统中还有分析器和其他工具可以提供有关并发问题的见解。而且,核心库包括许多可以安全地并发使用的类型和方法,例如并发集合,它们支持任意数量的并发读取器和写入器,而不会导致数据结构损坏。
运行时暴露了安全的和不安全的代码 模型。安全代码是默认的,开发人员必须选择使用不安全的代码。不安全的代码通常用于与底层平台进行互操作、与硬件进行交互或对关键性能路径进行手动优化。
沙盒 是一种特殊的隔离形式,提供了组件之间的隔离和访问限制。我们依赖于标准的隔离技术,如进程(和CGroups)、虚拟机和Wasm(及其不同的特性)。
异常是 .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");
}
.NET 设计模式包括在异常性能成本禁止的情况下错误处理的替代形式。例如,int.TryParse 返回一个 bool,在成功时包含解析后的有效整数作为 out参数。Dictionary<TKey, TValue>.TryGetValue 提供了类似的模型,在成功时返回一个有效的 TValue 类型作为 out参数在 true 情况下。
错误处理,以及更广泛的诊断功能,是通过低级运行时API、高级库和工具实现的。这些功能旨在支持像容器这样的新部署选项。例如,dotnet-monitor可以将运行时数据从应用程序传输到监听器,通过内置的面向诊断的Web服务器。
支持同时进行多项操作是几乎所有工作负载的基础,无论是客户端应用程序在保持用户界面响应的同时进行后台处理,还是服务处理成千上万的并发请求,设备响应多种同时的刺激,或者是高性能机器并行处理计算密集型操作。操作系统通过线程提供对这种并发的支持,使多个指令流能够独立处理,操作系统管理这些线程在机器中任何可用的处理器核心上的执行。操作系统还提供I/O支持,并提供机制以使I/O能够以可扩展的方式进行,许多I/O操作可以在任何时候“飞行”。编程语言和框架可以在这种核心支持之上提供各种抽象级别。
.NET 在多个抽象层次上提供了这样的并发和并行化支持,既可以借助库,也可以 deeply 集成到 C# 中。Thread 类位于层次结构的底部,代表操作系统线程,使开发人员能够创建新线程并随后与之合并。ThreadPool 基于线程,允许开发人员以工作项的方式进行思考,这些工作项异步地调度在一组线程上运行,而这些线程的管理(包括将线程添加到池中和从池中移除线程,以及将工作项分配给这些线程)由运行时负责。Task 然后提供了一个统一的表示方式,用于表示任何异步执行的操作,并且可以以多种方式创建和合并;例如,Task.Run 允许安排一个委托在 ThreadPool 上运行,并返回一个 Task 以表示该工作的最终完成,而 Socket.ReceiveAsync 返回一个 Task<int> (或 ValueTask<int>) 以表示从 Socket 异步 I/O 的最终完成以读取待处理或未来的数据。提供了大量的同步原语来协调线程和异步操作之间的同步和异步活动,并提供了许多高级别的 API 以简化常见并发模式的实现,例如 Parallel.ForEach 和 Parallel.ForEachAsync 使并行处理数据序列的所有元素更加容易。
异步编程支持也是 C# 编程语言的一等公民功能,该语言提供了 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>。如果这个算法从插件目录加载任意程序集,这种动态绑定的方法更有可能被使用。在程序集和类型不是提前知道的情况下,反射通常在这种场景中被使用。
反射可能是.NET提供的最具动态性的系统。它旨在使开发人员能够创建自己的二进制代码加载器和方法调度器,并且其语义可以匹配或偏离静态代码策略(由运行时定义)。反射暴露了一个丰富的对象模型,对于狭窄的使用案例来说很容易采用,但随着场景变得更加复杂,需要对.NET类型系统有更深入的理解。
反射还启用了一种单独的模式,其中生成的IL字节码可以在运行时JIT编译,有时用于将通用算法替换为专用算法。在已知对象模型和其他细节的情况下,它通常用于序列化器或对象关系映射器中。
应用程序和库编译成 标准化的 跨平台的 字节码,格式为 PE/COFF格式。 二进制分发是首要的性能特征。 它使应用程序能够扩展到越来越多的项目。 每个库都包含一个导入和导出类型数据库,称为 元数据,这对开发操作和运行应用程序都起着重要的作用。
编译的二进制文件包括两个主要方面:
对于开发,工具可以高效地读取元数据,以确定给定库暴露的类型集以及这些类型中哪些实现了某些接口,例如。这个过程使编译快速,并使 IDE 和其他工具能够准确地呈现给定上下文的类型和成员列表。
在运行时,元数据使库可以延迟加载,方法体更是如此。反射(稍后讨论)是元数据和IL的运行时API。对于工具,还有其他更合适的API。
IL格式随着时间的推移保持向后兼容。最新的 .NET 版本仍然可以加载和执行使用 .NET Framework 1.0 编译器生成的二进制文件。
共享库通常通过NuGet包分发。NuGet包默认情况下,使用单个二进制文件,可以在任何操作系统和架构上工作,但也可以针对特定环境提供特定行为。
.NET 字节码不是一种可由机器执行的格式,但它需要通过某种形式的代码生成器来使其可执行。这可以通过提前编译(AOT)、即时编译(JIT)、解释或翻译来实现。事实上,这些方法在当今各种场景中都有使用。
.NET 最为人知的是 JIT 编译。JITs 在应用程序运行时编译方法(和其他成员)到本机代码,并且只在需要时编译,因此得名“即时”。例如,一个程序可能在运行时只调用一个类型上的多个方法之一。JIT 还可以利用只有在运行时才可用的信息,例如已初始化的只读静态变量的值或程序正在运行的 CPU 模型,可以编译同一方法多次,以针对不同的目标和从前一次编译中学习到的经验进行优化。
JITs 为特定的操作系统和芯片架构生成代码。.NET 有支持 Arm64 和 x64 指令集以及 Linux、macOS 和 Windows 操作系统的 JIT 实现。作为一名 .NET 开发人员,您无需担心 CPU 指令集和操作系统调用约定之间的差异。JIT 会负责生成 CPU 想要的代码。它也知道如何为每种 CPU、操作系统和 CPU 厂商生成快速代码,而 CPU 厂商和操作系统厂商经常帮助我们做到这一点。
AOT 与 JIT 类似,区别在于代码在程序运行之前生成。开发人员选择此选项是因为它可以通过消除 JIT 所做的工作来显著提高启动时间。AOT 构建的应用程序本质上是操作系统和架构特定的,这意味着需要额外的步骤才能使应用程序在多个环境中运行。例如,如果您想支持 Linux 和 Windows 以及 Arm64 和 x64,则需要构建四个变体(以允许所有组合)。AOT 代码也可以提供有价值的优化,但通常没有 JIT 那么多。
我们将在以后的帖子中讨论解释和编译,然而,它们在我们的生态系统中也起着关键作用。
代码生成器优化之一是本机指令。硬件本机指令 是一个.NET API直接翻译成CPU指令的例子。这在.NET库中被广泛用于SIMD指令。
.NET 明确设计用于与本地库低成本互操作。.NET 程序和库可以无缝调用低级操作系统 API 或利用 C/C++ 库的庞大生态系统。现代 .NET 运行时专注于提供低级互操作构建块,例如通过函数指针调用本地方法的能力,将托管方法暴露为非托管回调或自定义接口转换。.NET 在这个领域也在不断发展,并在 .NET 7 中发布了源生成解决方案,这些解决方案进一步减少了开销并且与 AOT 兼容。
以下内容展示了 C# 函数指针在 .NET 7 中引入的源生成器的效率(这个源生成器支持在 .NET 初始支持的基础之上进行扩展)。LibraryImport源生成器在 .NET 7 中引入(这个源生成器支持在 .NET 初始支持的基础之上进行扩展)。DllImport源生成器在 .NET 7 中引入(这个源生成器支持在 .NET 初始支持的基础之上进行扩展)。
// 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);
独立的软件包通过利用这些低级构建块,提供更高层次的特定领域互操作解决方案,例如 ClangSharp、Xamarin.iOS & Xamarin.Mac、CsWinRT、CsWin32 和 DNNE。
这些新功能并不意味着内置的互操作解决方案(如内置的运行时管理/非管理 marshalling 或 Windows COM 互操作)不再有用——我们知道它们有用,并且人们已经依赖于它们。那些历史上内置在运行时中的功能将继续在 .NET 运行时中支持。然而,这些功能仅用于向后兼容,没有进一步发展的计划。所有未来的投资都将集中在互操作构建块及其所支持的领域特定解决方案上。
微软的.NET团队维护多个二进制分发包,最近支持了Android、iOS和Web Assembly。团队使用各种技术为每个环境专门化代码库。平台的大部分代码是用C#编写的,这使得移植可以集中在相对较少的组件上。
社区维护另一套发行版,主要关注Linux。例如,.NET 包含在Alpine Linux、Fedora、Red Hat Enterprise Linux和Ubuntu中。
社区还扩展了 .NET 以在其他平台上运行。三星为他们的基于 Arm 的 Tizen 平台移植了 .NET。红帽和IBM 将 .NET 移植到了 LinuxONE/s390x。 龙芯科技将.NET 移植到了 LoongArch。我们希望并期望新的合作伙伴将 .NET 移植到其他环境中。
Unity Technologies 已经启动了一项多年的计划来现代化他们的 .NET 运行时。
.NET开源项目由个人、公司和其他组织以传统的上游模型进行维护和组织。Microsoft是该平台的监护人,提供项目治理和项目基础设施(如CI管道)。Microsoft团队与组织合作,帮助他们成功使用和/或移植 .NET。该项目有广泛的上游政策,包括接受特定发行版特有的更改。
一个主要的关注点是源码构建项目,该多个组织使用它根据典型的发行规则来构建 .NET,例如Canonical (Ubuntu)。这个关注点最近扩展了,增加了一个虚拟单库(VMR)。.NET 项目由许多库组成,这有助于提高 .NET 开发者的效率,但也使得构建完整的产品变得更加困难。VMR 解决了这个问题。
我们已经进入了现代 .NET 的多个版本,最近发布了 .NET 7。我们觉得如果我们将自 .NET Core 1.0 以来一直在努力构建的东西进行总结,会很有用——在平台的最低层。虽然我们显然保持了原始 .NET 的精神,但结果是一个新的平台,开辟了一条新路,并为开发人员提供了新的且更具价值的服务。
让我们从一开始就结束。.NET 代表四种价值观:生产力、性能、安全性和可靠性。我们坚信,当不同的语言平台提供不同的方法时,开发者会受益最多。作为一个团队,我们致力于为 .NET 开发者提供高生产力,同时提供在性能、安全性和可靠性方面领先的平台。
我们计划在这个系列中添加更多的帖子。您希望先看到哪些主题?请在评论中告诉我们。您希望看到更多的“大图”内容吗?
如果你希望看到更多类似的内容,你可以查看 《通用语言运行时 (CLR) 入门》。
这篇文章由 Jan Kotas、Rich Lander、Maoni Stephens和 Stephen Toub撰写,由 .NET 团队的同事们提供见解和审阅。
本文翻译自:https://devblogs.microsoft.com/dotnet/why-dotnet/
本文更新于:2023年2月14日
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。