面向程序员的现代 Object Pascal 简介

2021-08-06 09:52:43

市面上有很多关于 Pascal 的书籍和资源,但其中太多谈论旧的 Pascal,没有类、单元或泛型。所以我写了这篇关于我称之为现代 Object Pascal 的快速介绍。大多数使用它的程序员并没有真正称其为“现代对象帕斯卡”,我们只是称其为“我们的帕斯卡”。但是在介绍该语言时,我觉得强调它是一种现代的、面向对象的语言很重要。自从许多人很久以前在学校学到的旧(Turbo)Pascal 以来,它已经有了很大的发展。在功能方面,它与 C++、Java 或 C# 非常相似。它具有您所期望的所有现代功能——类、单元、接口、泛型......它还有优秀的、可移植的和开源的编译器,称为 Free Pascal 编译器,http://freepascal.org/。还有一个名为 Lazarus http://lazarus.freepascal.org/ 的附带 IDE(编辑器、调试器、可视化组件库、表单设计器)。我自己是 Castle 游戏引擎 https://castle-engine.io/ 的创建者,这是一个开源 3D 和 2D 游戏引擎,使用现代 Pascal 在许多平台(Windows、Linux、macOS、 Android、iOS、Nintendo Switch;还有 WebGL 即将推出)。本介绍主要针对已经有其他语言经验的程序员。我们不会在这里介绍一些通用概念的含义,比如“什么是类”,我们只会展示如何在 Pascal 中实现它们。 {$mode objfpc} {$H+} {$J-} // 只需在所有现代源程序 MyProgram 中使用这一行; // 将此文件另存为 myprogram.lpr begin WriteLn( 'Hello world!');结尾。如果使用命令行 FPC,只需新建一个文件 myprogram.lpr 并执行 fpc myprogram.lpr。

如果您使用 Lazarus,请创建一个新项目(菜单 Project → New Project → Simple Program)。将其另存为 myprogram 并将此源代码粘贴为主文件。使用菜单项运行→编译进行编译。这是一个命令行程序,所以在任何一种情况下 - 只需从命令行运行编译的可执行文件。本文的其余部分将讨论 Object Pascal 语言,因此不要期望看到比命令行内容更花哨的东西。如果你想看到一些很酷的东西,只需在 Lazarus 中创建一个新的 GUI 项目(项目→新建项目→应用程序)。瞧——一个工作的 GUI 应用程序,跨平台,到处都有原生外观,使用舒适的可视化组件库。 Lazarus 和 Free Pascal Compiler 带有许多现成的单元,用于网络、GUI、数据库、文件格式(XML、json、图像……)、线程以及您可能需要的一切。我之前已经提到过我很酷的城堡游戏引擎:) {$mode objfpc} {$H+} {$J-} program MyProgram;过程 MyProcedure(const A: Integer); begin WriteLn(' A + 10 是:', A + 10);结尾;函数 MyFunction(const S: string): string;开始结果:= S + '字符串被自动管理';结尾; var X:单;开始 WriteLn(MyFunction('注:'));我的程序(5); // 使用 "/" 的除法总是产生浮点结果,使用 "div" 进行整数除法 X := 15 / 5; WriteLn(' X 现在是:', X); // 科学记数法 WriteLn( ' X 现在是: ', X: 1: 2); // 2 位小数结束。要从函数返回一个值,请为神奇的 Result 变量赋值。您可以自由地读取和设置 Result,就像一个局部变量一样。函数 MyFunction(const S: string): string;开始结果:= S + '东西';结果 := 结果 + ' 还有一些东西! ';结果 := 结果 + ' 等等! ';结尾;您还可以将函数名称(如上面示例中的 MyFunction)视为您可以分配的变量。但是我不鼓励在新代码中使用它,因为在赋值表达式的右侧使用时它看起来“可疑”。当您想要读取或设置函数结果时,请始终使用 Result 。

如果你想递归地调用函数本身,你当然可以这样做。如果您递归调用无参数函数,请务必指定括号 ()(即使在 Pascal 中您通常可以省略无参数函数的括号),这会递归调用无参数函数与访问此函数的当前结果不同。像这样:函数 SumIntegersUntilZero: Integer; var I:整数;开始阅读(I);结果:=我;如果我 <> 0 那么结果 := Result + SumIntegersUntilZero();结尾;您可以在过程或函数到达最终结束之前调用 Exit 来结束它的执行。如果您在函数中调用无参数 Exit,它将返回您设置为 Result 的最后一件事。您还可以使用 Exit(X) 构造来设置函数结果并立即退出 - 这就像类 C 语言中的 return X 构造一样。 function AddName( const ExistingNames, NewName: string): string; begin if ExistingNames = ' ' then Exit(NewName);结果 := ExistingNames + ' , ' + NewName;结尾;请注意,可以丢弃函数结果。任何函数都可以像过程一样使用。如果函数除了计算结果之外还有一些副作用(例如,它修改了一个全局变量),这是有道理的。例如: var Count: Integer; MyCount:整数;函数 CountMe:整数;开始公司(计数);结果:=计数;结尾;开始计数:= 10;把我算进去; // 函数结果被丢弃,但函数被执行,Count 现在是 11 MyCount := CountMe; // 使用函数的结果,MyCount 等于 Count,现在是 12 end。使用 if .. then 或 if .. then .. else 在满足某些条件时运行某些代码。与类 C 语言不同,在 Pascal 中,您不必将条件括在括号中。

var A:整数; B:布尔值;如果 A > 0 则开始做一些事情;如果 A > 0 则开始 DoSomething;并做更多的事情;结尾;如果 A > 10 则 DoSomething else DoSomethingElse; // 相当于上面的 B := A > 10; if B then DoSomething else DoSomethingElse;结尾;虽然上面嵌套 if 的示例是正确的,但在这种情况下,将嵌套 if 放在 begin ... end 块中通常会更好。这使得代码对读者来说更明显,即使你弄乱了缩进,它也会保持明显。该示例的改进版本如下。当您在下面的代码中添加或删除某个 else 子句时,很明显它将适用于哪个条件(适用于 A 测试或 B 测试),因此不太容易出错。逻辑运算符称为and、or、not、xor。它们的含义可能很明显(如果您不确定 xor 的作用,请搜索“exclusive or”:))。他们接受布尔参数,并返回一个布尔值。当两个参数都是整数值时,它们也可以充当按位运算符,在这种情况下,它们返回一个整数。关系(比较)运算符是 =、<>、>、<、<=、>=。如果您习惯了类 C 语言,请注意,在 Pascal 中,您使用单个相等字符 A = B 比较两个值(检查它们是否相等)(与在 C 中使用 A == B 不同)。 Pascal 中的特殊赋值运算符是:=。逻辑(或按位)运算符的优先级高于关系运算符。您可能需要在某些表达式周围使用括号以获得所需的计算顺序。 var A, B: 整数; begin if A = 0 and B <> 0 then ... // INCORRECT example 上面的编译失败,因为编译器首先要按位执行一个表达式:(0 and B)。这是一个逐位运算,它返回一个整数值。然后编译器应用 = 运算符,它产生一个布尔值 A = (0 和 B)。最后,在尝试比较布尔值 A = (0 和 B) 和整数值 0 后,出现了“类型不匹配”错误。

如果 MyFunction(X) 返回 false,则表达式的值是已知的(false 的值以及始终为 false 的值),并且根本不会执行 MyOtherFunction(Y)。类似的规则是 for 或 expression。在那里,如果已知表达式为真(因为第一个操作数为真),则不计算第二个操作数。即使 A 为零,这也能正常工作。关键字 nil 是一个等于零的指针(当表示为数字时)。它在许多其他编程语言中被称为空指针。如果应该根据某个表达式的值执行不同的操作,那么 case .. of .. end 语句很有用。 case SomeValue 为 0:DoSomething; 1:做别的事情; 2:开始IfItsTwoThenDoThis; AndAlsoDoThis;结尾; 3.. 10:DoSomethingInCaseItsInThisRange; 11、21、31:AndDoSomethingFor theseSpecialValues;否则 DoSomethingInCaseOfUnexpectedValue;结尾; else 子句是可选的(并且对应于类 C 语言中的默认值)。如果没有条件匹配,并且没有其他条件,则什么也不会发生。如果您来自类 C 语言,将其与这些语言中的 switch 语句进行比较,您会注意到没有自动失败。这在 Pascal 中是一种刻意的祝福。您不必记住放置中断说明。在每次执行中,最多执行一个 case 分支,仅此而已。

Pascal 中的枚举类型是一种非常好的不透明类型。您可能会比其他语言中的枚举更频繁地使用它:) 约定是在枚举名称前面加上一个两个字母的类型名称快捷方式,因此 ak = “动物种类”的快捷方式。这是一个有用的约定,因为枚举名称位于单元(全局)命名空间中。因此,通过给它们加上 ak 前缀,可以最大限度地减少与其他标识符发生冲突的机会。名称上的冲突并不是一个障碍。不同的单位定义相同的标识符是可以的。但无论如何尽量避免冲突是个好主意,以保持代码易于理解和 grep。您可以通过编译器指令 {$scopedenums on} 避免将枚举名称放置在全局命名空间中。这意味着您必须访问由类型名称限定的它们,例如 TAnimalKind.akDuck。在这种情况下不需要 ak 前缀,您可能只会调用枚举 Duck、Cat、Dog。这类似于 C# 枚举。枚举类型是不透明的这一事实意味着它不能仅分配给整数或从整数分配。但是,对于特殊用途,您可以使用 Ord(MyAnimalKind) 将 enum 强制转换为 int,或 typecast TAnimalKind(MyInteger) 将 int 强制转换为 enum。在后一种情况下,请确保首先检查 MyInteger 是否在良好的范围内(0 到 Ord(High(TAnimalKind)))。 type TArrayOfTenStrings = array [ 0.. 9] of string; TArrayOfTenStrings1Based = array [ 1.. 10] 的字符串; TMyNumber = 0.. 9; TAlsoArrayOfTenStrings = 字符串数组 [TMyNumber]; TAnimalKind = (akDuck, akCat, akDog); TAnimalNames = 字符串数组 [TAnimalKind];输入 TAnimalKind = (akDuck, akCat, akDog); TAnimals = TAnimalKind 集; var A: TAnimals;开始 A := []; A := [akDuck, akCat]; A := A + [akDog]; A := A * [akCat, akDog];包含(A, akDuck);排除(A,akDuck);结尾;

{$mode objfpc} {$H+} {$J-} {$R+} // 范围检查 - 非常适合调试 var MyArray: array [ 0.. 9] of Integer; I:整数; begin // 初始化为 I := 0 到 9 do MyArray[I] := I * I; // show for I := 0 to 9 do WriteLn(' Square is ', MyArray[I]); // 对 I := Low(MyArray) to High(MyArray) do WriteLn( ' Square is ', MyArray[I]); // 和上面一样 I := 0;当 I < 10 开始 WriteLn( ' Square is ', MyArray[I]);我 := 我 + 1; // 或 "I += 1", 或 "Inc(I)" end; // 和上面一样 I := 0; repeat WriteLn( ' Square is ', MyArray[I]);公司(一);直到我 = 10; // 与上述相同 // 注意:这里我枚举 MyArray 值,而不是 MyArray 中 I 的索引 do WriteLn( ' Square is ', I);结尾。循环条件具有相反的含义。在 while .. 你告诉它什么时候继续,但在重复 .. 直到你告诉它什么时候停止。在重复的情况下,在开始时不检查条件。所以重复循环总是至少运行一次。 for I := .. to .. do ... 构造它类似于类似 C 的 for 循环。但是,它受到更多限制,因为您无法指定任意操作/测试来控制循环迭代。这严格用于迭代连续数字(或其他序数类型)。您拥有的唯一灵活性是您可以使用 downto 而不是 to,使数字下降。作为交换,它看起来很干净,并且在执行上非常优化。特别是,在循环开始之前,下限和上限的表达式只计算一次。请注意,由于可能的优化,循环计数器变量(在本例中为 I)的值在循环完成后应被视为未定义。在循环之后访问 I 的值可能会导致编译器警告。除非您通过 Break 或 Exit 提前退出循环:在这种情况下,计数器变量保证保留最后一个值。 for I in .. do .. 类似于许多现代语言中的 foreach 结构。它可以在许多内置类型上智能地工作:

var 动物:TAnimals; AK:TAnimalKind;开始动物:= [akDog, akCat];对于动物中的 AK 做 ... {$mode objfpc} {$H+} {$J-} 使用 SysUtils,FGL;类型 TMyClass = I 类,正方形:整数;结尾; TMyClassList = 专门化 TFPGObjectList<TMyClass>;变量列表:TMyClassList; C:TMyClass; I:整数;开始列表 := TMyClassList.Create(true); // true = owns children try for I := 0 to 9 do begin C := TMyClass.Create; CI := I; C.Square := I * I; List.Add(C);结尾; for C in List do WriteLn(' Square of ', CI, ' is ', C.Square);最后 FreeAndNil(List);结尾;结尾。我们还没有解释类的概念,所以最后一个例子对你来说可能还不是很明显——继续,稍后会有意义:) 要在 Pascal 中简单地输出字符串,请使用 Write 或 WriteLn 例程。后者自动在末尾添加一个换行符。这是 Pascal 中的“神奇”例程。它需要可变数量的参数,它们可以有任何类型。它们在显示时都转换为字符串,使用特殊语法来指定填充和数字精度。 WriteLn('Hello world!');WriteLn('可以输出一个整数:', 3 * 4);WriteLn('可以填充一个整数:', 666: 10);WriteLn('可以输出一个浮点数: ', 圆周率: 1: 4);要在字符串中显式使用换行符,请使用 LineEnding 常量(来自 FPC RTL)。 (Castle Game Engine 还定义了一个较短的 NL 常量。)Pascal 字符串不解释任何特殊的反斜杠序列,因此编写

请注意,这仅适用于控制台应用程序。确保在主程序文件中定义了 {$apptype CONSOLE}(而不是 {$apptype GUI})。在某些操作系统上,它实际上无关紧要,并且始终有效(Unix),但在某些操作系统上,尝试从 GUI 应用程序写入内容是错误的(Windows)。在 Castle Game Engine 中:使用 WriteLnLog 或 WriteLnWarning,从不使用 WriteLn,打印调试信息。它们将始终指向一些有用的输出。在 Unix 上,标准输出。在 Windows GUI 应用程序上,日志文件。在 Android 上,Android 日志记录工具(使用 adb logcat 时可见)。 WriteLn 的使用应仅限于编写命令行应用程序(如 3D 模型转换器/生成器)并且您知道标准输出可用的情况。要将任意数量的参数转换为字符串(而不是直接输出它们),您有几个选项。您可以使用 IntToStr 和 FloatToStr 等专用函数将特定类型转换为字符串。此外,您可以在 Pascal 中简单地通过添加字符串来连接字符串。所以你可以创建一个这样的字符串:'My int number is ' + IntToStr(MyInt) + ',而 Pi 的值为 ' + FloatToStr(Pi)。优点:绝对灵活。有很多 XxxToStr 重载版本和朋友(比如 FormatFloat),涵盖了很多类型。它们中的大多数都在 SysUtils 单元中。另一个优点:与反向功能一致。要将字符串(例如,用户输入)转换回整数或浮点数,您可以使用 StrToInt、StrToFloat 和朋友(如 StrToIntDef)。缺点:许多 XxxToStr 调用和字符串的长连接看起来不太好。

Format 函数,类似于 Format('%d %f %s', [MyInt, MyFloat, MyString])。这就像类 C 语言中的 sprintf 函数。它将参数插入到模式中的占位符中。占位符可能会使用特殊语法来影响格式,例如 %.4f 会产生小数点后 4 位的浮点格式。优点:模式字符串与参数的分离看起来很干净。如果您需要在不触及参数的情况下更改模式字符串(例如在翻译时),您可以轻松完成。另一个优点:没有编译器魔法。您可以使用相同的语法在自己的例程中传递任意数量的任意类型的参数(将参数声明为 const 数组)。然后,您可以将这些参数向下传递给 Format,或者解构参数列表并使用它们执行任何您喜欢的操作。缺点:编译器不检查模式是否与参数匹配。使用错误的占位符类型将在运行时导致异常( EConvertError 异常,而不是像分段错误那样令人讨厌的东西)。 WriteStr(TargetString, … ) 例程的行为与 Write(… ) 非常相似,不同之处在于结果保存到 TargetString。优点:它支持 Write 的所有功能,包括用于格式化的特殊语法,如 Pi:1:4。缺点:格式化的特殊语法是“编译器魔术”,专门为这样的例程实现。这有时会很麻烦,例如您不能创建自己的例程 MyStringFormatter(... ) 也允许像 Pi:1:4 这样的特殊语法。出于这个原因(也因为它在主要的 Pascal 编译器中很久没有实现),这种结构不是很流行。

单元允许您将常见的东西(任何可以声明的东西)分组,供其他单元和程序使用。它们相当于其他语言中的模块和包。它们有一个接口部分,您可以在其中声明其他单元和程序可用的内容,然后是实现。将单位 MyUnit 保存为 myunit.pas(小写并带有 .pas 扩展名)。 {$mode objfpc} {$H+} {$J-} 单元 MyUnit;接口过程 MyProcedure( const A: Integer);函数 MyFunction(const S: string): string;执行程序 MyProcedure(const A: Integer); begin WriteLn(' A + 10 是:', A + 10);结尾;函数 MyFunction(const S: string): string;开始结果:= S + '字符串被自动管理';结尾;结尾。最终程序保存为 myprogram.lpr 文件( lpr = Lazarus 程序文件;在 Delphi 中,您将使用 .dpr)。请注意,其他约定在这里也是可能的,例如,有些项目只使用 .pas 作为主程序文件,有些使用 .pp 作为单位或程序。我建议将 .pas 用于单位,将 .lpr 用于 FPC/Lazarus 程序。一个单元也可能包含初始化和结束部分。这是程序开始和结束时执行的代码。一个单元也可以使用另一个单元。另一个单元可以用在接口部分,也可以只用在实现部分。前者允许在另一个单元的内容之上定义新的公共内容(程序、类型......)。后者更受限制(如果你只在实现部分使用一个单元,你只能在你的实现中使用它的标识符)。 {$mode objfpc} {$H+} {$J-} 单位另一个单位;接口使用类; { “TComponent”类型(类)定义在 Classes un ......