跳转至内容

.NET 开发基金会/系统类型主题

来自维基教科书,一个开放的世界中的开放书籍


系统类型和集合:主题


程序、对象和方法

[编辑 | 编辑源代码]

.NET 本质上是一套用于构建和执行计算机程序的工具。计算机程序是一组给定给计算机的指令,计算机自动执行这些指令以操作某些数据。.NET 遵循面向对象范式,这意味着指令和数据围绕表示事物或概念的“对象”进行分组。共享相同指令并操作相同类型数据的对象被分组到同一类型中。

面向对象程序是一系列类型的定义。对于每种类型,都会指定数据类型和指令。指令被分组在方法中。可用的指令之一是创建已定义类型的对象。另一种指令是请求执行与对象关联的方法。这称为调用方法。当调用方法时,其指令将被执行,直到方法终止(返回)。当方法返回时,调用(调用)方法将执行其下一个指令(语句)。

当您启动程序的执行时,系统将创建一个第一个对象并调用该对象的某个方法(这称为main方法)。在该方法中,通常会创建其他对象并调用这些对象的某些方法。这些方法将调用其他方法,并创建其他对象,依此类推。逐渐地,调用的方法将返回,并且“main”方法最终将完成,标志着程序执行的结束。

系统类型

[编辑 | 编辑源代码]

对于有经验的面向对象开发人员来说,本节将显而易见,但考试的一些具体目标与类型系统直接相关。

类型是一种对语言中的概念或对象进行分类的方法。这种分类的组织方式称为“类型系统”。类型系统还可以通过不同的方式对类型本身进行分类。

在 .NET 中对类型进行分类的第一种方法是在框架类库的一部分类型(系统类型)和由开发人员构建的类型(自定义类型)之间进行区分。

编写面向对象程序可以被视为定义一个或多个自定义类型。然后,这些类型被打包在某种执行单元中(在 .NET 的情况下为程序集)。程序集被编译,然后从某个入口点开始执行,该入口点将是某个自定义类型的指定方法。

这些自定义类型使用

  • 系统类型来执行“预编程”的指令序列
  • 其他自定义类型

系统类型也被打包在程序集中。自定义程序集必须引用系统程序集才能使用系统类型。

在 .NET 中还有其他方法可以对类型进行分类。其中一种方法是根据基于这些类型创建的对象映射到计算机内存的方式。这将为我们提供值类型和引用类型。

另一种方法是通过反射类别(类、值类型、接口、泛型等)。

另一种方法是区分由运行时直接支持的类型(内置类型)与在类库或自定义中定义的类型。

这些类别也可以相互交叉,这将为我们提供诸如“内置值类型”或“系统接口”之类的类型。当您遇到这些组合时,请注意所使用的分类。

命名空间是组织类型的常用方法,因此可以更容易地找到它们。有关命名空间的讨论,请参阅此处

在命名空间的上下文中,系统类型是包含在 System 命名空间或其子命名空间中的类型,而自定义类型(非系统类型)应使用其他命名空间。

要了解 Microsoft 如何描述 .NET 类型系统,请参阅MSDN。然后,要概述类库(系统类型),请参阅MSDN

事实上,考试的绝大部分内容都是基于如何使用类型库(系统类型)的常用部分。这就是为什么考试目标列表(以及本书的目录)如此之长的原因。

Hello world

[编辑 | 编辑源代码]

对于 .NET 的新手,您可能希望暂时从这里讨论的概念中休息一下,并确保您了解到目前为止所讨论的概念如何在非常简单的示例中使用。接下来的概念并非如此简单。我们将在此处放置这样一个示例。

值类型

[编辑 | 编辑源代码]

值类型表示类型系统中值/引用分类的一部分。

值类型的实例直接包含其数据(值)。例如,Int32 局部变量的内存直接分配在堆栈上。

值类型本身分为 3 个类别

  • 内置值类型
  • 用户定义的值类型
  • 枚举

请记住,内置类型是运行时直接支持的类型。

它们是任何程序的构建块,因为它们是机器指令最终作用的对象。其余部分本质上是这些类型的组合。

所有内置类型都是值类型,除了 Object 和 String。

内置值类型包括

  • 整数类型(Byte、SByte、Int16、Int32、Int64、UInt16、UInt32 和 UInt64)
  • 浮点类型(Single 和 Double)
  • 逻辑类型(Boolean)
  • 其他类型(Char、Decimal、InPtr 和 UInPtr)

所有内置值类型都在 System 命名空间中定义(例如 System.Int32),并且在 VB 和 C# 中都有代表它们的关键字(例如 C# 中的 int,VB.NET 中的 integer),除了 InPtr 和 UInPtr。

旁注

我们在上面提到过,类型分类有时会很令人困惑。例如,请注意,System.DateTime 在 培训套件(第 5 页)中被呈现为内置值类型,而 MSDN KB 中没有将其标识为错误。根据 官方规范(第 19 页),它不是内置类型。混淆之处在于,培训套件 没有明确区分系统类型(在类库中)和内置类型(运行时直接处理的基本类型)。

这里我们并非挑剔或贬低培训套件作者的工作。我们只是想指出,在开始编写代码之前,花几分钟时间明确区分这些概念可能很有价值。

所有值类型都直接或间接地从 System.ValueType 派生,对于内置类型和用户定义类型来说,它们直接从 System.ValueType 派生;对于枚举来说,它们则通过 System.Enum 间接派生。

枚举是一种为一组底层整型值(带符号或无符号)命名的方式。作为整型类型的限制,它们的作用与它们的底层类型相同。

用户定义的值类型和枚举都包含系统类型和自定义类型。

System 用户定义值类型的一个例子是 System.Drawing.Point,它用于绘图。

您可以使用特定的语言结构(C# 中的 struct,VB.NET 中的 Structure)来构建自定义值类型。

System 枚举的一个例子是 System.Data.CommandType 枚举,它指定表是文本命令、存储过程调用等。

您可以使用特定的语言结构(C# 中的 enum,VB.NET 中的 Enum)来构建自定义枚举。

有关使用值类型的示例和说明,请参见此 部分。另请参见 构建使用 用户定义值类型的示例。

引用类型

[edit | edit source]

引用类型代表类型系统中值/引用分类的另一部分。

与值类型相反,引用类型的实例不直接包含其数据(值),而是包含指向该值内存位置的某种引用。例如,String 局部变量在堆栈上分配了内存来存储对包含字符串的引用,而不是字符串本身。在这种情况下,字符串本身将在堆上分配并进行垃圾回收(稍后会详细介绍)。

有两个内置类型被认为是引用类型:Object 和 String,它们也在 System 库中被直接引用(例如 System.String),并且在 .NET 语言中拥有自己的结构(例如 C# 中的 string)。

引用类型本身分为四类:

  • 指针
  • 数组
  • 接口

我们不会在本书中讨论指针,因为没有任何考试目标涉及它们。

接下来的三个部分将介绍数组、类和接口。

要比较值/引用类型,请尝试 这个

有关使用引用类型的其他说明和示例,请参见 这个

数组

[edit | edit source]

数组本质上是一组对象,通常类型相同,可以通过索引访问。

要全面了解数组,您可以查看维基百科文章(在右侧)或访问关于 数据结构 的维基教科书。

数组曾经是编程语言中最常用的功能之一,因为您可以以非常高效的方式从数组中的一个项目跳转到下一个项目(如果您想了解更多信息,可以参考 C 指针和数组)。如今,随着“计算机能力”的显著提升,人们将注意力从数组转移到了集合。数组的两个主要问题是:

  • 它们是固定长度的
  • 它们只支持一种内部组织

集合解决了数组的大部分缺点(虽然代价不菲)。对于需要高效操作的固定组对象,数组仍然可以考虑使用。

我们将在 使用数组 部分提供一些示例。

类本身又分为三类:

  • 用户定义的类
  • 装箱的值类型
  • 委托

装箱的值类型将在后面的 装箱/拆箱 部分进行讨论。

委托也将在其 部分 中介绍。

用户定义的类是面向对象概念的基本实现。

关于类,有很多内容可以讨论,但由于没有考试目标涉及它们,因此我们假设读者已经熟悉该概念,并已使用过它们(在 .NET 或其他地方)。

您可以使用系统类(数百个甚至更多)和自定义类(您将在其中编写程序逻辑的本质)。

我们提供了 使用构建 类的示例。

接口

[edit | edit source]

如果您想真正弄清楚面向对象编程到底是什么,请查看维基百科关于多态性的文章:-)。

其思想是,能够使用单个类型的引用来引用具有“共同点”的不同类型中“共同部分”。

矩形和椭圆都是“形状”,那么我们如何才能让程序的一部分操作“形状”,而无需了解矩形或椭圆的具体情况呢?在这种情况下,我们说形状是多态的(字面意思是它们可以采用多种形式)。

在面向对象编程中,您可以通过继承获得这种行为。指向父类(Shape)的引用将毫无问题地操作子对象(矩形或椭圆)。

接口的开发是为了在基于组件的计算环境中获得相同的行为,在这种环境中,您无法访问源代码,因此无法使用继承。接口结构是 COM 中所有组件通信的基础。

接口是按照明确定义的方式(方法签名)实现一组方法的契约。如果您知道一个类实现了某个接口,那么您就知道可以使用任何定义的方法。

从这个定义可以很容易地想象拥有指向“接口实例”的引用,您可以从该引用调用任何接口方法。实际上,框架提供了这样一种机制。

现在假设,我们没有将 Shape 作为 Rectangle 的父类,而是仅仅有一个 Shape 接口,它由 Rectangle 和 Ellipse 同时实现。如果我有一个指向 Rectangle 对象的引用,那么我将能够将其“强制转换为”指向 Shape 接口的引用,并将其传递给只了解“Shape 对象”的程序部分。

这与我们通过继承获得的多态行为完全相同。

类库严重依赖接口,对该概念的清晰理解至关重要。

与继承相比,接口的一个有趣问题是,您没有获得默认实现(虚拟父方法),因此您必须重新编码所有内容。虽然存在技术和工具,但始终需要额外的工作。更多关于泛型的内容……

我们提供了 使用构建 接口的示例。一些 System 命名空间的接口在 标准接口 部分中有所展示。

属性

[edit | edit source]

现在,我们暂时中断对类型的讨论,来谈谈属性。首先,属性不是类型。它们是添加到程序元素(程序集、类、方法等)中的一些信息元素,用于在该元素的正常代码之外对其进行限定。

这些添加的“属性”用于对底层程序元素进行操作,而无需修改该元素的执行逻辑。

为什么我们要在执行代码之外操作程序元素?简短的答案是,面向对象的概念很难处理所谓的横切关注点,即应用于所有或大多数类的程序方面。此类方面的例子包括安全性、持久性、序列化等。与其修改每个类以添加序列化逻辑(这与业务规则无关),不如在类中添加序列化属性来直接控制这些类的序列化过程,而无需更改业务逻辑(执行代码)。

框架的许多功能都依赖于属性向程序元素添加信息。属性在编译过程中保留,并与程序集中的限定元素相关联。它们在运行时使用反射以编程方式进行分析,以调整“横切”功能的行为。

至于类型,我们有系统属性(框架的一部分)和自定义属性(由开发人员构建)。自定义属性的开发将在反射部分中进行处理。在此之前,我们将限制使用系统属性,并专注于定义它们以及了解它们如何确定框架的行为。

属性使用部分中,将提供一些关于如何使用简单系统属性的示例。

需要注意的是,属性并不是向程序添加“非执行”信息以修改其行为的唯一方式。例如,XML 配置文件可用于指定将影响程序执行的参数。我们将在本书的配置部分讨论这些内容。

有关 C# 中属性的讨论,请参阅MSDN

集合

[edit | edit source]

集合是一组对象。框架中包含许多类型的集合,涵盖了大多数处理对象组的情况。了解这些集合类型可以节省您重新编码等效逻辑的时间,并使您的程序更易于维护。

与数组不同,集合有多种类型,每种类型都有其特定的内部组织。每种类型的集合都与特定类型的问题相关联。我们将在给出每种类型集合的示例时指出问题的类型。

我们有两个部分包含集合示例

集合的主要缺点是,在“现实生活中”,分组在一起的对象通常具有某些共同特征(它们是相同类型、具有共同的父类型或支持共同的接口)。因此,大多数情况下,我们对对象了解的不仅仅是它们的组织方式。集合不允许我们使用这些知识来验证传递给集合的对象或适用于集合中所有对象的代码逻辑(没有强制转换和异常处理)。

泛型集合在框架的 2.0 版本中引入。它们在保持集合其他优势的同时解决了这个问题。因此,应尽可能使用泛型集合,而不是“普通”集合。

有关集合的一些外部链接:GotDotNetAspNetResources

有关数组、集合和数据结构的总体讨论,请参阅MSDN

泛型

[edit | edit source]

泛型编程或使用参数化类型不是面向对象的概念。从这个意义上说,泛型有点像属性,它们被添加到主流面向对象平台中,以处理面向对象技术难以解决的情况。

要开始我们的讨论,请阅读以下有趣的链接文章(附录),它介绍了泛型集合的背景下泛型的概念。这个外部教程也是如此。

泛型的概念很有趣,因为它将泛化概念应用于类型。类型本身是对象的泛化(面向对象编程的基础)。所以这里我们开始操作类型,即使用类型作为参数。

实际类型将在用特定类型替换参数类型时获得(例如,在声明该类型的变量时)。

// C#
List<int> myIntList = new List<int>()

'// VB.NET
Dim myIntList As New List(Of Integer)

在 .NET 框架中,这种替换是在第二次编译(从 CIL 到机器代码的即时编译)期间完成的。换句话说,泛型由 CIL 支持,因此是框架本身的一部分,而不是所用语言的一部分(例如 C#)。

有关泛型的更多内容,请参阅MSDN

我们有使用构建 泛型类型的示例。

有关泛型集合的示例,请参阅使用泛型集合部分。

异常

[edit | edit source]

以下是如何抛出一般异常

// C#
throw new Exception("This is an exception.");

'// VB.NET
Throw New Exception("This is an exception.")

以下是如何处理异常

// C#
try
{
   throw new Exception("This is an exception.");
}
catch (Exception e)
{
  Console.WriteLine(e.Message);
}

'// VB.NET
Try
  Throw New Exception("This is an exception.")
Catch ex As Exception
  Console.WriteLine(ex.Message)
End Try

事件和委托

[edit | edit source]

有关事件和委托的“官方”讨论,请参阅MSDN。我们这里将对所涉及的许多概念进行更一般的讨论。

第一个概念是委托,即类的部分功能可以在程序中的“其他位置”完成,可以委托出去。委托的重要好处是,“委托”对象不必知道委托功能的实际实现方式。一种“委托”方法是定义接口。如果一个对象引用了一个接口,它可以将部分功能委托给实现该接口的对象,而无需了解该对象的太多信息。

.NET 2.0 定义了另一种名为委托的引用类型,它以略微不同的方式实现了委托模式。委托是一种类型,它定义了对单个函数的引用,该函数必须与委托定义具有相同的签名。签名是函数参数类型及其返回类型的描述。与类一样,您可以创建该类型的对象。创建的对象是对函数的引用,可以将其分配(将引用设置为特定函数)、作为参数传递或执行(引用到的函数实际上被执行)。与接口不同,委托只定义一个函数,并且可以直接创建委托实例(无需另一个类实现接口)。

.NET 中的大多数委托都派生自多播委托。多播委托是一种委托,它维护一个指向具有与委托定义相同签名的函数的函数引用列表,而不是单个引用。可以使用 += 运算符向多播委托添加函数引用。当您执行多播委托时,每个引用的函数将依次执行。

如果一个对象来自实现多播委托成员的类,那么如果您拥有该对象的引用,就可以向多播委托“添加”您选择的函数的引用。如果该对象决定“执行”其委托成员,那么您添加了引用的函数将被执行。

此多播委托成员正是 .NET 中的事件。这就是为什么事件和委托几乎总是被一起讨论的原因。

定义事件的步骤是

  • 您将委托类型定义为指向具有特定签名的函数的引用
  • 您将 Event 成员添加到类,并将其与您刚定义的委托类型关联,这将实例化一个与事件关联的多播委托
  • 当您拥有该类的对象的引用时,您可以向任何具有与委托类型相同签名的方法添加引用。
  • 当该对象引发事件时,将执行关联的多播委托,从而触发对引用函数的执行。

大多数情况下,您将添加对您自己的方法之一的引用。当被引用的对象触发其事件时,它实际上会执行您的方法之一,而无需知道它。这种设置是发布/订阅模式的实现。您订阅某个事件,表示您希望在发生特定“事件”时得到通知。许多对象可以订阅同一个事件。当被引用的对象触发其事件时,它会“发布”一条消息,表明事件已有效地发生,形式是向所有“注册”的函数发出函数调用。

因此,任何类都可以定义事件。许多系统类定义事件,以向您的代码传达在执行环境中“发生了某些事情”的事实(鼠标移动、按键按下、套接字接收消息等)。构造一个“等待”事件发生然后对事件做出反应的程序被称为事件编程。大多数在线应用程序和服务都遵循这种设计。我们将在关于多线程的部分更详细地讨论捕获系统事件的方式。

有关事件和委托的示例,请参见本节.

华夏公益教科书