最近在不少自媒体上看到有关.NET与C#的资讯与评价,感觉大家对.NET与C#还是不太了解,尤其是对2016年6月发布的跨平台.NET Core 1.0,更是知之甚少。在考虑一番之后,还是决定写点东西总结一下,也回顾一下.NET的发展历史。
首先,你没看错,.NET是跨平台的,可以在Windows、Linux和MacOS以及它们的各个发行版上运行,不仅如此,从2022年4月28日开始,.NET开源社区正式支持国产龙芯芯片龙架构(LoongArch),这使得.NET在国家信创这一领域又迈出了一大步。咦?.NET跨平台这事情我知道呀,为何要特别提起呢?因为真的有很多人不知道,而且不乏各种短视频平台里出现的各种技术专家和技术大咖,纷纷表示.NET只能在Windows上运行,跨平台困难,落地和部署困难。
其次,.NET的成功案例有很多,大家都知道,.NET和C#会用在游戏开发、工控领域、老系统维护等项目和场景中,却并不了解其实下面这些也都是.NET的成功案例,或者有着.NET的影子:
在上面的列表中,有些可能你还不认识,剩下的认识的大部分都是微软自己的产品,你会说.NET只有微软自己用,但不管是谁用,这些站点和应用都是超大规模级别的,说明使用.NET来构建超大规模级别的大型应用是完全没有问题的:容器化、云原生、微服务、Serverless,.NET都可以胜任;AI时代,ML.NET、Semantic Kernel、LLamaSharp、Cognitive Services等等.NET原生框架和开发SDK,在微软Azure OpenAI Services的加持下,为.NET在AI领域发力提供了更多的机会。
此外,.NET是开源的,.NET的各个部分通过不同的Repository维护在Github上,这些仓储基本上都是以MIT和Apache 2的许可协议进行开源,由微软员工作为主要贡献者,因此,在开源的同时,又保证了仓储的持续更新维护和提交代码的质量。不仅如此,.NET Foundation还收纳了不少知名开源项目,这对完善整个.NET生态起到了非常积极的作用。
那这里的.NET是指.NET Framework吗?算是,但也不完全是。之前为大家熟知的那个只能在Windows下运行的版本,称为.NET Framework,自2016年开始,微软发布跨平台版本的.NET框架时,它作为经典.NET Framework的跨平台版本,被称之为.NET Core,.NET Core延续到3.1版之后,被改名为.NET 5,然后就是后来的6、7、8、9等各个版本。对于这部分内容,我后面会介绍,但我想在介绍更多详细信息之前,先简单地回顾一下.NET的历史。
大家都知道,刚刚退休的Java之父詹姆斯·高斯林曾经在1991年就开始参与Java语言的构建,然后Sun Microsystems公司于1996年发布了Java 1.0版本,但很少有人知道,微软在同年10月份发布了自己的Java语言:Visual J++。Visual J++开发的程序基于微软自研的MSJVM运行,虽然J++遵循Java语言规范,但微软并没有选择完全根据Sun公司的Java规范来设计自己的JVM,于是,MSJVM实际上并不完全支持Sun Java的所有能力,包括Java RMI和JNI。而另一方面,微软还在Visual J++中加入了一些自己的设计,比如通过回调函数和委托来实现事件处理,这就使得Visual J++变得更像另一种新的编程语言。不仅如此,Visual J++在访问系统资源方面,也不遵循Sun Java SDK规范,而是通过自己定义的J/Direct接口来让代码直接访问操作系统层面的API,于是,Visual J++在某些方面性能要大大优于Sun Java(很是怀疑这个J/Direct就是后来的P/Invoke)。从技术角度,使用了这些特殊功能的J++应用程序其实无法在Sun Java JVM上运行,不过当时也有不少开源项目,比如Kaffe项目,它们可以使J++应用程序无需移植即可直接在这些开源的JVM上运行。但这些开源项目也最终没有被广泛应用。
一开始的时候,Sun公司愿意在Java语言上与微软合作,而由于Visual J++里的各种骚功能和骚设计打破了Sun Java规范,以至于MSJVM未能通过Sun公司的合规性测试,于是,在Sun公司的起诉下,微软逐渐停止了Visual J++的研发。不过,在这个过程中,微软积累了丰富的技术和经验,这些技术和经验逐渐演化成后来的.NET平台和Visual J#(一种可以运行在.NET Framework上的Java编程语言,注意:是编程语言,就是用Java的语法写.NET Framework的应用程序)。Visual J#是Visual J++的续作,目的是能够让原来的Visual J++开发人员可以平滑地迁移到.NET平台上。
之后就是2002年4月15日发布的.NET Framework 1.0和Visual Studio.NET 2002(注意是Visual Studio.NET,不是Visual Studio,从Visual Studio 2005开始,名字里去掉了.NET字样),它可以安装并运行在Windows NT 4.0 SP6、Windows 98、Windows 98 SE、Windows ME、Windows 2000和Windows XP系统上。当年我正好读大三,记得数据库这门课的课程设计,就是用Visual Studio.NET 2002做的。由于操作系统可以是32或者64位的,属于不同的CPU架构,因此,从这个层面也可以说.NET是跨平台的:在Windows操作系统下跨平台。说起跨平台,虽然一开始由于市场策略、技术实现、开源生态等等因素,微软最终选择让.NET只在Windows下运行,但同时也力求将各种设计通过ISO和ECMA进行标准化,这为后续社区版跨平台.NET Framework:Mono Project、Xamarin以及.NET Core的出现,打下了坚实的基础。
值得一提的是,由于CLI标准化的存在,使得任何编程语言,只要符合相关规范,都可以在.NET上运行(这里我就不区分跨平台.NET还是经典.NET Framework了,单说编程语言这部分内容,两者原理是一样的),于是,你可能会看到有人使用以下语言来开发.NET应用程序:
从2002年4月15日到2022年8月9日这20年时间,.NET Framework经历了大大小小17个版本,功能也在不断的增强,在2016年随着.NET Standard的引入,从.NET Framework 4.5开始,不同版本的.NET Framework也被归属到对应版本的.NET Standard之下,而发布于2019年4月18日的.NET Framework 4.8,也成为了经典.NET Framework的最后一个版本(虽然2022年8月9日发布了.NET Framework 4.8.1,但并不是一个主版本)。
后续版本的.NET被称为.NET 5,从版本号上可以看到,.NET 5可以看成是.NET Framework 4.8的延续,它去掉了“Framework”字样,以示与之前经典.NET Framework的区别,.NET 5及后续版本都是跨平台的,现在回看这段历史可以发现,之前的.NET Core 1.0到3.1其实都是.NET跨平台历程的中间版本,这些版本存在的价值,就是让.NET开发人员和用.NET Framework开发的项目可以通过这些.NET Core的版本,能够无缝地、逐步地过渡到跨平台的、现代化的.NET上。不得不佩服微软在.NET跨平台这方面无论在市场战略上,还是技术战术上,都表现得非常出色。
.NET历史就介绍这么点,了解这些基本也就够了,接下来我会逐步从技术角度,来介绍一些与.NET相关的新概念。
可以这样理解:.NET Standard是.NET跨平台的基础。.NET Standard其实就是一套.NET下的API规范,它的第一个版本与2016年的.NET Core 1.0同时发布,每一个版本的.NET Standard都规定了实现这一版本规范的不同的.NET实现应该包含哪些API。更具体些:为了跨Linux、MacOS、Windows、iOS、Android等多个平台,.NET会针对这些平台提供不同的实现,这些实现包括:.NET Core、.NET Framework、Xamarin套件(iOS、Mac和Andriod)、用于游戏开发的Unity下的.NET等等,如果这些实现能够遵循某个版本的API标准,那么,基于这个版本的API标准所开发的应用程序,就可以运行于这些不同平台上,而这套API标准就是.NET Standard。另一方面,如果希望开发出来的类库和组件能够被不同平台的应用程序使用,那么,只需要指定这个类库和组件所基于的.NET Standard版本即可。
然而,最开始的.NET Framework并不能跨平台,微软为了逐步实现.NET跨平台这个目标,先后发布了从1.0到2.1一共9个版本的.NET Standard,每个.NET Standard版本下,都增加一部分.NET API的支持,比如,.NET Standard 1.0仅支持37118个API中的7949个,.NET Standard 2.0支持37118个API中的32638个,而最新的.NET Standard 2.1则支持所有37118个API。因此,在不同版本的.NET Standard下,就会有对应版本的.NET实现对其进行支持。比如对于.NET Standard 2.0,经典的.NET Framework需要4.6.1及以上的版本才支持.NET Standard 2.0,因为这些版本实现了.NET Standard 2.0中所定义的那32638个API。于是,使用.NET Standard 2.0开发的类库,就可以被.NET Framework 4.6.1所引用。
在Visual Studio 2022中,新建一个.NET Standard 2.0的Class Library:
然后加入一个类:
再新建一个经典.NET Framework 4.6.1的Console Application:
然后直接引用前面的ClassLibraryNetStandard20
类库并写代码调用里面的函数,可以看到,程序可以正常运行:
当然,这个ClassLibraryNetStandard20
也可以被一个.NET 8的应用程序调用:
打开这个ClassLibraryNetStandard20项目的输出目录,可以看到,编译出来的程序集被放在了Debug\netstandard2.0
目录下:
但是,如果我们新建一个.NET Standard 2.1的Class Library,则无法被.NET Framework 4.6.1的项目引用,此时会报错:
因为.NET Framework 4.6.1没有实现.NET Standard 2.1,换句话说,.NET Standard 2.1中的有些API在.NET Framework 4.6.1中并没有实现,那么基于.NET Standard 2.1开发出来的类库自然也不能被.NET Framework 4.6.1的项目所引用。经典.NET Framework的最后一个版本4.8.1仅实现了.NET Standard 2.0,因此,如果你打算开发一个既可以被经典.NET Framework项目使用,又可以被跨平台.NET(曾经的.NET Core,现在的.NET 5+)项目使用的话,你需要将你的类库定向(targeting)到.NET Standard 2.0,或者使用多目标框架(Multi-targeting)。
在【这个页面】中,有一张表,展示了.NET Standard各个版本与不同的.NET实现的版本之间的对应关系,可以通过下拉框来选择不同的.NET Standard版本来查看不同的.NET实现的哪些版本与之对应。理论如此,但是在真正实践的过程中,有些具体的问题是需要特殊处理的。比如:.NET Framework 4.7是支持.NET Standard 2.0的,但是,.NET Framework 4.7发布于2017年4月,而.NET Standard 2.0则晚于.NET Framework 4.7发布(2017年8月),那么如何让一个已经发布的.NET Framework版本支持新的.NET Standard呢?解决方案就是使用NuGet Package,将.NET Framework中未实现的.NET Standard API以NuGet Package的形式引入,从而弥补这个差异。因此你会发现,对于.NET Framework 4.7.1及其以前版本的.NET项目,如果需要引用一个由.NET Standard 2.0实现的类库的话,就需要额外引用NETStandard.Library
这个Meta Package,这个Meta Package中包含了不同版本.NET项目所需依赖的Assembly的版本信息。另外,Visual Studio在处理这个事情上也有差异:
NETStandard.Library
,Visual Studio会自动帮你完成这个工作。当然,这并不表示你的项目就不需要NETStandard.Library
了,只是在IDE的操作上比以前更加简单了在编译出来的结果上也存在差异,下面左图是一个.NET Framework 4.7的项目引用了一个.NET Standard 2.0的项目后的编译输出,右图是.NET Framework 4.8.1的项目引用了.NET Standard 2.0项目后的编译输出,可以看到,4.7的项目编译后,会在编译路径下生成一堆System DLL,外加一个netstandard.dll
,而4.8.1的项目编译生成路径下就非常干净,因为.NET Framework 4.8.1已经自带了这些DLL了:
尽管.NET Standard提供了不同.NET实现(.NET Framework、.NET Core和Xamarin等)之间统一的API规范,但在有些场景下,仍然希望能够充分利用不同平台的特性和性能优化,或者需要支持特定平台的功能,此时仅将项目定向到.NET Standard已经不能满足需求。对于这种场景,.NET允许开发面向多目标框架的类库,一方面可以通过条件编译指令来使用特定平台的API,另一方面也可以为类库的调用方提供不同平台的支持。
比如,在C#项目文件(.csproj文件)中,使用下面的方式,让类库同时支持.NET Standard 2.1项目和.NET Framework 4.8的项目:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.1;net4.8</TargetFrameworks>
<Nullable>enable</Nullable>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
</Project>
注意上面的TargetFrameworks标记,在这个XML标记下,列出了该类库所支持的.NET Standard/.NET Framework的版本,它在编译之后,会产生两个输出文件夹,其中包含了支持不同版本.NET的程序集:
另一个需要注意的地方是,在上面的csproj文件定义中,指定了所使用的C#版本为8.0,这是因为“可空引用类型”(Nullable Reference Types)是在C# 8.0中引入的,而如果将代码同时定向到.NET Framework 4.8,那么就无法支持可空引用类型,因为.NET Framework 4.8所支持的最高的C#版本为C# 7.3。有关.NET与C#版本之间的关系,后文我会介绍。
一个使用“多目标框架”的非常著名的开源框架就是log4net。log4net是一个古老的.NET下的日志输出工具,它一开始是log4j的.NET移植版本,所以,老版本的log4net使用了很多经典.NET Framework特有的API,比如System.Configuration命名空间下的基于XML的框架配置代码。随着.NET Core和跨平台.NET的发布,log4net也逐步提供了对.NET Standard 1.3和.NET Standard 2.0的支持,所以,现在的.NET 5/6/7/8项目都可以直接引用log4net用作日志输出。在log4net的官方代码库中,可以学习到它是如何实现多目标框架的。
事实上从.NET 5开始,.NET真正实现了跨平台,.NET Standard也基本完成了它的使命,但.NET Standard并不会退出历史舞台,微软会继续对其进行维护。
至此,与.NET跨平台和.NET Standard相关的内容就差不多介绍完了,当然还有部分细节和一些历史遗留问题的处理方式相关内容(比如Portable Class Library,PCLs),这里就不再赘述了,否则篇幅太长,hold不住啊。接下来我们聊聊C#和.NET Framework之间的关系吧。
如果你是C#语言的初学者,那么你一定会产生一个疑问:int
和System.Int32
有什么区别?在C#中到底是使用int
,还是用System.Int32
?其实,C#中的int
等价于System.Int32
,也就是说,你既可以使用int
这个C#关键字来定义一个32位有符号整数类型的变量,也可以使用System.Int32
这个.NET类型来定义一个32位有符号整数类型的变量。官方文档上说int
是System.Int32
的别名,但也可以理解为,int
是C#能够支持32位有符号整数类型的语言特性。
由此可以得出一个结论:C#的语言特性是需要.NET Framework(或者.NET)支持的(有些高级的语言特性甚至需要不同版本的.NET CLR支持),但两者的版本之间也不一定需要有严格的对应关系。C#语言特性本质上是通过C#编译器实现的,只要编译输出的MSIL代码能够在.NET Framework(或者.NET)上运行起来,那么这些语言特性就可以被该版本的.NET Framework所支持。下面请看一个具体的案例。
现在我们尝试在一个.NET Framework 4.6.1(发布于2015年11月)的项目中,使用C# 9.0(发布于2020年11月)的新特性。首先新建一个.NET Framework 4.6.1的Console Application:
然后,修改csproj文件,将C#语言版本升级到C# 9.0:
然后,使用C# 9.0中“关系型模式匹配”新特性编写一段代码:
然后直接运行,可以看到,程序是可以正常编译执行的:
那么,是不是所有的C# 9.0的新特性,都可以在.NET Framework 4.6.1中使用呢?答案是否定的,就要看C# 9.0的编译器是否可以生成能够被该版本.NET Framework所支持的代码,或者说,C#编译器所生成的代码中依赖的那些类型,是否在该版本的.NET Framework下支持。举个例子,同样是C# 9.0的新特性,仅限Init的资源库新特性则无法直接在一个.NET Framework 4.6.1的项目中使用,编译器提示:Predefined type 'System.Runtime.CompilerServices.IsExternalInit' is not defined or imported
:
原因是,C# 9.0编译器在编译带有这个新语法特性的代码时,需要检查.NET下是否包含IsExternalInit
类型,如果该类型存在,则允许使用该语法特性,而该类型在.NET Framework 4.6.1中不存在。因此,解决这个问题,既可以自己在项目中写一个空的IsExternalInit类型,或者引用一个第三方的IsExternalInit NuGet Package。之后就可以正常编译运行代码了:
请注意:经典.NET Framework最高支持到C# 7.3,C# 8.0是专门面向.NET Core的第一个主要C#版本,它的一些新功能依赖于新的Core CLR的相关功能。【这篇官方文档】详细列举了C#语言的发展历史以及各个版本的新功能,下面这张表格列举了不同.NET版本下,创建新项目时所使用的默认的C#语言版本(参考官方文档):
本文先从介绍.NET历史开始,引出跨平台的相关话题,进而介绍了.NET Standard、Multi-targeting以及C#版本与.NET Framework之间的关系,文章所引用的外部文档全都以超链接的形式嵌在文章之中,就不额外逐个列出。这些都不是最新内容,只是尽可能地做个总结,希望能够帮到平时需要的朋友,同时也对.NET相关的概念和知识扫扫盲,让更多的朋友了解.NET和C#这一优秀的框架和编程语言。