level 11
天道玄虚
楼主
这里指的字符串既不是PChar类型的C系字符串,也不是string类型的Pascal字符串,而是Firemonkey下的StringHelper。
之所以要有StringHelper,是因为在Windows下和其他的平台下,字符串string的起始索引不同,Windows下为1起始,而其余平台有些是0起始。
为了统一下标,Delphi提供了一个名为StringHelper的东西,不但统一下标均自0开始,而且提供了很多实用的针对字符串的方法。
4.0、字符串的声明和StringHelper的使用
如果想要使用StringHelper,那么就需要声明为string,而非PChar,如果需要可以在最后一步转换类型。
使用StringHelper方法只需要在字符串后面加点就好。比如:
s.Replace(‘ ‘, ‘’);
这个即使用了StringHelper方法。
但是需要注意一点,StringHelper不会修改字符串本身,而且使用了StringHelper之后,字符串将会变成不可更改其中字符,只可以覆盖或者说更改整个字符串。也就是说,像这样的语法:
s.Chars[i] := ‘0’;
将会是不合法的。
当然,这样依旧可以:
s[i] := ‘0’;
不过,这样就其实没有使用StringHelper了。
4.0.0、截取、引用
string.SubString方法可实现从字符串中截取某一个部分。
string.SubString(n);
n为起始索引,将会返回自第n个字符起,后续所有的字符,比如,当s = ‘asd’时,有:
s.SubString(1) = ‘sd’;
注意下标全部都会是从零开始。
string.SubString(n, c);
n为起始索引,c为数目,将会返回自第n个字符起,后续一共c个字符,比如当s = ‘asdfgh’时,有:
s.SubString(2, 3) = ‘dfg’;
string.Chars[i]方法可以引用第i个字符,注意下标从零开始。
4.0.1、替换
string.Replace方法可以实现字符串替换。
string.Replace(s1, s2);
s1,s2可以是字符,或者字符串,将会返回依次替换的值。如果需要多次替换,则可以连续使用Replace方法,比如:
Result :=Result.Replace('+', ' + ').Replace('-', ' - ').Replace('*', ' * ').Replace('/',' / ').Replace('^', ' ^ ');
这一行语句将会将Result中的四则运算符以及乘方运算符前后加上空格。
4.0.2、查找
string.IndexOf方法可以实现查找,如果不存在,返回-1。
string.IndexOf(s);
将会返回字符或者字符串s第一次出现的位置,比如,当s = ‘asdfg’时,有:
s.IndexOf(‘sdf’) = 1;
string.IndexOf(s, i);
将会返回自i索引之后(包含i),字符或者字符串s第一次出现的位置,也就是说,将会忽略索引i之前的字符。
string.IndexOf(s, i, c);
将会返回自i索引之后(包含i)c个字符范围内,字符或者字符串s第一次出现的位置,也就是说,将会忽略索引i之前的字符,以及索引i起,c个字符之后的字符。
string.LastIndexOf方法可以实现倒序查找,如果不存在,返回-1。
string.LastIndexOf(s);
将会返回字符或者字符串s最后一次出现的位置。
string.LastIndexOf(s, i);
将会范围自i索引之前(包含i),s最后一次出现的位置。也就是说,将会忽略索引i之后的字符。
string.LastIndexOf(s, i,c);
将会返回自i索引之前(包含i),c个字符范围内s最后一次出现的位置。也就是说,将会忽略索引i之后,以及自索引i起,之前c个字符以前的字符。
4.0.3、移除、插入字符
string.Remove方法可以移除字符。
string.Remove(i);
将会返回自索引i起所有字符的字符串。
string.Remove(i, c);
将会返回自索引i起,移除c个字符的字符串。
string.Insert方法可以插入字符。
string.Insert(i, s);
将会返回在索引i之前插入字符串s的字符串。比如当s = ‘0123456’时,有:
s.Insert(3, ‘asd’) = ‘012asd3456’;
4.0.4、分割
string.Split方法可以实现分割,返回值为TArray<String>。
直观上讲,这个方法就是将字符串依照某一些分隔符分割为字符串数组的一个方法。
string.Split([c1, c2, …]);
将会返回依照c1、c2等字符进行分割的结果。
stirng.Split([c1, c2, …],Count);
将会返回依照c1、c2等字符进行分割,并使数组的数量达到Count为止。
比如,当s = ‘asd f g h j jkl’时,有:
s.Split([‘ ‘]) = (‘asd’, ‘f’,‘g’, ‘h’, ‘jkl’);
s.Split([‘ ‘], 2) = (‘asd’, ‘f’);
4.1、所谓化归
化归是一种数学思想,当然也是一种编程思想。面对对象的编程思想也是化归思想的一种。
所谓化归,也就是将未解决的问题化为已解决问题或者其他更易解决问题的变形或者组合,从而实现简化思路的目的的一种方法。
举一个用烂了的好笑的例子。
如果要用空壶烧水,那么步骤应该是:打满水、放在炉子上,等水开。
如果要用已经有一半水的水壶烧水呢?
使用化归应该是先把水倒掉,那么我们就回到了空壶烧水的问题。
那么有人就会说,按照这样的思路编程,岂不是会让程序的性能受到较大的不良影响呢?程序的效率是不是就难以保障了呢?因为很显然,这里更优化的方法应该是直接加满水才对。
至于说直接将水加满再烧水,其实也算是化归。只不过这里,我们选用的“已解决问题”是使用装满了水的水壶烧水。请看:
空壶烧水:装满水那么得到装满的水壶。
装有一半水的水壶烧水:装满水那么得到装满水的水壶。
由此可见,如果选取适当的“已解决问题”,化归并不会让解决方法的效率受到很大的不良影响,相反,借助已有解决方法的问题,反而能够让问题变得更加简单。
让我们使用一个看起来很复杂的例子说明一下化归多么有用。
4.2、算式解释器
这里说的算式解释器,就是输入一个字符串形式的算式,然后输出结果的那种算式解释器。
4.2.0、先从只有一个运算符的算式说起
如果面对一个只有一个运算符的算式,要计算它的结果是不是一件很简单的事情呢?
当然是,我们要做的不过是将其中的运算符和数字分开,然后再计算就好。
为了便于处理,我们先约定一下算式的格式:首尾均没有空格,每一个运算符前后也有空格,没有连续的空格。
那么,我们就可以使用split方法将字符串依照空格分割为一个包含三个字符串的TArray<string>,然后使用if语句判别运算符,最后得出结果。注意如果是除法,除数不可以是零,如果是乘方,底数不应是负数。(因为指数未必是整数,当底数为负数、指数为实数的时候,结果可能是复数)
4.2.1、然后是没有括号的算式
如果面对一个没有括号的算式,那么我们应该怎么做呢?
当然是先乘方,后乘除,最后加减。
那么,让我们先将乘方完成。
首先我们可以查找乘方符号“^”的位置,当返回值不为-1的时候,意味着返回了这个符号的位置。如果按照我们方才对于算式格式的约定,那么乘方符号前后均有空格,在这两个空格前后分别有一个数,在这两个数前后分别又有一个空格。
那么,我们想要提取出这两个数的位置,是不是应该通过空格开实现呢?
那么,我们应该查找乘方符号之前那个空格以前,最后一次出现的空格位置。这个位置就是乘方运算中,底数的前一个位置。
随后,我们应该查找乘方符号之后那个空格以后,第一次出现的空格位置。这个位置就是乘方运算中,指数的后一个位置。
将这两个位置之间的字符串提取出来,岂不是得到了仅有一个运算符的算式?
至于之后的乘除运算,有一个小小的技巧。乘除的优先级相同,不能够先算哪个,后算哪个,而是从左到右依次计算,那么我们不妨先将除号也看做乘号,这当然仅仅是为了便于获取字符的位置(StringHelper方法恰巧是不改变字符串本身,仅仅提供返回值的)。
也就是说,我们便可以使用同样的伎俩对付乘除了。
加减则和乘除相似,不再赘述。
提取和运算完成后,我们只需要将这一段算式删除,随后将计算结果转化为字符串插入就好了。
4.2.2、面对括号吧
有括号的先算括号里面的,这括号可是开了挂的特权阶级,不太好对付啊。
不过,其实不然。我们首先这样规定:括号前后不要额外有空格。(如果括号前后有运算符,运算符应该提供空格)
如果是最内层括号,那么这一层括号内的算式是不是可以看做没有括号的算式?当然可以,那么我们要做的其实不过是将括号中的算式提取出来而已。
那么,我们只需要首先获取第一次出现的右括号的位置,然后获取在这之前的,最后一次出现的左括号的位置,岂不是就可以提取出那其中的无括号算式?通过计算这个算式,然后将括号连同算式一并删除,随后插入运算结果,那么我们就消除了一层括号。
那么,在套上一个循环之后,整个算式也就是我们的囊中之物了。
4.3、制作一个带有GUI的解释器吧。
同样是FMX,不要搞成了VCL。
我们的截面应该有一个输入框,一个输出框,一个输出按钮。
或者可以将输出框改成TMemo,然后使用Memo.Lines.Add(s)方法将字符串写入Memo,同样,应该设置输出部分为ReadOnly。
说起GUI,我们还需要说一些比较好的习惯。
我们当然希望自己的GUI做的好看,但是我们经常做的不尽人意。
这也就意味着,我们常常需要修改我们的GUI。如果有大量功能依赖于GUI而存在,那么这样的程序修改起来是很麻烦而且不安全的。所以我们需要新建一个用来实现功能的单元。
使用菜单项File->New->Unit – Delphi来新建一个单元,新建后推荐首先保存一下,并且命名。
完成这一步之后,请在GUI部分的interface下找到uses,然后再列表的最后加上你自己的单元名称。
如果要添加已有的单元,使用工具栏下的Standard按键组中的Add File to Project,快捷键是shift + F11(参考第零章)
4.3.0、控件布局
调整控件布局的是Align属性,其中Client是占用剩余的全部,Top、Left、Bottom、Right分别是占用边缘的全部位置,当占用上下边缘的时候,宽度将会达到最大,可以调整高度。当占用左右边缘的时候,高度将会达到最大,可以调整宽度。
有时候,我们为了方便布局,通常使用作为底板的TPanel组件,如果在选中这个组件的时候在其中添加控件,那么在结构视窗可以看到这个控件将会位于这个Panel之下的子组件中。
让我们在窗口中放置一个Panel,一个Memo。Memo也就是通常意义上的记事本控件,具有Windows下记事本的全部功能。我们这一次需要使用到的只是Memo.Lines.Add一个方法而已。记得将Memo的ReadOnly勾上。
然后我们在Panel中添加一个输入框,和一个按钮。我们需要为按钮单击添加计算的事件。
按照自己喜欢的方式布局,玄虚的界面很难看,各位将就一下。

我设置了Panel的Align属性为Bottom,输入按钮的为Right,Memo和Edit的为Client。
然后为单击按钮添加事件,当然在实现之前提到的计算功能之后,单击事件将会非常简单。如下:
(为了保证排版,将此处的空格换成了全角空格,文档内的没有替换)
procedureTMainForm.BtnInputClick(Sender: TObject);
var
s: string;
begin
s := ExprFormat(EdInput.Text);
Log.Lines.Add(s + ' = ' + CalculateExpr(s));
Log.Lines.Add('');
EdInput.Text := '';
end;
其中的ExprFormat就是利用替换将表达式改写成我们所约定形式的一个函数。请读者自己思考,详见源代码。
练习就是完成这个算式解释器。
2017年01月29日 02点01分
1
之所以要有StringHelper,是因为在Windows下和其他的平台下,字符串string的起始索引不同,Windows下为1起始,而其余平台有些是0起始。
为了统一下标,Delphi提供了一个名为StringHelper的东西,不但统一下标均自0开始,而且提供了很多实用的针对字符串的方法。
4.0、字符串的声明和StringHelper的使用
如果想要使用StringHelper,那么就需要声明为string,而非PChar,如果需要可以在最后一步转换类型。
使用StringHelper方法只需要在字符串后面加点就好。比如:
s.Replace(‘ ‘, ‘’);
这个即使用了StringHelper方法。
但是需要注意一点,StringHelper不会修改字符串本身,而且使用了StringHelper之后,字符串将会变成不可更改其中字符,只可以覆盖或者说更改整个字符串。也就是说,像这样的语法:
s.Chars[i] := ‘0’;
将会是不合法的。
当然,这样依旧可以:
s[i] := ‘0’;
不过,这样就其实没有使用StringHelper了。
4.0.0、截取、引用
string.SubString方法可实现从字符串中截取某一个部分。
string.SubString(n);
n为起始索引,将会返回自第n个字符起,后续所有的字符,比如,当s = ‘asd’时,有:
s.SubString(1) = ‘sd’;
注意下标全部都会是从零开始。
string.SubString(n, c);
n为起始索引,c为数目,将会返回自第n个字符起,后续一共c个字符,比如当s = ‘asdfgh’时,有:
s.SubString(2, 3) = ‘dfg’;
string.Chars[i]方法可以引用第i个字符,注意下标从零开始。
4.0.1、替换
string.Replace方法可以实现字符串替换。
string.Replace(s1, s2);
s1,s2可以是字符,或者字符串,将会返回依次替换的值。如果需要多次替换,则可以连续使用Replace方法,比如:
Result :=Result.Replace('+', ' + ').Replace('-', ' - ').Replace('*', ' * ').Replace('/',' / ').Replace('^', ' ^ ');
这一行语句将会将Result中的四则运算符以及乘方运算符前后加上空格。
4.0.2、查找
string.IndexOf方法可以实现查找,如果不存在,返回-1。
string.IndexOf(s);
将会返回字符或者字符串s第一次出现的位置,比如,当s = ‘asdfg’时,有:
s.IndexOf(‘sdf’) = 1;
string.IndexOf(s, i);
将会返回自i索引之后(包含i),字符或者字符串s第一次出现的位置,也就是说,将会忽略索引i之前的字符。
string.IndexOf(s, i, c);
将会返回自i索引之后(包含i)c个字符范围内,字符或者字符串s第一次出现的位置,也就是说,将会忽略索引i之前的字符,以及索引i起,c个字符之后的字符。
string.LastIndexOf方法可以实现倒序查找,如果不存在,返回-1。
string.LastIndexOf(s);
将会返回字符或者字符串s最后一次出现的位置。
string.LastIndexOf(s, i);
将会范围自i索引之前(包含i),s最后一次出现的位置。也就是说,将会忽略索引i之后的字符。
string.LastIndexOf(s, i,c);
将会返回自i索引之前(包含i),c个字符范围内s最后一次出现的位置。也就是说,将会忽略索引i之后,以及自索引i起,之前c个字符以前的字符。
4.0.3、移除、插入字符
string.Remove方法可以移除字符。
string.Remove(i);
将会返回自索引i起所有字符的字符串。
string.Remove(i, c);
将会返回自索引i起,移除c个字符的字符串。
string.Insert方法可以插入字符。
string.Insert(i, s);
将会返回在索引i之前插入字符串s的字符串。比如当s = ‘0123456’时,有:
s.Insert(3, ‘asd’) = ‘012asd3456’;
4.0.4、分割
string.Split方法可以实现分割,返回值为TArray<String>。
直观上讲,这个方法就是将字符串依照某一些分隔符分割为字符串数组的一个方法。
string.Split([c1, c2, …]);
将会返回依照c1、c2等字符进行分割的结果。
stirng.Split([c1, c2, …],Count);
将会返回依照c1、c2等字符进行分割,并使数组的数量达到Count为止。
比如,当s = ‘asd f g h j jkl’时,有:
s.Split([‘ ‘]) = (‘asd’, ‘f’,‘g’, ‘h’, ‘jkl’);
s.Split([‘ ‘], 2) = (‘asd’, ‘f’);
4.1、所谓化归
化归是一种数学思想,当然也是一种编程思想。面对对象的编程思想也是化归思想的一种。
所谓化归,也就是将未解决的问题化为已解决问题或者其他更易解决问题的变形或者组合,从而实现简化思路的目的的一种方法。
举一个用烂了的好笑的例子。
如果要用空壶烧水,那么步骤应该是:打满水、放在炉子上,等水开。
如果要用已经有一半水的水壶烧水呢?
使用化归应该是先把水倒掉,那么我们就回到了空壶烧水的问题。
那么有人就会说,按照这样的思路编程,岂不是会让程序的性能受到较大的不良影响呢?程序的效率是不是就难以保障了呢?因为很显然,这里更优化的方法应该是直接加满水才对。
至于说直接将水加满再烧水,其实也算是化归。只不过这里,我们选用的“已解决问题”是使用装满了水的水壶烧水。请看:
空壶烧水:装满水那么得到装满的水壶。
装有一半水的水壶烧水:装满水那么得到装满水的水壶。
由此可见,如果选取适当的“已解决问题”,化归并不会让解决方法的效率受到很大的不良影响,相反,借助已有解决方法的问题,反而能够让问题变得更加简单。
让我们使用一个看起来很复杂的例子说明一下化归多么有用。
4.2、算式解释器
这里说的算式解释器,就是输入一个字符串形式的算式,然后输出结果的那种算式解释器。
4.2.0、先从只有一个运算符的算式说起
如果面对一个只有一个运算符的算式,要计算它的结果是不是一件很简单的事情呢?
当然是,我们要做的不过是将其中的运算符和数字分开,然后再计算就好。
为了便于处理,我们先约定一下算式的格式:首尾均没有空格,每一个运算符前后也有空格,没有连续的空格。
那么,我们就可以使用split方法将字符串依照空格分割为一个包含三个字符串的TArray<string>,然后使用if语句判别运算符,最后得出结果。注意如果是除法,除数不可以是零,如果是乘方,底数不应是负数。(因为指数未必是整数,当底数为负数、指数为实数的时候,结果可能是复数)
4.2.1、然后是没有括号的算式
如果面对一个没有括号的算式,那么我们应该怎么做呢?
当然是先乘方,后乘除,最后加减。
那么,让我们先将乘方完成。
首先我们可以查找乘方符号“^”的位置,当返回值不为-1的时候,意味着返回了这个符号的位置。如果按照我们方才对于算式格式的约定,那么乘方符号前后均有空格,在这两个空格前后分别有一个数,在这两个数前后分别又有一个空格。
那么,我们想要提取出这两个数的位置,是不是应该通过空格开实现呢?
那么,我们应该查找乘方符号之前那个空格以前,最后一次出现的空格位置。这个位置就是乘方运算中,底数的前一个位置。
随后,我们应该查找乘方符号之后那个空格以后,第一次出现的空格位置。这个位置就是乘方运算中,指数的后一个位置。
将这两个位置之间的字符串提取出来,岂不是得到了仅有一个运算符的算式?
至于之后的乘除运算,有一个小小的技巧。乘除的优先级相同,不能够先算哪个,后算哪个,而是从左到右依次计算,那么我们不妨先将除号也看做乘号,这当然仅仅是为了便于获取字符的位置(StringHelper方法恰巧是不改变字符串本身,仅仅提供返回值的)。
也就是说,我们便可以使用同样的伎俩对付乘除了。
加减则和乘除相似,不再赘述。
提取和运算完成后,我们只需要将这一段算式删除,随后将计算结果转化为字符串插入就好了。
4.2.2、面对括号吧
有括号的先算括号里面的,这括号可是开了挂的特权阶级,不太好对付啊。
不过,其实不然。我们首先这样规定:括号前后不要额外有空格。(如果括号前后有运算符,运算符应该提供空格)
如果是最内层括号,那么这一层括号内的算式是不是可以看做没有括号的算式?当然可以,那么我们要做的其实不过是将括号中的算式提取出来而已。
那么,我们只需要首先获取第一次出现的右括号的位置,然后获取在这之前的,最后一次出现的左括号的位置,岂不是就可以提取出那其中的无括号算式?通过计算这个算式,然后将括号连同算式一并删除,随后插入运算结果,那么我们就消除了一层括号。
那么,在套上一个循环之后,整个算式也就是我们的囊中之物了。
4.3、制作一个带有GUI的解释器吧。
同样是FMX,不要搞成了VCL。
我们的截面应该有一个输入框,一个输出框,一个输出按钮。
或者可以将输出框改成TMemo,然后使用Memo.Lines.Add(s)方法将字符串写入Memo,同样,应该设置输出部分为ReadOnly。
说起GUI,我们还需要说一些比较好的习惯。
我们当然希望自己的GUI做的好看,但是我们经常做的不尽人意。
这也就意味着,我们常常需要修改我们的GUI。如果有大量功能依赖于GUI而存在,那么这样的程序修改起来是很麻烦而且不安全的。所以我们需要新建一个用来实现功能的单元。
使用菜单项File->New->Unit – Delphi来新建一个单元,新建后推荐首先保存一下,并且命名。
完成这一步之后,请在GUI部分的interface下找到uses,然后再列表的最后加上你自己的单元名称。
如果要添加已有的单元,使用工具栏下的Standard按键组中的Add File to Project,快捷键是shift + F11(参考第零章)
4.3.0、控件布局
调整控件布局的是Align属性,其中Client是占用剩余的全部,Top、Left、Bottom、Right分别是占用边缘的全部位置,当占用上下边缘的时候,宽度将会达到最大,可以调整高度。当占用左右边缘的时候,高度将会达到最大,可以调整宽度。
有时候,我们为了方便布局,通常使用作为底板的TPanel组件,如果在选中这个组件的时候在其中添加控件,那么在结构视窗可以看到这个控件将会位于这个Panel之下的子组件中。
让我们在窗口中放置一个Panel,一个Memo。Memo也就是通常意义上的记事本控件,具有Windows下记事本的全部功能。我们这一次需要使用到的只是Memo.Lines.Add一个方法而已。记得将Memo的ReadOnly勾上。
然后我们在Panel中添加一个输入框,和一个按钮。我们需要为按钮单击添加计算的事件。
按照自己喜欢的方式布局,玄虚的界面很难看,各位将就一下。

我设置了Panel的Align属性为Bottom,输入按钮的为Right,Memo和Edit的为Client。然后为单击按钮添加事件,当然在实现之前提到的计算功能之后,单击事件将会非常简单。如下:
(为了保证排版,将此处的空格换成了全角空格,文档内的没有替换)
procedureTMainForm.BtnInputClick(Sender: TObject);
var
s: string;
begin
s := ExprFormat(EdInput.Text);
Log.Lines.Add(s + ' = ' + CalculateExpr(s));
Log.Lines.Add('');
EdInput.Text := '';
end;
其中的ExprFormat就是利用替换将表达式改写成我们所约定形式的一个函数。请读者自己思考,详见源代码。
练习就是完成这个算式解释器。