易语言数组删除成员全解析:从原理到封装,彻底避开下标越界与漏删
写易语言数组删除这东西看着就是一行命令的事但真上手你就知道了十个新手九个在循环删除上翻过车。有人删除完直接下标越界崩溃有人删完发现该删的没删掉不该删的倒删了一堆。原因无他就是没搞明白删除成员命令背后位置前移这个机制。这篇文章我把删除成员、删除指定成员从原理到实战都拆开讲包括正序倒序怎么选、多维数组的坑、空成员清理、大数组性能优化最后附带可以直接抄走用的封装子程序。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}删偶数看看会发生什么 i1数组[1]1不删i变2 i2数组[2]2删除数组变成{1,2,3}i变3 i3数组[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}走一遍 i4数组[4]3不是偶数i变3 i3数组[3]2是偶数删除数组变成{1,2,3}i变2 i2数组[2]此时是原来前移过来的第二个2是偶数删除数组变成{1,3}i变1 i1数组[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列的二维数组删除一个成员之后数据整体前移你原本的逻辑行列对应关系直接就错乱了。我的建议是多维数组不要直接删除成员要么把它拆成一维数组处理后再组装回来要么用重定义数组整体重建。这个坑比较深初学者我建议先掌握一维数组的删除多维场景等真的遇到了再针对性研究不用提前浪费时间。我个人在实际操作中的体会是数组删除这个操作从语法层面看非常简单但真正决定代码质量的是对位置前移这个机制的深入理解以及循环结构选型的经验。我封装好的那三个子程序删匹配值、删空成员、去重放在工程里用了很久基本没改过。最后再分享一个小技巧写删除逻辑之前先在纸上画一下删除前后数组的站位变化画明白了再动手写代码比在电脑前瞎调试快得多。