C-9-和--NET5-高级教程-九-

94 阅读52分钟

C#9 和 .NET5 高级教程(九)

原文:Pro C# 9 with .NET 5

协议:CC BY-NC-SA 4.0

十六、构建和配置类库

对于本书到目前为止的大多数例子,你已经创建了“独立的”可执行应用,其中所有的编程逻辑都被打包在一个单独的汇编(*.dll)中,并使用dotnet.exe(或者以汇编命名的dotnet.exe的副本)来执行。这些程序集使用的内存比。NET 核心基本类库。虽然有些简单。NET 核心程序可能只使用基本类库来构建,您(或您的队友)将可重用的编程逻辑隔离到可以在应用之间共享的自定义类库(*.dll文件)中可能是很平常的事情。

在这一章中,你将从学习把类型划分成。NET 核心命名空间。在此之后,您将深入了解。NET 核心,NET 核心和。NET 标准、应用配置、发布。NET 核心控制台应用,并将您的库打包成可重用的 NuGet 包。

定义自定义命名空间

在深入库部署和配置方面之前,第一项任务是了解将自定义类型打包到。NET 核心命名空间。到本文的这一点,您已经构建了一些小型测试程序,这些程序利用了。净核心宇宙(System,特指)。但是,当您构建具有许多类型的大型应用时,将相关类型分组到自定义命名空间中会很有帮助。在 C# 中,这是通过使用namespace关键字来完成的。创建共享程序集时,显式定义自定义命名空间甚至更加重要,因为其他开发人员将需要引用该库并导入您的自定义命名空间来使用您的类型。自定义命名空间还通过将您的自定义类与可能同名的其他自定义类隔离开来,来防止名称冲突。

为了直接调查这些问题,首先创建一个新的。名为 CustomNamespaces 的. NET 核心控制台应用项目。现在,假设您正在开发一个名为SquareCircleHexagon的几何类集合。鉴于它们的相似性,您希望将它们分组到一个独特的名称空间中,这个名称空间在CustomNamespaces.exe程序集内被称为MyShapes

虽然 C# 编译器对包含多种类型的单个 C# 代码文件没有问题,但在团队环境中工作时,这可能会有问题。如果您正在处理Circle类型,而您的同事需要处理Hexagon类,那么您将不得不轮流处理单块文件,或者面临难以解决的(至少是耗时的)合并冲突。

更好的方法是将每个类放在自己的文件中,每个类都有一个名称空间定义。为了确保每个类型都被打包到同一个逻辑组中,只需将给定的类定义包装在同一个名称空间范围内,如下所示:

namespace MyShapes
{
  // Circle class
  public class Circle { /* Interesting methods... */ }
}

// Hexagon.cs
namespace MyShapes
{
  // Hexagon class
  public class Hexagon { /* More interesting methods... */ }
}

// Square.cs
namespace MyShapes
{
  // Square class
  public class Square { /* Even more interesting methods...*/}
}

Guidance

每个代码文件中只有一个类被认为是最佳实践。虽然一些早期的例子没有做到这一点,但这是为了简化教学。在本书中,我的意图是总是将每个类分离到它自己的文件中。

注意MyShapes名称空间是如何充当这些类的概念“容器”的。当另一个名称空间(比如CustomNamespaces)想要使用单独名称空间中的类型时,可以使用using关键字,就像使用。NET 核心基类库,如下所示:

// Bring in a namespace from the base class libraries.
using System;
// Make use of types defined the MyShapes namespace.
using MyShapes;

Hexagon h = new Hexagon();
Circle c = new Circle();
Square s = new Square();

对于这个例子,假设定义MyShapes名称空间的 C# 文件是同一个控制台应用项目的一部分;换句话说,所有文件都被编译成一个程序集。如果在外部程序集中定义了MyShapes名称空间,那么在成功编译之前,还需要添加一个对该库的引用。在这一章中,你将学到构建使用外部库的应用的所有细节。

解析具有完全限定名的名称类

从技术上讲,当引用外部命名空间中定义的类型时,不需要使用 C# using关键字。您可以使用类型的完全限定名*,您可能还记得第一章中的内容,这是以定义名称空间为前缀的类型名。这里有一个例子:*

// Note we are not importing MyShapes anymore!
using System;

MyShapes.Hexagon h = new MyShapes.Hexagon();
MyShapes.Circle c = new MyShapes.Circle();
MyShapes.Square s = new MyShapes.Square();

通常,不需要使用完全限定名。它不仅需要更多的击键次数,而且在代码大小或执行速度方面没有任何区别。事实上,在 CIL 代码中,类型总是用完全限定名定义。从这个角度来看,C# using关键字仅仅是一个节省打字时间的工具。

但是,当使用包含同名类型的多个命名空间时,完全限定名有助于(有时是必要的)避免潜在的名称冲突。假设您有一个名为My3DShapes的新名称空间,它定义了以下三个类,能够以令人惊叹的 3D 方式呈现形状:

// Another shape-centric namespace.
//Circle.cs
namespace My3DShapes
{
  // 3D Circle class.
  public class Circle { }
}
//Hexagon.cs
namespace My3DShapes
{
  // 3D Hexagon class.
  public class Hexagon { }
}
//Square.cs
namespace My3DShapes
{
  // 3D Square class.
  public class Square { }
}

如果如下所示更新顶级语句,会出现几个编译时错误,因为两个命名空间都定义了同名的类:

// Ambiguities abound!
using System;
using MyShapes;
using My3DShapes;

// Which namespace do I reference?
Hexagon h = new Hexagon(); // Compiler error!
Circle c = new Circle();   // Compiler error!
Square s = new Square();   // Compiler error!

可以使用类型的完全限定名来解决这种不确定性,如下所示:

// We have now resolved the ambiguity.
My3DShapes.Hexagon h = new My3DShapes.Hexagon();
My3DShapes.Circle c = new My3DShapes.Circle();
MyShapes.Square s = new MyShapes.Square();

解析带有别名的名称类

C# using关键字还允许您为类型的完全限定名创建别名。这样做时,您定义了一个在编译时替换类型全名的标记。定义别名提供了解决名称冲突的第二种方法。这里有一个例子:

using System;
using MyShapes;
using My3DShapes;

// Resolve the ambiguity using a custom alias.
using The3DHexagon = My3DShapes.Hexagon;

// This is really creating a My3DShapes.Hexagon class.
The3DHexagon h2 = new The3DHexagon();
...

这种替代的using语法还允许您为冗长的名称空间创建别名。其中一个较长的基类库名称空间是System.Runtime.Serialization.Formatters.Binary,它包含一个名为BinaryFormatter的成员。如果您愿意,您可以创建一个BinaryFormatter的实例,如下所示:

using bfHome = System.Runtime.Serialization.Formatters.Binary;
bfHome.BinaryFormatter b = new bfHome.BinaryFormatter();
...

以及传统的using指令:

using System.Runtime.Serialization.Formatters.Binary;
BinaryFormatter b = new BinaryFormatter();
...

在游戏的这一点上,没有必要关心这个BinaryFormatter类是用来做什么的(你会在第二十章中研究这个类)。现在,只要记住 C# using关键字可以用来为冗长的完全限定名定义别名,或者更常见的是,用来解决在导入多个定义同名类型的名称空间时可能出现的名称冲突。

Note

请注意,过度使用 C# 别名会导致混乱的代码库。如果您团队中的其他程序员不知道您的自定义别名,他们可能会认为别名引用了基类库中的类型,当他们在文档中找不到这些标记时会变得非常困惑!

创建嵌套命名空间

在组织类型时,您可以自由地在其他命名空间中定义命名空间。基类库在许多地方都这样做,以提供更深层次的类型组织。例如,IO名称空间嵌套在System中以产生System.IO

那个。NET Core 项目模板将Program.cs中的初始代码添加到以项目命名的名称空间中。这个基本名称空间被称为名称空间。在此示例中,根命名空间由。NET Core 模板为CustomNamespaces,如下图所示:

namespace CustomNamespaces
{
  class Program
  {
  ...
  }
}

Note

用顶级语句替换Program / Main()组合时,不能给这些顶级语句分配名称空间。

要在根名称空间中嵌套MyShapesMy3DShapes名称空间,有两种选择。第一种方法是简单地嵌套 namespace 关键字,就像这样:

namespace CustomNamespaces
{
    namespace MyShapes
    {
        // Circle class
        public class Circle
        {
            /* Interesting methods... */
        }
    }
}

另一种选择(也是更常见的)是在名称空间定义中使用“点符号”,如下所示:

namespace CustomNamespaces.MyShapes
{
    // Circle class
    public class Circle
    {
         /* Interesting methods... */
    }
}

命名空间不必直接包含任何类型。这允许开发人员使用名称空间来提供更高层次的范围。

假设您现在已经在CustomNamespaces根名称空间中嵌套了My3DShapes名称空间,那么您需要更新任何现有的using指令和类型别名,如下所示(假设您已经更新了嵌套在根名称空间下的所有示例类):

using The3DHexagon = CustomNamespaces.My3DShapes.Hexagon;
using CustomNamespaces.MyShapes;

Guidance

一种常见的做法是按目录将文件分组到一个命名空间中。如图所示,文件在目录结构中的位置对名称空间没有影响。但是,它确实使名称空间结构对其他开发人员来说更加清晰(和具体)。因此,许多开发人员和代码林挺工具希望名称空间与文件夹结构相匹配。

更改 Visual Studio 的根命名空间

如上所述,当您使用 Visual Studio(或。NET Core CLI),应用的根命名空间的名称将与项目名称相同。从这一点开始,当您使用 Visual Studio 通过项目➤的“添加新项”菜单选项插入新的代码文件时,类型将自动包装在根命名空间中。如果您想更改根命名空间的名称,只需使用项目属性窗口的应用选项卡访问“默认命名空间”选项(参见图 16-1 )。

img/340876_10_En_16_Fig1_HTML.jpg

图 16-1。

配置默认命名空间

Note

Visual Studio 属性页仍然将根命名空间称为默认的命名空间。接下来你会看到为什么我称它为名称空间。

如果不使用 Visual Studio(或者甚至使用 Visual Studio),也可以通过更新项目(*.csproj)文件来配置根命名空间。和。NET 核心项目,在 Visual Studio 中编辑项目文件就像在解决方案资源管理器中双击项目文件一样简单(或者在解决方案资源管理器中右击项目文件并选择“编辑项目文件”)。文件打开后,通过添加RootNamespace节点来更新主PropertyGroup,如下所示:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
    <RootNamespace>CustomNamespaces2</RootNamespace>
  </PropertyGroup>

</Project>

目前为止,一切顺利。既然您已经看到了关于如何将自定义类型打包到组织良好的命名空间中的一些细节,那么让我们快速回顾一下。网芯装配。此后,您将深入研究创建、部署和配置自定义类库的细节。

的作用.NETCore 组件

。NET 核心应用是由任意数量的组件拼凑而成的。简而言之,程序集是由。NET 核心运行时。尽管如此。NET Core 程序集与以前的 Windows 二进制文件具有相同的文件扩展名(*.exe*.dll),它们与那些文件几乎没有共同之处。在解开最后一个陈述之前,让我们考虑一下汇编格式提供的一些好处。

程序集促进代码重用

由于您已经在前面的章节中构建了您的控制台应用项目,似乎所有的应用的功能都包含在您正在构建的可执行程序集中。您的应用利用了包含在始终可访问的。NET 核心基本类库。

正如你可能知道的,一个代码库(也称为类库)是一个*.dll,它包含了外部应用想要使用的类型。当您创建可执行程序集时,您无疑会在创建应用时利用大量系统提供的和自定义的代码库。但是,请注意,代码库不需要使用*.dll文件扩展名。可执行程序集完全有可能(尽管肯定不常见)使用外部可执行文件中定义的类型。由此看来,引用的*.exe也可以被认为是一个代码库。

不管代码库是如何打包的。NET Core platform 允许您以独立于语言的方式重用类型。例如,您可以在 C# 中创建一个代码库,并在任何其他代码库中重用该库。NET 核心编程语言。不仅可以跨语言分配类型,还可以从中派生类型。C# 中定义的基类可以由 Visual Basic 中编写的类来扩展。F# 中定义的接口可以通过 C# 中定义的结构来实现,等等。重点是,当你开始把一个单一的可执行文件分解成许多个时。NET 核心程序集,您实现了一种与语言无关的代码重用形式。

程序集建立类型边界

回想一下,类型的完全限定名是通过将类型的名称空间(例如System)作为其名称(例如Console)的前缀来组成的。然而,严格地说,类型所在的程序集进一步建立了类型的标识。例如,如果您有两个唯一命名的程序集(比如,MyCars.dllYourCars.dll,它们都定义了一个包含名为SportsCar的类的名称空间(CarLibrary),那么它们在。净核心宇宙。

程序集是可版本化的单位

。NET Core 程序集被分配一个由四部分组成的数字版本号,形式为< major >。< 小调 >。< 打造 >。< 修订 >。(如果没有显式提供版本号,则会自动为程序集分配 1.0.0.0 版本(给定默认值)。NET 核心项目设置。)这个数字允许同一程序集的多个版本在一台机器上和谐共存。

程序集是自描述的

程序集被认为是自描述的,部分原因是它们在程序集的清单中记录了它们必须能够访问以正确运行的每个外部程序集。回想一下第一章中的内容,清单是描述程序集本身(名称、版本、所需的外部程序集等)的元数据块。).

除了清单数据,程序集还包含描述组成的元数据(成员名、实现的接口、基类、构造函数等)。)的所有包含类型。因为程序集记录得如此详细,所以。NET Core Runtime 不也不咨询 Windows 系统注册表来解析它的位置(与微软遗留的 COM 编程模型完全不同)。这种与注册中心的分离是使。NET 核心应用可以在除 Windows 之外的其他操作系统上运行,并且支持多个版本的。NET Core 在同一台机器上。

正如您将在本章中发现的。NET Core Runtime 使用了一种全新的方案来解析外部代码库的位置。

了解. NET 核心程序集的格式

既然您已经了解了。NET Core assembly,让我们换个话题,更好地了解一个程序集是如何组成的。从结构上讲,. NET 核心程序集(*.dll*.exe)由以下元素组成:

  • 操作系统(例如,Windows)文件头

  • CLR 文件头

  • CIL 队列

  • 类型元数据

  • 程序集清单

  • 可选嵌入式资源

虽然前两个元素(操作系统和 CLR 头)是您通常可以忽略的数据块,但它们确实值得简单考虑一下。以下是每个元素的概述。

安装 C++分析工具

接下来的几节使用一个实用程序调用dumpbin.exe,它是 C++分析工具附带的。安装时,在快速搜索栏中输入 C++评测工具,点击提示安装工具,如图 16-2 所示。

img/340876_10_En_16_Fig2_HTML.jpg

图 16-2。

从快速启动安装 C++分析工具

这将打开带有所选工具的 Visual Studio 安装程序。或者,您可以自己启动 Visual Studio 安装程序,并选择如图 16-3 所示的组件。

img/340876_10_En_16_Fig3_HTML.jpg

图 16-3。

安装 C++分析工具

操作系统(Windows)文件头

操作系统文件头确定了目标操作系统(在下面的示例中为 Windows)可以加载和操作程序集的事实。这个头数据还标识了操作系统托管的应用的种类(基于控制台、基于 GUI 或*.dll代码库)。

使用带有/headers标志的dumpbin.exe实用程序(通过开发者命令提示符)打开CarLibrary.dll文件(在书的回购中或在本章后面创建),如下所示:

dumpbin /headers CarLibrary.dll

这将显示程序集的操作系统头文件信息(在为 Windows 构建时,如下所示)。以下是CarLibrary.dll的(部分)窗口标题信息:

Dump of file carlibrary.dll
PE signature found
File Type: DLL

FILE HEADER VALUES
       14C machine (x86)
         3 number of sections
  BB89DC3D time date stamp
         0 file pointer to symbol table
         0 number of symbols
        E0 size of optional header
      2022 characteristics
             Executable
             Application can handle large (>2GB) addresses
             DLL
...

现在,记住这一点。NET 核心程序员永远不需要关心嵌入在. NET 核心程序集中的标题数据的格式。除非你碰巧在建造一个新的。NET Core language compiler(在这里,你会关心这些信息),你可以自由地保持幸福,不知道头数据的肮脏细节。但是,请注意,当操作系统将二进制映像加载到内存中时,这些信息是在幕后使用的。

CLR 文件头

CLR 头是一个数据块。NET 核心程序集必须支持由。NET 核心运行时。简而言之,这个头文件定义了许多标志,使运行时能够理解托管文件的布局。例如,存在标识元数据和资源在文件中的位置、生成程序集所依据的运行库版本、(可选)公钥的值等的标志。用/clrheader标志再次执行dumpbin.exe

dumpbin /clrheader CarLibrary.dll

您将看到给定的内部 CLR 头信息。NET 核心程序集,如下所示:

Dump of file CarLibrary.dll
File Type: DLL

  clr Header:

   48 cb
 2.05 runtime version
 2158 [ B7C] RVA [size] of MetaData Directory
    1 flags
        IL Only
    0 entry point token
    0 [   0] RVA [size] of Resources Directory
    0 [   0] RVA [size] of StrongNameSignature Directory
    0 [   0] RVA [size] of CodeManagerTable Directory
    0 [   0] RVA [size] of VTableFixups Directory
    0 [   0] RVA [size] of ExportAddressTableJumps Directory
    0 [   0] RVA [size] of ManagedNativeHeader Directory

  Summary

        2000 .reloc
        2000 .rsrc
        2000 .text

同样,作为. NET 核心开发人员,您不需要关心程序集的 CLR 头信息的血淋淋的细节。你只要明白。NET Core assembly 包含这些数据,这些数据由。当图像数据加载到内存中时。现在将注意力转向一些在日常编程任务中更有用的信息。

CIL 代码、类型元数据和程序集清单

在它的核心,一个汇编包含 CIL 代码,你还记得,这是一个平台和 CPU 无关的中间语言。在运行时,根据特定于平台和 CPU 的指令,使用实时(JIT)编译器动态编译内部 CIL。鉴于这种设计。NET 核心程序集确实可以在各种体系结构、设备和操作系统上执行。(尽管不理解 CIL 编程语言的细节,你也可以过上快乐而富有成效的生活,但第十九章提供了 CIL 语法和语义的介绍。)

程序集还包含完整描述所包含类型的格式以及该程序集引用的外部类型的格式的元数据。那个。NET Core runtime 使用此元数据来解析类型(及其成员)在二进制文件中的位置,在内存中布置类型,并方便远程方法调用。您将了解。NET 元数据格式在第十七章中。

一个程序集还必须包含一个关联的清单(也称为程序集元数据)。清单记录程序集内的每个模块,建立程序集的版本,并记录当前程序集引用的任何外部程序集。正如您将在本章中看到的,CLR 在定位外部程序集引用的过程中广泛使用了程序集清单。

可选程序集资源

最后,一个. NET 核心程序集可能包含任意数量的嵌入式资源,如应用图标、图像文件、声音剪辑或字符串表。事实上。NET 核心平台支持只包含本地化资源的附属程序集。如果您希望基于特定的文化(英语、德语等)来划分资源,这可能会很有用。)以构建国际软件为目的。生成附属程序集的主题超出了本文的范围。请参考。NET 核心文档,如果您感兴趣,可以获得有关附属程序集和本地化的信息。

类库与控制台应用

到目前为止,本书中的例子几乎都是独家的。NET 核心控制台应用。如果你正在阅读这本书。NET 开发人员,这些就像。NET 控制台应用,主要区别在于配置过程(稍后将介绍)以及它们运行的环境。NET 核心。控制台应用具有单一入口点(指定的Main()方法或顶级语句),可以与控制台交互,并且可以直接从操作系统启动。另一个区别是。网芯和。NET 控制台应用是控制台应用在。NET 核心是使用。NET 核心应用主机(dotnet.exe)。

另一方面,类库没有入口点,因此不能直接启动。它们用于封装逻辑、自定义类型等,并被其他类库和/或控制台应用引用。换句话说,类库是用来包含“的角色”中谈到的东西的。NET 核心程序集”一节。

.NET 标准与。NET 核心类库

。NET 核心类库运行在。网核,还有。NET 类库运行在. NET 上。非常简单。但是,这有一个问题。假设你有一个大的。NET 代码库,在您和您的团队的支持下(可能)进行了多年的开发。您和您的团队多年来构建的应用可能利用了大量的共享代码。也许是集中的日志记录、报告或特定领域的功能。

现在,您(和您的组织)想搬到。所有新开发的 NET Core。那些共享代码呢?将所有遗留代码重写为。NET 核心程序集可能会很重要,直到您的所有应用都被迁移到。NET Core,您必须支持两个版本(一个在。网和一个在。网芯)。这会让生产率嘎然而止。

幸运的是。NET Core 想通了这个场景。。NET Standard 是一种新型的类库项目,由。NET 核心并可以被。NET 以及。NET 核心应用。不过,在你燃起希望之前,还有一个难题。NET(核心)5。稍后会有更多内容。

每个。NET 标准版定义了一组所有人都必须支持的通用 API。NET 版本(。. NET。网芯,Xamarin 等。)要符合标准。例如,如果您要将类库构建为. NET Standard 2.0 项目,它可以被。NET 4.61+和。NET Core 2.0+(加上各种版本的 Xamarin、Mono、Universal Windows Platform、Unity)。

这意味着您可以将代码从。NET 类库转换成。NET 标准 2.0 类库,它们可以由。NET 核心和。NET 应用。这比支持相同代码的重复拷贝(每个框架一个)要好得多。

现在开始抓东西。每个。NET 标准版代表了它所支持的框架的最小公分母。这意味着版本越低,你在类库中能做的就越少。而。NET(核心)5 和。NET Core 3.1 都可以引用. NET Standard 2.0 库,但不能在. NET Standard 2.0 库中使用大量 C# 8.0(或任何 C# 9.0)功能。你必须使用。NET Standard 2.1,完全支持 C# 8.0 和 C# 9.0。还有。NET 4.8(原版的最新/最后版本。NET Framework)只上升到。NET 标准 2.0。

对于在新的应用中利用现有代码来说,这仍然是一个很好的机制,但不是一个灵丹妙药。

配置应用

虽然有可能保留您的所需的所有信息。NET 核心应用的源代码中,能够在运行时更改某些值在大多数重要的应用中是至关重要的。这通常是通过应用附带的配置文件来完成的。

Note

以前的。NET 框架主要依赖于名为app.config(对于 ASP.NET 应用来说,名为web.config)的 XML 文件。虽然仍然可以使用基于 XML 的配置文件,但是配置的主要方法。NET 核心应用是与 JavaScript 对象符号(JSON)文件一起使用的,如本节所示。配置将在“ASP.NET 核心”和“WPF”章节中深入讨论。

为了演示这个过程,创建一个新的。名为 FunWithConfiguration 的. NET Core 控制台应用,并将以下包引用添加到您的项目中:

dotnet new console -lang c# -n FunWithConfiguration -o .\FunWithConfiguration -f net5.0
dotnet add FunWithConfiguration package Microsoft.Extensions.Configuration.Json

这为基于 JSON 文件的。NET 核心配置子系统(及其依赖项)到您的项目中。为了利用这一点,首先在项目中添加一个名为appsettings.json的新 JSON 文件。更新项目文件,以确保在生成项目时,该文件始终被复制到输出目录中。

<ItemGroup>
  <None Update="appsettings.json">
    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
  </None>
</ItemGroup>

最后,更新文件以匹配以下内容:

{
  "CarName": "Suzy"
}

Note

如果您不熟悉 JSON,它是一种名称-值对格式,每个对象都用花括号括起来。整个文件可以作为单个对象读取,子对象也用花括号标记。在本书的后面,您将使用更复杂的 JSON 文件。

最后一步是读取配置文件并获得CarName值。将Program.cs中的using语句更新如下:

using System;
using System.IO;
using Microsoft.Extensions.Configuration;

Main()方法更新如下:

static void Main(string[] args)
{
  IConfiguration config = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json", true, true)
    .Build();
}

新的配置系统以一个ConfigurationBuilder开始。这允许您添加多个文件,设置属性(比如配置文件的位置),然后最终将配置构建到一个IConfiguration实例中。

一旦有了IConfiguration的实例,就可以像在。净 4.8。将以下内容添加到Main()方法的底部,当您运行应用时,您将看到写入控制台的值:

Console.WriteLine($"My car's name is {config["CarName"]}");
Console.ReadLine();

除了 JSON 文件,还有支持环境变量、Azure Key Vault、命令行参数等的配置包。参考。NET 核心文档以了解更多信息。

构建和使用. NET 核心类库

开始探索。NET 核心类库,您将首先创建一个包含一小组公共类型的*.dll程序集(名为CarLibrary)。首先,创建章节解决方案。如果你还没有这样做,创建一个名为CarLibrary的类库,并将其添加到你的章节解决方案中。

dotnet new sln -n Chapter16_AllProjects
dotnet new classlib -lang c# -n CarLibrary -o .\CarLibrary -f net5.0
dotnet sln .\Chapter16_AllProjects.sln add .\CarLibrary

第一个命令在当前目录中创建一个名为Chapter16_AllProjects ( -n)的空解决方案文件。下一个命令创建一个新的。NET 5.0 ( -f)类库名为CarLibrary ( -n),在子目录名为CarLibrary ( -o)中。输出(-o)位置是可选的。如果关闭,将在与项目名称同名的子目录中创建项目。最后一个命令将新项目添加到解决方案中。

Note

那个。NET Core CLI 有很好的帮助系统。要获得任何命令的帮助,请将-h添加到命令中。例如,要查看所有模板,请键入dotnet new -h。要获得更多关于创建类库的信息,请键入dotnet new classlib -h

既然已经创建了项目和解决方案,就可以在 Visual Studio(或 Visual Studio 代码)中打开它,开始构建类。打开解决方案后,删除自动生成的文件Class1.cs

你的汽车库的设计从EngineStateEnumMusicMediaEnum枚举开始。向您的项目添加两个名为MusicMediaEnum.csEngineStateEnum.cs的文件,并分别添加以下代码:

//MusicMediaEnum.cs
namespace CarLibrary
{
    // Which type of music player does this car have?
    public enum MusicMediaEnum
    {
        MusicCd,
        MusicTape,
        MusicRadio,
        MusicMp3
    }
}
//EngineStateEnum.cs
namespace CarLibrary
{
    // Represents the state of the engine.
    public enum EngineStateEnum
    {
        EngineAlive,
        EngineDead
    }
}

接下来,添加一个名为Car的抽象基类,它通过自动属性语法定义各种状态数据。这个类还有一个名为TurboBoost()的抽象方法,它使用一个自定义枚举(EngineState)来表示汽车引擎的当前状态。将一个名为Car.cs的新 C# 类文件插入到您的项目中,该文件包含以下代码:

using System;

namespace CarLibrary
{
  // The abstract base class in the hierarchy.
  public abstract class Car
  {
    public string PetName {get; set;}
    public int CurrentSpeed {get; set;}
    public int MaxSpeed {get; set;}

    protected EngineStateEnum State = EngineStateEnum.EngineAlive;
    public EngineStateEnum EngineState => State;
    public abstract void TurboBoost();

    protected Car(){}
    protected Car(string name, int maxSpeed, int currentSpeed)
    {
      PetName = name;
      MaxSpeed = maxSpeed;
      CurrentSpeed = currentSpeed;
    }
  }
}

现在假设您有两个名为MiniVanSportsCarCar类型的直接后代。每个都通过控制台消息显示适当的消息来覆盖抽象的TurboBoost()方法。在你的项目中插入两个新的 C# 类文件,分别命名为MiniVan.csSportsCar.cs。用相关代码更新每个文件中的代码。

//SportsCar.cs
using System;
namespace CarLibrary
{
  public class SportsCar : Car
  {
    public SportsCar(){ }
    public SportsCar(
      string name, int maxSpeed, int currentSpeed)
      : base (name, maxSpeed, currentSpeed){ }

    public override void TurboBoost()
    {
      Console.WriteLine("Ramming speed! Faster is better...");
    }
  }
}

//MiniVan.cs
using System;
namespace CarLibrary
{
  public class MiniVan : Car
  {
    public MiniVan(){ }
    public MiniVan(
      string name, int maxSpeed, int currentSpeed)
      : base (name, maxSpeed, currentSpeed){ }

    public override void TurboBoost()

    {
      // Minivans have poor turbo capabilities!
      State = EngineStateEnum.EngineDead;
      Console.WriteLine("Eek! Your engine block exploded!");
    }
  }
}

探索清单

在从客户端应用使用CarLibrary.dll之前,让我们看看代码库是如何在幕后组成的。假设您已经编译了这个项目,对编译后的程序集运行ildasm.exe。如果你没有ildasm.exe(本书前面已经介绍过了),它也位于本书资源库的第十六章目录中。

ildasm /all /METADATA /out=CarLibrary.il .\CarLibrary\bin\Debug\net5.0\CarLibrary.dll

拆解结果的Manifest部分从//Metadata version: 4.0.30319开始。紧随其后的是类库所需的所有外部程序集的列表,如下所示:

// Metadata version: v4.0.30319
.assembly extern System.Runtime
{
  .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A )
  .ver 5:0:0:0
}
.assembly extern System.Console
{
  .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A )
  .ver 5:0:0:0
}

每个.assembly extern块都由.publickeytoken.ver指令限定。只有当程序集配置了一个强名称时,.publickeytoken指令才会出现。.ver标记定义了引用程序集的数字版本标识符。

Note

以前版本的。NET Framework 在很大程度上依赖于强命名,这涉及到使用公钥/私钥组合。在 Windows 上,要将程序集添加到全局程序集缓存中,这是必需的,但随着的出现,这种需要已经大大减少了。NET 核心。

在外部引用之后,您会发现许多标识汇编级属性的.custom标记(有些是系统生成的,但也有版权信息、公司名称、汇编版本等。).以下是这部分清单数据的(非常)部分清单:

.assembly CarLibrary
{
...
  .custom instance void ... TargetFrameworkAttribute ...
  .custom instance void ... AssemblyCompanyAttribute ...
  .custom instance void ... AssemblyConfigurationAttribute ...
  .custom instance void ... AssemblyFileVersionAttribute ...
  .custom instance void ... AssemblyProductAttribute ...
  .custom instance void ... AssemblyTitleAttribute ...

可以使用 Visual Studio 属性页设置这些设置,也可以编辑项目文件并添加正确的元素。若要在 Visual Studio 中进行编辑,请在解决方案资源管理器中右击该项目,选择“属性”,然后导航到窗口左栏中的“包”菜单。这将弹出如图 16-4 所示的对话框。

img/340876_10_En_16_Fig4_HTML.jpg

图 16-4。

使用 Visual Studio 的属性窗口编辑程序集信息

将元数据添加到程序集的另一种方式是直接在*.csproj项目文件中。以下对项目文件中主PropertyGroup的更新与填写如图 16-4 所示的表格做同样的事情。

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <Copyright>Copyright 2020</Copyright>
    <Authors>Phil Japikse</Authors>
    <Company>Apress</Company>
    <Product>Pro C# 9.0</Product>
    <PackageId>CarLibrary</PackageId>
    <Description>This is an awesome library for cars.</Description>
    <AssemblyVersion>1.0.0.1</AssemblyVersion>
    <FileVersion>1.0.0.2</FileVersion>
    <Version>1.0.0.3</Version>
  </PropertyGroup>

Note

图 16-4 (和项目文件列表)中的其余条目在从您的程序集生成 NuGet 包时使用。这将在本章的后面介绍。

探索 CIL

回想一下,程序集不包含特定于平台的指令;相反,它包含平台无关的通用中间语言(CIL)指令。当。NET 核心运行库将程序集加载到内存中,基础 CIL 被编译(使用 JIT 编译器)成目标平台可以理解的指令。例如,SportsCar类的TurboBoost()方法由下面的 CIL 表示:

.method public hidebysig virtual
   instance void  TurboBoost() cil managed
{
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldstr "Ramming speed! Faster is better..."
  IL_0006:  call  void [System.Console]System.Console::WriteLine(string)
  IL_000b:  nop
  IL_000c:  ret
}
// end of method SportsCar::TurboBoost

正如本书中的其他 CIL 例子一样,大多数。NET Core 开发者不需要深度关注细节。第十九章提供了关于它的语法和语义的更多细节,这在你构建需要高级服务的更复杂的应用时会很有帮助,比如程序集的运行时构造。

探索类型元数据

在构建一些使用您的自定义。在. NET 库中,检查CarLibrary.dll程序集中类型的元数据。举个例子,下面是EngineStateEnumTypeDef:

 TypeDef #1 (02000002)
 -------------------------------------------------------
  TypDefName: CarLibrary.EngineStateEnum
  Flags     : [Public] [AutoLayout] [Class] [Sealed] [AnsiClass]
  Extends   : [TypeRef] System.Enum
  Field #1
  -------------------------------------------------------
   Field Name: value__
   Flags     : [Public] [SpecialName] [RTSpecialName]
   CallCnvntn: [FIELD]
   Field type:  I4

  Field #2
  -------------------------------------------------------
   Field Name: EngineAlive
   Flags     : [Public] [Static] [Literal] [HasDefault]
  DefltValue: (I4) 0
   CallCnvntn: [FIELD]
   Field type:  ValueClass CarLibrary.EngineStateEnum

  Field #3
  -------------------------------------------------------
   Field Name: EngineDead
   Flags     : [Public] [Static] [Literal] [HasDefault]
  DefltValue: (I4) 1
   CallCnvntn: [FIELD]
   Field type:  ValueClass CarLibrary.EngineStateEnum

正如下一章所解释的,程序集的元数据是。NET 核心平台,并作为众多技术(对象序列化、延迟绑定、可扩展应用等)的主干。).无论如何,现在你已经看到了CarLibrary.dll程序集的内部,你可以构建一些使用你的类型的客户端应用。

构建 C# 客户端应用

因为每个 CarLibrary 类型都是使用关键字public声明的,所以其他。NET 核心应用也能够使用它们。回想一下,您也可以使用 C# internal关键字定义类型(事实上,这是类的默认 C# 访问模式)。内部类型只能由定义它们的程序集使用。外部客户端既不能看到也不能创建用internal关键字标记的类型。

Note

这个规则的例外是,当一个程序集使用InternalsVisibleTo属性显式地允许访问另一个程序集时,这一点很快就会谈到。

要使用库的功能,请在与 CarLibrary 相同的解决方案中创建一个名为 CSharpCarClient 的新 C# 控制台应用项目。您可以使用 Visual Studio(右击该解决方案并选择“添加➤新项目”)或使用命令行(三行,每一行单独执行)来完成此操作。

dotnet new console -lang c# -n CSharpCarClient -o .\CSharpCarClient -f net5.0
dotnet add CSharpCarClient reference CarLibrary
dotnet sln .\Chapter16_AppRojects.sln add .\CSharpCarClient

前面的命令创建了控制台应用,为新项目添加了对 CarLibrary 项目的项目引用,并将其添加到您的解决方案中。

Note

add reference命令创建一个项目引用。这便于开发,因为 CSharpCarClient 将始终使用最新版本的 CarLibrary。你也可以直接引用一个组件*。直接引用是通过引用编译后的类库创建的。*

*如果您仍然在 Visual Studio 中打开了该解决方案,您会注意到新项目会显示在解决方案资源管理器中,无需您进行任何干预。

最后要做的更改是在解决方案资源管理器中右击 CSharpCarClient 并选择“设为启动项目”。如果您没有使用 Visual Studio,您可以通过执行项目目录中的dotnet run来运行新项目。

Note

您也可以在 Visual Studio 中设置项目引用,方法是在解决方案资源管理器中右击 CSharpCarClient 项目,选择“添加➤引用”,然后从项目节点中选择 CarLibrary 项目。

此时,您可以构建您的客户端应用来使用外部类型。按如下方式更新您的初始 C# 文件:

using System;
// Don't forget to import the CarLibrary namespace!
using CarLibrary;

Console.WriteLine("***** C# CarLibrary Client App *****");
// Make a sports car.
SportsCar viper = new SportsCar("Viper", 240, 40);
viper.TurboBoost();

// Make a minivan.
MiniVan mv = new MiniVan();
mv.TurboBoost();

Console.WriteLine("Done. Press any key to terminate");
Console.ReadLine();

这段代码看起来就像书中到目前为止开发的其他应用的代码。唯一有趣的一点是,C# 客户端应用现在使用在单独的自定义库中定义的类型。运行您的程序,并验证您是否看到消息的显示。

您可能想知道当您引用 CarLibrary 项目时到底发生了什么。当产生一个项目引用时,解决方案构建顺序被调整,使得依赖项目(本例中为 CarLibrary)首先构建,然后该构建的输出被复制到父项目(CSharpCarLibrary)的输出目录中。编译后的客户端库引用编译后的类库。当重新构建客户端项目时,从属库也会重新构建,新版本会再次复制到目标文件夹中。

Note

如果您使用的是 Visual Studio,可以在解决方案资源管理器中单击“显示所有文件”按钮,这样就可以看到所有的输出文件,并验证编译后的类库是否存在。如果您使用的是 Visual Studio 代码,请在资源管理器选项卡中导航到bin/debug/net5.0目录。

当进行直接引用时,编译后的库也被复制到客户端库的输出目录中,但是在进行引用时。如果没有适当的项目引用,项目可能会彼此独立地构建,并且文件可能会变得不同步。简而言之,如果您正在开发依赖库(实际软件项目通常都是这样),最好引用项目而不是项目输出。

构建 Visual Basic 客户端应用

回想一下。NET 核心平台允许开发人员跨编程语言共享编译后的代码。来说明。NET 核心平台,让我们创建另一个控制台应用项目(VisualBasicCarClient),这次使用 Visual Basic(注意每个命令都是一行命令)。

dotnet new console -lang vb -n VisualBasicCarClient -o .\VisualBasicCarClient -f net5.0
dotnet add VisualBasicCarClient reference CarLibrary
dotnet sln .\Chapter16_AllProjects.sln add VisualBasicCarClient

像 C# 一样,Visual Basic 允许您列出当前文件中使用的每个命名空间。然而,Visual Basic 提供了Imports关键字而不是 C# using关键字,所以在Program.vb代码文件中添加下面的Imports语句:

Imports CarLibrary
Module Program
  Sub Main()
  End Sub
End Module

请注意,Main()方法是在 Visual Basic 模块类型中定义的。简而言之,模块是一种 Visual Basic 符号,用于定义只能包含静态方法的类(很像 C# 静态类)。在任何情况下,要使用 Visual Basic 的语法来练习MiniVanSportsCar类型,请按如下方式更新您的Main()方法:

Sub Main()
  Console.WriteLine("***** VB CarLibrary Client App *****")
  ' Local variables are declared using the Dim keyword.
  Dim myMiniVan As New MiniVan()
  myMiniVan.TurboBoost()

  Dim mySportsCar As New SportsCar()
  mySportsCar.TurboBoost()
  Console.ReadLine()
End Sub

当您编译并运行您的应用时(确保在 Visual Studio 中将 VisualBasicCarClient 设置为启动项目),您将再次发现显示了一系列消息框。此外,这个新的客户端应用在bin\Debug\net5.0文件夹下有自己的CarLibrary.dll本地副本。

跨语言继承在起作用

迷人的一面。NET 核心开发的概念是跨语言继承。为了说明,让我们创建一个从SportsCar(使用 C# 编写)派生的新 Visual Basic 类。首先,在当前的 Visual Basic 应用中添加一个名为PerformanceCar.vb的新类文件。通过使用Inherits关键字从SportsCar类型派生来更新初始类定义。然后,使用Overrides关键字覆盖抽象的TurboBoost()方法,就像这样:

Imports CarLibrary
' This VB class is deriving from the C# SportsCar.
Public Class PerformanceCar
  Inherits SportsCar
  Public Overrides Sub TurboBoost()
    Console.WriteLine("Zero to 60 in a cool 4.8 seconds...")
  End Sub
End Class

为了测试这个新的类类型,更新模块的Main()方法,如下所示:

Sub Main()
...
  Dim dreamCar As New PerformanceCar()

  ' Use Inherited property.
  dreamCar.PetName = "Hank"
  dreamCar.TurboBoost()
  Console.ReadLine()
End Sub

注意,dreamCar对象可以调用继承链中的任何公共成员(比如PetName属性),尽管基类是用完全不同的语言和完全不同的程序集中定义的!以独立于语言的方式跨程序集边界扩展类的能力是。净核心开发周期。这使得使用由不愿意用 C# 构建共享代码的人编写的编译代码变得容易。

向其他程序集公开内部类型

如前所述,内部类仅对定义它们的程序集中的其他对象可见。例外情况是,当另一个项目被明确授予可见性时。

首先向 CarLibrary 项目添加一个名为MyInternalClass的新类,并将代码更新如下:

namespace CarLibrary
{
  internal class MyInternalClass
  {
  }
}

Note

为什么要公开内部类型呢?这通常是为单元和集成测试而做的。开发人员希望能够测试他们的代码,但不一定要将代码暴露在程序集之外。

使用程序集属性

第十七章将深入讨论属性,但现在打开 CarLibrary 项目中的Car.cs类,并添加以下属性和using语句:

using System.Runtime.CompilerServices;
[assembly:InternalsVisibleTo("CSharpCarClient")]
namespace CarLibrary
{
}

InternalsVisibleTo属性采用项目的名称,该项目可以查看设置了属性的类。请注意,其他项目不能“请求”此权限;它必须由持有内部类型的项目授予。

Note

以前版本的。NET 利用了AssemblyInfo.cs类,它仍然存在于。NET Core,但它是自动生成的,不是供开发人员使用的。

现在,您可以通过向Main()方法添加以下代码来更新 CSharpCarClient 项目:

var internalClassInstance = new MyInternalClass();

这工作完美。现在尝试在VisualBasicCarClient Main方法中做同样的事情。

'Will not compile
'Dim internalClassInstance = New MyInternalClass()

因为没有授予 VisualBasicCarClient 库查看内部的权限,所以前面一行代码将不会编译。

使用项目文件

完成同样事情的另一种方法(也可能被认为更符合。NET 核心方式)是使用。NET 核心项目文件。

注释掉您刚刚添加的属性,并打开 CarLibrary 的项目文件。在项目文件中添加以下ItemGroup:

<ItemGroup>
  <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
    <_Parameter1>CSharpCarClient</_Parameter1>
  </AssemblyAttribute>
</ItemGroup>

这实现了与在类上使用属性相同的事情,在我看来,这是一个更好的解决方案,因为其他开发人员将在项目文件中看到它,而不必知道在整个项目中的何处查找。

努杰和。净核心

NuGet 是的包管理器。NET 和。NET 核心。它是一种以某种格式共享软件的机制。NET 核心应用理解并且是默认的加载机制。NET 核心及其相关框架组件(ASP.NET 核心、EF 核心等)。).许多组织将用于横切关注点(如日志和错误报告)的标准程序集打包到 NuGet 包中,以供他们的业务线应用使用。

用 NuGet 打包程序集

为了看到这一点,我们将把 CarLibrary 转换成一个 NuGet 包,然后从两个客户机应用中引用它。

可以从项目的属性页访问 NuGet 包属性。右键单击 CarLibrary 项目,然后选择“属性”。导航到 Package 页面,查看我们之前输入的值,以自定义程序集。可以为 NuGet 包设置其他属性(例如,许可协议接受和项目信息,如 URL 和存储库位置)。

Note

Visual Studio 包页面 UI 中的所有值都可以手动输入到项目文件中,但是您需要知道关键字。至少使用一次 Visual Studio 来填写所有内容会有所帮助,然后您可以手动编辑项目文件。您还可以在。NET 核心文档。

对于这个例子,我们不需要设置任何额外的属性,除了选中“在构建时生成 NuGet 包”复选框或者用以下内容更新项目文件:

<PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <Copyright>Copyright 2020</Copyright>
    <Authors>Phil Japikse</Authors>
    <Company>Apress</Company>
    <Product>Pro C# 9.0</Product>
    <PackageId>CarLibrary</PackageId>
    <Description>This is an awesome library for cars.</Description>
    <AssemblyVersion>1.0.0.1</AssemblyVersion>
    <FileVersion>1.0.0.2</FileVersion>
    <Version>1.0.0.3</Version>
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
  </PropertyGroup>

这将导致每次构建软件时都要重新构建软件包。默认情况下,将在bin/Debugbin/Release文件夹中创建包,这取决于所选择的配置。

也可以从命令行创建包,CLI 提供了比 Visual Studio 更多的选项。例如,要构建包并将其放在名为Publish的目录中,输入以下命令(在CarLibrary项目目录中)。第一个命令构建程序集,第二个命令打包 NuGet 包。

dotnet build -c Release
dotnet pack -o .\Publish -c Debug

Note

Debug 是默认配置,所以没有必要指定-c Debug,但是我明确地包含了这个选项,以便完全清楚它的意图。

CarLibrary.1.0.0.3.nupkg文件现在位于Publish目录中。要查看它的内容,用任何 zip 实用程序(比如 7-Zip)打开文件,就可以看到整个内容,包括程序集,还包括附加的元数据。

引用 NuGet 包

您可能想知道在前面的例子中添加的包是从哪里来的。NuGet 包的位置由一个名为NuGet.Config的基于 XML 的文件控制。在 Windows 上,该文件位于%appdata%\NuGet目录中。这是主文件。打开它,你会看到几个包源。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
    <add key="Microsoft Visual Studio Offline Packages" value="C:\Program Files (x86)\Microsoft SDKs\NuGetPackages\" />
  </packageSources>
</configuration>

前面的文件清单显示了两个来源。第一个指向 NuGet。org ,这是世界上最大的 NuGet 包存储库。第二个在本地驱动器上,由 Visual Studio 用作包的缓存。

需要注意的重要一点是,NuGet.Config文件默认为附加。要添加额外的源而不改变整个系统的列表,您可以添加额外的NuGet.Config文件。每个文件对它所在的目录以及任何子目录都有效。在解决方案目录中添加一个名为NuGet.Config的新文件,并将内容更新为:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <packageSources>
        <add key="local-packages" value=".\CarLibrary\Publish" />
    </packageSources>
</configuration>

您还可以通过将<clear/>添加到<packageSources>节点来重置包列表,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="local-packages" value=".\CarLibrary\Publish" />
    <add key="NuGet" value="https://api.nuget.org/v3/index.json" />
  </packageSources>
</configuration>

Note

如果您使用的是 Visual Studio,您必须重新启动 IDE,更新后的nuget.config设置才会生效。

从 CSharpCarClient 和 VisualBasicCarClient 项目中移除项目引用,然后像这样添加包引用(从解决方案目录中):

dotnet add CSharpCarClient package CarLibrary
dotnet add VisualBasicCarClient package CarLibrary

一旦设置了引用,构建解决方案并查看目标目录(bin\Debug\new5.0)中的输出,您将在目录中看到CarLibrary.dll,而不是CarLibrary.nupkg文件。这是因为。NET Core 将内容解包并添加到作为直接引用包含的程序集中。

现在,将其中一个客户端设置为启动项目并运行应用,它的工作方式与之前完全一样。

接下来,将 CarLibrary 的版本号更新为 1.0.0.4,并重新打包。在Publish目录中,现在有两个 CarLibrary NuGet 包。如果您重新运行add package命令,项目将被更新以使用新版本。如果旧版本是首选,那么add package命令允许为特定的包添加版本号。

发布控制台应用(已更新。净 5)

现在您已经有了 C# CarClient 应用(及其相关的 CarLibrary 程序集),如何将它提供给用户呢?打包应用及其相关依赖项被称为发布。出版。NET Framework 应用要求在目标计算机上安装 Framework,而。NET 核心应用也可以以类似的方式发布,称为依赖于框架的部署。然而,。NET 核心应用也可以发布为一个自包含应用,这并不需要。NET 核心到底要不要装!

将应用发布为独立的应用时,必须指定目标运行时标识符。运行时标识符用于为特定的操作系统打包应用。有关可用运行时标识符的完整列表,请参见。 https://docs.microsoft.com/en-us/dotnet/core/rid-catalog 的网芯 RID 目录。

Note

发布 ASP.NET 核心应用是一个更复杂的过程,将在本书后面介绍。

发布依赖框架的应用

依赖于框架的部署是dotnet publish命令的默认模式。要打包您的应用和所需的文件,您只需使用 CLI 执行以下命令:

dotnet publish

Note

publish命令使用项目的默认配置,通常是 debug。

这将把您的应用及其支持文件(总共 16 个文件)放到bin\Debug\net5.0\publish目录中。检查添加到该目录的文件,您会看到两个包含所有应用代码的*.dll文件(CarLibrary.dllCSharpCarClient.dll)。提醒一下,CSharpCarClient.exe文件是dotnet.exe的打包版本,被配置为启动CSharpCarClient.dll。目录中的附加文件是。不属于。NET 核心运行时。

要创建一个发布版本(将放在bin\release\net5.0\publish目录中),输入以下命令:

dotnet publish -c release

发布独立的应用

与依赖于框架的部署一样,自包含部署包括所有应用代码和引用的程序集,但也包括。应用所需的. NET 核心运行时文件。要将您的应用发布为自包含部署,请使用以下 CLI 命令(选择名为selfcontained的文件夹作为输出目标):

dotnet publish  -r win-x64 -c release -o selfcontained --self-contained true

Note

当创建自包含部署时,运行时标识符是必需的,因此发布过程知道应用代码要包含哪些运行时文件。

这也将您的应用及其支持文件(总共 235 个文件)放入到selfcontained目录中。如果将这些文件复制到另一台 64 位 Windows 计算机上,即使。没有安装. NET 5 运行时。

将独立的应用作为单个文件发布

在大多数情况下,部署 235 个文件(对于打印几行文本的应用)可能不是将应用展示给用户的最有效方式。幸运的是。NET 5 极大地提高了将应用和跨平台运行时文件发布到单个文件中的能力。唯一不包括的文件是必须存在于单个 EXE 之外的本地库。

以下命令为 64 位 Windows 操作系统创建一个单文件、自包含的部署包,并将生成的文件放在名为singlefile的文件夹中。

dotnet publish -r win-x64 -c release -o singlefile --self-contained true -p:PublishSingleFile=true

当您检查创建的文件时,您会发现一个可执行文件(CSharpCarClient.exe)、一个调试文件(CSharpCarClient.pdb)和四个特定于操作的 dll。之前的发布过程产生了 235 个文件,而CSharpCarClient.exe的单个文件版本达到了 54 MB!创建单一文件发布会将 235 个文件打包成一个文件。文件数量减少的补偿是以文件大小为代价的。

最后要注意的是,本地库可以与单个文件打包在一起。将CSharpCarClient.csproj文件更新为以下内容:

<Project Sdk="Microsoft.NET.Sdk">
  <ItemGroup>
    <PackageReference Include="CarLibrary" Version="1.0.0.3" />
  </ItemGroup>
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
    <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
  </PropertyGroup>
</Project>

如果再次运行相同的命令,输出确实是一个文件。然而,这只是一种传输机制。执行应用时,本地文件将被提取到目标机器上的临时位置。

怎么会。NET Core 定位程序集

到目前为止,在本书中,您构建的所有程序集都是直接相关的(除了您刚刚完成的 NuGet 示例)。您添加了项目引用或项目间的直接引用。在这些情况下(以及 NuGet 示例),依赖程序集被直接复制到客户端应用的目标目录中。定位依赖程序集不成问题,因为它们就在需要它们的应用旁边的磁盘上。

但是。NET 核心框架?这些是如何定位的?以前版本的。NET 将框架文件安装到全局程序集缓存(GAC)中。NET 应用知道如何定位框架文件。

但是,GAC 阻止了中的并行功能。NET 核心,所以没有运行时和框架文件的单一存储库。相反,组成框架的文件一起安装在C:\Program Files\dotnet(在 Windows 上),由版本分开。基于应用的版本(如在.csproj文件中指定的),从指定版本的目录中为应用加载必要的运行时和框架文件。

具体来说,当一个版本的运行时启动时,运行时主机提供一组探测路径,它将使用这些路径来查找应用的依赖项。有五种探测属性(每种都是可选的),如表 16-1 所列。

表 16-1。

应用探测属性

|

[计]选项

|

生命的意义

| | --- | --- | | TRUSTED_PLATFORM_ASSEMBLIES | 平台和应用程序集文件路径列表 | | PLATFORM_RESOURCE_ROOTS | 搜索附属资源程序集的目录路径列表 | | NATIVE_DLL_SEARCH_DIRECTORIES | 用于搜索非托管(本机)库的目录路径列表 | | APP_PATHS | 用于搜索托管程序集的目录路径列表 | | APP_NI_PATHS | 用于搜索托管程序集的本机映像的目录路径列表 |

若要查看这些的默认路径,请创建一个新的。NET 核心控制台应用命名为 FunWithProbingPaths。将顶级语句更新为以下内容:

using System;
using System.Linq;

Console.WriteLine("*** Fun with Probing Paths ***");
Console.WriteLine($"TRUSTED_PLATFORM_ASSEMBLIES: ");
//Use ':' on non-Windows platforms
var list = AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")
              .ToString().Split(';');
foreach (var dir in list)
{
  Console.WriteLine(dir);
}
Console.WriteLine();
Console.WriteLine($"PLATFORM_RESOURCE_ROOTS: {AppContext.GetData ("PLATFORM_RESOURCE_ROOTS")}");
Console.WriteLine();
Console.WriteLine($"NATIVE_DLL_SEARCH_DIRECTORIES: {AppContext.GetData ("NATIVE_DLL_SEARCH_DIRECTORIES")}");
Console.WriteLine();
Console.WriteLine($"APP_PATHS: {AppContext.GetData("APP_PATHS")}");
Console.WriteLine();
Console.WriteLine($"APP_NI_PATHS: {AppContext.GetData("APP_NI_PATHS")}");
Console.WriteLine();
Console.ReadLine();

当你运行这个应用时,你会看到大部分的值来自于TRUSTED_PLATFORM_ASSEMBLIES变量。除了在目标目录中为这个项目创建的程序集之外,您还会看到当前运行时目录中的基类库列表,C:\Program Files\dotnet\shared\Microsoft.NETCore.App\5.0.0(您的版本号可能不同)。

应用直接引用的每个文件以及应用所需的任何运行时文件都会添加到列表中。运行时库的列表由一个或多个用。NET 核心运行时。在 SDK(用于构建软件)和运行时(用于运行软件)的安装目录中有几个。在我们的简单例子中,唯一使用的文件是Microsoft.NETCore.App.deps.json

随着应用复杂性的增加,TRUSTED_PLATFORM_ASSEMBLIES中的文件列表也会增加。例如,如果您添加一个对Microsoft.EntityFrameworkCore包的引用,那么所需程序集的列表就会增加。为了演示这一点,在包管理器控制台中输入以下命令(与*.csproj文件在同一个目录中):

dotnet add package Microsoft.EntityFrameworkCore

添加完包后,重新运行应用,注意又列出了多少文件。即使您只添加了一个新的引用,Microsoft.EntityFrameworkCore包也有它的依赖项,这些依赖项被添加到可信文件列表中。

摘要

本章考察了。NET 核心类库(又名。NET *.dll s)。如你所见,类库是。NET Core 二进制文件,其中包含的逻辑可以在各种项目中重用。

您了解了将类型划分为。NET 核心命名空间以及。NET 标准,从应用配置开始,深入研究类库的组成。接下来你学习了如何出版。NET 核心控制台应用。最后,您学习了如何使用 NuGet 打包您的应用。**

十七、类型反射、延迟绑定和基于属性的编程

如第十六章所示,组件是部署的基本单位。净核心宇宙。使用 Visual Studio 的集成对象浏览器(以及许多其他 ide),您可以检查项目引用的程序集集合中的类型。此外,像ildasm.exe这样的外部工具允许您查看给定的底层 CIL 代码、类型元数据和程序集清单。网芯二进制。除了设计时对。NET 核心程序集,您也能够通过编程方式使用System.Reflection名称空间获得相同的信息。为此,本章的首要任务是界定反思的作用和必要性。NET 核心元数据。

*本章的剩余部分研究了几个密切相关的主题,它们依赖于反射服务。例如,您将了解. NET 核心客户端如何使用动态加载和延迟绑定来激活它在编译时不了解的类型。您还将了解如何将自定义元数据插入到您的。NET 核心程序集使用系统提供的和自定义的属性。为了将所有这些(看似深奥的)主题放在适当的位置,本章最后演示了如何构建几个可以插入可扩展控制台应用的“管理单元对象”。

类型元数据的必要性

使用元数据完整描述类型(类、接口、结构、枚举和委托)的能力是。NET 核心平台。很多。NET 核心技术,如对象序列化,需要能够在运行时发现类型的格式。此外,跨语言的互操作性、众多的编译器服务和 IDE 的智能感知能力都依赖于对类型的具体描述。

回想一下,ildasm.exe实用程序允许您查看程序集的类型元数据。在生成的CarLibrary.il文件中(来自第章第十六部分,导航到METAINFO部分查看所有卡莉图库的元数据。这里有一小段:

// ==== M E T A I N F O ===

// ===========================================================
// ScopeName : CarLibrary.dll
// MVID      : {598BC2B8-19E9-46EF-B8DA-672A9E99B603}
// ===========================================================
// Global functions
// -------------------------------------------------------
//
// Global fields
// -------------------------------------------------------
//
// Global MemberRefs
// -------------------------------------------------------
//
// TypeDef #1
// -------------------------------------------------------
//   TypDefName: CarLibrary.Car
//   Flags     : [Public] [AutoLayout] [Class] [Abstract] [AnsiClass] [BeforeFieldInit]
//   Extends   : [TypeRef] System.Object
//   Field #1
//   -------------------------------------------------------
//     Field Name: value__
//     Flags     : [Private]
//     CallCnvntn: [FIELD]
//     Field type:  String
//

如您所见。NET 核心类型元数据很冗长(实际的二进制格式要简洁得多)。事实上,如果我要列出代表CarLibrary.dll程序集的整个元数据描述,它将跨越几页。考虑到这种行为是对纸张的严重浪费,让我们来看一下CarLibrary.dll程序集的一些关键元数据描述。

Note

不要太在意每一段。NET 核心元数据。更重要的是。NET Core 元数据非常具有描述性,列出了在给定代码库中找到的每个内部定义(和外部引用)的类型。

查看 EngineStateEnum 枚举的(部分)元数据

当前程序集中定义的每个类型都使用一个TypeDef #n标记进行记录(其中TypeDef类型定义的缩写)。如果所描述的类型使用在单独的。NET 核心程序集,引用的类型使用一个TypeRef #n标记来记录(其中TypeRef类型引用的缩写)。TypeRef标记是一个指针(如果你愿意的话),指向外部程序集中被引用类型的完整元数据定义。简单地说,。NET Core 元数据是一组清楚地标记所有类型定义(TypeDef s)和引用类型(TypeRef s)的表,所有这些都可以使用ildasm.exe来检查。

CarLibrary.dll而言,一个TypeDefCarLibrary.EngineStateEnum枚举的元数据描述(你的数字可能不同;TypeDef编号基于 C# 编译器处理文件的顺序)。

// TypeDef #2
// -------------------------------------------------------
//   TypDefName: CarLibrary.EngineStateEnum
//   Flags     : [Public] [AutoLayout] [Class] [Sealed] [AnsiClass]
//   Extends   : [TypeRef] System.Enum
//   Field #1
//   -------------------------------------------------------
//     Field Name: value__
//     Flags     : [Public] [SpecialName] [RTSpecialName]
//     CallCnvntn: [FIELD]
//     Field type:  I4
//
//   Field #2
//   -------------------------------------------------------
//     Field Name: EngineAlive
//     Flags     : [Public] [Static] [Literal] [HasDefault]
//     DefltValue: (I4) 0
//     CallCnvntn: [FIELD]
//     Field type:  ValueClass CarLibrary.EngineStateEnum
//
...

这里,TypDefName标记用于建立给定类型的名称,在本例中是自定义的CarLibrary.EngineStateEnum枚举。Extends元数据标记用于记录给定。NET 核心类型(在本例中是引用类型,System.Enum)。枚举的每个字段都用Field #n标记。为了简洁起见,我只简单地列出了部分元数据。

Note

虽然看起来像是一个错别字,TypDefName并没有人们所期望的“e”。

查看汽车类型的(部分)元数据

下面是Car类的部分转储,说明了以下内容:

  • 如何定义字段?网络核心元数据

  • 方法是如何通过?网络核心元数据

  • 中如何表示自动属性.NETCore 元数据

// TypeDef #1
// -------------------------------------------------------
//   TypDefName: CarLibrary.Car
//   Flags     : [Public] [AutoLayout] [Class] [Abstract] [AnsiClass] [BeforeFieldInit]
//   Extends   : [TypeRef] System.Object
//   Field #1
//   -------------------------------------------------------
//     Field Name: <PetName>k__BackingField
//     Flags     : [Private]
//     CallCnvntn: [FIELD]
//     Field type:  String
...

  Method #1
-------------------------------------------------------
    MethodName: get_PetName
    Flags      : [Public] [HideBySig] [ReuseSlot] [SpecialName]
    RVA        : 0x000020d0
    ImplFlags  : [IL] [Managed]
    CallCnvntn: [DEFAULT]
    hasThis
    ReturnType: String
    No arguments.

...

//   Method #2
//   -------------------------------------------------------
//     MethodName: set_PetName
//     Flags     : [Public] [HideBySig] [ReuseSlot] [SpecialName]
//     RVA       : 0x00002058
//     ImplFlags : [IL] [Managed]
//     CallCnvntn: [DEFAULT]
//     hasThis
//     ReturnType: Void
//     1 Arguments
//       Argument #1:  String
//     1 Parameters
//       (1) ParamToken : Name : value flags: [none]
...

//   Property #1
//   -------------------------------------------------------
//     Prop.Name : PetName
//     Flags     : [none]
//     CallCnvntn: [PROPERTY]
//     hasThis
//     ReturnType: String
//     No arguments.
//     DefltValue:
//     Setter    : set_PetName
//     Getter    : get_PetName
//     0 Others
...

首先,请注意,Car类元数据标记了该类型的基类(System.Object),并包括描述该类型如何构造的各种标志(例如,[Public][Abstract]等等)。方法(如Car的构造函数)由它们的参数、返回值和名称来描述。

请注意自动属性是如何产生编译器生成的私有支持字段(名为<PetName>k__BackingField)和两个编译器生成的方法(在读写属性的情况下)的,在本例中,这两个方法名为get_PetName()set_PetName()。最后,实际属性被映射到内部的get / set方法。NET Core 元数据Getter / Setter令牌。

检查 TypeRef

回想一下,程序集的元数据将不仅描述内部类型的集合(CarEngineStateEnum等)。)以及内部类型引用的任何外部类型。例如,假设CarLibrary.dll已经定义了两个枚举,您可以为System.Enum类型找到一个TypeRef块,如下所示:

// TypeRef #19
// -------------------------------------------------------
// Token:             0x01000013
// ResolutionScope:   0x23000001
// TypeRefName:       System.Enum

记录定义程序集

CarLibrary.il文件还允许您查看。使用Assembly标记描述程序集本身的. NET 核心元数据。以下是CarLibrary.dll货单的部分转储:

// Assembly
// -------------------------------------------------------
//   Token: 0x20000001
//   Name : CarLibrary
//   Public Key    :
//   Hash Algorithm : 0x00008004
//   Version: 1.0.0.1
//   Major Version: 0x00000001
//   Minor Version: 0x00000000
//   Build Number: 0x00000000
//   Revision Number: 0x00000001
//   Locale: <null>
//   Flags : [none] (00000000)

记录引用的程序集

除了Assembly令牌和一组TypeDefTypeRef块之外。NET Core 元数据还利用AssemblyRef #n标记来记录每个外部程序集。鉴于每个人。NET Core 程序集引用了System.Runtime基类库程序集,您为System.Runtime程序集找到了一个AssemblyRef,如下面的代码所示:

// AssemblyRef #1 (23000001)
// -------------------------------------------------------
//   Token: 0x23000001
//   Public Key or Token: b0 3f 5f 7f 11 d5 0a 3a
//   Name: System.Runtime
//   Version: 5.0.0.0
//   Major Version: 0x00000005
//   Minor Version: 0x00000000
//   Build Number: 0x00000000
//   Revision Number: 0x00000000
//   Locale: <null>
//   HashValue Blob:
//   Flags: [none] (00000000) 

记录字符串文字

最后一个有趣的地方是。NET Core 元数据是这样一个事实,即代码库中的每个字符串都记录在User Strings标记下。

// User Strings
// -------------------------------------------------------
// 70000001 : (23) L"CarLibrary Version 2.0!"
// 70000031 : (13) L"Quiet time..."
// 7000004d : (11) L"Jamming {0}"
// 70000065 : (32) L"Eek! Your engine block exploded!"
// 700000a7 : (34) L"Ramming speed! Faster is better..."

Note

如最后一个元数据列表所示,请始终注意,所有字符串都清楚地记录在程序集元数据中。如果您使用字符串来表示密码、信用卡号或其他敏感信息,这可能会带来巨大的安全后果。

您脑海中的下一个问题可能是(在最好的情况下)“我如何在我的应用中利用这些信息?”或者(在最坏的情况下)“我为什么要关心元数据?”为了回应这两种观点,请允许我介绍。NET 核心反射服务。请注意,在本章结束之前,接下来几页中介绍的主题的有用性可能有点让人摸不着头脑。所以,坚持住。

Note

您还会发现一些由METAINFO部分显示的CustomAttribute标记,它记录了代码库中应用的属性。你将会了解到。NET 核心属性。

理解反射

在。NET 核心宇宙,反射是运行时类型发现的过程。使用反射服务,您可以使用友好的对象模型以编程方式获得由ildasm.exe生成的相同元数据信息。例如,通过反射,您可以获得包含在给定的*.dll*.exe程序集内的所有类型的列表,包括由给定类型定义的方法、字段、属性和事件。您还可以动态地发现给定类型支持的接口集、方法的参数以及其他相关细节(基类、命名空间信息、清单数据等)。).

像任何名称空间一样,System.Reflection(在System.Runtime.dll中定义)包含几个相关的类型。表 17-1 列出了一些你应该熟悉的核心项目。

表 17-1。

系统成员的抽样。反射名称空间

|

类型

|

生命的意义

| | --- | --- | | Assembly | 此抽象类包含允许您加载、调查和操作程序集的成员。 | | AssemblyName | 这个类允许您发现程序集标识背后的许多细节(版本信息、区域性信息等)。). | | EventInfo | 这个抽象类保存给定事件的信息。 | | FieldInfo | 这个抽象类保存给定字段的信息。 | | MemberInfo | 这是一个抽象基类,定义了EventInfoFieldInfoMethodInfoPropertyInfo类型的通用行为。 | | MethodInfo | 这个抽象类包含给定方法的信息。 | | Module | 这个抽象类允许您访问多文件程序集中的给定模块。 | | ParameterInfo | 这个类保存给定参数的信息。 | | PropertyInfo | 这个抽象类保存给定属性的信息。 |

了解如何利用System.Reflection名称空间以编程方式读取。NET 核心元数据,您需要首先接受System.Type类。

系统。类型类别

System.Type类定义了可用于检查类型元数据的成员,其中许多成员从System.Reflection名称空间返回类型。例如,Type.GetMethods()返回一组MethodInfo对象,Type.GetFields()返回一组FieldInfo对象,依此类推。System.Type曝光的全套成员相当膨胀;然而,表 17-2 提供了由System.Type支持的成员的部分快照(参见。NET 核心文档以了解全部详细信息)。

表 17-2。

选择系统的成员。类型

|

成员

|

生命的意义

| | --- | --- | | IsAbstract``IsArray``IsClass``IsCOMObject``IsEnum``IsGenericTypeDefinition``IsGenericParameter``IsInterface``IsPrimitive``IsNestedPrivate``IsNestedPublic``IsSealed``IsValueType | 这些属性(以及其他属性)允许您发现您所引用的Type的一些基本特征(例如,如果它是一个抽象实体、一个数组、一个嵌套类等等。). | | GetConstructors()``GetEvents()``GetFields()``GetInterfaces()``GetMembers()``GetMethods()``GetNestedTypes()``GetProperties() | 这些方法(以及其他方法)允许您获得一个表示项目(接口、方法、属性等)的数组。)你感兴趣。每个方法返回一个相关的数组(例如,GetFields()返回一个FieldInfo数组,GetMethods()返回一个MethodInfo数组,等等)。).请注意,这些方法都有单数形式(例如,GetMethod()GetProperty()等)。)允许您按名称检索特定的项,而不是所有相关项的数组。 | | FindMembers() | 该方法根据搜索条件返回一个MemberInfo数组。 | | GetType() | 这个静态方法返回一个给定字符串名称的Type实例。 | | InvokeMember() | 该方法允许给定项目的“延迟绑定”。在本章的后面你会学到延迟绑定。 |

使用系统获取类型引用。Object.GetType()

您可以通过多种方式获得Type类的实例。然而,有一件事你不能做,那就是使用new关键字直接创建一个Type对象,因为Type是一个抽象类。关于您的第一个选择,回想一下System.Object定义了一个名为GetType()的方法,它返回一个代表当前对象元数据的Type类的实例。

// Obtain type information using a SportsCar instance.
SportsCar sc = new SportsCar();
Type t = sc.GetType();

显然,只有当您知道要反射的类型(在本例中为SportsCar)的编译时知识,并且当前在内存中有该类型的实例时,这种方法才有效。考虑到这个限制,像ildasm.exe这样的工具不通过直接调用每种类型的System.Object.GetType()来获取类型信息应该是有意义的,因为ildasm.exe不是针对你的定制程序集编译的。

使用 typeof()获取类型引用

获取类型信息的下一种方法是使用 C# typeof操作符,如下所示:

// Get the type using typeof.
Type t = typeof(SportsCar);

System.Object.GetType()不同,typeof操作符非常有用,因为您不需要首先创建一个对象实例来提取类型信息。然而,你的代码库必须仍然有你感兴趣的类型的编译时知识,因为typeof期望类型的强类型名称。

使用系统获取类型引用。Type.GetType()

为了以更灵活的方式获得类型信息,您可以调用System.Type类的静态GetType()成员,并指定您感兴趣的类型的完全限定字符串名称。使用这种方法,你不需要而不是知道你从中提取元数据的类型,因为Type.GetType()取了一个无所不在的System.String的实例。

Note

当我说您在调用Type.GetType()时不需要编译时知识时,我指的是这个方法可以接受任何字符串值(而不是强类型变量)。当然,您仍然需要知道“stringified”格式的类型名!

Type.GetType()方法已被重载,允许您指定两个布尔参数,其中一个控制如果找不到类型是否应该抛出异常,另一个确定字符串的大小写。为了说明这一点,请思考以下几点:

// Obtain type information using the static Type.GetType() method
// (don't throw an exception if SportsCar cannot be found and ignore case).
Type t = Type.GetType("CarLibrary.SportsCar", false, true);

在前面的例子中,请注意您传递到GetType()中的字符串没有提到包含该类型的程序集。在这种情况下,假设该类型是在当前执行的程序集中定义的。但是,当您想要获取外部程序集中某个类型的元数据时,字符串参数的格式是使用该类型的完全限定名,后跟一个逗号,再后跟包含该类型的程序集的友好名称(没有任何版本信息的程序集名称),如下所示:

// Obtain type information for a type within an external assembly.
Type t = Type.GetType("CarLibrary.SportsCar, CarLibrary");

同样,要知道传入Type.GetType()的字符串可能会指定一个加号(+)来表示一个嵌套类型。假设您想要获取嵌套在名为JamesBondCar的类中的枚举(SpyOptions)的类型信息。为此,您应该编写以下代码:

// Obtain type information for a nested enumeration
// within the current assembly.
Type t = Type.GetType("CarLibrary.JamesBondCar+SpyOptions");

构建自定义元数据查看器

为了说明反射的基本过程(以及System.Type的用处),让我们创建一个名为 MyTypeViewer 的控制台应用项目。这个程序将显示System.Runtime.dll中任何类型的方法、属性、字段和支持的接口的详细信息(除了一些其他感兴趣的点)。NET 核心应用自动访问此核心框架类库)或 MyTypeViewer 本身内的类型。一旦创建了应用,一定要导入SystemSystem.ReflectionSystem.Linq名称空间。

// Need to import this namespace to do any reflection!
using System;
using System.Linq;
using System.Reflection;

反思方法

几个静态方法将被添加到Program类中,每个方法接受一个System.Type参数并返回void。首先是ListMethods(),它(正如您可能猜到的那样)打印由传入类型定义的每个方法的名称。注意Type.GetMethods()如何返回一个System.Reflection.MethodInfo对象的数组,可以用一个标准的foreach循环来枚举,如下所示:

// Display method names of type.
static void ListMethods(Type t)
{
  Console.WriteLine("***** Methods *****");
  MethodInfo[] mi = t.GetMethods();
  foreach(MethodInfo m in mi)
  {
    Console.WriteLine("->{0}", m.Name);
  }
  Console.WriteLine();
}

这里,您只是使用MethodInfo.Name属性打印方法的名称。正如您可能猜到的,MethodInfo有许多额外的成员,允许您确定方法是静态的、虚拟的、泛型的还是抽象的。同样,MethodInfo类型允许您获得方法的返回值和参数集。您将很快完善ListMethods()的实现。

如果愿意,还可以构建一个合适的 LINQ 查询来枚举每个方法的名称。回想一下第十三章,对象的 LINQ 允许你构建强类型查询,这些查询可以应用于内存中的对象集合。一个好的经验法则是,无论何时发现循环或决策编程逻辑块,都可以利用相关的 LINQ 查询。例如,您可以用 LINQ 重写前面的方法,如下所示:

using System.Linq;
static void ListMethods(Type t)
{
  Console.WriteLine("***** Methods *****");
  var methodNames = from n in t.GetMethods() select n.Name;
  foreach (var name in methodNames)
  {
    Console.WriteLine("->{0}", name);
  }
  Console.WriteLine();
}

反思字段和属性

ListFields()的实现也差不多。唯一值得注意的区别是对Type.GetFields()的调用和由此产生的FieldInfo数组。同样,为了简单起见,使用 LINQ 查询只打印出每个字段的名称。

// Display field names of type.
static void ListFields(Type t)
{
  Console.WriteLine("***** Fields *****");
  var fieldNames = from f in t.GetFields() select f.Name;
  foreach (var name in fieldNames)
  {
    Console.WriteLine("->{0}", name);
  }
  Console.WriteLine();
}

显示类型属性的逻辑是相似的。

// Display property names of type.
static void ListProps(Type t)
{
  Console.WriteLine("***** Properties *****");
  var propNames = from p in t.GetProperties() select p.Name;
  foreach (var name in propNames)
  {
    Console.WriteLine("->{0}", name);
  }
  Console.WriteLine();
}

反思实现的接口

接下来,您将编写一个名为ListInterfaces()的方法,该方法将打印传入类型支持的任何接口的名称。这里唯一有趣的一点是对GetInterfaces()的调用返回了一个System.Type的数组!鉴于接口确实是类型,这应该是有意义的。

// Display implemented interfaces.
static void ListInterfaces(Type t)
{
  Console.WriteLine("***** Interfaces *****");
  var ifaces = from i in t.GetInterfaces() select i;
  foreach(Type i in ifaces)
  {
    Console.WriteLine("->{0}", i.Name);
  }
}

Note

要知道大多数的System.Type ( GetMethods()GetInterfaces()等的“get”方法。)已被重载,以允许您从BindingFlags枚举中指定值。这提供了对应该搜索什么的更高级别的控制(例如,仅静态成员、仅公共成员、包括私有成员等)。).有关详细信息,请参考文档。

展示各种零碎的东西

最后但同样重要的是,您有一个最终的 helper 方法,它将简单地显示各种统计信息(指示类型是否是泛型、基类是什么、类型是否是密封的,等等)。)关于传入类型。

// Just for good measure.
static void ListVariousStats(Type t)
{
  Console.WriteLine("***** Various Statistics *****");
  Console.WriteLine("Base class is: {0}", t.BaseType);
  Console.WriteLine("Is type abstract? {0}", t.IsAbstract);
  Console.WriteLine("Is type sealed? {0}", t.IsSealed);
  Console.WriteLine("Is type generic? {0}", t.IsGenericTypeDefinition);
  Console.WriteLine("Is type a class type? {0}", t.IsClass);
  Console.WriteLine();
}

添加顶级语句

Program.cs文件的顶层语句提示用户输入类型的全限定名。一旦获得这个字符串数据,就将它传递给Type.GetType()方法,并将提取的System.Type发送给每个助手方法。这个过程一直重复,直到用户按下 Q 来终止应用。

Console.WriteLine("***** Welcome to MyTypeViewer *****");
string typeName = "";

do
{
  Console.WriteLine("\nEnter a type name to evaluate");
  Console.Write("or enter Q to quit: ");

  // Get name of type.
  typeName = Console.ReadLine();

  // Does user want to quit?
  if (typeName.Equals("Q",StringComparison.OrdinalIgnoreCase))
  {
    break;
  }

  // Try to display type.
  try
  {
    Type t = Type.GetType(typeName);
    Console.WriteLine("");
    ListVariousStats(t);
    ListFields(t);
    ListProps(t);
    ListMethods(t);
    ListInterfaces(t);
  }
  catch
  {
    Console.WriteLine("Sorry, can't find type");
  }
} while (true);

至此,MyTypeViewer.exe准备试驾了。例如,运行您的应用并输入以下完全限定的名称(注意Type.GetType()需要区分大小写的字符串名称):

  • System.Int32

  • System.Collections.ArrayList

  • System.Threading.Thread

  • System.Void

  • System.IO.BinaryWriter

  • System.Math

  • MyTypeViewer.Program

例如,下面是指定System.Math时的部分输出:

***** Welcome to MyTypeViewer *****
Enter a type name to evaluate
or enter Q to quit: System.Math

***** Various Statistics *****
Base class is: System.Object
Is type abstract? True
Is type sealed? True
Is type generic? False
Is type a class type? True

***** Fields *****
->PI
->E

***** Properties *****

***** Methods *****
->Acos
->Asin
->Atan
->Atan2
->Ceiling
->Cos

...

反思静态类型

如果在前面的方法中输入了System.Console,那么在第一个帮助器方法中将会抛出一个异常,因为t的值将会是 null。不能使用Type.GetType(typeName)方法加载静态类型。相反,你必须使用另一种机制,来自System.Typetypeof函数。更新程序以处理System.Console的特殊情况,如下所示:

Type t = Type.GetType(typeName);
if (t == null && typeName.Equals("System.Console",
         StringComparison.OrdinalIgnoreCase))
{
  t = typeof(System.Console);
}

思考泛型类型

当您调用Type.GetType()来获取泛型类型的元数据描述时,您必须使用一种特殊的语法,包括一个“反勾”字符(```cs),后跟一个表示该类型支持的类型参数数量的数值。例如,如果您想打印出System.Collections.Generic.List<T>的元数据描述,您需要将以下字符串传递到您的应用中:

System.Collections.Generic.List`1

```cs

这里,您使用的是`1`的数值,因为`List<T>`只有一个类型参数。然而,如果你想反映`Dictionary<TKey, TValue>`,提供值`2`,像这样:

System.Collections.Generic.Dictionary`2


### 反映方法参数和返回值

到目前为止,一切顺利!接下来,我们将对当前应用做一个小小的增强。具体来说,您将更新`ListMethods()` helper 函数,不仅列出给定方法的名称,还列出返回类型和传入参数类型。类型为这些任务提供了`ReturnType`属性和`GetParameters()`方法。在下面修改过的代码中,请注意,您正在使用嵌套的`foreach`循环(没有使用 LINQ)构建一个包含每个参数的类型和名称的字符串:

static void ListMethods(Type t) { Console.WriteLine("***** Methods *****"); MethodInfo[] mi = t.GetMethods(); foreach (MethodInfo m in mi) { // Get return type. string retVal = m.ReturnType.FullName; string paramInfo = "( "; // Get params. foreach (ParameterInfo pi in m.GetParameters()) { paramInfo += string.Format("{0} {1} ", pi.ParameterType, pi.Name); } paramInfo += " )";

// Now display the basic method sig.
Console.WriteLine("->{0} {1} {2}", retVal, m.Name, paramInfo);

} Console.WriteLine(); }


如果您现在运行这个更新的应用,您会发现给定类型的方法更加详细。如果您输入您的好朋友`System.Object`作为程序的输入,将显示以下方法:

***** Methods ***** ->System.Type GetType ( ) ->System.String ToString ( ) ->System.Boolean Equals ( System.Object obj ) ->System.Boolean Equals ( System.Object objA System.Object objB ) ->System.Boolean ReferenceEquals ( System.Object objA System.Object objB ) ->System.Int32 GetHashCode ( )


`ListMethods()`的当前实现很有帮助,因为您可以使用`System.Reflection`对象模型直接研究每个参数和方法返回类型。作为一个极端的捷径,请注意所有的`XXXInfo`类型(`MethodInfo`、`PropertyInfo`、`EventInfo`等)。)已经覆盖了`ToString()`以显示所请求项目的签名。因此,您也可以如下实现`ListMethods()`(再次使用 LINQ,您只需选择所有`MethodInfo`对象,而不仅仅是`Name`值):

static void ListMethods(Type t) { Console.WriteLine("***** Methods *****"); var methodNames = from n in t.GetMethods() select n; foreach (var name in methodNames) { Console.WriteLine("->{0}", name); } Console.WriteLine(); }


有趣的东西,是吧?显然,`System.Reflection`名称空间和`System.Type`类允许你反映类型的许多其他方面,而不仅仅是`MyTypeViewer`当前显示的内容。正如您所希望的,您可以获得类型的事件,获得给定成员的任何泛型参数的列表,并收集许多其他细节。

尽管如此,此时您已经创建了一个(有点功能的)对象浏览器。这个特定示例的主要限制是,除了当前程序集(`MyTypeViewer`)或基类库中始终被引用的程序集(如`mscorlib.dll`)之外,您无法进行反射。这就引出了一个问题“我如何构建可以加载(并反射)编译时未引用的程序集的应用?”很高兴你问了。

## 动态加载程序集

有时您需要以编程方式动态加载程序集,即使清单中没有该程序集的记录。正式来说,按需加载外部程序集的行为被称为*动态加载*。

`System.Reflection`定义一个名为`Assembly`的类。使用此类,您可以动态加载程序集,以及发现有关程序集本身的属性。使用`Assembly`类型,您可以动态加载程序集,也可以加载位于任意位置的程序集。从本质上讲,`Assembly`类提供了允许您以编程方式从磁盘加载程序集的方法。

为了演示动态加载,创建一个名为 ExternalAssemblyReflector 的新控制台应用项目。您的任务是构造代码,提示输入要动态加载的程序集的名称(不含任何扩展名)。您将把`Assembly`引用传递到一个名为`DisplayTypes()`的 helper 方法中,该方法将简单地打印它包含的每个类、接口、结构、枚举和委托的名称。代码非常简单。

using System; using System.Reflection; using System.IO; // For FileNotFoundException definition.

Console.WriteLine("***** External Assembly Viewer *****"); string asmName = ""; Assembly asm = null; do { Console.WriteLine("\nEnter an assembly to evaluate"); Console.Write("or enter Q to quit: "); // Get name of assembly. asmName = Console.ReadLine(); // Does user want to quit? if (asmName.Equals("Q",StringComparison.OrdinalIgnoreCase)) { break; }

// Try to load assembly. try { asm = Assembly.LoadFrom(asmName); DisplayTypesInAsm(asm); } catch { Console.WriteLine("Sorry, can't find assembly."); } } while (true);

static void DisplayTypesInAsm(Assembly asm) { Console.WriteLine("\n***** Types in Assembly *****"); Console.WriteLine("->{0}", asm.FullName); Type[] types = asm.GetTypes(); foreach (Type t in types) { Console.WriteLine("Type: {0}", t); } Console.WriteLine(""); }


如果您想通过`CarLibrary.dll`进行反射,您需要将`CarLibrary.dll`二进制文件(来自上一章)复制到 ExternalAssemblyReflector 应用的项目目录(如果使用 Visual Studio 代码)或`\bin\Debug\net5.0`(如果使用 Visual Studio)目录,以运行该程序。出现提示时,输入 **CarLibrary** (扩展名可选),输出如下:

***** External Assembly Viewer ***** Enter an assembly to evaluate or enter Q to quit: CarLibrary

***** Types in Assembly ***** ->CarLibrary, Version=1.0.0.1, Culture=neutral, PublicKeyToken=null Type: CarLibrary.MyInternalClass Type: CarLibrary.EngineStateEnum Type: CarLibrary.MusicMedia Type: CarLibrary.Car Type: CarLibrary.MiniVan Type: CarLibrary.SportsCar


`LoadFrom`方法也可以获取您想要查看的程序集的绝对路径(例如,`C:\MyApp\MyAsm.dll`)。使用此方法,您可以传入控制台应用项目的完整路径。因此,如果`CarLibrary.dll`位于`C:\MyCode`下,您可以输入 **C:\MyCode\CarLibrary** (注意扩展名是可选的)。

## 反思框架程序集

`Assembly.Load()`方法有几个重载。一种变体允许您指定区域性值(对于本地化程序集),以及版本号和公钥标记值(对于框架程序集)。总的来说,标识一个组件的项目集被称为*显示名*。显示名称的格式是以逗号分隔的名称-值对字符串,以程序集的友好名称开头,后跟可选的限定符(可以按任何顺序出现)。下面是要遵循的模板(可选项目出现在括号中):

Name (,Version = major.minor.build.revision) (,Culture = culture token) (,PublicKeyToken= public key token)


当你创建一个显示名时,约定`PublicKeyToken=null`表明需要绑定和匹配一个非强名称的程序集。此外,`Culture=""`表示匹配目标计算机的默认区域性。这里有一个例子:

// Load version 1.0.0.0 of CarLibrary using the default culture. Assembly a = Assembly.Load("CarLibrary, Version=1.0.0.0, PublicKeyToken=null, Culture="""); // The quotes must be escaped with back slashes in C#


还要注意,`System.Reflection`名称空间提供了`AssemblyName`类型,这允许您在一个方便的对象变量中表示前面的字符串信息。通常,这个类与`System.Version`一起使用,后者是一个封装程序集版本号的面向对象包装器。一旦建立了显示名称,就可以将它传递给重载的`Assembly.Load()`方法,如下所示:

// Make use of AssemblyName to define the display name. AssemblyName asmName; asmName = new AssemblyName(); asmName.Name = "CarLibrary"; Version v = new Version("1.0.0.0"); asmName.Version = v; Assembly a = Assembly.Load(asmName);


加载. NET Framework 程序集(不是。NET Core),`Assembly.Load()`参数应该指定一个`PublicKeyToken`值。和。NET Core,它不是必需的,因为强命名的使用减少了。例如,假设您有一个名为 FrameworkAssemblyViewer 的新控制台应用项目,该项目引用了 Microsoft。EntityFrameworkCore 包。提醒一下,这都可以通过。NET 5 命令行界面(CLI)。

dotnet new console -lang c# -n FrameworkAssemblyViewer -o .\FrameworkAssemblyViewer -f net5.0 dotnet sln .\Chapter17_AllProjects.sln add .\FrameworkAssemblyViewer dotnet add .\FrameworkAssemblyViewer package Microsoft.EntityFrameworkCore -v 5.0.0


回想一下,当您引用另一个程序集时,该程序集的副本被复制到引用项目的输出目录中。使用 CLI 构建项目。

dotnet build


随着项目的创建、`EntityFrameworkCode`的引用以及项目的构建,您现在可以加载并检查它了。鉴于该程序集中的类型数量相当大,下面的应用使用简单的 LINQ 查询,仅打印出公共枚举的名称:

using System; using System.Linq; using System.Reflection;

Console.WriteLine("***** The Framework Assembly Reflector App *****\n");

// Load Microsoft.EntityFrameworkCore.dll var displayName = "Microsoft.EntityFrameworkCore, Version=5.0.0.0, Culture="", PublicKeyToken=adb9793829ddae60"; Assembly asm = Assembly.Load(displayName); DisplayInfo(asm); Console.WriteLine("Done!"); Console.ReadLine();

private static void DisplayInfo(Assembly a) { Console.WriteLine("***** Info about Assembly *****"); Console.WriteLine("AsmName:a.GetName().Name");Console.WriteLine("Asm Name: {a.GetName().Name}" ); Console.WriteLine("Asm Version: {a.GetName().Version}"); Console.WriteLine($"Asm Culture: {a.GetName().CultureInfo.DisplayName}"); Console.WriteLine("\nHere are the public enums:");

// Use a LINQ query to find the public enums. Type[] types = a.GetTypes(); var publicEnums = from pe in types where pe.IsEnum && pe.IsPublic select pe;

foreach (var pe in publicEnums) { Console.WriteLine(pe); } }


此时,您应该理解如何使用`System.Reflection`名称空间的一些核心成员在运行时发现元数据。当然,我意识到尽管有“酷的因素”,你可能不需要在你工作的地方经常构建定制的对象浏览器。但是,请记住,反射服务是许多常见编程活动的基础,包括延迟绑定。

## 了解延迟绑定

简单地说,*延迟绑定*是一种技术,在这种技术中,您可以创建给定类型的实例,并在运行时调用其成员,而无需硬编码编译时了解其存在。当您正在生成延迟绑定到外部程序集中的类型的应用时,您没有理由设置对该程序集的引用;因此,调用方的清单没有程序集的直接列表。

乍一看,并不容易看出延迟绑定的价值。的确,如果您可以“早期绑定”到一个对象(例如,添加一个程序集引用并使用 C# `new`关键字分配类型),您应该选择这样做。出于一个原因,早期绑定允许您在编译时确定错误,而不是在运行时。尽管如此,延迟绑定在您可能构建的任何可扩展应用中确实扮演着重要角色。在本章末尾的“构建可扩展的应用”一节中,您将有机会构建这样一个“可扩展的”程序在此之前,让我们检查一下`Activator`类的作用。

### 系统。活化剂类别

类是。网芯延迟绑定流程。对于下一个例子,您只对`Activator.CreateInstance()`方法感兴趣,该方法用于通过延迟绑定创建一个类型的实例。此方法已被重载多次,以提供很大的灵活性。`CreateInstance()`成员最简单的变体是接受一个有效的`Type`对象,该对象描述了您想要动态分配到内存中的实体。

创建一个名为 LateBindingApp 的新控制台应用项目,并通过 C# `using`关键字导入`System.IO`和`System.Reflection`名称空间。现在,更新`Program.cs`文件,如下所示:

using System; using System.IO; using System.Reflection;

// This program will load an external library, // and create an object using late binding. Console.WriteLine("***** Fun with Late Binding *****"); // Try to load a local copy of CarLibrary. Assembly a = null; try { a = Assembly.LoadFrom("CarLibrary"); } catch(FileNotFoundException ex) { Console.WriteLine(ex.Message); return; } if(a != null) { CreateUsingLateBinding(a); } Console.ReadLine();

static void CreateUsingLateBinding(Assembly asm) { try { // Get metadata for the Minivan type. Type miniVan = asm.GetType("CarLibrary.MiniVan");

// Create a Minivan instance on the fly.
object obj = Activator.CreateInstance(miniVan);
Console.WriteLine("Created a {0} using late binding!", obj);

} catch(Exception ex) { Console.WriteLine(ex.Message); } }


现在,在运行这个应用之前,您需要手动将`CarLibrary.dll`的副本放入这个新应用的项目文件夹(或者如果您使用 Visual Studio,则放入`bin\Debug\net5.0`文件夹)中。

Note

这个例子不要添加对`CarLibrary.dll`的引用!延迟绑定的全部意义在于,您试图创建一个在编译时未知的对象。

注意,`Activator.CreateInstance()`方法返回一个`System.Object`,而不是一个强类型的`MiniVan`。因此,如果您在`obj`变量上应用点操作符,您将看不到`MiniVan`类的任何成员。乍一看,您可能认为可以通过显式强制转换来解决这个问题,如下所示:

// Cast to get access to the members of MiniVan? // Nope! Compiler error! object obj = (MiniVan)Activator.CreateInstance(minivan);


但是,因为您的程序没有添加对`CarLibrary.dll`的引用,所以您不能使用 C# `using`关键字来导入`CarLibrary`名称空间,因此,您不能在转换操作期间使用`MiniVan`类型!请记住,延迟绑定的要点是创建没有编译时知识的对象实例。考虑到这一点,如何调用存储在`System.Object`引用中的`MiniVan`对象的底层方法呢?答案当然是通过使用反射。

### 调用不带参数的方法

假设您想要调用`MiniVan`的`TurboBoost()`方法。正如您所记得的,这个方法将把引擎的状态设置为“dead ”,并显示一个信息消息框。第一步是使用`Type.GetMethod()`为`TurboBoost()`方法获取一个`MethodInfo`对象。从产生的`MethodInfo`中,您可以使用`Invoke()`调用`MiniVan.TurboBoost`。`MethodInfo.Invoke()`要求您将所有参数发送给由`MethodInfo`表示的方法。这些参数由一组`System.Object`类型表示(因为给定方法的参数可以是任意数量的各种实体)。

鉴于`TurboBoost()`不需要任何参数,可以简单地通过`null`(意思是“这个方法没有参数”)。更新您的`CreateUsingLateBinding()`方法如下:

static void CreateUsingLateBinding(Assembly asm) { try { // Get metadata for the Minivan type. Type miniVan = asm.GetType("CarLibrary.MiniVan");

// Create the Minivan on the fly.
object obj = Activator.CreateInstance(miniVan);
Console.WriteLine($"Created a {obj} using late binding!");
// Get info for TurboBoost.
MethodInfo mi = miniVan.GetMethod("TurboBoost");

// Invoke method ('null' for no parameters).
mi.Invoke(obj, null);

} catch(Exception ex) { Console.WriteLine(ex.Message); } }


此时,您将在控制台中看到您的发动机爆炸的消息。

### 调用带参数的方法

当您想要使用延迟绑定来调用需要参数的方法时,您应该将参数打包成一个松散类型的数组`object`。这个版本的`Car`类有一个无线电,并有以下方法:

public void TurnOnRadio(bool musicOn, MusicMediaEnum mm) => MessageBox.Show(musicOn ? $"Jamming {mm}" : "Quiet time...");


这个方法有两个参数:一个布尔值表示汽车的音乐系统应该打开还是关闭,一个枚举表示音乐播放器的类型。回想一下,该枚举的结构如下:

public enum MusicMediaEnum { musicCd, // 0 musicTape, // 1 musicRadio, // 2 musicMp3 // 3 }


这里有一个`Program`类的新方法,它调用`TurnOnRadio()`。请注意,您正在使用`MusicMediaEnum`枚举的底层数值来指定一个“收音机”媒体播放器。

static void InvokeMethodWithArgsUsingLateBinding(Assembly asm) { try { // First, get a metadata description of the sports car. Type sport = asm.GetType("CarLibrary.SportsCar");

// Now, create the sports car.
object obj = Activator.CreateInstance(sport);
// Invoke TurnOnRadio() with arguments.
MethodInfo mi = sport.GetMethod("TurnOnRadio");
mi.Invoke(obj, new object[] { true, 2 });

} catch (Exception ex) { Console.WriteLine(ex.Message); } }


理想情况下,此时,您可以看到反射、动态加载和延迟绑定之间的关系。可以肯定的是,反射 API 提供了许多超出这里所讨论的特性,但是如果您感兴趣的话,您应该准备好深入研究更多的细节。

同样,您可能仍然想知道*何时*您应该在您自己的应用中使用这些技术。本章的结论应该阐明这个问题;然而,下一个研究的主题是。净核心属性。

## 理解的作用.NET 属性

如本章开头所述,. NET 核心编译器的一个作用是为所有定义和引用的类型生成元数据描述。除了任何程序集中包含的标准元数据之外。NET Core platform 为程序员提供了一种使用*属性*将附加元数据嵌入到程序集中的方法。简而言之,属性只不过是可以应用于给定类型(类、接口、结构等)的代码注释。)、成员(属性、方法等。)、程序集或模块。

。NET Core 属性是扩展抽象基类`System.Attribute`的类类型。当你探索。NET 核心命名空间,您会发现许多预定义的属性,您可以在您的应用中使用。此外,您可以自由构建定制属性,通过创建一个从`Attribute`派生的新类型来进一步限定您的类型的行为。

那个。NET Core 基本类库在各种命名空间中提供属性。表 17-3 给出了一些预定义属性的快照,但是*绝对*否意味着全部。

表 17-3。

预定义属性的微小样本

<colgroup><col class="tcol1 align-left"> <col class="tcol2 align-left"></colgroup> 
| 

属性

 | 

生命的意义

 |
| --- | --- |
| `[CLSCompliant]` | 强制带批注的项目符合公共语言规范(CLS)的规则。回想一下,CLS 兼容的类型保证可以在所有。NET 核心编程语言。 |
| `[DllImport]` | 允许。NET 核心代码调用任何基于非托管 C 或 C++的代码库,包括底层操作系统的 API。 |
| `[Obsolete]` | 标记不推荐使用的类型或成员。如果其他程序员试图使用这样的项目,他们将收到一个编译器警告,描述他们的方法的错误。 |

请理解,当您在代码中应用属性时,嵌入的元数据基本上是无用的,直到另一个软件明确地反映了这些信息。如果不是这样,嵌入在程序集中的元数据的格式回复将被忽略,并且完全无害。

### 属性消费者

正如您所猜测的。NET Core Framework 附带了许多实用程序,它们确实在寻找各种属性。C# 编译器(`csc.exe`)本身已经被预编程,以便在编译周期中发现各种属性的存在。例如,如果 C# 编译器遇到了`[CLSCompliant]`属性,它将自动检查属性化的项,以确保它只公开符合 CLS 的构造。作为另一个例子,如果 C# 编译器发现一个具有`[Obsolete]`属性的项,它将在 Visual Studio 错误列表窗口中显示一个编译器警告。

除了开发工具之外。NET 核心基本类库被预编程以反映特定的属性。第二十章介绍 XML 和 JSON 序列化,两者都使用属性来控制序列化过程。

最后,您可以自由地构建应用,这些应用被编程为反映您自己的自定义属性以及。NET 核心基本类库。通过这样做,您基本上能够创建一组“关键字”,这些关键字被一组特定的程序集所理解。

### 在 C# 中应用属性

为了演示在 C# 中应用属性的过程,创建一个名为 applying attributes 的新控制台应用项目,并添加对`System.Text.Json`的引用。假设您想要构建一个名为`Motorcycle`的类,它可以持久化为 JSON 格式。如果您有一个不应该导出到 JSON 的字段,您可以应用`[JsonIgnore]`属性。

public class Motorcycle { [JsonIgnore] public float weightOfCurrentPassengers; // These fields are still serializable. public bool hasRadioSystem; public bool hasHeadSet; public bool hasSissyBar; }


Note

属性适用于“下一个”项目。

此时,不要关心对象序列化的实际过程(第二十章讨论了细节)。请注意,当您想要应用一个属性时,属性的名称被夹在方括号中。

正如您可能猜到的那样,一个单一的项目可以有多个属性。假设您有一个遗留的 C# 类类型(`HorseAndBuggy`),它被认为有一个定制的 XML 名称空间。随着时间的推移,代码库已经发生了变化,该类现在被认为对于当前的开发已经过时。您可以用`[Obsolete]`属性来标记这个类,而不是从您的代码库中删除这个类定义(并冒着破坏现有软件的风险)。要将多个属性应用于单个项目,只需使用逗号分隔的列表,如下所示:

using System; using System.Xml.Serialization;

namespace ApplyingAttributes { [XmlRoot(Namespace = "www.MyCompany.com"), Obsolete("Use another vehicle!")] public class HorseAndBuggy { // ... } }


或者,您也可以将多个属性应用于单个项目,方法是按如下方式堆叠每个属性:

[XmlRoot(Namespace = "www.MyCompany.com")] [Obsolete("Use another vehicle!")] public class HorseAndBuggy { // ... }


### C# 属性速记符号

如果你在咨询。NET 核心文档,您可能已经注意到了`[Obsolete]`属性的实际类名是`ObsoleteAttribute`,而不是`Obsolete`。作为命名约定,所有。NET Core 属性(包括您可能自己创建的自定义属性)的后缀是`Attribute`标记。然而,为了简化应用属性的过程,C# 语言不要求您键入`Attribute`后缀。考虑到这一点,下面的`HorseAndBuggy`类型的迭代与前面的相同(它只是涉及到更多的击键):

[SerializableAttribute] [ObsoleteAttribute("Use another vehicle!")] public class HorseAndBuggy { // ... }


要知道这是 C# 提供的一种礼貌。不全是。NET 核心语言支持这种速记属性语法。

### 为属性指定构造函数参数

注意,`[Obsolete]`属性可以接受看起来像是构造函数的参数。如果您通过在代码编辑器中右键单击项目并选择 Go To Definition 菜单选项来查看`[Obsolete]`属性的正式定义,您会发现这个类确实提供了一个接收`System.String`的构造函数。

public sealed class ObsoleteAttribute : Attribute { public ObsoleteAttribute(string message, bool error); public ObsoleteAttribute(string message); public ObsoleteAttribute(); public bool IsError { get; } public string? Message { get; } }


请理解,当您向属性提供构造函数参数时,属性是*而不是*分配到内存中的,直到参数被另一个类型或外部工具反射。在属性级别定义的字符串数据只是作为元数据的格式回复存储在程序集中。

### 作用中的过时属性

既然`HorseAndBuggy`已经被标记为过时,如果您要分配这种类型的实例:

using System; using ApplyingAttributes;

Console.WriteLine("Hello World!"); HorseAndBuggy mule = new HorseAndBuggy();


您会发现会发出一个编译器警告。该警告特别是 CS0618,并且该消息包括传递到属性中的信息。

‘HorseAndBuggy’ is obsolete: ‘Use another vehicle!'


Visual Studio 和 Visual Studio 代码也有助于智能感知,它通过反射获取信息。图 17-1 显示了 Visual Studio 中`Obsolete`属性的结果,图 17-2 正在使用 Visual Studio 代码。注意,两者都使用了术语*弃用的*而不是*废弃的*。

![img/340876_10_En_17_Fig2_HTML.jpg](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8b7a175d3d1a48b08caedc4727e17652~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1771672152&x-signature=e8nawzwpWqlBxCbmHHYno2OmM1c%3D)17-2。

Visual Studio 代码中的实际属性

![img/340876_10_En_17_Fig1_HTML.jpg](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4ea27fda5d184236a9808cb81a4be796~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1771672152&x-signature=KWVtVLIhEtYN3gjx0zRr5wMnN%2Fc%3D)17-1。

Visual Studio 中的实际属性

理想情况下,在这一点上,你应该了解以下关键点.NETCore 属性:

*   属性是从`System.Attribute`派生的类。

*   属性导致嵌入的元数据。

*   属性基本上是无用的,直到另一个代理反映出来。

*   在 C# 中,使用方括号应用属性。

接下来,让我们看看如何构建自己的定制属性和一个定制软件来反映嵌入的元数据。

## 构建自定义属性

构建定制属性的第一步是创建一个从`System.Attribute`派生的新类。与本书通篇使用的汽车主题保持一致,假设您已经创建了一个名为 AttributedCarLibrary 的新 C# 类库项目。

这个程序集将定义一些车辆,每个车辆都使用一个名为`VehicleDescriptionAttribute`的自定义属性进行描述,如下所示:

using System; // A custom attribute. public sealed class VehicleDescriptionAttribute :Attribute { public string Description { get; set; }

public VehicleDescriptionAttribute(string description) => Description = description; public VehicleDescriptionAttribute(){ } }


如您所见,`VehicleDescriptionAttribute`维护了一段使用自动属性(`Description`)操作的字符串数据。除了这个类是从`System.Attribute`派生出来的这个事实之外,这个类的定义并没有什么独特之处。

Note

出于安全原因,将所有自定义属性设计为密封的被认为是. NET 核心最佳实践。事实上,Visual Studio 和 Visual Studio 代码都提供了一个名为`Attribute`的代码片段,它将在您的代码窗口中搭建一个新的`System.Attribute`派生类。您可以通过键入代码段的名称并按 Tab 键来展开任何代码段。

### 应用自定义属性

鉴于`VehicleDescriptionAttribute`是从`System.Attribute`衍生而来的,你现在可以给你的车辆添加你认为合适的注释了。出于测试目的,将以下类添加到新的类库中:

//Motorcycle.cs namespace AttributedCarLibrary { // Assign description using a "named property." [Serializable] [VehicleDescription(Description = "My rocking Harley")] public class Motorcycle { }

//HorseAndBuggy.cs namespace AttributedCarLibrary { [Serializable] [Obsolete ("Use another vehicle!")] [VehicleDescription("The old gray mare, she ain't what she used to be...")] public class HorseAndBuggy { } }

//Winnebago.cs namespace AttributedCarLibrary

{ [VehicleDescription("A very long, slow, but feature-rich auto")] public class Winnebago { } }


### 命名属性语法

注意到,`Motorcycle`的描述被分配了一个描述,它使用了一个新的属性语法,称为*命名属性*。在第一个`[VehicleDescription]`属性的构造函数中,使用`Description`属性设置底层字符串数据。如果这个属性被一个外部代理反射,那么这个值就被输入到`Description`属性中(只有当这个属性提供了一个可写的。净核心财产)。

相比之下,`HorseAndBuggy`和`Winnebago`类型不使用命名属性语法,只是通过自定义构造函数传递字符串数据。在任何情况下,一旦编译了`AttributedCarLibrary`程序集,您就可以使用`ildasm.exe`来查看为您的类型注入的元数据描述。例如,下面显示了对`Winnebago`类的嵌入式描述:

// CustomAttribute #1 // ------------------------------------------------------- // CustomAttribute Type: 06000005 // CustomAttributeName: AttributedCarLibrary.VehicleDescriptionAttribute :: instance void .ctor(class System.String) // Length: 45 // Value : 01 00 28 41 20 76 65 72 79 20 6c 6f 6e 67 2c 20 > (A very long, < // : 73 6c 6f 77 2c 20 62 75 74 20 66 65 61 74 75 72 >slow, but feature< // : 65 2d 72 69 63 68 20 61 75 74 6f 00 00 >e-rich auto < // ctor args: ("A very long, slow, but feature-rich auto")


### 限制属性使用

默认情况下,自定义属性可以应用于代码的任何方面(方法、类、属性等)。).因此,如果这样做有意义的话,您可以使用`VehicleDescription`来限定方法、属性或字段(等等)。

[VehicleDescription("A very long, slow, but feature-rich auto")] public class Winnebago { [VehicleDescription("My rocking CD player")] public void PlayMusic(bool On) { ... } }


在某些情况下,这正是您需要的行为。但是,在其他时候,您可能希望构建一个只能应用于选定代码元素的自定义属性。如果您想要约束一个定制属性的范围,您将需要在您的定制属性的定义上应用`[AttributeUsage]`属性。`[AttributeUsage]`属性允许您从`AttributeTargets`枚举中提供值的任意组合(通过一个`OR`操作),如下所示:

// This enumeration defines the possible targets of an attribute. public enum AttributeTargets { All, Assembly, Class, Constructor, Delegate, Enum, Event, Field, GenericParameter, Interface, Method, Module, Parameter, Property, ReturnValue, Struct }


此外,`[AttributeUsage]`还允许您有选择地设置一个命名属性(`AllowMultiple`),指定该属性是否可以在同一个项目上多次应用(默认为`false`)。同样,`[AttributeUsage]`允许您使用`Inherited`命名的属性(默认为`true`)来确定属性是否应该被派生类继承。

要确定`[VehicleDescription]`属性只能在一个类或结构上应用一次,可以按如下方式更新`VehicleDescriptionAttribute`定义:

// This time, we are using the AttributeUsage attribute // to annotate our custom attribute. [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false)] public sealed class VehicleDescriptionAttribute : System.Attribute { ... }


这样,如果开发人员试图在类或结构之外的任何东西上应用`[VehicleDescription]`属性,他们会收到一个编译时错误。

## 程序集级属性

也可以使用`[assembly:]`标签将属性应用于给定程序集中的所有类型。例如,假设您希望确保程序集中定义的每个公共类型的每个公共成员都符合 CLS 标准。为此,只需在任何 C# 源代码文件的顶部添加下面的程序集级属性。请注意,所有程序集或模块级属性都必须在任何命名空间范围之外列出!我建议向您的项目添加一个名为`AssemblyAttributes.cs`(不是`AssemblyInfo.cs`,因为它是自动生成的)的新文件,并将您的程序集级属性放在那里。

Note

使用单独的文件没有技术上的原因;这纯粹是为了你的代码的可支持性。将程序集属性放在一个单独的文件中可以清楚地表明您的项目使用程序集级属性以及它们的位置。

如果将程序集级或模块级属性添加到项目中,下面是一个推荐的文件布局:

// List "using" statements first. using System;

// Now list any assembly- or module-level attributes. // Enforce CLS compliance for all public types in this // assembly. [assembly: CLSCompliant(true)]


如果您现在添加了一点 CLS 规范之外的代码(比如一个无符号数据的暴露点),您将会收到一个编译器警告。

// Ulong types don't jibe with the CLS. public class Winnebago { public ulong notCompliant; }


Note

中有两个重要的变化。NET 核心。首先是`AssemblyInfo.cs`文件现在是从项目属性中自动生成的,不建议定制。第二个(也是相关的)变化是许多先前的汇编级属性(`Version`、`Company`等)。)已被替换为项目文件中的属性。

### 将项目文件用于部件属性

如第十六章与`InternalsVisibleToAttribute`所示,装配属性也可以添加到项目文件中。有一个问题,只有单字符串参数属性可以这样使用。对于可以在项目属性中的 Package 选项卡上设置的属性来说,也是如此。

Note

在撰写本文时,MSBuild GitHub repo 上正在积极讨论增加非字符串参数支持的功能。这将允许使用项目文件而不是`*.cs`文件来添加`CLSCompliant`属性。

继续设置一些属性(比如`Authors`、`Description`),方法是在 Solution Explorer 中右键单击项目,选择 properties,然后单击 Package。同样,像你在第十六章中做的那样添加`InternalsVisibleToAttribute`。您的项目文件现在看起来将如下所示:

net5.0 Philip Japikse Apress This is a simple car library with attributes <_Parameter1>CSharpCarClient

编译完项目后,导航到`\obj\Debug\net5.0`目录,并查找`AttributedCarLibrary.AssemblyInfo.cs`文件。打开它,您将看到这些属性(不幸的是,这种格式可读性不强):

using System; using System.Reflection;

[assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("CSharpCarClient")] [assembly: System.Reflection.AssemblyCompanyAttribute("Philip Japikse")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyDescriptionAttribute("This is a sample car library with attributes")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")] [assembly: System.Reflection.AssemblyProductAttribute("AttributedCarLibrary")] [assembly: System.Reflection.AssemblyTitleAttribute("AttributedCarLibrary")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]


关于汇编属性的最后一句结束语是,如果您想自己管理流程,可以关闭`AssemblyInfo.cs`类的生成。

## 使用早期绑定反映属性

请记住,在另一个软件反映出它的值之前,一个属性是毫无用处的。一旦发现了给定的属性,该软件就可以采取任何必要的行动。现在,像任何应用一样,这个“软件的另一部分”可以使用早期绑定或晚期绑定来发现自定义属性的存在。如果您想利用早期绑定,您将需要客户端应用有一个正在讨论的属性的编译时定义(在本例中为`VehicleDescriptionAttribute`)。鉴于`AttributedCarLibrary`程序集已经将这个自定义属性定义为一个公共类,早期绑定是最好的选择。

为了演示反射自定义属性的过程,向解决方案添加一个名为`VehicleDescriptionAttributeReader`的新 C# 控制台应用项目。接下来,添加对`AttributedCarLibrary`项目的引用。使用 CLI,执行以下命令(每个命令必须各占一行):

dotnet new console -lang c# -n VehicleDescriptionAttributeReader -o .\VehicleDescriptionAttributeReader -f net5.0 dotnet sln .\Chapter17_AllProjects.sln add .\VehicleDescriptionAttributeReader dotnet add VehicleDescriptionAttributeReader reference .\AttributedCarLibrary


用以下代码更新`Program.cs`文件:

using System; using AttributedCarLibrary;

Console.WriteLine("***** Value of VehicleDescriptionAttribute *****\n"); ReflectOnAttributesUsingEarlyBinding(); Console.ReadLine();

static void ReflectOnAttributesUsingEarlyBinding() { // Get a Type representing the Winnebago. Type t = typeof(Winnebago);

// Get all attributes on the Winnebago. object[] customAtts = t.GetCustomAttributes(false);

// Print the description. foreach (VehicleDescriptionAttribute v in customAtts) { Console.WriteLine("-> {0}\n", v.Description); } }


`Type.GetCustomAttributes()`方法返回一个对象数组,表示应用于由`Type`表示的成员的所有属性(布尔参数控制搜索是否应该沿着继承链向上扩展)。一旦获得了属性列表,迭代每个`VehicleDescriptionAttribute`类,并打印出`Description`属性获得的值。

## 使用延迟绑定反射属性

前面的例子使用早期绑定来打印出`Winnebago`类型的车辆描述数据。这是可能的,因为`VehicleDescriptionAttribute`类类型被定义为`AttributedCarLibrary`程序集中的公共成员。还可以利用动态加载和延迟绑定来反映属性。

将名为`VehicleDescriptionAttributeReaderLateBinding`的新项目添加到解决方案中,将其设置为启动项目,并将`AttributedCarLibrary.dll`复制到项目的文件夹中(如果使用 Visual Studio,则复制到`\bin\Debug\net5.0`)。现在,更新您的`Program`类,如下所示:

using System; using System.Reflection;

Console.WriteLine("***** Value of VehicleDescriptionAttribute *****\n"); ReflectAttributesUsingLateBinding(); Console.ReadLine();

static void ReflectAttributesUsingLateBinding() { try { // Load the local copy of AttributedCarLibrary. Assembly asm = Assembly.LoadFrom("AttributedCarLibrary");

// Get type info of VehicleDescriptionAttribute.
Type vehicleDesc =
  asm.GetType("AttributedCarLibrary.VehicleDescriptionAttribute");

// Get type info of the Description property.
 PropertyInfo propDesc = vehicleDesc.GetProperty("Description");

// Get all types in the assembly.
 Type[] types = asm.GetTypes();

// Iterate over each type and obtain any VehicleDescriptionAttributes.
foreach (Type t in types)
{
  object[] objs = t.GetCustomAttributes(vehicleDesc, false);

  // Iterate over each VehicleDescriptionAttribute and print
  // the description using late binding.
  foreach (object o in objs)
  {
    Console.WriteLine("-> {0}: {1}\n", t.Name,
      propDesc.GetValue(o, null));
  }
}

} catch (Exception ex) { Console.WriteLine(ex.Message); } }


如果您能够按照本章中的示例进行操作,这段代码应该(或多或少)是不言自明的。唯一有趣的地方是使用了`PropertyInfo.GetValue()`方法,该方法用于触发属性的访问器。以下是当前示例的输出:

***** Value of VehicleDescriptionAttribute ***** -> Motorcycle: My rocking Harley

-> HorseAndBuggy: The old gray mare, she ain't what she used to be...

-> Winnebago: A very long, slow, but feature-rich auto


## 正确看待反射、延迟绑定和自定义属性

尽管您已经看到了这些技术的大量实例,但您可能仍然想知道何时在程序中使用反射、动态加载、延迟绑定和自定义属性。可以肯定的是,这些主题可能看起来有点像编程的学术方面(这可能是也可能不是一件坏事,取决于你的观点)。为了帮助将这些主题映射到真实世界的情况,您需要一个可靠的示例。现在假设您在一个编程团队中,该团队正在构建一个具有以下需求的应用:

该产品必须使用额外的第三方工具进行扩展。

*可延伸*到底是什么意思?好吧,考虑一下 Visual Studio IDE。当开发该应用时,各种“挂钩”被插入到代码库中,以允许其他软件供应商将定制模块“嵌入”(或插入)到 IDE 中。显然,Visual Studio 开发团队没有办法设置对外部。NET 程序集(因此,没有早期绑定),那么应用将如何提供所需的钩子呢?这里有一个解决这个问题的可能方法:

1.  首先,可扩展的应用必须提供某种输入机制,以允许用户指定要插入的模块(例如对话框或命令行标志)。这就需要*动态加载*。

2.  第二,可扩展的应用必须能够确定模块是否支持要插入到环境中的正确功能(例如一组必需的接口)。这就需要*反思*。

3.  最后,可扩展的应用必须获得对所需基础结构的引用(例如一组接口类型),并调用成员来触发底层功能。这可能需要*延迟绑定*。

简单地说,如果可扩展的应用已经被预编程为查询特定的接口,它能够在运行时确定该类型是否可以被激活。一旦通过了验证测试,所讨论的类型就可以支持为其功能提供多态结构的附加接口。这正是 Visual Studio 团队所采用的方法,不管您怎么想,这一点也不难!

## 构建可扩展的应用

在接下来的小节中,我将通过一个例子来说明构建一个可以通过外部程序集的功能来扩充的应用的过程。作为路线图,可扩展的应用需要以下组件:

*   `CommonSnappableTypes.dll`:此程序集包含将由每个管理单元对象使用的类型定义,并将由 Windows 窗体应用直接引用。

*   `CSharpSnapIn.dll`:用 C# 写的一个管理单元,利用了`CommonSnappableTypes.dll`的类型。

*   `VBSnapIn.dll`:用 Visual Basic 编写的管理单元,利用了`CommonSnappableTypes.dll`的类型。

*   `MyExtendableApp.exe`:可以通过每个管理单元的功能扩展的控制台应用。

这个应用将使用动态加载、反射和延迟绑定来动态获取它事先不知道的程序集的功能。

Note

您可能会想,“我的老板从来没有要求我构建一个控制台应用”,您可能是正确的!使用 C# 构建的业务线应用通常属于智能客户端(WinForms 或 WPF)、web 应用/RESTful 服务(ASP.NET 核心)或无头流程(Azure 函数、Windows 服务等)的范畴。).我们使用控制台应用来关注示例中的特定概念,在本例中是动态加载、反射和延迟绑定。在本书的后面,你将使用 ASP.NET 核心和 WPF 探索“真正的”面向用户的应用。

### 构建多项目可扩展应用解决方案

到目前为止,本书中的大多数应用都是独立的项目,只有少数例外(就像前一个)。这样做是为了让例子简单明了。然而,在现实世界的开发中,您通常会在一个解决方案中同时处理多个项目。

#### 使用 CLI 创建解决方案和项目

要开始使用 CLI,请输入以下命令来创建新的解决方案、类库和控制台应用以及项目引用:

dotnet new sln -n Chapter17_ExtendableApp

dotnet new classlib -lang c# -n CommonSnappableTypes -o .\CommonSnappableTypes -f net5.0 dotnet sln .\Chapter17_ExtendableApp.sln add .\CommonSnappableTypes

dotnet new classlib -lang c# -n CSharpSnapIn -o .\CSharpSnapIn -f net5.0 dotnet sln .\Chapter17_ExtendableApp.sln add .\CSharpSnapIn dotnet add CSharpSnapin reference CommonSnappableTypes

dotnet new classlib -lang vb -n VBSnapIn -o .\VBSnapIn -f net5.0 dotnet sln .\Chapter17_ExtendableApp.sln add .\VBSnapIn dotnet add VBSnapIn reference CommonSnappableTypes

dotnet new console -lang c# -n MyExtendableApp -o .\MyExtendableApp -f net5.0 dotnet sln .\Chapter17_ExtendableApp.sln add .\MyExtendableApp dotnet add MyExtendableApp reference CommonSnappableTypes


##### 将后期生成事件添加到项目文件中

当构建项目时(无论是从 Visual Studio 还是从命令行),都有可以挂接的事件。例如,我们希望在每次成功构建后,将两个管理单元程序集复制到控制台应用项目目录(用`dotnet run`调试时)和控制台应用输出目录(用 Visual Studio 调试时)。为此,我们将利用几个内置的宏。

将这个标记块复制到`CSharpSnapIn.csproj`和`VBSnapIn.vbproj`文件中,这将编译后的程序集复制到`MyExtendableApp`项目目录和输出目录(`MyExtendableApp\bin\debug\net5.0`):


现在,当构建每个项目时,它的程序集也被复制到`MyExtendableApp`的目标目录中。

#### 使用 Visual Studio 创建解决方案和项目

回想一下,默认情况下,Visual Studio 将该解决方案命名为在该解决方案中创建的第一个项目。但是,您可以很容易地更改解决方案的名称,如图 17-3 所示。

![img/340876_10_En_17_Fig3_HTML.jpg](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/de6c390708d4451ead0103bb70207d7b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1771672152&x-signature=E7UV8NWGr065FrooOLfMkx9R968%3D)17-3。

创建 CommonSnappableTypes 项目和 ExtendableApp 解决方案

要创建 ExtendableApp 解决方案,首先选择“文件”“➤新建项目”以加载“新建项目”对话框。选择类库并输入名称 **CommonSnappableTypes** 。在点击确定之前,输入解决方案名称 **ExtendableApp** ,如图 17-3 所示。

若要将另一个项目添加到解决方案中,请在解决方案资源管理器中右键单击解决方案名称(ExtendableApp )(或单击“文件”“➤”“添加➤新项目”),然后选择“添加➤新项目”。当向现有解决方案中添加另一个项目时,添加新项目对话框现在略有不同;解决方案选项不再存在,所以您将只看到项目信息(名称和位置)。将类库项目命名为 CSharpSnapIn,然后单击“创建”。

接下来,从 CSharpSnapIn 项目添加对 CommonSnappableTypes 项目的引用。若要在 Visual Studio 中执行此操作,请右击 CSharpSnapIn 项目,然后选择“添加➤项目引用”。在“引用管理器”对话框中,从左侧选择“项目➤解决方案”(如果尚未选择),然后选中“CommonSnappableTypes”旁边的框。

对引用 CommonSnappableTypes 项目的新 Visual Basic 类库(`VBSnapIn`)重复该过程。

要添加的最后一个项目是名为 MyExtendableApp 的. NET 核心控制台应用。添加对 CommonSnappableTypes 项目的引用,并将控制台应用设置为解决方案的启动项目。为此,在解决方案资源管理器中右键单击`MyExtendableApp`项目,并选择 Set as StartUp Project。

Note

如果右击 ExtendableApp 解决方案而不是其中一个项目,则显示的上下文菜单选项是“设置启动项目”。除了在单击“运行”时只执行一个项目之外,还可以设置多个项目来执行。这将在后面的章节中演示。

##### 设置项目生成依赖项

当 Visual Studio 获得运行解决方案的命令时,如果检测到任何更改,将生成启动项目和所有引用的项目。但是,任何未被引用的项目都是*而不是*构建的。这可以通过设置项目依赖关系来更改。为此,请在解决方案资源管理器中右击该解决方案,选择“项目生成顺序”,然后在出现的对话框中,选择“依赖项”选项卡,并将项目更改为 MyExtendableApp。

请注意,已经选择了 CommonSnappableTypes 项目,并且复选框被禁用。这是因为它是直接引用的。同时选中 CSharpSnapIn 和 VBSnapIn 项目复选框,如图 17-4 所示。

![img/340876_10_En_17_Fig4_HTML.jpg](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/551dd9fbb454496692a3262b0e5b4b37~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1771672152&x-signature=kYjLIBwxAFo7JT%2Fd%2FpQR5Ejlev8%3D)17-4。

访问项目构建顺序上下文菜单

现在,每次构建 MyExtendableApp 项目时,也会构建 CSharpSnapIn 和 VBSnapIn 项目。

##### 添加后期生成事件

打开 CSharpSnapIn 的项目属性(右击解决方案资源管理器并选择“属性”),然后导航到“生成事件”页(C#)。单击编辑后期生成按钮,然后单击宏> >。在这里你可以看到可用的宏,它们都指向路径和/或文件名。在构建事件中使用这些宏的优点是它们是独立于机器的,并且在相对路径上工作。例如,我正在一个名为`c-sharp-wf\code\chapter17`的目录中工作。您可能正在使用不同的目录。通过使用宏,MSBuild 将总是使用相对于`*.csproj`文件的正确路径。

在 PostBuild 框中,输入以下内容(两行):

copy (TargetPath)(TargetPath) (SolutionDir)MyExtendableApp$(OutDir)(TargetFileName)/Ycopy(TargetFileName) /Y copy (TargetPath) (SolutionDir)MyExtendableApp\(TargetFileName) /Y


对 VBSnapIn 项目执行相同的操作,只是属性页名为 Compile,您可以从这里单击 Build Events 按钮。

添加这些后期生成事件命令后,每次编译时,每个程序集都将被复制到 MyExtendableApp 的项目和输出目录中。

### 建设 CommonSnappableTypes.dll

在 CommonSnappableTypes 项目中,删除默认的`Class1.cs`文件,添加一个名为`IAppFunctionality.cs`的新接口文件,并将该文件更新为:

namespace CommonSnappableTypes { public interface IAppFunctionality { void DoIt(); } }


添加名为`CompanyInfoAttribute.cs`的类文件,并将其更新为:

using System; namespace CommonSnappableTypes { [AttributeUsage(AttributeTargets.Class)] public sealed class CompanyInfoAttribute : System.Attribute { public string CompanyName { get; set; } public string CompanyUrl { get; set; } } }


`IAppFunctionality`接口为可扩展应用使用的所有管理单元提供了一个多态接口。假设这个例子纯粹是说明性的,您提供一个名为`DoIt()`的方法。

`CompanyInfoAttribute`类型是一个定制属性,可以应用于任何想要嵌入到容器中的类类型。从这个类的定义可以看出,`[CompanyInfo]`允许管理单元的开发人员提供一些关于组件起点的基本细节。

### 构建 C# 管理单元

在 CSharpSnapIn 项目中,删除`Class1.cs`文件并添加一个名为`CSharpModule.cs`的新文件。更新代码以匹配以下内容:

using System; using CommonSnappableTypes;

namespace CSharpSnapIn { [CompanyInfo(CompanyName = "FooBar", CompanyUrl = "www.FooBar.com")] public class CSharpModule : IAppFunctionality { void IAppFunctionality.DoIt() { Console.WriteLine("You have just used the C# snap-in!"); } } }


注意,当支持`IAppFunctionality`接口时,我选择了使用显式接口实现(参见第章第 8 )。这不是必需的;然而,这个想法是,系统中唯一需要与这个接口类型直接交互的部分是宿主应用。通过显式实现这个接口,`DoIt()`方法不会直接从`CSharpModule`类型中暴露出来。

### 构建 Visual Basic 管理单元

转到 VBSnapIn 项目,删除`Class1.vb`文件并添加一个名为`VBSnapIn.vb`的新文件。代码(再次)故意简单。

Imports CommonSnappableTypes

<CompanyInfo(CompanyName:="Chucky's Software", CompanyUrl:="www.ChuckySoft.com")> Public Class VBSnapIn Implements IAppFunctionality

Public Sub DoIt() Implements CommonSnappableTypes.IAppFunctionality.DoIt Console.WriteLine("You have just used the VB snap in!") End Sub End Class


请注意,在 Visual Basic 的语法中应用属性需要尖括号(`< >`),而不是方括号(`[ ]`)。还要注意,`Implements`关键字用于实现给定类或结构的接口类型。

### 为 ExtendableApp 添加代码

最后要更新的项目是 C# 控制台应用(`MyExtendableApp`)。在将 MyExtendableApp 控制台应用添加到解决方案中并将其设置为启动项目后,添加对 CommonSnappableTypes 项目的引用,但*不是*`CSharpSnapIn.dll`或`VBSnapIn.dll`项目。

首先将位于`Program.cs`类顶部的`using`语句更新为:

using System; using System.Linq; using System.Reflection; using CommonSnappableTypes;


`LoadExternalModule()`方法执行以下任务:

*   将选定的程序集动态加载到内存中

*   确定程序集是否包含任何实现`IAppFunctionality`的类型

*   使用延迟绑定创建类型

如果找到实现`IAppFunctionality`的类型,调用`DoIt()`方法,然后发送给`DisplayCompanyData()`方法,输出反射类型的附加信息。

static void LoadExternalModule(string assemblyName) { Assembly theSnapInAsm = null; try { // Dynamically load the selected assembly. theSnapInAsm = Assembly.LoadFrom(assemblyName); } catch (Exception ex) { Console.WriteLine($"An error occurred loading the snapin: {ex.Message}"); return; }

// Get all IAppFunctionality compatible classes in assembly. var theClassTypes = theSnapInAsm .GetTypes() .Where(t => t.IsClass && (t.GetInterface("IAppFunctionality") != null)) .ToList(); if (!theClassTypes.Any()) { Console.WriteLine("Nothing implements IAppFunctionality!"); }

// Now, create the object and call DoIt() method. foreach (Type t in theClassTypes) { // Use late binding to create the type. IAppFunctionality itfApp = (IAppFunctionality) theSnapInAsm.CreateInstance(t.FullName, true); itfApp?.DoIt(); // Show company info. DisplayCompanyData(t); } }


最后一项任务是显示由`[CompanyInfo]`属性提供的元数据。如下创建`DisplayCompanyData()`方法。注意这个方法只有一个`System.Type`参数。

static void DisplayCompanyData(Type t) { // Get [CompanyInfo] data. var compInfo = t .GetCustomAttributes(false) .Where(ci => (ci is CompanyInfoAttribute)); // Show data. foreach (CompanyInfoAttribute c in compInfo) { Console.WriteLine($"More info about {c.CompanyName} can be found at {c.CompanyUrl}"); } }


最后,将顶级语句更新为以下内容:

Console.WriteLine("***** Welcome to MyTypeViewer *****"); string typeName = ""; do { Console.WriteLine("\nEnter a snapin to load"); Console.Write("or enter Q to quit: ");

// Get name of type. typeName = Console.ReadLine();

// Does user want to quit? if (typeName.Equals("Q", StringComparison.OrdinalIgnoreCase)) { break; } // Try to display type. try { LoadExternalModule(typeName); } catch (Exception ex) { Console.WriteLine("Sorry, can't find snapin"); } } while (true);


太棒了!这就结束了示例应用。我希望您可以看到,本章中介绍的主题在现实世界中可以相当有帮助,并且不限于世界的工具构建者。

## 摘要

反射是健壮的 OO 环境的一个有趣的方面。在的世界里。NET 核心,反射服务的关键围绕着`System.Type`类和`System.Reflection`名称空间。正如您所看到的,反射是在运行时将一个类型放在放大镜下以理解给定项目的谁、什么、哪里、何时、为什么以及如何的过程。

延迟绑定是创建一个类型的实例并调用其成员的过程,而事先不知道这些成员的具体名称。延迟绑定通常是动态加载的直接结果,它允许你以编程的方式将. NET 核心程序集加载到内存中。正如本章的可扩展应用示例所示,这是工具构建者和工具消费者使用的一种强大技术。

本章还研究了基于属性的编程的作用。当您用属性修饰您的类型时,结果是基础程序集元数据的增加。*