(delphi11最新学习资料) Object Pascal 学习笔记---第2章第三节(简单的自定义数据类型)

发布时间:2024年01月20日

Object Pascal 手册,Delphi 11 编程语言的完整介绍 作者: Marco Cantu 笔记:豆豆爸

2.3 简单的用户自定义数据类型

? 除了类型的概念,Niklaus Wirth 在最初的 Pascal 语言中引入的一个伟大想法是在程序中定义新的数据类型(我们今天认为这是理所当然的,但在当时并不明显)。你可以通过类型定义的方式定义自己的数据类型,如子界类型、数组类型、记录类型、枚举类型、指针类型和集合类型。类是最重要的用户自定义数据类型,它是面向对象语言功能的一部分,将在本书第二部分介绍。

? 如果你认为类型构造函数在许多编程语言中都很常见,那么你是对的,但 Pascal 是第一种以正式和非常精确的方式引入这种思想的语言。Object Pascal 仍有一些相当独特的功能,比如子界、枚举和集合的定义,这些将在下面的章节中介绍。第 5 章将介绍更复杂的数据类型构造函数(如数组和记录)。

2.3.1 已命名类型与未命名类型

? 用户定义的数据类型可以为以后使用的变量命名,也可以直接应用于变量。Object Pascal 的惯例是使用字母 T 前缀来表示任何数据类型,包括类但不限于类。我强烈建议你遵守这一规则,如果你来自 Java 或 C# 背景,即使一开始可能感觉不自然。

? 为类型命名时,必须在程序的 "type"部分进行(每个单元中可以添加任意多的类型)。下面是几个类型声明的简单示例:

type
          // 子界定义
          TUppercase = 'A'..'Z';
          // 枚举类型定义
          TMyColor = (Red, Yellow, Green, Cyan, Blue, Violet);
          // 集合定义
          TColorPalette = set of TMyColor;

? 有了这些类型,现在您就可以定义一些变量:

var
       UpSet: TUpperLetters;
       Color1: TMyColor;

? 在上述情况中,我使用的是命名类型。另一种方法是直接使用类型定义来定义变量,而不使用明确的类型名称,如下面的代码:

var
       Palette: set of TMyColor;

? 一般来说,应避免使用上述代码中的未命名类型,因为您不能将它们作为参数传递给例程或声明相同类型的其他变量。鉴于语言最终采用的是类型名称等价而非结构类型等价,因此对每种类型使用单个定义确实很重要。另外请记住,通过 uses 语句,可以在任何其他单元的代码中看到单元interface部分的类型定义。

? 上述类型定义是什么意思?我将为那些不熟悉传统 Pascal 类型结构的人提供一些说明。我还会尽量强调与其他编程语言中相同结构的差异,所以无论如何,你都可能有兴趣阅读下面的章节。

2.3.2 类型别名

? 正如我们所看到的,Delphi 语言在检查类型兼容性时使用的是类型名称(而不是实际定义)。两个定义相同但名称不同的类型就是两个不同的类型。

? 在定义类型别名(即基于现有类型的新类型名称)时,部分情况也是如此。令人困惑的是,同样的语法有两种变体,产生的效果却略有不同。请看 TypeAlias 示例中的代码:

type
       TNewInt = Integer;
       TNewInt2 = type Integer;

? 这两种新类型与 Integer 类型保持赋值兼容(通过自动转换);但是,TNewInt2 类型不会完全匹配,例如,它不能作为引用参数传递给期望使用别名类型的函数:

procedure Test(var N: Integer);
begin
end;

procedure TForm40.Button1Click(Sender: TObject); 
var
  I: Integer;
  NI: TNewInt;
  NI2: TNewInt2;
begin
  I := 10;
  NI := I; // 正常工作
  NI2 := I; // 正常工作
  Test(I);
  Test(NI);
  Test(NI2); // 错误

? 最后一行会产生错误:

E2033 Types of actual and formal var parameters must be identical

? 与类型助手一样,Integer 类型助手可用于 TNewInt,但不能用于 TNewInt2。这在后面讨论记录助手时有具体涉及。

2.3.3 子范围类型

? 子范围(subrange)类型定义了另一个类型范围内的值范围(因此称为子范围)。例如,您可以定义一个从1到10或从100到1000的字符类型的子范围,或者您可以定义一个只包含英文字符的字符类型的子范围,如下所示:

type
       TTen = 1..10;
       TOverHundred = 100..1000;
       TUpperCase = 'A'..'Z';

? 在子范围的定义中,您无需指定基本类型的名称。您只需提供该类型的两个常量。原始类型必须是序数类型,结果类型将是另一种序数类型。当您将变量定义为子范围时,然后可以将任何在该范围内的值分配给它。以下代码是有效的:

var
       UppLetter: TUpperCase;
begin
       UppLetter := 'F';

? 但这是无效的:

var
       UppLetter: TUpperCase;
begin
       UppLetter := 'e'; // 编译时错误

? 上述代码会导致编译错误,“常量表达式违反子范围边界”。如果您改为编写以下代码,编译器将通过编译:

var
         UppLetter: TUpperCase;
         Letter: Char;
begin
         Letter :='e';
         UppLetter := Letter;
end;

? 在运行时,如果启用了范围检查编译器选项(在 "项目选项 "对话框的 "编译器 "页面),就会如预期一样收到范围检查错误信息。这与我前面描述的整数类型溢出错误类似。

? 我建议你在开发程序时打开这个编译器选项,这样程序会更健壮,调试起来也更容易,因为如果出现错误,你会得到一个明确的信息,而不是一个不确定的行为。在最终编译程序时,你可以禁用该选项,这样程序运行速度会更快一些。不过 速度的提高几乎可以忽略不计,因此我建议将所有这些运行时检查都打开、 即使是在运行中的程序中。

2.3.4 枚举类型

? 枚举类型(通常称为 “枚举”)是另一种用户定义的序数类型。在枚举类型中,用户列出的不是现有类型的范围,而是该类型的所有可能值。换句话说,枚举是一个(常量)值列表。下面是一些示例:

type
         TColors = (Red, Yellow, Green, Cyan, Blue, Violet);
         TSuit = (Club, Diamond, Heart, Spade);

? 列表中的每个值都有一个相关的常量,从 0 开始。对枚举类型的值应用 Ord 函数时,就会得到这个 "基于零 "的值。例如,Ord(Diamond) 返回 1。

? 枚举类型可以有不同的内部表示法。默认情况下,Delphi 使用 8 位表示法,除非有超过 256 个不同的值,在这种情况下使用 16 位表示法。此外还有 32 位表示法,有时这种表示法对于与 C 或 C++ 库兼容非常有用。

注解: 您可以通过使用 $Z 编译器指令来更改枚举类型的默认表示法,即无论枚举中的元素数量多少,都要求使用较大的表示法。这是一种相当罕见的设置。

域枚举

? 枚举类型的特定常量值可以被视为全局常量,不同的枚举值之间可能存在命名冲突的情况。这就是语言支持作用域枚举的原因,这是一种您可以使用特定编译器指令 $SCOPEDENUMS 来激活的功能,并且需要您使用类型名称作为前缀来引用枚举值:

// 经典的枚举值
S1 := Club;
// 作用域枚举值
S1 := TSuit.Club;

? 当引入该功能时,为了避免破坏现有代码,保留了传统的默认编码样式。实际上,域枚举改变了枚举的行为,使其必须使用完全限定的类型前缀来引用。

? 拥有一个绝对名称来引用枚举值消除了冲突的风险,可以避免只使用枚举的初始前缀从而与其他枚举值区分开,这样即使写起来更长,也使代码更易读。

? 例如,System.IOUtils 单元定义了此类型:

{$SCOPEDENUMS ON}
type
TSearchOption = (soTopDirectoryOnly, soAllDirectories);

? 这意味着您不能将第二个值称为 soAllDirectories,而必须使用其完整名称引用枚举值:

TSearchOption.soAllDirectories

? FireMonkey 平台库也使用了大量域枚举,要求将类型作为实际值的前缀,而较早的 VCL 库一般采用更传统的模式。RTL 是采用了两者的混合体。

注解: Object Pascal 库中的枚举值通常在值的开头使用两个或三个类型的首字母,按惯例使用小写字母,如上例中的搜索选项(Search Options)的 “so”。在使用类型作为前缀时,这似乎有点多余,但考虑到这种方法的普遍性,我认为它不会很快消失。

2.3.5 集合类型

? 集合类型表示一组值,可用值的列表由集合所基于的序数类型指示。这些序数类型通常是有限的,通常用枚举或子界来表示。

? 如果我们让子界取值为 1 到 3,用 Pascal 符号写成 1…3,那么基于它的集合的可能取值包括只有 1、只有 2、只有 3、既有 1 又有 2、既有 1 又有 3、既有 2 又有 3、所有三个值或一个值都没有。

? 变量通常包含其类型范围的可能值之一。另一方面,集合类型变量可以不包含任何值,也可以包含一个、两个、三个或任意多个范围内的可能值。它甚至可以包含所有的值。

? 下面是一个集合的示例:

type
  TSuit = (Club, Diamond, Heart, Spade);
  TSuits = set of TSuit;

? 现在,我可以定义一个这种类型的变量,并为其分配一些原始类型的值。要表示集合中的某些值,可以写一个用逗号分隔的列表,并用方括号括起来。下面的代码显示了给变量赋值的情况,包括几个值、一个值和一个空值:

var
	Cards1, Cards2, Cards3: TSuits;
begin
	Cards1 := [Club, Diamond, Heart];
	Cards2 := [Diamond];
	Cards3 := [];

? 在 Object Pascal 中,集合通常用于指示多个非排他性标志。例如,基于集类型的值是字体样式。可能的值表示粗体、斜体、下划线和删除线字体。当然,相同的字体可以是斜体和粗体,没有属性,也可以具有全部属性。因此,它被声明为一个集合。

? 您可以在程序代码中为该集合赋值,具体方法如下:

Font.Style := []; // No style
Font.Style := [fsBold]; // Bold style only
Font.Style := [fsBold, fsItalic]; // Two styles active

集合运算符

? 我们已经看到,集合是一种非常具有Pascal风格的用户自定义数据类型。这就是为什么集合运算符值得特别介绍的原因。它们包括并(+)、差(-)、交(*)、成员测试(in)以及一些关系运算符。

? 要将元素添加到集合中,可以将该集合与另一个仅包含所需元素的集合合并。下面是一个与字体样式相关的示例:

// Add bold
Style := Style + [fsBold];
// Add bold and italic, but remove underline if present
Style := Style + [fsBold, fsItalic] - [fsUnderline];

? 作为替代方法,您可以使用标准的Include和Including过程,这要有效得多(但不能与set类型的组件属性一起使用):

Include(Style, fsBold);
Exclude(Style, fsItalic);
文章来源:https://blog.csdn.net/langfengyl/article/details/135615381
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。