Clipper 教程:开源 Clipper(s) 指南/基础语言教程
请注意:如果有人要贡献,Clipper 教程应该只包含可以在所有兼容 Clipper 的编译器上运行的“标准”代码。特定于编译器的代码应该放在其他地方,或者在框中明确标明。
这里的代码应该是简单的控制台模式应用程序,只使用最简单的输入/输出形式,并且只关注算法,因为创建和管理用户界面的方法将在第 5 章“创建用户界面”中描述。
让我们尝试突出显示 helloworld 的各个部分
function MAIN
* This is an example
clear
?"Hello, the weather is fine today"
return nil
(在 GeoCities 中,此代码以及所有其他代码来源都是由在线服务 CodeColorizer 在 http://www.chami.com/colorizer/ 突出显示的,或者,也可以使用 http://tohtml.com/Clipper/ 上的服务 - 它们看起来比 Wikibook 的突出显示要好一些)。
我们将对 helloworld 程序的每一行进行注释。
function MAIN
第一行定义了一个名为 MAIN 的函数。使用 Harbour 时,定义这样的函数不是强制性的,如果省略了它,编译时会发生错误,但我将保留它以确保示例在所有编译器上都能正常工作。在点提示符(hbrun/xbscript)下输入命令时,不能定义函数。
每种 xBase 语言都是不区分大小写的,这意味着以下所有行都是一样的
function MAIN FUNCTION main FuNcTiOn mAiN
当然,只有当你使用此特性来提高代码可读性时,它才有利。
我们将在后面学习如何定义和使用函数和过程。
* This is an example
第二行是注释。对程序进行注释有助于你在以后修改它们时更容易理解。如果代码没有注释,修改它们将是一项非常艰巨的任务。如果你要修改别人编写的代码,而他们没有进行注释,那么修改会更加困难:仅使用代码来弄清楚一个程序的功能非常困难,即使是最干净的编程语言,如果没有适当的注释,也可能会变得难以理解。
你可以使用多种不同的风格编写注释:使用星号 (*)、双斜杠 (//)、双和号 (&&) 或一对斜杠-星号 (/*) 和星号-斜杠 (*/), 如下所示
* This is a comment... // ...and so is this... && ...and this. /* This is an example of the fourth commenting style, which may span over several lines.*/
第二种和第四种注释风格来自 C 编程语言,第一种和第三种是 Clipper/xBase 标准特有的。xbscript 不接受任何这些注释,但 hbrun (Harbour) 接受最后三种。
clear
不,此命令的目的是不是为了使源代码清晰(那太容易了!编写清晰的源代码是我们自己的责任,为了做到这一点,我们必须对其进行良好的注释),而是为了清除屏幕。:-)你也可以使用 clear screen 或 cls,尽管这两个命令与 clear 不完全相同(区别将在我们能够GET到这一点的时候变得清晰 - 然后,你也可以欣赏这段话中的双关语 - 但更有可能不会)。
什么是命令?
Harbour 命令是通过 #command 宏指令定义的 (http://harbour.edu.pl/clipper/en/ng122d3c.html)。我们遇到的第一个命令是在文件 std.ch 中由以下行定义的
#command CLS => Scroll() ; SetPos(0,0) #command ? [<explist,...>] => QOut( <explist> )
? "Hello, the weather is fine today"
? 是一个命令,表示打印。在 BASIC 编程语言中,它也是其打印命令的缩写(xBase 的语法与 BASIC 编程语言非常相似)。
在这种情况下, ? 打印字符串“Hello, the weather is fine today”。请注意,字符串可以用单引号(或撇号)、双引号或方括号括起来:以下所有都是相同的
? "Hello, the weather is fine today" ? 'Hello, the weather is fine today' ? [Hello, the weather is fine today]
最后一行
return nil
Return 用于标记函数的结束。我们将在后面解释 return 到底做了什么。
如今,Harbour 不需要显式定义 main 函数/过程。helloworld 的以下版本可以编译,并且与前面的版本完全相同
&& hello world without commands and main
Scroll() ; SetPos(0,0)
QOut( "Hello world" )
Harbour 解释器和虚拟机
上面的程序存储在一个名为 hello.prg 的文本文件中,可以使用以下命令运行
hbrun hello.prg
hbrun 是 Harbour 的解释器。这意味着如果我们运行它,我们就会看到一个屏幕,屏幕的最后一行有一个“点提示符”,我们可以在那里输入并运行指令。例如,输入
? "Hello world"
将把 Hello world 打印到屏幕上,但只有在 hbrun 解释器本身关闭后才会打印。要退出解释器并打印问候语,请输入
QUIT
总而言之,hbrun 允许你体验在旧的 dBase “点提示符”模式下是如何处理数据库的 - 也就是像这样:http://manmrk.net/tutorials/database/dbase/IntrodBASEIIIPlus.htm
在包含以下行的 printA.prg 上运行编译器 harbour(Windows 上的 harbour.exe)
? "A"
将输出一个 printA.c 文件。该文件的底部内容如下
HB_FUNC( PRINTA )
{
static const HB_BYTE pcode[] =
{
36,1,0,176,1,0,106,2,65,0,20,1,7
};
hb_vmExecute( pcode, symbols );
}
也就是说,作为中间步骤,Harbour 编译器会编译 pcode,或用于虚拟机的 bytecode。
如果编译是用 /gh 选项完成的
hbmk2 /gh printA.prg harbour /gh printA.prg
结果将是一个名为 printA.hrb 的文件,这是一个“Harbour 可移植对象”文件,可以使用以下命令执行
hbrun printA.hrb
这样,hbmk2 和 hbrun 就像一对工具,与 Java 编译器 javac 和 Java 解释器 java 完全等效。
顺便说一下,虚拟机并不是一个新概念。如果我们看一下复古计算领域,我们会发现,在 20 世纪 70 年代后期和 80 年代初期,加州大学圣地亚哥分校有一个基于 pcode 的可移植操作系统,即 UCSD P-System,它是用 UCSD Pascal 编写的,可以在 6502、8080、Z-80 和 PDP-11 上运行 http://www.threedee.com/jcm/psystem/index.html(它实际上是 MS-DOS、PC DOS 和 CP/M 的竞争对手 - http://www.digitalresearch.biz/CPM.HTM 和 http://www.seasip.info/Cpm/index.html)。
此外,在 1979 年,Joel Berez 和 Marc Blank 开发了 Z-machine 作为运行 Infocom 的文字冒险游戏的虚拟机,这个名字毫不掩饰地表明,他们主要是在考虑 Zork。Z-machine 的编程语言被称为 ZIL (Zork Implementation Language)。ZIL 到 Microsoft .NET 的现代编译器是 ZILF https://bitbucket.org/jmcgrew/zilf/wiki/Home,并且有一个 Z-machine.NET 可用 https://zmachine.codeplex.com/。更多信息请访问 http://inform-fiction.org/zmachine/index.html。
除了 JVM (Java 虚拟机) 之外,Microsoft .NET CLR (公共语言运行时) 也是另一个现代的虚拟机。
hbmk2 是为了支持所有 shell、所有平台上的所有编译器而创建的,也是为了替代旧的 'bld.bat' 解决方案,同时保持与现有 hbmk 脚本功能和选项的兼容性。
有关如何在不使用 hbmk2 的情况下获得可执行文件的相关信息,可以在 http://www.kresin.ru/en/hrbfaq.html 上找到。
请记住,必须更新 PATH 环境变量(在 Windows 中)以包含包含编译器的目录:在命令提示符窗口中本地使用命令 SET PATH=C:\hb32\bin;%PATH% 或全局更新它:“开始”→“控制面板”→“系统”→“高级系统设置”→“环境变量...”→ 选择“路径”,然后单击“编辑...”
Clipper 支持的数据类型列表如下:
- A Array(数组)
- B (Code) Block(代码块)
- C Character(字符)
- D Date(日期)
- L Logical(逻辑)
- M Memo(备注)
- N Numeric(数值)
- O Object(对象)
- U NIL(空值)
这些字母可以用作变量名的前缀,以便“一目了然”地指示其类型。这样,oBox 就是一个对象的名称,aNames 是一个数组,dBirthday 是一个日期,等等。逻辑变量的另一种选择是添加 is 前缀,例如 isOk 或 isExisting(比 lOk 和 lExisting 更具可读性)。这种命名约定称为 匈牙利命名法,它也适用于函数以及它们的概要:ACOPY( <aSource>, <aTarget>, [<nStart>], [<nCount>], [<nTargetPos>] ) 是将数组元素从一个数组复制到另一个数组的函数,它接受两个数组 aSource 和 aTarget,以及三个可选的数值参数 nStart、nCount 和 nTargetPos;CToD(cDate) 接收一个字符参数并将其转换为日期,DBCreate() 是一个创建数据库的函数。
Memo 类型只能在数据库中使用(它是指向 DBF 表的辅助文件的链接,如 维基百科 DBF 条目 中所述)。
以下是 VALTYPE() 函数返回的结果列表。TYPE() 函数也返回:
- U NIL、局部或静态
- UE 语法错误
- UI 不确定的错误
Harbour 有一个更长的列表 (http://www.fivetechsoft.com/harbour-docs/harbour.html)
- HB_ISARRAY
- HB_ISBLOCK
- HB_ISCHAR
- HB_ISDATE
- HB_ISDATETIME
- HB_ISHASH
- HB_ISLOGICAL
- HB_ISMEMO
- HB_ISNIL
- HB_ISNULL
- HB_ISNUMERIC
- HB_ISOBJECT
- HB_ISPOINTER
- HB_ISPRINTER
- HB_ISREGEX
- HB_ISSTRING
- HB_ISSYMBOL
- HB_ISTIMESTAMP
例如,ISPOINTER() (http://www.marinas-gui.org/projects/harbour_manual/ispointer.htm) 被标记为:CA-Cl*pper 中不可用。它与 HB_IT_POINTER 和 HB_ISPOINTER 一起用于 Harbour 的内部机制(C 级 API:http://www.fivetechsoft.com/harbour-docs/clevelapi.html#ispointer)。
让我们来看一个展示最常用数据类型的小程序。
我想在这个例子中展示 π 的计算而不是 √2 的计算,但是 xBase 缺少 ArcTan 函数。我们可以通过从外部库导入它或者自己提供它来解决这个问题。(这两种方法都应该在这个教程中进行研究)。
最后两种数据类型与前面几种略有不同:“Memo” 在数据库外部使用时不是很有用,而数组不能在数据库中使用。
&& example of compilation command
&& A:\>c:\hb32\bin\hbmk2 -quiet -oc:\hbcode\test.exe test.prg -run
&& please note that this example has not a MAIN function
&& an example of string concatenation
? "Hello" + " " + "World!"
&& let us do now a few numerical computations, integer numbers are a good starting point
? 5+13
? 12*8
? 3/2
SET DECIMALS TO 15
sqrt2=sqrt(2) && computing the square root of 2...
? sqrt2 && ... and printing it
&& ? caporetto
&& if the line above was not commented, the compiler would issue the two following lines
&& Error BASE/1003 Variable does not exist: CAPORETTO
&& Called from TEST(8)
&& as xBase is not good at history, let us revise it: http://www.historyofwar.org/articles/battles_caporetto.html, http://www.firstworldwar.com/battles/caporetto.htm
&& we can then assign the correct date to this war
caporetto := ctod("10-24-1917")
a := date() && system date
? a + 1 && will print tomorrow's date
? a - caporetto && this will tell us how many days have passed since Caporetto's battle (difference between two dates)
SET DECIMALS TO 2
? (a - caporetto) / 365
?? " years have passed since Caporetto's battle"
? 1+1=3
&& it will print ".F.", that is, "FALSE" (of course...)
&& The following two instructions should be discussed to expand on the subject of operator precedence
? 1/2+2^3*sqrt(25)-3^2
? 1/2+2^3*(sqrt(25)-3^2)
SQR 函数已从 Harbour 中删除,并由 SQRT 替换。尝试使用它会导致链接错误“undefined reference to 'HB_FUN_SQR'”。
从下面的例子(它可以运行)我们可以看出,Harbour 是一种 弱类型 编程语言:一个变量,例如我们例子中的 a,可以是数字,然后变成文本字符串。
a := 1+1
? a
a := "abc"
? a
存在一个问题:如果变量类型发生了改变,而程序员没有意识到这一点,他可能会发出一些指令,在运行时会导致错误。如果我们在上面的程序中添加另一行代码
?a+1
,Harbour 会尝试将数字 1 添加到字符串 "abc" 中,结果将是错误
错误 BASE/1081 参数错误:+.
字符串本质上是一个字符列表。它们用双引号括起来(例如,"Hello")。请注意区别
123 => a number "123" => a string
让我们看看如何使用 Stuff() 函数来更改字符串中的字符
sTest:="fun"
?sTest, STUFF(sTest,2,1,"a")
&& => fun fan
以下是处理字符串时最常用的函数列表
Lower() Convert uppercase characters to lowercase Upper() Converts a character expression to uppercase format Chr() Convert an ASCII code to a character value Asc() Convert a character to its ASCII value Len() Return the length of a character string or the number of elements in an array At() Return the position of a substring within a character string
然而,在 Clipper 和 Clipper 工具库中,也可以找到非常奇特的函数,例如这些函数
AsciiSum() Finds the sum of the ASCII values of all the characters of a string Soundex() is a character function that indexes and searches for sound-alike or phonetic matches CHARONE() Search and remove repeating characters CHARONLY() Remove non-standard characters CHAROR() "OR" bytes in a string
让我们看看我们可以用这些函数做什么。
我们展示了与用户按下的键对应的 ASCII 码 (http://www.asciitable.com/),在按下 ESC 键时结束程序。
#include "Inkey.ch"
DO WHILE .T.
WAIT "" TO cChar
?? " ", Asc( cChar )
IF ( LastKey() == K_ESC )
EXIT
ENDIF
ENDDO
inkey.ch 是 Inkey() 函数的 eader 文件,它用于从键盘缓冲区获取以下键码。
数据类型的第一种分类将它们分为 简单(或 标量)类型和 结构化(或 复合、聚合或 复合)类型。
在 xBase 中,简单数据类型 是数字、逻辑值、字符串和日期,即可以在数据库表中作为字段类型的类型。备注不是,因为它们是指向外部文件的指针。
标量数据类型可以组合起来构建结构化数据类型:数组、关联数组(哈希表)、表或数据库(它们是存储在磁盘上的“数据类型”)。w:Primitive_data_type
代码块和对象是特殊数据类型。
&& First we print the values of two expressions with the exponentiation operator
? 3**3**3 && is 19683
? 3**(3**3) && is 7625597484987
&& Then we let our compiler tell us if they are the same
? 3**3**3 = 3**(3**3) && result is .F.
&& We've a look at how our compiler deals with overflow and underflow...
? 9**9**9
&& ***********************
? 1/9**9**9
&& 0.00
? -(9**9**9)
&& ***********************
在这个例子中,我们看到幂运算符 ** 是左结合的(与数学中幂运算为右结合运算相反)。
3**3**3 和 3**(3**3) 给出两个不同的结果,这意味着括号改变了运算符优先级:括号内的东西先计算。
即使运算符是左结合的,9**9**9 的结果也是一个相当大的数字(确切地说,是 196627050475552913618075908526912116283103450944214766927315415537966391196809,你可以很容易地用一些任意精度计算器程序(如 GNU bc,它在每个 Linux 发行版中都可用,也可以从 http://gnuwin32.sourceforge.net/packages/calc.htm 下载到 Windows)或手工计算来验证),它超出了 Harbour 为一个数字分配的资源(这称为溢出),因此它会打印 23 个 * 来表示这个错误。如果我们尝试计算它的倒数(这称为下溢),它也会给出零(0.00)。溢出和下溢错误发生在计算结果分别太大或太小,无法用编译器为我们尝试将结果存储的变量分配的位数来表示。
日期也是如此。那么
date()-1000000
(从当前日期减去 100 万天)的结果是什么?让我们看看
&& today my outputs are:
?date()
&& ==> 05/15/19
?date()-365000 && more or less 1000 years
&& ==> 01/13/20
set century on && we change an option...
?date()-365000 && ... and try ageing
&& ==> 01/13/1020
?date()-1000000 && more or less 2700 years (i.e. the results should be around 700 B.C.)
&& ==> 00/00/00
?ctod("31-12-9999")+1
&& ==> 00/00/00
在这里,最后两个输出("00/00/00")表示无效日期,就像上面看到的 23 行星号表示无效的数字结果一样。可接受的日期是那些可以存储在 dBase 表(即 .dbf 文件)中的日期,在这些文件中,日期是一个 8 个字符的字符串,格式为 "YYYYDDMM"(4 个字符用于年,2 个字符用于月,2 个字符用于日)。因此,负年(公元前)或超过 5 位数的年是不被接受的。
Harbour 使用 SET 命令更改其行为之前,只会显示两位数的年份,并且在使用这些大年份时会给出错误的结果。原因很简单:在数据库应用程序(库存管理、预订系统)中,我们无需处理几个世纪的时间间隔,而 Harbour 处理日期的例程在尝试这样做时无法正常工作。在其他领域,例如 w:Archaeoastronomy 情况就是如此,那些处理这些问题的人(例如,检查公元前 2000 年卡纳克冬至日出的精度或巨石阵的方位)会使用他们自己的特殊例程来计算日期。
在使用计算机进行计算时,还有一个难题。看看
store 7 to n && an alternate way of an assignment (dates back to dBase II)
?sqrt(sqrt(n))^2^2=n
&& the result is .F.
set decimals to 15
?sqrt(sqrt(n))^2^2
&& 7.000000000000003
现在,如果我们从一个数字中提取两次平方根,然后将结果平方两次,我们应该得到起始数字。事实上,计算机并非如此精确,检查两个浮点数是否相等可能会产生意外的结果。
如果你想了解更多信息,可以看看每个计算机科学家应该了解的关于浮点数运算的知识,地址为 https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html(它是 Sun Microsystems 一本书《数值计算指南》的一部分),以及十进制算术常见问题解答,地址为 http://speleotrove.com/decimal/decifaq.html。
幸运的是,xBase 语言中使用的变量的精度以及在常见的商业应用程序中,导致精度损失的平方根、对数和其他数学运算相当罕见,这些情况在一定程度上减轻了这个问题。
SET OPTIONS
我们已经看到了 SET DECIMALS TO n 和 SET CENTURY ON/OFF 命令。但实际上,有很多选项可以设置。SETMODE(<nRows>, <nCols>) 函数在使用 hbrun 或 xbscript 时特别有用:它的目的是确定窗口的大小。
我们可以打印比较的结果,它是一个布尔值(表示为 .T. 或 .F.),但是它们与特殊的单词一起使用,以分支或重复执行一条或多条指令。
当布尔表达式的结果为真(其 xBase 符号为 .T.)或假(.F.)时,我们就有一个布尔表达式。获得布尔结果的最简单方法是使用比较(关系)运算符:=,==, !=,<>,#,>,<,>= 和 <=. 始终记住比较运算符 =(等于)、==(完全等于)和赋值运算符 := 之间的区别。
实际上,布尔表达式的“真正”(数学)定义与上一段中所述的不同。一个“真正”的布尔表达式只包含两个布尔值 .T. 和 .F. 以及布尔运算符(.AND.、.OR.、.NOT.)。
布尔代数自 1937 年克劳德·香农的著作以来就被应用于电子数字电路。请参阅 http://www.allaboutcircuits.com/textbook/digital/chpt-7/introduction-boolean-algebra/ (以及该在线教科书的后续页面)以了解布尔代数在电路(包括微处理器)中应用于位时的含义。在这里,我们正在更高层次上使用它。
考虑《哈姆雷特》第三幕第一场:“[…]生存还是毁灭?这是一个问题—— 究竟哪样更崇高,在心智中忍受 残酷命运的冷箭毒矢……”。如果王子哈姆雷特问布尔(但他不可能,因为这发生在 14 世纪或 15 世纪,而布尔出生于 1815 年),他的回答肯定会让他困惑。为什么呢?
to_be=.T. && is it true?
?to_be .OR. .NOT. to_be
&& .T.
to_be=.T. && is it false?
?to_be .OR. .NOT. to_be
&& .T.
正如哈伯告诉我们的,在布尔代数中,答案是“真”。请查看此页面:https://vicgrout.net/2014/07/18/to-be-or-not-to-be-a-logical-perspective/.
已经开发出许多图形化方法来查看程序的流程控制是如何转移的:这些方法包括 **流程图** (https://www.lucidchart.com/pages/what-is-a-flowchart-tutorial)、**数据流图** (https://www.lucidchart.com/pages/data-flow-diagram)、**Nassi-Shneiderman 图** 以及迈克尔·A·杰克逊的 **JSP** 和 **JSD** 图(这几乎肯定不是你想到的第一个迈克尔·杰克逊)。
以下示例从键盘获取一个数字并将其打印出来。如果数字为 0,它会对该值进行注释(我将此示例作为典型的 _虚无主义_ 编程)。流程图显示了为什么 _条件执行_ 有时被称为 _分叉_。
Function MAIN()
LOCAL number
INPUT "Key in a number: " TO number
IF number = 0
?? "Congratulations, you keyed the fabolous number "
ENDIF
? number
RETURN
此示例打印两个不同的注释,无论输入数字除以 2 的余数是否为 0。
Function MAIN()
LOCAL number
INPUT "Key in a number: " TO number
IF number % 2 = 0
? "You keyed in an even number"
ELSE
? "You keyed in an odd number"
ENDIF
RETURN
此示例表明可以执行多个命令。
Function MAIN()
LOCAL number
INPUT "Key in a number: " TO number
IF number % 2 = 0
? "You keyed in an even number"
? "I can prove it:"
? "the result of ",number," % 2 = 0 is ", number % 2 = 0
ELSE
? "You keyed in an odd number"
? "I can prove it:"
? "the result of ",number," % 2 = 0 is ", number % 2 = 0
ENDIF
RETURN
此示例添加了另一个关键字 ELSEIF,以表明选择并非一定是二分的。这是一个链式条件。只有一个分支将被执行。
function MAIN
LOCAL nNumber := 0
//
INPUT "Key in a number: " TO nNumber
//
?? "Your number is "
IF nNumber < 50
? "less than 50"
ELSEIF nNumber = 50
? "equal to 50"
ELSE
? "greater than 50"
ENDIF
以下示例检查系统时间是否早于 18 点(下午 6 点),并根据一天中的时间打印相应的问候。
? Time()
// CToN source is numconv.prg, library is libct.
IF CToN( SubStr( Time(), 1, 2 ) ) < 18
? "Good day"
ELSE
? "Good evening"
ENDIF
有三个逻辑运算符:.AND.、.OR. 和 .NOT.
这是一个具有较长条件的示例,使用 .OR. 运算符协调更多比较。该示例非常愚蠢,但它展示了如何使用分号在下一行继续执行指令。
WAIT "Key in a letter: " TO char
IF char = "a" .OR. char = "A" .OR. char = "e" .OR. char = "E" .OR. ;
char = "i" .OR. char = "I" .OR. char = "o" .OR. char = "O" .OR. ;
char = "u" .OR. char = "U"
? char, " is a vowel"
ELSE
? char, " is not a vowel"
ENDIF
但是,使用子字符串比较运算符 **$**(或正则表达式,我们将在后面学习的更高级主题)可以更简洁地实现相同的功能。
WAIT "Key in a letter: " TO char
IF char $ [aeiouAEIOU]
? char, " is a vowel"
ELSE
? char, " is not a vowel"
ENDIF
DO WHILE、EXIT、LOOP、ENDDO 是用于 _在条件为真(即其结果为 .T.)时重复执行一系列语句(循环体)_ 的关键字。
循环有几种形式:**先测试循环**、**后测试循环** 以及 **确定迭代**,后者通过 _计数控制循环_(通常称为 for 循环)实现。在实践中,我们也可能有一个 **中测试循环**,但没有特定的语法...我们需要在现有的循环体中添加一个 EXIT 语句来获得这个循环。我们将键入的下一个程序对数学家来说很有趣(真正的程序员不怕数学,你知道这个著名的格言吗?)
这里就是它
&& Function MAIN LOCAL number, sum, n
&& An anonymous contributor renamed the variable "num" into "number", increasing this short program readability, but the line above would give
&& Error E0030 Syntax error "syntax error at 'LOCAL'"
Function MAIN
LOCAL number, sum, n
CLS
? "Let's sum up the first n odd numbers."
INPUT "How many numbers shall I sum up? " TO n
sum=0
number=1
DO WHILE number <= 2*n
sum=sum+number
number=number+2
ENDDO
? "The sum requested is ", sum
如您所见,此循环语句类似于 IF 语句:它们都以 END 对应语句结尾,它们都包含一个逻辑表达式。
此循环语句将持续执行,直到其条件保持为真(将评估为 .T.)。
它重复执行的两个指令是 sum=sum+num 和 num=num+2。第二个是根本性的:如果没有它或它出错(例如,如果键入了 num=num/2),条件将不会评估为 .F.,程序将不会停止执行(这被称为无限循环)。当这种情况发生时,同时按下 Ctrl 和 C 键。这应该说服计算机将注意力转移到你身上,而不是运行循环。
上面的程序很好地说明了如何使用一个毫无创造力的机械计算器来解决求解前 n 个奇数之和的问题。有关其创造性方法的说明可以在以下地址找到:http://betterexplained.com/articles/techniques-for-adding-the-numbers-1-to-100/(以及关于高斯在小学炫耀的典型轶事)。
WHILE 循环语句被称为在循环头部具有控制表达式,与 Pascal 的 REPEAT-UNTIL 循环语句(后测试循环)相反,后测试循环在循环尾部具有条件控制(这些是意大利计算机科学俚语中的先测试循环和后测试循环,它们很有趣,不是吗?)。xBase/Clipper 如何说 REPEAT-UNTIL?它没有。以下是如何模拟它(从诺顿指南中复制和粘贴)
LOCAL lMore := .T.
DO WHILE lMore
loopbody
lMore := condition
ENDDO
如您所见,我们首先将一个变量设置为 true,进入循环,并在循环末尾指定条件,以确保其主体至少执行一次。
这是一个示例,等效于 tutorialspoint.com 中的 pascal_repeat_until_loop 页面上的示例。
LOCAL a := 10
LOCAL lMore := .T.
DO WHILE lMore
? 'value of a: ', a
a := a + 1
lMore := ( a != 20 )
ENDDO
但是上面的代码更改了条件。如果我们不想发生这种情况,我们可以简单地否定我们在 Pascal 代码中使用的条件
然后变为
LOCAL a := 10
LOCAL lMore := .T.
DO WHILE lMore
? 'value of a: ', a
a := a + 1
lMore := .NOT. ( a = 20 ) && or, in alternative, lMore := ! ( a = 20 )
ENDDO
现在让我们看看这种类型的循环
DO WHILE .T.
? "I won't stop."
ENDDO
只要“真”为真,此循环就会打印“我不会停止”。因此,它被称为无限循环,因为结束条件永远不会满足。这是计算机科学课程中研究的第一个 _**控制缺陷**_ 示例。根据定义,每次您指定一个同义反复作为要检查的条件时,您都会陷入无限循环。检测到这个和其他控制缺陷和错误并不总是很明显——事实上,这是一个涉及使用称为 **调试器** 的特定软件的过程。
在本节中,我们将学习如何使用 FOR 循环打印乘法表,如何嵌套循环,如何格式化控制台屏幕上的输出以及缩进代码以提高可读性的重要性。
FOR...NEXT 结构(计数控制循环)在我们知道希望循环体执行多少次时很有用。我们现在将使用它来打印并修改乘法表。
CLEAR
FOR I := 1 TO 8
FOR J := 1 TO 8
?? I*J
NEXT
NEXT
(请注意,在此示例中,我们在另一个循环的主体内部有一个循环:这被称为嵌套循环)。好吧,这可以工作,但它的输出不太好……此外,我们习惯上将乘法表打印到 10,因为我们使用的是十进制计数系统……让我们再试一次!
CLEAR
FOR I := 1 TO 10
FOR J := 1 TO 10
@i,j*4 SAY I*J PICTURE "999"
NEXT
NEXT
这样我们就可以更漂亮地打印它……顺便说一句,在嵌套循环和分支语句时,为了使程序更易于阅读,对缩进进行排列非常重要……在绿洲中,有 “源代码美化器”,例如 dst314.zip。 Harbour 为我们提供了 hbformat 工具,https://vivaclipper.wordpress.com/tag/hbformat/.
PICTURE "999" 是指定我们想要输出格式的指令——正如我们指定了三个数字,输出中的数字将像它们是三位数一样打印,因此在我们的乘法表中对齐它们。
作为嵌套循环的第二个示例,以下是我编写过的最令人讨厌的程序之一。它要求用户提供意见,然后始终回复其相反。它甚至在用户说他还有话要告诉他的时候退出,并在用户说他没有什么要补充的时候继续要求输入。由于程序很难回答用户断言的相反,因此此程序只是使用 SubStr 函数将输入字符串反转打印出来(SubStr(<cString>, <nStart>, [<nCount>]) --> cSubstring - 从字符字符串 cString 中提取子字符串,在索引 nStart 处,长度为 nCount)。
用户输入通过 ACCEPT 命令(COBOL 的回忆)获得,控制流程使用 DO WHILE 循环(来自 FORTRAN)和 FOR ... TO ... STEP 循环(来自 BASIC)。
LOCAL cContinue := "N"
DO WHILE cContinue = "N"
ACCEPT "What's your opinion? " TO cOpinion
?""
?? "NO!! According to me, on the contrary: "
FOR i := Len( cOpinion ) TO 1 STEP -1
??SubStr( cOpinion, i, 1 )
NEXT
WAIT "Tell me, do you have any other opinions to discuss with me? (Y/N) " TO cContinue
cContinue := Upper( cContinue )
IF cContinue = "N"
? "Ok, then. "
ELSE
? "In this case let me go."
ENDIF
ENDDO
我们如何处理如此尴尬的客户?只需尝试输入 " _remmargorp revelc yrev a era uoy_ ",解决方案就清晰了。在意大利,有一种表达方式来称呼总是与他的对话者唱反调的人:他被称为“bastian contrario”。他们的工作方式就像这个程序一样。
考虑这段简短的代码
i = 2
myMacro = "i + 10"
i = &myMacro && will execute "i+10"
? i && will print "12"
& 是一个运算符,它将评估(运行时编译)表达式,并允许在字符串中进行文本替换(请参阅 https://harbour.github.io/ng/c53g01c/ng110bc2.html)。让我们使用它来制作一个简单的计算器。
ACCEPT "Please enter the first number: " TO cNumber1
ACCEPT "Please enter the second number: " TO cNumber2
WAIT "Enter operation: " TO cOperation && we get a single character for the operation
? Number1+cOperation+Number2+"=" && we print the operation and an equal sign
? &(Number1+cOperation+Number2) && and the result on the following line, evaluating the string as it appeared on the line above
这是一个与该程序交互的示例
Please enter the first number: 12 Please enter the second number: 23.5 Enter operation: - 12-23.5= -11.5
数组是数据的有序分组,其元素由称为索引的数字标识。在许多编程语言中,索引(或“索引”)从 0 开始,但在 xBase 中,它们从 1 开始。由于在数学中,索引写在变量旁边,略低于该行,因此有时它们被称为 _下标_,因此,数组有时被称为 _带下标的变量_。
考虑这个第一个数组示例。我们创建了两个数组,一个包含一年中月份的名称,另一个包含它们的天数,然后我们以有序的方式打印它们。使用关联数组可以以更优雅的方式获得相同的结果。
&& C:\hb32\bin\hbmk2 -quiet -oc:\hbcode\months.exe months.prg -run
DECLARE months [12]
? LEN (months) && the function LEN() shows how many items an array has
&& We shall now load some data into our arrays
PRIVATE month_names: = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
PRIVATE months: = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} && for simplicity we do not consider leap years
&& Let us make some output (using a FOR loop - see below)
FOR I: = 1 TO LEN (months)
? month_names [i], "is", months [i], "days long."
NEXT
在下一个示例中,我们将展示 xBase 对 _动态数组_ 的高级支持(在像 C 这样的低级语言中实现它们需要指针算术和内存管理函数)。
注意: 此示例将在本笔记中多次出现。 通常当我们介绍语言的新功能时,我们会编写一个新程序,以不同的方式执行与该程序相同的操作。 这是我能给你的最好的教学建议之一。 由于我没有提供练习,请考虑使用新的语言功能重新编写某些程序的不同方法,例如,给你一个想法:使用代码块在屏幕上显示乘法表,这个程序有多难重写呢?
&& compile with ''hbmk2 averaging.prg -lhbmisc''
PROCEDURE MAIN
LOCAL n, sum := 0, average && let's initialize the variables
aNumbers := {} && and an array without elements
? "enter a list of integer numbers, and a non-number when you are finished"
WHILE .T. && we begin an endless loop
ACCEPT "next element: " TO item
IF IsDec( item ) && the function IsDec() from the libmisc library checks if we have entered a number
AAdd ( aNumbers, item ) && if we did, then we add the number as a new element to the array
sum := sum + &item && (and we update the sum of the elements in our array)...
ELSE
EXIT && ...if we did not, we quit this loop
ENDIF
ENDDO
average := sum / Len( aNumbers ) && we compute the average of the values in the array
?
? "to average the array you must correct each element by adding to it the corresponding value below"
?
FOR n := 1 to LEN ( aNumbers )
?? AVERAGE - aNumbers[n]
NEXT
RETURN
以下是 hbmisc 中的函数列表:https://harbour.github.io/doc/hbmisc.html。
上面的程序并不十分令人满意:由于使用了 IsDec() 函数,它只能处理正整数。 如果我们想让它接受 -8 和 2.78 这样的数字怎么办? 我们可以想到三种解决方案:编写一个我们自己的函数来检查字符串是否代表一个数字(IsDec() 的改进版本),使用正则表达式测试输入,使用 xBase 函数进行错误处理。 我们将在本教程的下一部分中使用最后一种技术。
FOR EACH n in aNumbers && FOR EACH is a more modern control flow statement Harbour syntax ?? AVERAGE - &n NEXT
除了一个将元素添加到数组的函数外,我们还有一个删除它们的函数
aFruitSalad:={"peaches", "plums"}
AAdd ( aFruitSalad, "onions" ) && we add onions
ADel(aFruitSalad, 3) && on second thought, onions don't mix well: we remove them
local f; for each f in aFruitSalad; ?? f," " ; next && we print our ingredients
&& => peaches plums NIL
Harbour provides a function, hb_ADel, which accepts a third Boolean element and (if it's .T.) shrinks the array.
多维数组是通过将数组嵌套在其他数组中创建的。
到目前为止展示的所有程序,如果用户输入了意外的输入(简单来说,它们崩溃),都将失败并显示运行时错误。 被称为“健壮”的程序不会。 检查可能发生错误的条件并采取措施防止崩溃是一项通常用 if-then-else 完成的任务。
BEGIN SEQUENCE ... END[SEQUENCE] 是一种控制结构,它执行一些代码,如果代码产生了错误,可以选择执行一些语句。 在我们的示例中,如果将用户输入转换为数字产生了错误,我们将调用 EXIT 退出 while 循环并继续我们的程序。
PROCEDURE MAIN
LOCAL n, sum := 0, average && let's initialize the variables
local bError := errorblock ( { |oError| break ( oError ) } )
aNumbers := {} && and an array without elements
? "enter a list of integer numbers, and a non-number when you are finished"
WHILE .T. && we begin an endless loop
BEGIN SEQUENCE
ACCEPT "next element: " TO item
AAdd ( aNumbers, &item ) && if we did, then we add the number as a new element to the array
sum := sum + &item && (and we update the sum of the elements in our array)...
RECOVER
EXIT && ...if we did not, we quit this loop
END SEQUENCE
ENDDO
average := sum / Len( aNumbers ) && we compute the average of the values in the array
?
? "to average the array you must correct each element by adding to it the corresponding value below"
?
FOR n := 1 to Len ( aNumbers )
?? average - aNumbers[n]
NEXT
RETURN
Clipper 5 (C5) 提供了四类
Error Provides objects with information about runtime errors Get Provides objects for editing variables and database fields TBrowse Provides objects for browsing table-oriented data TBColumn Provides the column objects TBrowse objects
Clipper 5.3 Norton Guide 列出了这些类
Error class Provides objects with information about runtime errors CheckBox class Provides objects for creating checkboxes Get class Provides objects for editing variables and database fields ListBox class Provides objects for creating list boxes MenuItem class Provides objects for creating menu items PopUpMenu class Provides objects for creating pop-up menus PushButton class Provides objects for creating push buttons RadioButto class Provides objects for creating radio buttons RadioGroup class Provides objects for creating radio button groups Scrollbar class Provides objects for creating scroll bars TBColumn class Provides objects for browsing table-oriented data TBrowse class Provides the column objects TBrowse objects TopBarMenu class Provides objects for creating top menu bars
函数是一段代码,每次调用它都会返回一个结果。 让我们考虑一个将摄氏度转换为华氏度的函数(libct 提供了相同的函数,但我们出于教育目的重新编写了它)
FUNCTION cels2f( tempCelsius )
tempFahrenheit := (tempCelsius*1.8 + 32)
RETURN tempFahrenheit
FUNCTION MAIN()
? cels2f(100)
? cels2f(0)
但请看以下内容
/**
* by choosing better variable names this function becomes self-explanatory
*/
function convert_Celsius_to_Fahrenheit(Celsius_temperature)
return Celsius_temperature * 1.8 + 32
FUNCTION MAIN()
? convert_Celsius_to_Fahrenheit(100)
? convert_Celsius_to_Fahrenheit(0)
考虑以下 HB_Random() 函数的使用示例
PROCEDURE Main
// Random number between 0.01 and 0.99
? HB_Random()
// Random number between 0.01 and 9.99
? HB_Random(10)
// Random number between 8.01 and 9.99
? HB_Random(8,10)
// Random integer number between -100 and 100
? HB_RandomInt(-100,100)
RETURN
一个好的函数应该充当一个“黑盒子”,也就是说,你可以使用它而不必担心它是如何工作的。 通常在这个时候给出的一个例子是驾驶汽车:你可以使用它而不必担心发动机是如何工作的(你认识有人在买新车之前会检查发动机的活塞吗?)。 其他人设计了发动机。 但是可能存在问题:如果我们将摄氏度转换为华氏度的函数更改为如下所示
FUNCTION cels2f( tempCelsius )
tempFahrenheit := (tempCelsius*1.8 + 32)
number := number + 1
RETURN tempFahrenheit
FUNCTION MAIN()
number := 0
? cels2f( 100 )
? cels2f( 0 )
? number
我们将得到一个副作用。 该程序的输出是
212.0 32.0 2
也就是说,我们的新函数不仅返回了一个值,还将变量number增加了 1。
Clipper Norton Guides 上写着,“CA-Clipper 中的过程与用户定义的函数相同,区别在于它始终返回 NIL”(http://www.oohg.org/cl53/ng10a778.html),因此过程实际上只是一种依赖于这些副作用来完成工作的函数。
函数的效用在于它们提高了程序的可读性和可维护性。 将程序拆分成函数是一种良好的设计方法,它允许将一个复杂的问题分解成更小、更简单的问题(自顶向下)。
Harbour 有四种不同类型的子程序/子程序
- FUNCTION 和
- PROCEDURE(它是一个没有返回值的 FUNCTION),
- method(它没有关联的关键字,就像每种面向对象的编程语言一样)和
- code block。
主函数是 C 或 C++ 中编写的程序的指定起点(Java 使用一个静态 main 方法)。 由于过程是一个返回 NIL 的函数,因此我们可以在程序中使用 procedure main 而不是 function main。 使用 Harbour,它不再是必需的。
变量有名称、类型、作用域和生命周期(http://www.dbase.com/Knowledgebase/dbulletin/bu05vari.htm),可以显式声明为(下面的列表从w:Harbour (software)中逐字复制)
- LOCAL:仅在声明它的例程中可见。 值在例程退出时丢失。
- STATIC:仅在声明它的例程中可见。 值在例程的后续调用中保留。 如果在定义任何过程/函数/方法之前声明了一个 STATIC 变量,它将具有 MODULE 作用域,并在同一个源文件中定义的任何例程中可见,它将保持其生命周期,直到应用程序生命周期结束。
- PRIVATE:在声明它的例程及其所有调用的例程中可见。
- PUBLIC:对同一个应用程序中的所有例程可见。
在我们使用函数和过程之前,它们没有意义。
/* Work on the following.
* hbformat was run on this piece of code
* need to provide comments, nest more functions and procedures to help figuring what goes on with scope modifiers
*/
STATIC x := 9
? x
A()
? x
B()
? x
C()
? x
D()
? x
E()
? x
PROCEDURE A
LOCAL x := 10
? "x from A=", x
RETURN
PROCEDURE B
PRIVATE x := 5
? "x from B=", x
RETURN
PROCEDURE C
PUBLIC x := -1
? "x from C=", x
RETURN
PROCEDURE D
? "x from D before updating value=", x
x := 12
? "x from D=", x
RETURN
PROCEDURE E
? "x from E=", x
RETURN
运行它时,我们会得到一个奇怪的输出
9 x from A= 10 9 x from B= 5 9 x from C= -1 9 x from D before updating value= -1 x from D= 12 9 x from E= 12 9
该程序设置了一个变量x和五个不同的过程,A、B、C、D、E。 前三个过程在自身内部定义了一个变量x,为它分配一个值并打印它。 第四个函数将一个新值分配给一个名为x的变量,而没有声明它。 第五个函数显示了某个x变量的值,它恰好是第四个x变量的值。 这里要记住的主要事实是,两个变量并不相同,即使它们的名称相同,只要它们有不同的作用域,这个特性被称为遮蔽。
x := 9
? x
A()
? x
B()
? x
C()
? x
D()
? x
E()
? x
PROCEDURE A
LOCAL x := 10
? "x from A=", x
RETURN
PROCEDURE B
PRIVATE x := 5
? "x from B=", x
RETURN
PROCEDURE C
PUBLIC x := -1
? "x from C=", x
RETURN
PROCEDURE D
? "x from D before updating value=", x
x := 12
? "x from D=", x
RETURN
PROCEDURE E
? "x from E=", x
RETURN
9 x from A= 10 9 x from B= 5 9 x from C= -1 -1 x from D before updating value= -1 x from D= 12 12 x from E= 12 12
由于静态变量似乎需要更多信息(它们的工作原理类似于 C 的static 变量),以下是一个示例
// adapted from "An example of static local variable in C" (from Wikipedia Static_variable)
FUNCTION func()
static x := 0
/* x is initialized only once, the first time func() is called */
x := x+1
? x
RETURN
FUNCTION main()
func() // calls func() a first time: it prints 1
func() // value 1 was preserved; this time it prints 2
func() // prints 3
func() // prints 4
func() // prints 5
请参阅 tutorialspoint.com 中的 pascal_variable_scope 和http://aelinik.free.fr/c/ch14.htm
变量作用域是面向对象编程的预兆,因为它预示着某些封装概念。
如果你查看了函数列表,你会注意到 xBases 没有三角函数。 并不是说它们通常缺少它们:这些语言的目标是数据库应用程序,例如“会计系统和航空公司预订系统”(参见w:Database application。)在其中三角函数并不十分重要。 但是现在我们问自己如何计算 Ludolphine 数 π 的近似值(有关此“搜索”的更多信息,请访问http://www.mathpages.com/home/kmath457.htm),我们可以使用 CT 库(参见http://vouch.info/harbour/index.html?hbct.htm),或导入 C 标准库函数 atan。
&& compile with ''hbmk2 computepi.prg -lhbct''
PROCEDURE MAIN
SET DECIMAL TO 14
? 4 * ATAN (1)
我们从 Alexander Kresin 的Harbour for beginners 获得灵感(http://www.kresin.ru/en/hrbfaq_3.html)。
#pragma BEGINDUMP
#include <extend.h>
#include <math.h>
HB_FUNC( ATAN )
{
double x = hb_parnd(1);
hb_retnd( atan( x ) );
};
#pragma ENDDUMP
SET DECIMAL TO 14
? 4 * ATAN (1)
需要注意的是,extend.h 文件(*CA-Cl*pper Extend System 的兼容头文件)包含以下警告
/* DON'T USE THIS FILE FOR NEW HARBOUR C CODE */ /* This file is provided to support some level of */ /* Harbour compatibility for old Clipper C extension code */
事实上,这个例子仅仅是为了说明开源交叉编译器(这里指的是 Harbour)是如何使用 C 库函数进行扩展的,这给了它们很大的灵活性。并不是说计算 π 本身对一般的 xBase 程序员来说有什么用(顺便说一下,CT 库包含一个直接计算 π 的函数)。我们不会关心如何计算它,而是会转向一些更常规的东西。
FizzBuzz 是一个在面试中经常被问到的一个小问题。在 https://micheleriva.medium.com/about-coding-the-fizzbuzz-interview-question-9bcd08d9dfe5 页面上可以找到很好的讨论。和以往一样,即使像这样简单的问题,也有很多解决方法,可以这样描述:
Write a program that prints the numbers from 1 to 100. But for multiples of three print “Fizz” instead of the number and for the multiples of five print “Buzz”. For numbers which are multiples of both three and five print “FizzBuzz”.
首先,要尽可能多地思考解决问题的方法。另外,你可能会被要求在没有电脑的情况下证明你的解决方案有效:这样做也是阅读他人编写的程序,甚至检查你自己编写的程序的最佳方法之一。你可以拿一张纸和一支笔,检查程序指令如何改变变量,以及它们产生什么输入,方法是在表格中排列变量的内容,并像电脑一样进行操作。我听说这被称为 w:Trace table、追踪算法、手工追踪、干运行。目前我推荐你看一个 YouTube 教程,它应该能让你了解这个东西是如何工作的:https://www.youtube.com/watch?v=UbANyxE7pGE (Trace tables tutorial GCSE Computer Science)。
以以下 FizzBuzz 的简单解决方案为例
FOR I = 1 TO 100
FLAG = 0
IF I % 5 = 0
FLAG = 1
?? "Fizz"
ENDIF
IF I % 3 = 0
FLAG = 1
?? "Buzz"
ENDIF
IF FLAG = 0
?? I
ENDIF
?? ", "
NEXT
这个例子介绍了 标志变量,它是一个用来检查程序是否执行了特定指令或指令集的变量。
对数据进行排序是计算机科学中的一个重要应用。事实上,在古代(指的是 50 年代),计算机的一个重要部件是一个叫做 卡片分类机 的硬件,它被用来对穿孔卡片进行排序,参见 http://www.columbia.edu/cu/computinghistory/sorter.html 上的一个例子,但是 Hollerith 在 19 世纪 80 年代建造的统计机器已经有了自己的 分类箱,而法语中“计算机”一词 ordinateur,甚至可以字面翻译为 分类器。
让我们从一段代码开始
// we'll declare and load random numbers from 1 to 100 in an array
DECLARE arrayToSort [12]
FOR I = 1 TO 12
arrayToSort [I] = Int( HB_Random( 1,101 ) )
NEXT
// we review our array
FOR I = 1 TO 12
?? arrayToSort [I]
NEXT
// we'll do Exchange Sort as described in http://www.ee.ryerson.ca/~courses/coe428/sorting/exchangesort.html
// where there is also a nice animation showing how it works! :)
for i := 2 TO Len(arrayToSort)
for j := Len(arrayToSort) TO i step -1
IF arrayToSort [j - 1] > arrayToSort [j]
TEMP := arrayToSort [j - 1]
arrayToSort [j - 1] := arrayToSort [j]
arrayToSort [j] := TEMP
ENDIF
NEXT
NEXT
?
? "Sorted array:"
?
// we look at our sorted array
FOR I = 1 TO 12
?? arrayToSort [I]
NEXT
从上面的版本中,我们可以(也应该)将排序程序分离到一个过程中,这样
PROCEDURE ExchangeSort(array)
// we'll do Exchange Sort as described in http://www.ee.ryerson.ca/~courses/coe428/sorting/exchangesort.html
// where there is also a nice animation showing how it works! :)
for i := 2 TO Len(array)
for j := Len(array) TO i step -1
IF array [j - 1] > array [j]
TEMP := array [j - 1]
array [j - 1] := array [j]
array [j] := TEMP
ENDIF
NEXT
NEXT
RETURN
FUNCTION MAIN
// we'll declare and load random numbers from 1 to 100 in an array
DECLARE arrayToSort [12]
FOR I = 1 TO 12
arrayToSort [I] = Int( HB_Random( 1,101 ) )
NEXT
// we review our array
FOR I = 1 TO 12
?? arrayToSort [I]
NEXT
ExchangeSort(arrayToSort) // we call the procedure and pass to it our array called arrayToSort
?
? "Sorted array:"
?
// we show the array is sorted
FOR I = 1 TO 12
?? arrayToSort [I]
NEXT
通常,第一个介绍的排序算法(即使在我的高中课本中)是冒泡排序,这是一个糟糕的选择。这里我们使用交换排序,它同样糟糕,但至少我们做了一点变化。
当一个函数调用自身时,我们就称之为递归。这个定义真的就这么简单。
这样说的话,它似乎不是一个好主意,因为递归函数似乎会将程序陷入无限循环。实际上,递归函数不仅仅是调用自身,它还包含一个停止执行的条件,并提供一些实际结果。
第一个例子是阶乘函数,我们从字典中获取它的定义 (https://www.dictionary.com/browse/factorial):“ 数学。给定正整数乘以所有较小正整数的乘积:四阶乘 (4!) 的数量 = 4 ⋅ 3 ⋅ 2 ⋅ 1 = 24。符号:n!,其中 n 是给定的整数”。
FUNCTION main
? factorial(10)
RETURN 0
FUNCTION factorial(n)
IF n = 0
RETURN 1
ELSE
RETURN n * factorial(n - 1)
ENDIF
RETURN -1
https://github.com/vszakats/harbour-core/blob/master/doc/codebloc.txt
网络上的网站说 Clipper 5 文档将它们称为 无名可分配函数。
让我们回到我们的摄氏度到华氏度的温度转换函数
FUNCTION cels2f( tempCelsius )
tempFahrenheit := (tempCelsius*1.8 + 32)
RETURN tempFahrenheit
并将其重写为代码块
Cels2F := { | celsius | Fahrenheit := (Celsius * 1.8 + 32) } // our first codeblock
? Eval(Cels2F,100)
? Eval(Cels2F,0)
在第一行,我们将一个代码块分配给一个名为 Cels2F 的变量。代码块包含在花括号中。首先是一个要传递给代码块的变量,放在竖线 (|) 之间。多个变量应以逗号分隔。然后我们可以编写指令(多条指令应以逗号分隔)。
之后,我们两次使用 Eval 函数,分别传递两个参数:我们为代码块分配的名称和要传递给它的参数。除了 Eval,其他函数也可以评估代码块:AEval() 和 dbEval()。Ascan() 和 Asort() 可以传递代码块来改变它们的执行行为。
由于代码块不能包含命令,因此我们不能在它们中使用 ? 命令,而必须使用相应的 QOut() 函数。
假设我们想编写一个 Unix cat 命令的克隆,这意味着我们将有一个程序,我们将向它传递一个命令行参数,即文件名,并输出文件的内容。
PROCEDURE MAIN(file)
? MEMOREAD(file)
RETURN
就是这样!事实上,它相当有限,更有可能是一个 DOS type 命令(甚至 CP/M type 命令)的克隆!程序该文件的程序 MAIN 接收一个参数,并从命令行获取它。然后将它传递给 MEMOREAD 函数(描述:将文本文件的内容作为字符字符串返回),并将结果发送到控制台输出。
可以使用低级函数访问文件,这些函数包括:FOPEN()、FCREATE()、FWRITE()、FREAD()、FREASDSTR()、FERASE()、FERROR()、FSEEK()、FCLOSE() 和 FRENAME()。
我们应该如何编写一个使用这些函数的示例?这里有一个想法
- 使用 FCREATE() 创建一个新文件,并使用 FWRITE() 向其中写入文本。
- 使用 FSEEK() 将指针定位到文件末尾。
- 使用 FREAD() 读取文件内容。
- 使用 FCLOSE() 关闭文件。
- 使用 FRENAME() 重命名文件。
- 最后,使用 FERASE() 删除重命名的文件。
但是,Harbour 提供了类似于 DBF 文件操作文本文件的函数 (https://vivaclipper.wordpress.com/tag/memoread/)。