写易语言数组删除这东西,看着就是一行命令的事,但真上手你就知道了,十个新手九个在循环删除上翻过车。有人删除完直接下标越界崩溃,有人删完发现该删的没删掉,不该删的倒删了一堆。原因无他,就是没搞明白删除成员命令背后"位置前移"这个机制。这篇文章我把删除成员、删除指定成员从原理到实战都拆开讲,包括正序倒序怎么选、多维数组的坑、空成员清理、大数组性能优化,最后附带可以直接抄走用的封装子程序。
1. 删除成员命令的正确用法:三个参数一次讲透
1.1 起始位置从1开始,别拿0当起点
删除成员命令的标准格式是:删除成员 (数组变量, 删除位置, 删除数量)。很多从其它语言转过来的朋友容易犯一个毛病,脑子里总惦记着下标从0开始。易语言默认数组下限就是1,第一个成员的位置就是1,不是0。你写删除成员 (数组, 0, 1),系统直接就给你报下标越界。
我先给一个最朴素的例子:
.版本 2 .子程序 _按钮1_被单击 局部变量 数组, 整数型, , "0" 局部变量 i, 整数型 数组 = { 10, 20, 30, 40, 50 } 删除成员 (数组, 3, 1) ' 数组变为 { 10, 20, 40, 50 } 调试输出 (数组)这里删除位置填的是3,删掉的是原本的第3个成员,也就是30。删除之后数组长度自动变成4,后面40和50自动往前补位。这里有个关键点:删除成员之后数组会自动缩短,不需要你再手动重定义数组大小。有些人删完担心数组占着内存不释放,又去重定义一遍,纯属多此一举。
再说一个容易忽略的边界情况:如果数组下限不是1,比如你用重定义数组把下限改成了0,那位置参数的起点也要跟着变。这种需求一般出现在特殊算法里,新手阶段遇到得少,但既然学了,脑子里要有这根弦:位置参数跟随当前数组的实际下界,不是永远从1数。
1.2 删除数量的隐藏规则
第三个参数"删除数量"是可以省略的,不写的时候默认删除1个。但我个人的建议是:永远把删除数量写完整。为什么?因为代码是给人看的,你写删除成员 (数组, 2, 3)别人一眼就能看出你要从第2个位置开始连删3个;你写删除成员 (数组, 2)别人还得反应一下才知道你只删1个。团队协作或者三个月后你自己回来看代码,可读性差距很明显。
删除数量这个参数最有价值的地方,就是它能实现"连续区间删除"。举个例子:
数组 = { 1, 2, 3, 4, 5, 6 } 删除成员 (数组, 2, 3) ' 从第2个成员开始删3个,数组变为 { 1, 5, 6 }这就相当于一次操作砍掉一整块。很多人在需要删除一大段连续数据的时候,还在傻傻地循环一个个删,性能白白浪费。一次删除3个成员,实际底层只做一次数组移动,和循环三次做三次移动,开销差距在数组大的时候非常明显。
1.3 数组下标的前移逻辑
这是整个删除操作的核心中的核心:删除一个成员后,它后面所有成员的下标都会往前移动一位。举个形象点的例子,一列人排队买奶茶,排在第三位的人突然走了,后面所有人都得往前挪一步,原来第四位的人变成了第三位,原来第五位的人变成了第四位,以此类推。
这个"前移"逻辑带来两个直接后果:
第一,删除之后你手里那些"记住的位置号"全部作废。你本来有个变量记录着"第5个成员"的位置,删了一个前面的成员之后,你再访问第5个,拿到的已经不是你原来记住的那个东西了。
第二,如果你用循环从前往后边遍历边删除,删除一个之后,后面的成员前移,你的循环变量还在往前走,那些前移过来的成员很可能就被你跳过去了。这就是漏删问题的根源。
理解了这个前移逻辑,我们才能理解为什么倒序删除是天下无敌的写法。
2. 关键原理:为什么倒序循环删除才不会翻车
2.1 正序删除的翻车现场:越界和漏删
我先模拟一个最典型的错误写法,很多新手就是这么写的:
局部变量 数组, 整数型, , "0" 局部变量 i, 整数型 数组 = { 1, 2, 2, 3, 4, 5, 6 } 计次循环首 (取数组成员数 (数组), i) 如果真 (数组 [i] % 2 = 0) 删除成员 (数组, i, 1) 如果真结束 计次循环尾 ()这段代码想删除数组中的所有偶数,表面看着逻辑没毛病,跑起来就是灾难。计次循环首的特点是循环次数在进入循环那一刻就固定了,它不会因为数组变短而减少循环次数。假设数组有7个成员,它就固定转7次,可你删着删着数组只剩4个成员了,第5次循环去访问数组[5]直接越界崩溃。
可能有人会抬杠:我不用计次循环首,用判断循环首动态判断数组长度行不行?我再给一个常见的动态长度正序循环写法:
i = 1 判断循环首 (i ≤ 取数组成员数 (数组)) 如果真 (数组 [i] % 2 = 0) 删除成员 (数组, i, 1) 如果真结束 i = i + 1 判断循环尾 ()数组{1,2,2,3},删偶数,看看会发生什么: i=1,数组[1]=1,不删,i变2 i=2,数组[2]=2,删除,数组变成{1,2,3},i变3 i=3,数组[3]=3,不删,i变4 循环结束
你发现了什么?原来数组里的第二个2跟着第一个2一起被删了吗?没有。删除第一个2之后,第二个2前移到了位置2,但你的i已经变成3了,直接把这个前移过来的2跳过去了。结果就是:数组里留着个偶数没删干净。
正序循环的问题总结起来就是:要么越界,要么漏删。想靠正序遍历删除把条件匹配的成员全部清理干净,你得在删除后手动把i减回去再检查一次当前位置,写起来麻烦还不直观。
2.2 倒序删除的原理拆解
倒序删除的思路是从数组尾部往头部遍历,如果当前成员满足删除条件,就删掉它。因为删除操作影响的只是它后面成员的位置,而后面那些成员已经遍历过了,前面成员的位次完全没有受影响。
看这段代码:
局部变量 数组, 整数型, , "0" 局部变量 i, 整数型 数组 = { 1, 2, 2, 3, 4, 5, 6 } 变量循环首 (取数组成员数 (数组), 1, -1, i) 如果真 (数组 [i] % 2 = 0) 删除成员 (数组, i, 1) 如果真结束 变量循环尾 ()用刚才那个翻车例子{1,2,2,3}走一遍: i=4,数组[4]=3,不是偶数,i变3 i=3,数组[3]=2,是偶数,删除,数组变成{1,2,3},i变2 i=2,数组[2]此时是原来前移过来的第二个2,是偶数,删除,数组变成{1,3},i变1 i=1,数组[1]=1,不是偶数,循环结束
数组变成{1,3},偶数全部清干净。为什么倒序就没事?因为删除只会让后面的成员往前挪,我每次从尾部往前处理,删除位置后面的区域已经是我处理完的了,哪怕它们往前挪了位置,对我要处理的前半部分没有任何影响。倒序删除从机制上就规避了漏删和越界,这也是易语言社区公认的标准写法。
2.3 变量循环首和计次循环首的正确选择
删除成员时循环结构的选择,我只推荐一个:变量循环首,初值取数组当前成员数,终值取1,步长取-1。
为什么不推荐在倒序循环里用计次循环首?还是那个老问题,计次循环的循环次数在入口处就定死了。假设数组原本10个成员,你倒序计次循环首(10, i),转到第8次时数组已经被删得只剩6个成员了,你的i取到3,访问数组[3]依然可能越界。除非你在每次循环里都判断如果真 (i > 取数组成员数 (数组)) 跳出循环 (),这样写也不是不行,就是绕。
变量循环首每次循环结束都会重新判断循环变量和目标值的大小关系,所以它更适合"一边删除一边改变数组长度"这种场景。我建议你在自己电脑上把上面正序和倒序两份代码各跑一遍,配合调试输出看数组的实时变化,比看十篇文章都管用。
3. 封装通用删除子程序:一处封装,处处复用
3.1 按值删除所有匹配成员
我不喜欢每次写删除逻辑都从零敲一遍循环。把最常用的"删除所有等于指定值的成员"封装成一个子程序,以后直接调用,省心还不容易错。
.版本 2 .子程序 删除数组中所有匹配项, 整数型, 公开 .参数 数组, 整数型, 参考 数组 .参数 匹配值, 整数型 .局部变量 i, 整数型 .局部变量 删除计数, 整数型 变量循环首 (取数组成员数 (数组), 1, -1, i) 如果真 (数组 [i] = 匹配值) 删除成员 (数组, i, 1) 删除计数 = 删除计数 + 1 如果真结束 变量循环尾 () 返回 (删除计数)这里有几个要命的小细节。第一,参数"数组"必须勾选"参考",而且类型声明成数组,否则你这个子程序拿到的只是数组的副本,你在子程序里删得再开心,外面的原数组纹丝不动。第二,返回值是实际删除的数量,方便调用方知道删了几个,别小看这个数字,排查问题的时候很有用。第三,参数类型根据自己的需求改成文本型、字节型都行,一套逻辑吃遍所有类型。
调用方式也很简单:
局部变量 数据, 整数型, , "0" 局部变量 删了几个, 整数型 数据 = { 3, 5, 3, 8, 3, 9 } 删了几个 = 删除数组中所有匹配项 (数据, 3) ' 数据变为 { 5, 8, 9 }3.2 删除文本数组中的空成员
文本数组里的空成员有个很经典的来源:分割文本。你从外部文件读了一段多行文本,用换行符分割之后,每一行变成了一个数组成员,但文本末尾如果有个换行符,分割出来的数组末尾就会带一个空字符串成员。做数据处理的时候这种空成员非常讨厌,它会导致你的后续统计全部出错。
删除空成员的逻辑很简单,复用上面倒序循环的思路,判断条件从等于某个值改成等于空文本:
.版本 2 .子程序 删除文本数组空成员, 整数型, 公开 .参数 文本数组, 文本型, 参考 数组 .局部变量 i, 整数型 .局部变量 删除计数, 整数型 变量循环首 (取数组成员数 (文本数组), 1, -1, i) 如果真 (文本数组 [i] = "") 删除成员 (文本数组, i, 1) 删除计数 = 删除计数 + 1 如果真结束 变量循环尾 () 返回 (删除计数)使用范例:
局部变量 行数组, 文本型, , "0" 局部变量 有效的行数, 整数型 分割文本 (编辑框1.内容, #换行符, 行数组) 有效的行数 = 删除文本数组空成员 (行数组)分割文本命令会自动清空目标数组再填充内容,这个流程你只需要跑一次。清理之后,行数组里每一行都是非空文本,拿去循环处理不会踩空值异常的坑。顺手提一句:如果数据里不仅有连续换行还有空格,你可以在判断条件里加上或 文本数组 [i] = " ",或者用删首尾空(文本数组 [i]) = ""来判断,这样更稳健。
3.3 删除重复成员的两种方案
数组去重是另一个高频需求,我提供两套方案。
方案一:纯循环倒序删除,适合成员数量在几千以内的中小数组。思路是双重循环,外层从后往前,内层从外层的当前位置往前找,一旦发现相同的值就删除外层。写成代码就是:
.版本 2 .子程序 数组去重, 整数型, 公开 .参数 数组, 整数型, 参考 数组 .局部变量 i, 整数型 .局部变量 j, 整数型 .局部变量 删除计数, 整数型 变量循环首 (取数组成员数 (数组), 2, -1, i) 变量循环首 (i - 1, 1, -1, j) 如果真 (数组 [i] = 数组 [j]) 删除成员 (数组, i, 1) 删除计数 = 删除计数 + 1 跳出循环 () 如果真结束 变量循环尾 () 变量循环尾 () 返回 (删除计数)外层从尾部开始,内层只检查当前成员前面的区域,发现重复就删除外层那个成员。为什么外层的i要从数组末尾到2?因为第1个成员永远不可能被前面的成员重复,它不用参与删除。
方案二:借助易语言的节点对象或哈希表去重,适合几万甚至几十万成员的大数组。单纯用循环的话,数据量一大双重循环的时间开销能让你怀疑人生。用节点去重的思路是:把数组里的值作为键写入节点,写入成功就说明没出现过这个键,写入失败说明键已存在,那这个成员就是重复的,标记出来最后统一删除。这种方式的时间复杂度接近线性,在大数据量场景下优势碾压双重循环。
4. 典型应用场景实操:句柄清理与任务队列
4.1 窗口句柄无效清理,别再用遍历标记了
先说一个我实际做过很多次的场景。程序运行时要枚举所有顶层窗口,把窗口句柄存进一个数组,然后逐个判断窗口是否有效、是否可见、是否属于目标进程。一段时间之后,有些窗口被用户关掉了,句柄数组里这些失效句柄就成了垃圾。
很多人的第一反应是正序循环,删了就调整i,代码写得又臭又长。用倒序删除就非常干净:
.版本 2 .子程序 清理无效窗口句柄 .参数 句柄数组, 整数型, 参考 数组 .局部变量 i, 整数型 .局部变量 删除计数, 整数型 变量循环首 (取数组成员数 (句柄数组), 1, -1, i) 如果真 (窗口句柄是否有效 (句柄数组 [i]) = 假) 删除成员 (句柄数组, i, 1) 删除计数 = 删除计数 + 1 如果真结束 变量循环尾 () 调试输出 ("清理无效句柄数量", 删除计数)窗口句柄是否有效是易语言核心库自带的命令,直接判断窗口句柄有没有失效。这套逻辑同样适用于多线程环境下的任务队列清理,某个线程任务执行完了,从待办任务数组里把它删掉。唯一要提醒的是多线程同时操作同一个数组时得加锁,不然一边删除一边遍历,竞态条件跑起来你连错在哪都找不到。
4.2 分割文本后的空行过滤,一个经典组合拳
我再完整演示一个"读文本文件 → 分割 → 去空行 → 统计有效行数"的组合,这个组合在日志分析、配置读取场景里非常常见。
.版本 2 .子程序 _读取配置_被单击 局部变量 原始内容, 文本型 局部变量 行数组, 文本型, , "0" 局部变量 i, 整数型 局部变量 有效行数, 整数型 原始内容 = 读入文本文件 (取运行目录 () + "config.ini") 分割文本 (原始内容, #换行符, 行数组) 有效行数 = 删除文本数组空成员 (行数组) 计次循环首 (取数组成员数 (行数组), i) 调试输出 (行数组 [i]) 计次循环尾 ()分割文本这个命令有个隐藏行为:它会把目标数组重置,往里面填充分割结果。如果你的原始文本结尾带换行符,分割出来的最后一个成员就是空字符串。经过我们的空成员清理,行数组里每一个成员都是真实有效的内容。我实测过,这个组合拳处理几万行日志文件也没有明显卡顿,属于生产级可用的方案。
4.3 大数组删除的性能优化经验
如果你处理的数组有一万成员以上,而且其中一大半都要删除,用删除成员循环抠的效率就很低了。为什么?因为删除成员本质上是把后面的所有成员往前移动,你删100个成员,最坏情况下数组里的数据要被搬运很多次。
我推荐一种思路:不删,改成"建新数组"。先重定义目标数组到同样大小,遍历原数组时把需要保留的成员依次放进新数组,最后重定义新数组到实际保留数量。
.版本 2 .子程序 筛选保留成员, 整数型, 公开 .参数 源数组, 整数型, 参考 .参数 结果数组, 整数型, 参考 数组 .局部变量 i, 整数型 .局部变量 保留数量, 整数型 重定义数组 (结果数组, 假, 取数组成员数 (源数组)) 计次循环首 (取数组成员数 (源数组), i) 如果真 (源数组 [i] % 2 = 0) 保留数量 = 保留数量 + 1 结果数组 [保留数量] = 源数组 [i] 如果真结束 计次循环尾 () 重定义数组 (结果数组, 真, 保留数量) 返回 (保留数量)这是空间换时间的思路,内存占用多一点,但交换次数降下来了。你不用真的去数底层移动了多少次,只要记住这个结论:数组越大、删除比例越高,建新数组比就地删除更快。反过来,如果数组很小或者删除比例低于10%,原地倒序删除反而是更简洁的选择。
5. 常见报错与排查技巧实录
5.1 下标越界的真实原因
删除数组成员时报"下标越界",九成原因是你在循环里用了固定的循环次数,或者删除位置写在了当前数组长度之外。比如数组只剩3个成员了,你还在用计次循环首(5, i)转圈,跑到第4次访问数组[4]就炸了。
有一个很容易被忽略的场景:动态数组在循环过程中被别的子程序偷偷删了成员。排查这类问题,我的习惯是在循环体开头加一句调试输出 (i, 数组 [i]),跑到哪一步崩的,一看输出就明白了。不要靠猜,没有现场数据的排查都是瞎蒙。
5.2 漏删的三种隐蔽情况
漏删比越界更隐蔽,因为程序正常跑完,你不看结果根本发现不了。我总结三种常见漏删原因:
第一种,正序循环导致的前移跳过,前文已经详细讲过。第二种,循环里删除后忘记调整循环变量,比如你用判断循环首正序删,删完i没有减回来,把前移过来的成员跳过了。第三种,删除时用了错误的匹配条件,比如你要删的是空字符串,但实际成员里面存的是全角空格或换行符,判断条件永远不成立。
遇到漏删,我的排查步骤是固定的:先打印删除前后的数组长度和所有成员,用眼睛对比一遍,再加断点逐步走循环,看每次删除操作发生时成员是怎么移动的。这种问题看代码很难看出来,但只要把过程摊开,原因会自己浮出来。
5.3 子程序修改数组必须加"参考"
这是易语言新手最容易踩的一个坑。你写了一个子程序,参数里声明了一个数组变量,子程序里面又是删除又是重定义,执行完出来一看,外面的原数组一点变化都没有。原因就是你忘了勾选参数类型里的"参考"选项。
不加参考,易语言会把你传入的数组复制一份给子程序用,子程序操作的只是那份拷贝。加了参考,子程序操作的才是你原来的那个数组。所有我前面封装的子程序,参数声明里都写的是参考 数组,你抄代码的时候千万不要把这个词给漏了。还有一点,如果子程序内部会改变数组长度,比如删除成员导致数组缩短,配合参考参数才能保证调用方的数组同步缩短。如果你不需要修改原数组、只是想读取数据,那不加参考也行,还能顺便保护原数组不被误改。
5.4 多维数组删除的边界提醒
多维数组使用删除成员命令要非常谨慎。易语言的多维数组在内存里是连续排布的,删除成员本质上是把一整段连续内存往前搬,多维情况下很容易把行结构给弄乱。比如一个3行4列的二维数组,删除一个成员之后,数据整体前移,你原本的逻辑行列对应关系直接就错乱了。
我的建议是:多维数组不要直接删除成员,要么把它拆成一维数组处理后再组装回来,要么用重定义数组整体重建。这个坑比较深,初学者我建议先掌握一维数组的删除,多维场景等真的遇到了再针对性研究,不用提前浪费时间。
我个人在实际操作中的体会是,数组删除这个操作,从语法层面看非常简单,但真正决定代码质量的,是对"位置前移"这个机制的深入理解,以及循环结构选型的经验。我封装好的那三个子程序,删匹配值、删空成员、去重,放在工程里用了很久,基本没改过。最后再分享一个小技巧:写删除逻辑之前,先在纸上画一下删除前后数组的站位变化,画明白了再动手写代码,比在电脑前瞎调试快得多。