七库下载 手游攻略 手游攻略 c#界面组件,c# ui组件

c#界面组件,c# ui组件

时间:2024-03-30 05:40:09 来源:头条 浏览:0

它是.NET和所有类型容器中部署的基本单元。程序集包含已编译类型及其中间语言(IL) 代码、运行时资源以及可帮助您版本化和引用其他程序集的信息。程序集还定义类型解析的边界。在.NET 中,程序集由扩展名为.NET 的单个文件组成。

注意在.NET 中构建可执行应用程序会创建两个文件:程序集() 和针对目标平台的可执行启动程序()。

这与.NET Framework 发生的情况不同,NET Framework 生成可执行(PE) 程序集。 PE 具有作为程序集和应用程序启动器的扩展和功能。 PE 可以针对32 位和64 位版本的Windows。

本章中的大多数类型都取自以下命名空间:

System.ReflectionSystem.ResourcesSystem.Globalization 程序集内容程序集包含四种类型的内容: 程序集清单向CLR 提供信息,例如程序集的名称、其版本以及它引用的其他程序集。提供应用程序清单。操作系统的信息(例如,应如何部署程序集以及是否应管理编译类型的升级) 程序集中定义的类型的已编译IL 代码和元数据资源程序集中嵌入的其他数据可本地化文本(例如图像)和图像)。

其中,仅需要程序集清单,并且程序集几乎总是包含编译类型(除非它们是资源)。看)。

程序集清单程序集清单有两个用途:

描述组装到受管理的托管环境中。它用作程序集中的模块、类型和资源的目录。大会就是这样。用户无需额外文件即可发现程序集的所有数据、类型和功能。

请注意,程序集清单不会显式添加到程序集中;它们会作为编译的一部分自动嵌入到程序集中。

以下是清单中存储的重要功能数据的摘要。

程序集的简单名称版本号(程序集的版本) 程序集的公钥和签名哈希(如果它具有强名称) 引用的程序集列表,包括其版本和公钥目标程序集中定义的类型列表文化是一个辅助程序集(AssemblyCulture),清单还可以存储以下信息数据:

完整标题和说明(程序集标题和程序集说明) 公司和版权信息(程序集公司和程序集版权) 显示版本(程序集信息版本) 自定义数据的其他属性编译器使用从提供给的参数派生的部分数据。例如:引用的程序集的列表或用于对程序集进行签名的公钥。其余部分取自括号中显示的程序集属性。

请注意,您可以使用.NET 工具查看程序集清单的内容。在第1 节中,我们学习了如何使用反射以编程方式执行相同的操作。

指定程序集属性您可以在Visual Studio 中项目属性页的“包”选项卡上指定常见的程序集属性。该选项卡上的设置将添加到项目文件( ) 中。

如果要指定“包”选项卡上不支持的属性,或者不使用文件,则可以在源代码中指定程序集属性(这通常在名为的文件中完成)。

私有属性文件仅包含using 语句和程序集属性声明。例如,要将内部范围类型公开给单元测试项目:

using System.Runtime.CompilerServices;[ assembly:InternalsVisibleTo('MyUnitTestProject')] 应用程序清单(Windows) 应用程序清单是一个XML 文件,它将有关程序集的信息传递给操作系统。在构建过程中,应用程序清单作为Win32 资源嵌入到启动可执行文件中。如果清单存在,则会在CLR 加载程序集之前读取并处理它,这可能会影响Windows 启动应用程序进程的方式。

.NET 应用程序清单在XML 命名空间urn:schemas-microsoft-com:asm.v1 中有一个名为Assembly 的根元素。

xml version='1.0'coding='utf-8' 以下清单告诉操作系统请求管理提升。

xml version='1.0'coding='utf-8'(UWP 应用程序有更详细的清单,如文件中所述。编辑此文件的最简单方法是使用Visual Studio。双击清单文件会显示一个对话框盒子。

部署应用程序清单对于.NET 项目,您可以通过右键单击“解决方案资源管理器”中的项目并选择“添加”>“新项”>“应用程序清单文件”,将应用程序清单添加到Visual Studio。生成后,清单将嵌入到输出程序集中。

请注意,NET 工具不知道嵌入式应用程序清单的存在。但是,当您在解决方案资源管理器中双击程序集时,Visual Studio 会指示存在嵌入式应用程序清单。

模块程序集的内容实际上被打包到一个名为的中间容器中。模块对应于包含程序集内容的文件。这个附加容器层的原因是允许程序集跨越多个文件。这是.NET Framework 中存在的功能,但.NET 5 和.NET Core 中不存在。表明了这种关系。

单文件程序集尽管.NET 不支持多文件程序集,但您可能需要了解模块所施加的额外级别的容器交付。主要场景是反射(参见“”和“”)。

程序集类System.Reflection 程序集类是用于在运行时访问程序集元数据的网关。有多种方法可以获取装配对象。最简单的方法是使用Type 的装配属性。

Assembly a=typeof (Program).Assembly; 您还可以通过调用Assembly 的静态方法之一来获取程序集对象: GetExecutingAssembly 返回定义当前执行函数的类型的程序集。一旦我们收到对程序集的调用,它就会执行与以下相同的操作:当您检索调用当前正在执行的函数的函数的入口程序集时,GetExecutingAssembly 返回定义应用程序原始输入方法的程序集。

拥有程序集对象后,您可以使用其属性和方法来查询程序集的元数据以反映其类型。以下是这些功能的概述。

议会成员

功能

目的

请参阅本节.

全名、检索到的姓名

返回完全限定名称或程序集名称对象。

「集合名称」

代码库、位置

程序集文件位置

“加载、解析和分离程序集”

正在加载,正在加载,

手动将程序集加载到内存中

“加载、解析和分离程序集”

获取卫星组件

查找特定文化的附属程序集

“资源和卫星组件”

获取类型,获取类型

返回程序集中定义的一种或多种类型。

入口点

以MethodInfo 形式返回应用程序的输入方法。

获取模块、获取模块、清单模块

返回程序集的所有模块或主模块。

中间

获取自定义属性、获取自定义属性

返回程序集的属性

中间

关于强名称和程序集签名的注意事项程序集的强命名在.NET Framework 中非常重要,原因有两个:

这允许将程序集加载到“全局程序集缓存”中。这允许通过其他强名称程序集引用该程序集。在.NET 5 和.NET Core 中,强命名不太重要,因为这些运行时没有全局程序集缓存,并且不受第二个限制。

程序集具有唯一的ID。它的工作原理是向清单中添加两位元数据。

对于属于程序集创建者的唯一程序集,需要公钥-私钥对来证明唯一编号所有者生成了该程序集。提供唯一的识别号码以便于签名。

请注意,签名与签名不同。本章稍后将解释验证码。

公钥有助于确保程序集引用的唯一性。强命名程序集在其标识中包含公钥。

在.NET Framework 中,私钥可以保护程序集免遭篡改,因为如果没有私钥,您就无法在不破坏签名的情况下发布程序集的修改版本。事实上,当将程序集加载到.NET Framework 的全局程序集缓存中时,这非常有用。在.NET 5和.NET Core中,签名没有被选择,并且用处不大。

向以前具有“弱”名称的程序集添加强名称会更改该程序集的标识。因此,如果您认为将来可能需要为您的理事会提供一个响亮的名称,那么从一开始就值得为您的理事会指定一个响亮的名称。

如何对程序集进行强命名要为程序集指定强名称,请首先使用以下实用程序生成公钥/私钥对:

sn.exe -k MyKeyPair.snk Visual Studio 安装名为sn.exe -k MyKeyPair.snk 的快捷方式。这将启动一个带有开发工具的命令提示符,例如您的PATH 中的工具。

这会生成一个新的密钥对并将其保存到名为.如果此文件随后丢失,则使用相同ID 重新编译程序集的能力将完全丧失。

您可以通过更新项目文件来使用此文件对程序集进行签名。从Visual Studio 中,转到“项目属性”窗口,选中“签名”选项卡上的“签名程序集”复选框,然后选择文件。

您可以使用相同的密钥对对多个程序集进行签名。即使简单的名称不同,ID 也不同。

程序集名称程序集的“身份”由清单中的四部分元数据组成。

简单名称版本(如果不存在,则为“0.0.0.0”) 文化(如果不是卫星,则为“中性”) 公钥令牌(如果不是强名称,则为“空”) 简单名称不是从属性和原始编译的名称派生的文件(不包括扩展名)。因此,程序集的简单名称是“System.Xml”。重命名文件不会更改程序集的简单名称。

版本号是从AssemblyVersion 属性获取的。这是一个分为四个部分的字符串:

Major.minor.build.revision 您可以指定版本号,如下所示:

[ assembly: AssemblyVersion ('2.5.6.7')] 区域性从AssemblyCulture 属性获取,并应用于本节后面描述的附属程序集。

如上一节所述,公钥令牌是从编译时提供的强名称获取的。

完全限定名称完全限定程序集名称是包含所有四个标识组件的字符串,格式如下:

simple-name、Version=version、Culture=culture、PublicKeyToken=public-key 例如,System.Private.CoreLib.dll 的完全限定名称是。

如果程序集没有AssemblyVersion 属性,版本将显示为0.0.0.0。如果未签名,公钥令牌将显示为空。

程序集对象的FullName 属性返回其完全限定名称。在清单中记录程序集引用时,编译器始终使用完全限定名称。

请注意,完全限定的程序集名称不包含目录路径,以便更容易在磁盘上查找。查找存在于不同目录中的程序集是一个完全不同的问题。

程序集名称类AssemblyName 是一个具有完全限定程序集名称的四个组件中每一个组件的类型化属性的类。程序集名称有两个用途:

解析或生成完全限定的程序集名称。存储有助于解析(搜索)程序集的附加数据。您可以通过以下方式之一获取程序集名称对象:

实例化程序集名称,指定完全限定名称。对现有程序集调用GetName。调用AssemblyName.GetAssemblyName 并指定磁盘上程序集文件的路径。您还可以实例化不带参数的AssemblyName 对象,并设置其属性以生成完全限定名称。当以这种方式构建时,可以更改程序集名称。

其基本属性和方法是:

string FullName { get; } //完全限定名称string Name { get; set; }//简单名称版本版本{ get; set; }//程序集版本CultureInfo CultureInfo { get; set; }//卫星程序集的字符串代码库{ get ; set ; }//Locationbyte[] GetPublicKey(); //160 bytes void SetPublicKey (byte[] key);byte[] GetPublicKeyToken(); //8 bytes versionvoid SetPublicKeyToken (byte[] publicKeyToken); 版本本身是一个具有“major”、“minor”、“build”和“revision”属性的强大类型表达式。 GetPublicKey 返回完整加密的公钥,GetPublicKeyToken 返回用于建立身份的最后8 个字节。

使用程序集名称获取程序集的简单名称。

Console.WriteLine(typeof(string).Assembly.GetName().Name);//System.Private.CoreLib获取程序集版本。

string v=myAssembly.GetName().Version.ToString(); 程序集信息和文件版本另外两个程序集属性可用于表示与版本相关的信息。与AssemblyVersion 不同,以下两个属性不会影响程序集的标识,因此不会影响其编译时或运行时行为。程序集信息版本对最终用户可见。这在Windows 文件属性对话框中显示为“产品版本”。您可以在此处输入任何字符串,例如“5.1 Beta 2”。应用程序中的所有程序集通常都分配有相同的信息版本号。程序集文件版本这是指程序集内部版本号。这在Windows 文件属性对话框中显示为文件版本。与AssemblyVersion 一样,它必须包含最多由句点分隔的四个数字组成的字符串。

CAPTCHA 签名是一种代码签名系统,旨在证明发布者的身份。验证和签名是独立的。您可以使用其中一个系统或两个系统对程序集进行签名。

强名称签名可以证明程序集A、B 和C 是同一方(假设私钥没有泄露),但无法判断该方是谁。要了解您是与Joe Albahari 还是Microsoft 打交道,您需要Authenticode。

从Internet 下载程序时,验证码非常有用,因为它们可以保证程序来自证书颁发机构指定的人员,并且在传输过程中没有被修改。它还可以防止您第一次运行下载的应用程序时出现“未知发布者”警告。将应用程序提交到Windows 应用商店时,还需要验证代码签名。

验证代码不仅适用于.NET 程序集,还适用于非托管可执行文件和二进制文件(例如部署文件)。当然,Authenticode 并不能保证程序没有恶意软件,但它确实降低了这种可能性。任何愿意将其姓名(由护照或公司文件支持)放在可执行文件或库后面的个人或实体。

请注意,CLR 并不将验证代码签名视为程序集标识的一部分。但是,正如我们很快就会看到的,您可以按需读取和验证CAPTCHA 签名。

要使用Authenticode 进行签名,您必须联系(CA) 并提供您的个人或企业身份证明(例如公司章程)。检查文档后,CA 颁发X.509 代码签名证书。该证书的有效期通常为1 至5 年。这将允许您使用该实用程序来签署程序集。您还可以使用实用程序自行创建证书,但只有显式安装该证书的计算机才能识别该证书。

证书(非自签名)是否可以在任何计算机上运行取决于您的基础设施。基本上,一个证书是用属于某个CA 的另一个证书进行签名的。 CA 是受信任的,因为所有CA 都已加载到操作系统中。 (转至Windows 控制面板并在搜索框中键入。在管理工具部分中,单击管理计算机证书。这将启动证书管理器。打开根证书颁发机构节点并单击证书。如果满足以下条件,CA 可以吊销颁发者的证书:因此,要验证CAPTCHA 签名,您应该定期联系您的CA 以获取最新的证书吊销列表。

验证码使用加密签名,因此如果后来有人篡改该文件,验证码签名将失效。加密、散列和签名将在各章中讨论。

如何使用CAPTCHA 签名获取并安装证书第一步是从CA 获取代码签名证书(请参见下面的边栏)。然后,您可以将该证书用作受密码保护的文件或将证书加载到计算机的证书存储中。执行后者的优点是您不必指定密码即可签名。这是有利的,因为它可以防止密码在自动生成的脚本或批处理文件中可见。

从哪里获取代码签名证书只有少数代码签名CA 随Windows 预装为根证书颁发机构。其中包括Comodo、Go Daddy、GlobalSign、DigiCert、Thawte 和Symantec。

一些经销商(例如K Software)提供来自上述机构的折扣代码签名证书。

K Software、Comodo、GoDaddy 和GlobalSign 颁发的Authenticode 证书被认为限制较少,因为它们也签署非Microsoft 程序。所有供应商的产品在其他方面都具有相同的功能。

请注意,SSL 证书通常不能用于CAPTCHA 签名(尽管使用相同的X.509 基础设施)。原因之一是SSL 证书证明域名的所有权,而Authenticode 则证明您是谁。

要将证书加载到计算机的证书存储中,请按照上述方式打开证书管理器。打开您的个人文件夹,右键单击其证书文件夹,然后选择所有任务/导入。导入向导将指导您完成此过程。导入完成后,单击证书的“查看”按钮,转到“详细信息”选项卡,然后复制证书。这是SHA-256 哈希值,您稍后在签名时将需要它来识别证书。

注意如果使用强名称对程序集进行签名,则必须在验证代码签名后执行此操作。这是因为CLR 知道CAPTCHA 签名,但反之则不然。因此,如果您在对程序集进行CAPTCHA 签名时使用强名称对其进行签名,则后者会将添加CLR 强名称视为对该程序集进行未经授权的修改;

使用签名工具对.exe 进行签名您可以使用Visual Studio 提供的实用程序验证程序的签名(请参阅下的文件夹)。名为.exe 的文件使用具有安全SHA256 哈希算法的证书进行签名。名为“Joseph Albahari”的计算机:

Signtool Sign /n 'Joseph Albahari' /fd sha256 LINQPad.exe 您还可以使用/d 和/du 指定说明和产品URL。

. /d LINQPad /du http://www.linqpad.net 在大多数情况下,您还需要指定它。

一旦时间戳证书过期,您将无法再对程序进行签名。但是,如果在签名时使用/tr 开关指定,则过期后签名的程序将仍然有效。 CA 为此目的提供一个URI。以下内容适用于Comodo(或K 软件)。

. /tr http://timestamp.comodoca.com/authenticode /td SHA256 验证程序已签名。查看文件身份验证代码签名的最简单方法是在Windows 资源管理器中查看文件的属性([数字签名] 选项卡)。该实用程序还提供了用于此目的的选项。

资源和附属组件应用程序通常不仅包含可执行代码,还包含文本、图像和XML 文件等内容。此类内容可以在程序集中表示。资源有两个重叠的用例。

合并无法包含在源代码中的数据,例如图像。多语言应用程序存储可能需要翻译的数据。程序集资源最终作为命名字节流。您可以将程序集视为包含字节数组的字典,其键是字符串。当使用ildasm 反汇编包含名为的资源和名为的资源的Assembly.jpg 时,它看起来像这样:

.mresource public Banner.jpg{ //Offset:0x00000F58 Length:0x000004F6}.mresource public data.xml{ //Offset:0x00001458 Length:0x0000027E} 在这种情况下,每个数据都作为自己的嵌入资源直接构建到程序集中。这是最简单的工作方式。

.NET 还允许您通过中间容器添加内容。这些旨在保存需要翻译成另一种语言的内容。本地化可以打包为单独的附属程序集,在运行时根据用户的操作系统语言自动选择这些程序集。

显示一个程序集,其中包含两个直接嵌入的资源和一个名为Container 的容器,该容器创建了两个本地化卫星服务器。

资源直接嵌入资源注意:Windows 应用商店应用程序不支持在程序集中嵌入资源。相反,将其他文件添加到部署包中,并通过从应用程序的StorageFolder (Package.Current.InstalledLocation) 中读取来访问它们。

要使用Visual Studio 直接嵌入资源:

将文件添加到您的项目中。将构建操作设置为“嵌入式资源”。 Visual Studio 始终在资源名称前面添加项目的默认命名空间和包含该文件的子文件夹的名称。因此,如果您的项目的默认命名空间是Westwind.Reports 并且您的文件名为.jpg,则该文件夹中的资源名称将为。

请注意,资源名称区分大小写。这有效地使包含资源的项目子文件夹名称在Visual Studio 中区分大小写。

要检索资源,请对包含该资源的程序集调用GetManifestResourceStream。这将返回一个可以像任何其他流一样读取的流。

程序集a=Assembly.GetEntryAssembly();using (Stream s=a.GetManifestResourceStream('TestProject.data.xml'))using (XmlReader r=XmlReader.Create(s)) .System.Drawing.Image Image;using (Stream s=a.GetManifestResourceStream('TestProject.banner.jpg')) image=System.Drawing.Image.FromStream(s); 返回的流是可搜索的,因此您还可以:

byte[] data;using (Stream s=a.GetManifestResourceStream ('TestProject.banner.jpg')) data=new BinaryReader (s).ReadBytes ((int) s.Length);使用Visual Studio 嵌入资源请记住包括需要基于命名空间的前缀if您可以在另一个参数中指定前缀以避免错误。该类型的名称空间用作前缀。

using (Stream s=a.GetManifestResourceStream (typeof (X), 'data.xml'))X 可以是具有所需资源命名空间的任何类型,通常位于同一项目文件夹中。

注意在Visual Studio 中将项目项的生成操作设置为Windows Presentation Foundation (WPF) 应用程序中的资源与将生成操作设置为嵌入资源不同。前者实际上将一个项目添加到名为.使用URI 作为键通过WPF 应用程序类访问其内容。

更令人困惑的是,WPF 进一步重载了术语“资源”。资源和动态资源与程序集无关。

GetManifestResourceNames 返回程序集中所有资源的名称。

.资源文件文件是可本地化内容的容器。该文件最终作为程序集中的嵌入资源创建,就像任何其他类型的文件一样。不同之处在于您需要:

内容首先打包到文件中,然后代表您通过ResourceManager 或GetManifestResourceStream 访问该内容。这些文件以二进制格式构建,无法由人类编辑。因此,您必须依赖.NET和Visual Studio提供的工具来处理这些文件。字符串或简单数据类型的标准方法是使用.resx 格式,可以通过Visual Studio 或工具将其转换为文件。此格式也适用于Windows 窗体或ASP.NET 应用程序中使用的图像。

在WPF 应用程序中,URI 必须指定必须引用的图像。

或类似内容使用 Visual Studio 的“资源”生成操作。无论是否需要本地化,这都适用。 我们将在以下各节中介绍如何执行其中的每一个操作。 .resx 文件文件是用于生成 文件的设计时格式。 文件使用 XML,其结构为名称/值对,如下所示: hello 10 若要在 Visual Studio 中创建 文件,请添加类型为“资源文件”的项目项。其余工作将自动完成: 将创建正确的标头。提供了一个设计器,用于添加字符串、图像、文件和其他类型的数据。文件会自动转换为 格式,并在编译时嵌入到程序集中。编写一个类来帮助您稍后访问数据。注意资源设计器将图像添加为类型化图像对象 (),而不是字节数组,这使得它们不适合 WPF 应用程序。 读取 .resources 文件注意如果在 Visual Studio 中创建 文件,则会自动生成一个同名的类,其中包含用于检索其每个项的属性。 类读取程序集中嵌入的 文件: ResourceManager r = new ResourceManager ("welcome", Assembly.GetExecutingAssembly());(如果资源是在 Visual Studio 中编译的,则第一个参数必须以命名空间为前缀。 然后,您可以通过使用强制转换调用 GetString 或 GetObject 来访问内部内容: string greeting = r.GetString ("Greeting");int fontSize = (int) r.GetObject ("DefaultFontSize");Image image = (Image) r.GetObject ("flag.png"); 枚举 文件的内容: ResourceManager r = new ResourceManager (...);ResourceSet set = r.GetResourceSet (CultureInfo.CurrentUICulture, true, true);foreach (System.Collections.DictionaryEntry entry in set) Console.WriteLine (entry.Key);在 Visual Studio 中创建包 URI 资源在 WPF 应用程序中,XAML 文件需要能够通过 URI 访问资源。例如: 或者,如果资源位于另一个程序集中: (组件是一个文字关键字。 若要创建可以这种方式加载的资源,不能使用 文件。相反,必须将文件添加到项目中,并将其生成操作设置为“资源”(而不是“嵌入的资源”)。然后,Visual Studio 将它们编译为名为 的 . 文件,该文件也是编译的 XAML () 文件的主页。 若要以编程方式加载 URI 键资源,请调用 Application.GetResourceStream: Uri u = new Uri ("flag.png", UriKind.Relative);using (Stream s = Application.GetResourceStream (u).Stream)请注意,我们使用了相对 URI。您还可以使用完全以下格式的绝对 URI(三个逗号不是拼写错误): Uri u = new Uri ("pack://application:,,,/flag.png");如果您希望指定程序集对象,则可以使用资源管理器来检索内容: Assembly a = Assembly.GetExecutingAssembly();ResourceManager r = new ResourceManager (a.GetName().Name + ".g", a);using (Stream s = r.GetStream ("flag.png")) ...资源管理器还允许您枚举给定程序集中 . 容器的内容。 附属组件中的数据是可本地化的。 当应用程序在构建为以不同语言显示所有内容的 Windows 版本上运行时,资源本地化是相关的。为了保持一致性,应用程序也应使用相同的语言。 典型的设置如下: 主程序集包含默认语言或语言的 。单独的包含翻译成不同语言的本地化 。当应用程序运行时,.NET 会检查当前操作系统的语言(来自 CultureInfo.CurrentUICulture )。每当使用 资源管理器 请求资源时,运行时都会查找本地化的附属程序集。如果可用(并且它包含您请求的资源密钥),则使用该密钥代替主程序集的版本。 这意味着您只需添加新的附属组件即可增强语言支持,而无需更改主程序集。 注意附属程序集不能包含可执行代码,只能包含资源。 附属程序集部署在程序集文件夹的子目录中 programBaseFolder\MyProgram.exe \MyLibrary.exe \XX\MyProgram.resources.dll \XX\MyLibrary.resources.dllXX指两个字母的语言代码(例如“de”表示德语)或语言和地区代码(例如“en-GB”表示英国的英语)。此命名系统允许 CLR 自动查找并加载正确的附属程序集。 构建附属程序集回想一下我们之前的 示例,其中包括以下内容: ... hello 然后,我们在运行时检索问候语,如下所示: ResourceManager r = new ResourceManager ("welcome", Assembly.GetExecutingAssembly());Console.Write (r.GetString ("Greeting"));假设我们希望它改为在德语版本的Windows上运行时写“hallo”。第一步是添加另一个名为文件,该文件用代替: hallo 在 Visual Studio 中,只需执行以下操作 — 重新生成时,将在名为 的子目录中自动创建名为 的附属程序集。 测试附属程序集若要模拟在具有不同语言的操作系统上运行,必须使用 Thread 类更改 CurrentUICulture: System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo ("de");CultureInfo.CurrentUICulture 是同一属性的只读版本。 注意一个有用的测试策略是将 lѻ¢αlïʐɘ 转换为仍然可以读作英语的单词,但不要使用标准的罗马 Unicode 字符。 Visual Studio 设计器支持Visual Studio 中的设计器为本地化组件和可视元素提供了扩展支持。WPF 设计器具有自己的本地化工作流;其他基于组件的设计器使用仅设计时属性来使组件或 Windows 窗体控件看起来具有语言属性。若要针对其他语言进行自定义,只需更改 Language 属性,然后开始修改组件。属性为可本地化的控件的所有属性都将保存到该语言的 文件中。只需更改 Language 属性,即可随时在语言之间切换。 文化和亚文化文化分为文化和亚文化。文化代表一种特定的语言;亚文化代表该语言的区域变体。.NET 运行时遵循 RFC1766 标准,该标准使用两个字母的代码表示区域性和子区域性。以下是英语和德语区域性的代码: Ende以下是澳大利亚英语和奥地利德语亚文化的代码: en-AUde-AT区域性在 .NET 中使用 System.Globalization.CultureInfo 类表示。可以检查应用程序的当前区域性,如下所示: Console.WriteLine (System.Threading.Thread.CurrentThread.CurrentCulture);Console.WriteLine (System.Threading.Thread.CurrentThread.CurrentUICulture);在针对澳大利亚本地化的计算机上运行此函数说明了两者之间的区别: en-AUen-USCurrentCulture 反映 Windows 控制面板的区域设置,而 CurrentUICulture 反映操作系统的语言。 区域设置包括时区以及货币和日期的格式等。CurrentCulture 确定诸如 DateTime.Parse 之类的函数的默认行为。可以自定义区域设置,使其不再类似于任何特定区域性。 CurrentUICulture 确定计算机与用户通信的语言。澳大利亚不需要单独的英语版本,所以它只使用美国的英语版本。如果我在奥地利工作了几个月,我会转到控制面板并将我的 CurrentCulture 更改为奥地利德语。但是,鉴于我不会说德语,我的 CurrentUICulture 将仍然是美国英语。 默认情况下,资源管理器使用当前线程的 CurrentUICulture 属性来确定要加载的正确附属程序集。资源管理器在加载资源时使用回退机制。如果定义了亚文化程序集,则使用该组合体;否则,它将回退到通用区域性。如果泛型区域性不存在,它将回退到主程序集中的默认区域性。 加载、解析和隔离程序集从已知位置加载程序集是一个相对简单的过程。我们将其称为。 但是,更常见的是,您(或 CLR)需要加载仅知道其完整(或简单)名称的程序集。这称为。程序集分辨率与加载的不同之处在于,必须首先找到程序集。 在两种情况下触发程序集解析: 通过 CLR,当它需要解析依赖项时显式地,当您调用诸如 Assembly.Load(AssemblyName) 之类的方法时为了说明第一种方案,请考虑一个包含主程序集和一组静态引用的库程序集(依赖项)的应用程序,如以下示例所示: AdventureGame.dll // Main assemblyTerrain.dll // Referenced assemblyUIEngine.dll // Referenced assembly通过“静态引用”,我们的意思是.dll是引用和编译的。编译器本身不需要执行程序集解析,因为它被告知(显式或由 MSBuild)在哪里查找 .dll 和 。在编译过程中,它将 Terrain 和 UIEngine 程序集的写入 的元数据中,但没有关于在哪里可以找到它们的信息。因此,在运行时,必须地形和 UIEngine 程序集。 程序集加载和解析由 (ALC) 处理;具体来说,System.Runtime.Loader 中的 AssemblyLoadContext 类的一个实例。由于 是应用程序的主程序集,因此 CLR 使用 ( AssemblyLoadContext.Default ) 来解析其依赖关系。默认 ALC 首先通过查找并检查名为 的文件(该文件描述了在何处查找依赖项)来解决依赖项,或者如果不存在,它会在应用程序基文件夹中查找,在该文件夹中可以找到 和 。(默认 ALC 还会解析 .NET 运行时程序集。 作为开发人员,您可以在程序执行期间动态加载其他程序集。例如,您可能希望将可选功能打包到仅在购买这些功能后部署的程序集中。在这种情况下,您可以通过调用 Assembly 来加载额外的程序集(如果存在)。加载(程序集名称) . 一个更复杂的示例是实现插件系统,用户可以在其中提供应用程序在运行时检测和加载的第三方程序集,以扩展应用程序的功能。之所以出现复杂性,是因为每个插件程序集可能都有自己的依赖项,这些依赖项也必须解决。 通过子类化 AssemblyLoadContext 并重写其程序集解析方法 ( Load ),可以控制插件查找其依赖项的方式。例如,您可能决定每个插件都应驻留在其自己的文件夹中,并且其依赖项也应驻留在该文件夹中。 ALC 还有另一个用途:通过为每个 ALC 实例化一个单独的 AssemblyLoadContext(插件 + 依赖项),您可以保持每个 ALC 的隔离,确保它们的依赖项并行加载并且不会相互干扰(或主机应用程序)。例如,每个都可以有自己的 JSON.NET 版本。因此,除了和之外,ALC还提供了机制。在某些条件下,甚至可以ALC,从而释放其内存。 在本节中,我们将详细阐述这些原则中的每一个,并描述以下内容: ALC 如何处理负载和分辨率默认 ALC 的角色Assembly.Load and for context ALC如何使用程序集依赖项解析程序如何加载和解析非托管库卸载铝型铝旧程序集加载方法然后,我们将理论付诸实践,并演示如何编写具有ALC隔离的插件系统。 注意AssemblyLoadContext 类是 .NET 5 和 .NET Core 的新增功能。在 .NET Framework 中,ALC 存在,但受到限制和隐藏:创建和与它们交互的唯一方法是间接通过程序集类上的 LoadFile(string)、LoadFrom(string) 和 Load(byte[]) 静态方法。与 ALC API 相比,这些方法不灵活,它们的使用可能会导致意外(尤其是在处理依赖项时)。因此,最好支持在 .NET 5 和 .NET Core 中显式使用 AssemblyLoadContext API。 程序集加载上下文正如我们刚才所讨论的,AssemblyLoadContext 类负责加载和解析程序集,并提供隔离机制。 每个 .NET 程序集对象只属于一个 AssemblyLoadContext 。您可以获取程序集的 ALC,如下所示: Assembly assem = Assembly.GetExecutingAssembly();AssemblyLoadContext context = AssemblyLoadContext.GetLoadContext (assem);Console.WriteLine (context.Name);相反,您可以将 ALC 视为“包含”或“拥有”程序集,可以通过其 Assemblies 属性获取这些程序集。继上一个之后: foreach (Assembly a in context.Assemblies) Console.WriteLine (a.FullName);类还具有枚举所有 ALC 的静态 All 属性。 你可以通过实例化AssemblyLoadContext并提供一个名称来创建新的ALC(该名称在调试时很有帮助),尽管更常见的是,你首先要子类AssemblyLoadContext,以便你可以实现逻辑来依赖关系;换句话说,从程序集加载程序集。 加载程序集AssemblyLoadContext 提供了以下方法来将程序集显式加载到其上下文中: public Assembly LoadFromAssemblyPath (string assemblyPath);public Assembly LoadFromStream (Stream assembly, Stream assemblySymbols);第一种方法从文件路径加载程序集,而第二种方法从 Stream(可以直接来自内存)加载程序集。第二个参数是可选的,对应于项目 debug () 文件的内容,该文件允许堆栈跟踪在代码执行时包含源代码信息(在异常报告中很有用)。 使用这两种方法时,不会进行。 下面将程序集 加载到其自己的 ALC 中: var alc = new AssemblyLoadContext ("Test");Assembly assem = alc.LoadFromAssemblyPath (@"c:\temp\foo.dll");如果程序集有效,则加载将始终成功,但要遵守一条重要规则:程序集的在其 ALC 中必须是唯一的。这意味着不能将同名程序集的多个版本加载到单个 ALC 中;为此,您必须创建其他 ALC。我们可以加载另一个 的副本 var alc2 = new AssemblyLoadContext ("Test 2");Assembly assem2 = alc2.LoadFromAssemblyPath (@"c:\temp\foo.dll");请注意,源自不同程序集对象的类型是不兼容的,即使这些程序集在其他方面是相同的。在我们的示例中,assem 中的类型与 assem2 中的类型不兼容。 加载组件后,除非卸载其 ALC,否则无法卸载该组件(请参见)。CLR 在加载文件的持续时间内保持文件的锁定。 注意您可以通过字节数组加载程序集来避免锁定文件: bytes[] bytes = File.ReadAllBytes (@"c:\temp\foo.dll");var ms = new MemoryStream (bytes);var assem = alc.LoadFromStream (ms);这有两个缺点: 程序集的“位置”属性最终将为空。有时,了解程序集的加载位置很有用(某些 API 依赖于填充程序集)。专用内存消耗必须立即增加,以适应程序集的完整大小。如果改为从文件名加载,CLR 将使用内存映射文件,这将启用延迟加载和进程共享。此外,如果内存不足,操作系统可以释放其内存并根据需要重新加载,而无需写入页面文件。LoadFromAssemblyNameAssemblyLoadContext 还提供了以下方法,该方法按加载程序集: public Assembly LoadFromAssemblyName (AssemblyName assemblyName);与刚才讨论的两种方法不同,您不会传入任何信息来指示程序集所在的位置;相反,您正在指示 ALC 程序集。 解析程序集上述方法触发。CLR 还会在加载依赖项时触发程序集解析。例如,假设程序集 A 静态引用程序集 B。为了解析引用 B,CLR 会在加载 程序集上触发程序集解析。 注意CLR 通过触发程序集解析(无论触发程序集是默认程序集还是自定义 ALC)来解析依赖项。不同之处在于,使用默认 ALC,解析规则是硬编码的,而使用自定义 ALC,您可以自己编写规则。 然后会发生什么: CLR 首先检查该 ALC 中是否已发生相同的解析(具有匹配的完整程序集名称);如果是这样,它将返回之前返回的程序集。否则,它将调用 ALC 的(虚拟受保护)Load 方法,该方法执行定位和加载程序集的工作。默认 ALC 的加载方法应用我们在中描述的规则。使用自定义 ALC,完全取决于您如何定位程序集。例如,您可以在某个文件夹中查找,然后在找到程序集时调用 LoadFromAssemblyPath。从相同或另一个 ALC 返回已加载的程序集也是完全合法的(我们在中演示了这一点)。如果步骤 2 返回 null,则 CLR 将在默认 ALC 上调用 Load 方法(这用作解析 .NET 运行时和常见应用程序程序集的有用“回退”)。如果步骤 3 返回 null,则 CLR 将在两个 ALC 上触发解析事件 — 首先在默认 ALC 上触发,然后在原始 ALC 上触发。(为了与 .NET Framework 兼容):如果程序集仍未解析,则会触发 AppDomain.CurrentDomain.AssemblyResolve 事件。注意此过程完成后,CLR 将执行“健全性检查”,以确保加载的任何程序集的名称都与请求的名称兼容。简单名称必须匹配;公钥标记必须匹配()。版本不需要匹配 - 它可以高于或低于请求的版本。由此,我们可以看到有两种方法可以在自定义 ALC 中实现程序集解析: 覆盖 ALC 的加载方法。这使您的ALC“首先决定”发生的事情,这通常是可取的(并且在您需要隔离时是必不可少的)。处理 ALC 的解析事件。仅当默认 ALC 解析程序集失败,才会触发此操作。注意如果将多个事件处理程序附加到 Resolve 事件,则第一个返回非 null 值的事件处理程序优先。 为了说明这一点,假设我们要加载一个主应用程序在编译时一无所知的程序集,称为 ,位于 (与我们的应用程序文件夹不同)。我们还假设 对 有私有依赖。我们希望确保当我们加载 并执行其代码时, 可以正确解析。我们还希望确保foo及其私有依赖项bar不会干扰主应用程序。 让我们首先编写一个覆盖 Load 的自定义 ALC: using System.IO;using System.Runtime.Loader;class FolderBasedALC : AssemblyLoadContext{ readonly string _folder; public FolderBasedALC (string folder) => _folder = folder; protected override Assembly Load (AssemblyName assemblyName) { // Attempt to find the assembly: string targetPath = Path.Combine (_folder, assemblyName.Name + ".dll"); if (File.Exists (targetPath)) return LoadFromAssemblyPath (targetPath); // Load the assembly return null; // We can’t find it: it could be a .NET runtime assembly }}请注意,在 Load 方法中,如果程序集文件不存在,则返回 null。此检查很重要,因为 也将依赖于 .NET BCL 程序集;因此,Load 方法将在诸如 System.Runtime 之类的程序集上调用。通过返回 null,我们允许 CLR 回退到默认 ALC,这将正确解析这些程序集。 注意请注意,我们没有尝试将 .NET 运行时 BCL 程序集加载到我们自己的 ALC 中。这些系统程序集不是设计为在默认 ALC 之外运行,尝试将它们加载到您自己的 ALC 中可能会导致不正确下降和意外的类型不兼容。 以下是我们如何使用自定义 ALC 在 中加载 程序集: var alc = new FolderBasedALC (@"c:\temp");Assembly foo = alc.LoadFromAssemblyPath (@"c:\temp\foo.dll");...当我们随后开始在 foo 程序集中调用代码时,CLR 在某些时候将需要解决对 的依赖关系。此时,自定义 ALC 的 Load 方法将触发并在 中成功找到 程序集。 在这种情况下,我们的 Load 方法也能够解析 ,因此我们可以将代码简化为: var alc = new FolderBasedALC (@"c:\temp");Assembly foo = alc.LoadFromAssemblyName (new AssemblyName ("foo"));...现在,让我们考虑一个替代解决方案:我们可以实例化一个普通的 AssemblyLoadContext 并处理其解析事件,而不是子类化 AssemblyLoadContext 并覆盖 Load: var alc = new AssemblyLoadContext ("test");alc.Resolving += (loadContext, assemblyName) =>{ string targetPath = Path.Combine (@"c:\temp", assemblyName.Name + ".dll"); return alc.LoadFromAssemblyPath (targetPath); // Load the assembly};Assembly foo = alc.LoadFromAssemblyName (new AssemblyName ("foo"));现在请注意,我们不需要检查程序集是否存在。由于 Resolve 事件在默认 ALC 有机会解析程序集触发(并且仅在它失败时触发),因此我们的处理程序不会为 .NET BCL 程序集触发。这使得此解决方案更简单,尽管存在缺点。请记住,在我们的场景中,主应用程序在编译时对 或 一无所知。这意味着主应用程序本身可能依赖于称为 或 的程序集。如果发生这种情况,解析事件将永远不会触发,而是加载应用程序的 foo 和 bar 程序集。换言之,我们将无法实现。 注意我们的 FolderBasedALC 类非常适合说明程序集解析的概念,但它在现实生活中的用处较少,因为它无法处理特定于平台和(对于库项目)开发时 NuGet 依赖项。在”中,我们描述了这个问题的解决方案,在中,我们给出了一个详细的例子。 默认 ALC当应用程序启动时,CLR 会为静态 AssemblyLoadContext 分配一个特殊的 ALC。默认属性。默认 ALC 是加载启动程序集及其静态引用依赖项和 .NET 运行时 BCL 程序集的位置。 默认 ALC 首先在默认路径中查找以自动解析程序集(请参阅);这通常等同于应用程序的 . 和 . 文件中指示的位置。 如果 ALC 在其默认探测路径中找不到程序集,则会触发其解析事件。通过处理此事件,您可以从其他位置加载程序集,这意味着您可以将应用程序的依赖项部署到其他位置,例如子文件夹、共享文件夹,甚至作为宿主程序集内的二进制资源: AssemblyLoadContext.Default.Resolving += (loadContext, assemblyName) =>{ // Try to locate assemblyName, returning an Assembly object or null. // Typically you’d call LoadFromAssemblyPath after finding the file. // ...};当自定义 ALC 无法解析时(换句话说,当其 Load 方法返回 null 时),默认 ALC 中的 Resolve 事件也会触发,并且默认 ALC 无法解析程序集。 还可以从解析事件外部将程序集加载到默认 ALC 中。但是,在继续之前,您应该首先确定是否可以通过使用单独的 ALC 或使用我们在下一节中描述的方法(使用和 ALC)来更好地解决问题。硬编码为默认 ALC 会使代码变得脆弱,因为它不能作为一个整体进行隔离(例如,通过单元测试框架或 LINQPad)。 如果仍要继续,最好调用(即 LoadFromAssemblyName)而不是(例如 LoadFromAssemblyPath),尤其是在程序集被静态引用的情况下。这是因为程序集可能已经加载,在这种情况下,LoadFromAssemblyName 将返回已加载的程序集,而 LoadFromAssemblyPath 将引发异常。 (使用 LoadFromAssemblyPath ,您还可以冒着从与 ALC 的默认解析机制不一致的位置加载程序集的风险。 如果程序集位于 ALC 不会自动找到它的位置,您仍然可以按照此过程进行操作,并另外处理 ALC 的解析事件。 请注意,调用 LoadFromAssemblyName 时,不需要提供全名;简单名称就可以了(即使程序集是强名称的,也是有效的): AssemblyLoadContext.Default.LoadFromAssemblyName ("System.Xml");但是,如果在名称中包含公钥标记,则它必须与加载的内容匹配。 默认探测默认探测路径通常包括以下内容: 中指定的路径(其中 是应用程序主程序集的名称)。如果此文件不存在,则改用应用程序基文件夹。包含 .NET 运行时系统程序集的文件夹(如果应用程序依赖于框架)。MSBuild 自动生成一个名为 的文件,该文件描述了在何处查找其所有依赖项。其中包括放置在应用程序基文件夹中的与平台无关的程序集,以及放置在运行时子目录下(如 或 )下的特定于平台的程序集。 生成的 . 文件中指定的路径相对于应用程序基文件夹,或您在 AppName.runtimeconfig.json 和/或 配置文件的 additionalProbingPath 部分中指定的任何其他文件夹(后者仅适用于开发环境)。 “当前”的 ALC在上一节中,我们警告不要将程序集显式加载到默认 ALC 中。相反,您通常想要的是加载/解析到“当前”ALC 中。 在大多数情况下,“当前”ALC 是包含当前正在执行的程序集的 ALC: var executingAssem = Assembly.GetExecutingAssembly();var alc = AssemblyLoadContext.GetLoadContext (executingAssem);Assembly assem = alc.LoadFromAssemblyName (...); // to resolve by name // OR: = alc.LoadFromAssemblyPath (...); // to load by path以下是获取 ALC 的更灵活、更明确的方法: var myAssem = typeof (SomeTypeInMyAssembly).Assembly;var alc = AssemblyLoadContext.GetLoadContext (myAssem);...有时,无法推断“当前”ALC。例如,假设您负责编写 .NET 二进制序列化程序(我们将 的联机补充中介绍序列化)。像这样的序列化程序写入它序列化的类型的全名(包括其程序集名称),必须在反序列化期间这些名称。问题是,您应该使用哪种 ALC?依赖正在执行的程序集的问题在于,它将返回包含反序列化程序的任何程序集,而不是反序列化程序的程序集。 最好的解决方案不是猜测,而是问: public object Deserialize (Stream stream, AssemblyLoadContext alc){ ...}明确可以最大限度地提高灵活性并最大限度地减少犯错的机会。调用方现在可以决定什么应该算作“当前”ALC: var assem = typeof (SomeTypeThatIWillBeDeserializing).Assembly;var alc = AssemblyLoadContext.GetLoadContext (assem);var object = Deserialize (someStream, alc);Assembly.Load and Concontext ALC帮助处理将程序集加载到当前执行的 ALC 中的常见情况;那是 var executingAssem = Assembly.GetExecutingAssembly();var alc = AssemblyLoadContext.GetLoadContext (executingAssem);Assembly assem = alc.LoadFromAssemblyName (...);Microsoft已在程序集类中定义了以下方法 public static Assembly Load (string assemblyString);以及接受 AssemblyName 对象的功能相同的版本: public static Assembly Load (AssemblyName assemblyRef);(不要将这些方法与传统的 Load(byte[]) 方法混淆,后者的行为方式完全不同 — 请参阅 与 LoadFromAssemblyName 一样,您可以选择指定程序集的简单名称、部分名称或全名: Assembly a = Assembly.Load ("System.Private.Xml");这会将 System.Private.Xml 程序集加载到的任何 ALC 中。 在本例中,我们指定了一个简单的名称。以下字符串也是有效的,并且在 .NET 中所有字符串的结果都相同: "System.Private.Xml, PublicKeyToken=cc7b13ffcd2ddd51""System.Private.Xml, Version=4.0.1.0""System.Private.Xml, Version=4.0.1.0, PublicKeyToken=cc7b13ffcd2ddd51"如果选择指定公钥标记,则它必须与加载的内容匹配。 注意Microsoft开发人员网络 (MSDN) 警告不要从部分名称加载程序集,建议您指定确切的版本和公钥标记。它们的基本原理基于与 .NET Framework 相关的因素,例如全局程序集缓存和代码访问安全性的影响。在 .NET 5 和 .NET Core 中,不存在这些因素,从简单名称或部分名称加载通常是安全的。 这两种方法都严格用于,因此无法指定文件路径。(如果在 AssemblyName 对象中填充 CodeBase 属性,则将忽略该属性。 警告不要落入使用 Assembly.Load 加载静态引用程序集的陷阱。在这种情况下,您需要做的就是引用程序集中的一个类型,并从中获取程序集: Assembly a = typeof (System.Xml.Formatting).Assembly;或者,您甚至可以这样做: Assembly a = System.Xml.Formatting.Indented.GetType().Assembly;这可以防止对程序集名称进行硬编码(将来可能会更改),同时在 ALC 上触发程序集解析(就像 一样)。 如果你要写大会.自己加载方法,它(几乎)看起来像这样: [MethodImpl(MethodImplOptions.NoInlining)]Assembly Load (string name){ Assembly callingAssembly = Assembly.GetCallingAssembly(); var callingAlc = AssemblyLoadContext.GetLoadContext (callingAssembly); return callingAlc.LoadFromAssemblyName (new AssemblyName (name));}进入情境反思集会。加载 使用调用程序集的 ALC 上下文的策略在程序集 失败时失败。负载通过中介(如反序列化程序或单元测试运行程序)调用。如果中介在不同的程序集中定义,则使用中介的加载上下文,而不是调用方的加载上下文。 注意我们之前在讨论如何编写反序列化程序时描述了这种情况。在这种情况下,理想的解决方案是强制调用方指定 ALC,而不是使用 Assembly.Load(string) 推断它。 但是,由于 .NET 5 和 .NET Core 是从 .NET Framework 演变而来的(在 .NET Framework 中,隔离是通过应用程序域而不是 ALC 完成的),因此理想的解决方案并不普遍,并且在无法可靠地推断 ALC 的情况下,有时会不恰当地使用 Assembly.Load(string)。一个例子是 .NET 二进制序列化程序。 要允许程序集 。加载在这种情况下仍然有效,Microsoft向AssemblyLoadContext添加了一个名为EnterContextualReflection的方法。这会将 ALC 分配给 AssemblyLoadContext。当前上下文反射上下文 .尽管这是一个静态属性,但其值存储在 AsyncLocal 变量中,因此它可以在不同的线程上保存单独的值(但仍会在整个异步操作中保留)。 如果此属性为非空,则程序集 .Load 自动使用它,而不是调用 ALC: Method1();var myALC = new AssemblyLoadContext ("test");using (myALC.EnterContextualReflection()){ Console.WriteLine ( AssemblyLoadContext.CurrentContextualReflectionContext.Name); // test Method2();}// Once disposed, EnterContextualReflection() no longer has an effect.Method3();void Method1() => Assembly.Load ("..."); // Will use calling ALCvoid Method2() => Assembly.Load ("..."); // Will use myALCvoid Method3() => Assembly.Load ("..."); // Will use calling ALC我们之前演示了如何编写功能类似于 汇编 的方法。负荷。下面是一个更准确的版本,它考虑了上下文反射上下文: [MethodImpl(MethodImplOptions.NoInlining)]Assembly Load (string name){ var alc = AssemblyLoadContext.CurrentContextualReflectionContext AssemblyLoadContext.GetLoadContext (Assembly.GetCallingAssembly()); return alc.LoadFromAssemblyName (new AssemblyName (name));}尽管上下文反射上下文在允许旧代码运行方面很有用,但更可靠的解决方案(如前所述)是修改调用 Assembly.Load 的代码,使其改为在调用方传入的 ALC 上调用 LoadFromAssemblyName。 注意.NET Framework 没有等效的 EnterContextualReflection,也不需要它,尽管具有相同的程序集。加载方法。这是因为使用 .NET Framework,隔离主要通过而不是 ALC 实现。应用程序域提供了更强的隔离模型,其中每个应用程序域都有自己的默认加载上下文,因此即使仅使用默认加载上下文,隔离仍然可以工作。 加载和解析非托管库ALC 还可以加载和解析本机库。调用标有 [DllImport] 属性的外部方法时,将触发本机解析: [DllImport ("SomeNativeLibrary.dll")]static extern int SomeNativeMethod (string text);由于我们没有在 [DllImport] 属性中指定完整路径,因此调用 SomeNativeMethod 会在包含定义 SomeNativeMethod 的程序集的任何 ALC 中触发解析。 ALC 中的虚拟方法称为 LoadUnmanagedDll ,方法称为 LoadUnmanagedDllFromPath: protected override IntPtr LoadUnmanagedDll (string unmanagedDllName){ // Locate the full path of unmanagedDllName... string fullPath = ... return LoadUnmanagedDllFromPath (fullPath); // Load the DLL}如果找不到该文件,可以返回 IntPtr.Zero 。然后,CLR 将触发 ALC 的 ResolvevingUnmanagedDll 事件。 有趣的是,LoadUnmanagedDllFromPath 方法受到保护,因此通常无法从 ResolvevingUnmanagedDll 事件处理程序调用它。但是,您可以通过调用静态 NativeLibrary.Load 来获得相同的结果: someALC.ResolvingUnmanagedDll += (requestingAssembly, unmanagedDllName) =>{ return NativeLibrary.Load ("(full path to unmanaged DLL)");};尽管本机库通常由 ALC 解析和加载,但它们并不“属于”ALC。加载后,本机库将独立存在,并负责解析它可能具有的任何传递依赖项。此外,本机库是进程的全局库,因此如果它们具有相同的文件名,则无法加载两个不同版本的本机库。 程序集依赖解析程序在中,我们说过默认 ALC 会读取 .deps.json 和 . 文件(如果存在),以确定在何处查找以解析特定于平台和开发时 NuGet 依赖项。 如果要将程序集加载到具有特定于平台或 NuGet 依赖项的自定义 ALC 中,则需要以某种方式重现此逻辑。可以通过分析配置文件并仔细遵循特定于平台的名字对象上的规则来实现此目的,但这样做不仅困难,而且如果规则在更高版本的 .NET 中发生更改,则编写的代码将中断。 程序集依赖解析器类解决了这个问题。若要使用它,请使用要探测其依赖项的程序集的路径实例化它: var resolver = new AssemblyDependencyResolver (@"c:\temp\foo.dll");然后,若要查找依赖项的路径,请调用 ResolveAssemblyToPath 方法: string path = resolver.ResolveAssemblyToPath (new AssemblyName ("bar"));在没有 . 文件的情况下(或者如果 . 不包含任何与 bar.dll) 相关的内容,这将计算为 。 同样,可以通过调用 ResolveUnmanagedDllToPath 来解析非托管依赖项。 说明更复杂的方案的一个好方法是创建一个名为 ClientApp 的新控制台项目,然后添加对 的 NuGet 引用。添加以下类: using Microsoft.Data.SqlClient;namespace ClientApp{ public class Program { public static SqlConnection GetConnection() => new SqlConnection(); static void Main() => GetConnection(); // Test that it resolves }}现在构建应用程序并查看输出文件夹:您将看到一个名为 的文件。但是,此文件在运行时加载,尝试显式加载它会引发异常。实际加载的程序集位于(或)子文件夹中;默认 ALC 知道加载它,因为它解析 文件。 如果要尝试从另一个应用程序加载 .dll 程序集,则需要编写一个可以解析其依赖项的 ALC,。这样做时,仅查看 所在的文件夹是不够的(就像中所做的那样)。相反,您需要使用 AssemblyDependencyResolver 来确定该文件对于正在使用的平台的位置: string path = @"C:\source\ClientApp\bin\Debug\netcoreapp3.0\ClientApp.dll";var resolver = new AssemblyDependencyResolver (path);var sqlClient = new AssemblyName ("Microsoft.Data.SqlClient");Console.WriteLine (resolver.ResolveAssemblyToPath (sqlClient));在 Windows 计算机上,这将输出以下内容: C:\source\ClientApp\bin\Debug\netcoreapp3.0\runtimes\win\lib\netcoreapp2.1\Microsoft.Data.SqlClient.dll我们在中给出了一个完整的示例。 卸载铝型铝在简单的情况下,可以卸载非默认的 AssemblyLoadContext ,释放内存并释放它加载的程序集上的文件锁。为此,ALC 必须已使用 isCollectible 参数 true 进行实例化: var alc = new AssemblyLoadContext ("test", isCollectible:true);然后,可以在 ALC 上调用 Unload 方法来启动卸载过程。 卸载模型是合作的,而不是抢占的。如果 ALC 的任何程序集中的任何方法正在执行,则卸载将延迟到这些方法完成。 实际卸载发生在垃圾收集期间;如果来自 ALC 外部的任何内容对 ALC 内部的任何内容(包括对象、类型和程序集)有任何(非弱)引用,则不会发生这种情况。API(包括 .NET BCL 中的 API)在静态字段或字典中缓存对象或订阅事件的情况并不少见,这使得创建防止卸载的引用变得容易,尤其是在 ALC 中的代码以非平凡的方式使用其 ALC 外部的 API 时。确定卸载失败的原因很困难,需要使用 WinDbg 等工具。 旧版加载方法如果您仍在使用 .NET Framework(或编写面向 .NET Standard 的库,并希望支持 .NET Framework),您将无法使用 AssemblyLoadContext 类。加载是通过使用以下方法完成的: public static Assembly LoadFrom (string assemblyFile);public static Assembly LoadFile (string path);public static Assembly Load (byte[] rawAssembly);LoadFile 和 Load(byte[]) 提供隔离,而 LoadFrom 不提供。 解析是通过处理应用程序域的 AssemblyResolve 事件来实现的,该事件的工作方式类似于默认 ALC 的解析事件。 Assembly.Load(string) 方法也可用于触发解析,并以类似的方式工作。 加载自LoadFrom 将程序集从给定路径加载到默认 ALC 中。这有点像调用AssemblyLoadContext.Default.LoadFromAssemblyPath,除了: 如果默认 ALC 中已存在具有相同简单名称的程序集,则 LoadFrom 将返回该程序集,而不是引发异常。如果默认 ALC 中具有相同简单名称的程序集,并且发生了加载,则会为该程序集提供特殊的“LoadFrom”状态。此状态会影响默认 ALC 的解析逻辑,因为如果该程序集在同一有任何依赖项,则这些依赖项将自动解析。注意.NET Framework 有一个 (GAC)。如果程序集存在于 GAC 中,则 CLR 将始终从那里加载。这适用于所有三种加载方法。 LoadFrom 自动解析传递相同文件夹依赖项的能力可能很方便 - 直到它加载不应该加载的程序集。由于此类方案可能难以调试,因此最好使用 Load(string) 或 LoadFile 并通过处理应用程序域的 AssemblyResolve 事件来解析传递依赖项。这使您能够决定如何解析每个程序集,并允许调试(通过在事件处理程序中创建断点)。 加载文件和加载(字节[])LoadFile 和 Load(byte[]) 将程序集从给定的文件路径或字节数组加载到新的 ALC 中。与 LoadFrom 不同,这些方法提供隔离,并允许您加载同一程序集的多个版本。但是,有两个注意事项: 使用相同的路径再次调用 LoadFile 将返回以前加载的程序集。在 .NET Framework 中,这两种方法都首先检查 GAC 并从那里加载(如果程序集存在)。使用 LoadFile 和 Load(byte[]) ,您最终会得到每个程序集的单独 ALC(请注意)。这样可以实现隔离,尽管它可能会使管理更加尴尬。 要解析依赖关系,请处理 AppDomain 的解析事件,该事件在所有 ALC 上触发: AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>{ string fullAssemblyName = args.Name; // return an Assembly object or null ...};args 变量还包括一个名为 请求程序集 ,它告诉您哪个程序集触发了解析。 找到程序集后,可以调用程序集 。加载文件以加载它。 注意可以使用 AppDomain.CurrentDomain.GetAssemblies() 枚举已加载到当前应用程序域中的所有程序集。这也适用于 .NET 5,它等效于以下内容: AssemblyLoadContext.All.SelectMany (a => a.Assemblies)编写插件系统为了充分演示本节中介绍的概念,让我们编写一个插件系统,该系统使用可卸载的 ALC 来隔离每个插件。 我们的演示系统最初将包含三个 .NET 项目:插件.通用 (库)定义插件将实现的接口资本化器(图书馆)将文本大写的插件Plugin.Host (控制台应用程序)查找和调用插件 假设项目驻留在以下目录中: c:\source\PluginDemo\Plugin.Commonc:\source\PluginDemo\Capitalizerc:\source\PluginDemo\Plugin.Host所有项目都将引用 Plugin.Common 库,并且不会有其他项目间引用。 注意如果 Plugin.Host 引用 Capitalizer,我们就不会编写插件系统;中心思想是插件是在 Plugin.Host 和 Plugin.Common 发布后由第三方编写的。 如果您使用的是 Visual Studio,为了本演示,将所有三个项目放入一个解决方案中会很方便。如果这样做,请右键单击 Plugin.Host 项目,选择“构建依赖项”>“项目依赖项”,然后勾选 Capitalizer 项目。这会强制 Capitalizer 在运行 Plugin.Host 项目时构建,而不添加引用。 插件.常见让我们从Plugin.Common开始。我们的插件将执行一个非常简单的任务,即转换字符串。以下是我们如何定义接口: namespace Plugin.Common{ public interface ITextPlugin { string TransformText (string input); }}这就是Plugin.Common的全部内容。 资本化器(插件)我们的 Capitalizer 插件将引用 Plugin.Common 并包含一个类。现在,我们将保持逻辑简单,以便插件没有额外的依赖项: public class CapitalizerPlugin : Plugin.Common.ITextPlugin{ public string TransformText (string input) => input.ToUpper();}如果同时生成这两个项目并查看 Capitalizer 的输出文件夹,您将看到以下两个程序集: Capitalizer.dll // Our plug-in assemblyPlugin.Common.dll // Referenced assembly插件主机Plugin.Host 是一个具有两个类的控制台应用程序。第一个类是用于加载插件的自定义 ALC: class PluginLoadContext : AssemblyLoadContext{ AssemblyDependencyResolver _resolver; public PluginLoadContext (string pluginPath, bool collectible) // Give it a friendly name to help with debugging: : base (name: Path.GetFileName (pluginPath), collectible) { // Create a resolver to help us find dependencies. _resolver = new AssemblyDependencyResolver (pluginPath); } protected override Assembly Load (AssemblyName assemblyName) { // See below if (assemblyName.Name == typeof (ITextPlugin).Assembly.GetName().Name) return null; string target = _resolver.ResolveAssemblyToPath (assemblyName); if (target != null) return LoadFromAssemblyPath (target); // Could be a BCL assembly. Allow the default context to resolve. return null; } protected override IntPtr LoadUnmanagedDll (string unmanagedDllName) { string path = _resolver.ResolveUnmanagedDllToPath (unmanagedDllName); return path == null IntPtr.Zero : LoadUnmanagedDllFromPath (path); }}在构造函数中,我们传入主插件程序集的路径以及一个标志,以指示我们是否希望 ALC 是可收集的(以便可以卸载它)。 Load 方法是我们处理依赖项解析的地方。所有插件都必须引用 Plugin.Common,以便它们可以实现 ITextPlugin 。这意味着 Load 方法将在某个时候触发以解析 Plugin.Common。我们需要小心,因为插件的输出文件夹可能不仅包含 .dll,还包含它自己的 副本。如果我们要把 的副本加载到 PluginLoadContext 中,我们最终会得到程序集的两个副本:一个在主机的默认上下文中,另一个在插件的 PluginLoadContext 中。程序集将不兼容,主机会抱怨插件没有实现 ITextPlugin! 为了解决这个问题,我们显式检查此条件: if (assemblyName.Name == typeof (ITextPlugin).Assembly.GetName().Name) return null;返回 null 允许主机的默认 ALC 改为解析程序集。 注意我们可以返回typeof(ITextPlugin),而不是返回null。组装 ,它也可以正常工作。我们如何确定 ITextPlugin 将在主机的 ALC 上解析,而不是在我们的 PluginLoadContext 上解析?请记住,我们的 PluginLoadContext 类是在 Plugin.Host 程序集中定义的。因此,从此类静态引用的任何类型都将在 Plugin.Host 的 ALC 上触发程序集解析。 检查公共程序集后,我们使用 AssemblyDependencyResolver 来查找插件可能具有的任何私有依赖项。(现在,不会有。 请注意,我们还重写了 LoadUnamangedDll 方法。这可确保如果插件具有任何非托管依赖项,这些依赖项也将正确加载。 在 Plugin.Host 中编写的第二个类是主程序本身。为简单起见,让我们对 Capitalizer 插件的路径进行硬编码(在现实生活中,您可能会通过在已知位置查找 DLL 或从配置文件中读取来发现插件的路径): class Program{ const bool UseCollectibleContexts = true; static void Main() { const string captializer = @"C:\source\PluginDemo\" + @"Capitalizer\bin\Debug\netcoreapp3.0\Capitalizer.dll"; Console.WriteLine (TransformText ("big apple", captializer)); } static string TransformText (string text, string pluginPath) { var alc = new PluginLoadContext (pluginPath, UseCollectibleContexts); try { Assembly assem = alc.LoadFromAssemblyPath (pluginPath); // Locate the type in the assembly that implements ITextPlugin: Type pluginType = assem.ExportedTypes.Single (t => typeof (ITextPlugin).IsAssignableFrom (t)); // Instantiate the ITextPlugin implementation: var plugin = (ITextPlugin)Activator.CreateInstance (pluginType); // Call the TransformText method return plugin.TransformText (text); } finally { if (UseCollectibleContexts) alc.Unload(); // unload the ALC } }}让我们看一下 TransformText 方法。我们首先为插件实例化一个新的 ALC,然后要求它加载主插件程序集。接下来,我们使用 Reflection 来定位实现 ITextPlugin 的类型(我们将在第 中详细介绍)。然后,我们实例化插件,调用 TransformText 方法,并卸载 ALC。 注意如果需要重复调用 TransformText 方法,更好的方法是缓存 ALC,而不是在每次调用后卸载它。 下面是输出: BIG APPLE添加依赖项我们的代码完全能够解析和隔离依赖项。为了说明这一点,让我们首先添加对 的 NuGet 引用。您可以通过Visual Studio UI或将以下元素添加到文件来执行此操作: 现在,修改资本插件,如下所示: using Humanizer;namespace Capitalizer{ public class CapitalizerPlugin : Plugin.Common.ITextPlugin { public string TransformText (string input) => input.Pascalize(); }}如果重新运行该程序,输出现在将是这样的: BigApple接下来,我们创建另一个名为 Pluralizer 的插件。创建一个新的 .NET 库项目,并添加对 的 NuGet 引用: 现在,添加一个名为 复数器插件 。这将类似于 资本化插件 ,但我们调用 Pluralize 方法,改为: using Humanizer;namespace Pluralizer{ public class PluralizerPlugin : Plugin.Common.ITextPlugin { public string TransformText (string input) => input.Pluralize(); }}最后,我们需要在 Plugin.Host 的 Main 方法中添加代码来加载和运行 Pluralizer 插件: static void Main() { const string captializer = @"C:\source\PluginDemo\" + @"Capitalizer\bin\Debug\netcoreapp3.0\Capitalizer.dll"; Console.WriteLine (TransformText ("big apple", captializer)); const string pluralizer = @"C:\source\PluginDemo\" + @"Pluralizer\bin\Debug\netcoreapp3.0\Pluralizer.dll"; Console.WriteLine (TransformText ("big apple", pluralizer)); }输出现在将如下所示: BigApplebig apples若要完全了解发生了什么,请将 UseCollectibleContexts 常量更改为 false,并将以下代码添加到 Main 方法以枚举 ALC 及其程序集: foreach (var context in AssemblyLoadContext.All){ Console.WriteLine ($"Context: {context.GetType().Name} {context.Name}"); foreach (var assembly in context.Assemblies) Console.WriteLine ($" Assembly: {assembly.FullName}");}在输出中,您可以看到两个不同版本的 Humanizer,每个版本都加载到自己的 ALC 中: Context: PluginLoadContext Capitalizer.dll Assembly: Capitalizer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=... Assembly: Humanizer, Version=2.6.0.0, Culture=neutral, PublicKeyToken=...Context: PluginLoadContext Pluralizer.dll Assembly: Pluralizer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=... Assembly: Humanizer, Version=2.7.0.0, Culture=neutral, PublicKeyToken=...Context: DefaultAssemblyLoadContext Default Assembly: System.Private.CoreLib, Version=4.0.0.0, Culture=neutral,... Assembly: Host, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null ...注意即使两个插件都使用相同的 Humanizer 版本,隔离单独的程序集仍然是有益的,因为每个程序集都有自己的静态变量。
标题:c#界面组件,c# ui组件
链接:https://www.7kxz.com/news/gl/18554.html
版权:文章转载自网络,如有侵权,请联系删除!
资讯推荐
更多
天地劫幽城再临归真4-5攻略:第四章归真4-5八回合图文通关教学

天地劫幽城再临归真4-5攻略:第四章归真4-5八回合图文通关教学[多图],天地劫幽城再临归真4-5怎么样八回合内通

2024-03-30
航海王热血航线艾尼路怎么玩?艾尼路加点连招攻略大全

航海王热血航线艾尼路怎么玩?艾尼路加点连招攻略大全[多图],航海王热血航线艾尼路怎么加点?艾尼路怎么连招?关

2024-03-30
坎公骑冠剑国际服怎么玩?国际服新手攻略

坎公骑冠剑国际服怎么玩?国际服新手攻略[多图],坎公骑冠剑国际服的玩法是什么样的?关于游戏中的一些新手玩法

2024-03-30
王者荣耀鸿运6+1地狱之眼怎么抽?鸿运抽奖地狱之眼概率获取攻略

王者荣耀鸿运6+1地狱之眼怎么抽?鸿运抽奖地狱之眼概率获取攻略[多图],王者荣耀鸿运抽奖活动的奖池中还有传说

2024-03-30