首页 / 资讯中心 / 文章详情

易语言数组删除成员全解析:原理、陷阱与批量删除优化

易语言数组删除成员全解析:原理、陷阱与批量删除优化 ★ FEATURED ARTICLE
前几天一个读者把自己写的课堂点名程序发给我说批量删除请假名单的时候总是漏人有时候还会直接弹“数组下标越界”。我让他把删除那段代码发过来一看果然又踩进了同一个坑在正向计次循环里直接调用删除成员。这类问题我这些年见过太多次了归根结底还是没有把易语言数组删除成员的底层行为搞透。今天干脆把这个话题彻底掰开讲一遍从删除成员命令本身到删除指定成员的几种标准写法再到批量删除时的性能优化最后给一个可以直接抄的封装例子保证你下次写相关代码时不再跳坑。这篇东西适合刚接触易语言数组的朋友也适合写过一阵子但总被下标问题折磨的人。我不会讲太多玄乎的理论重点放在“删除成员之后数组到底发生了什么变化”以及“为什么很多人写着写着就乱套”这两件事上。1. 删除成员前先弄明白数组在易语言里到底是怎么存数据的1.1 连续格子模型为什么删除不是“清空”而是“搬家”数组在内存里是一排连续的格子每个格子存一份数据。你可以把它想象成电影院的一排座位座位号从1号开始编。易语言的删除成员命令干的活不是把某个座位上的垃圾扫掉而是把这个人从座位上拽走然后让他后面所有的观众都往前挪一个座位最后这一排的有效座位数减一。举个例子。数组里放了五个数字10, 20, 30, 40, 50如果你执行删除成员 (数组, 3, 1)意思是从下标3开始删1个也就是把30删掉。删除之后数组会变成10, 20, 40, 50注意看40跑到了下标3的位置50跑到了下标4的位置数组最大下标从5变成了4。这就是“搬家”的直观效果。很多人误以为删除成员只是把某个格子里的值改成0或者空文本然后数组长度不变这是完全错误的。删除成员是对数组结构本身的修改它会实打实地减少数组的长度并且改变被删位置之后所有成员的下标。理解了这一点后面所有坑就都能解释了。为什么按位置删完再按原来的下标去找数据会找错因为下标变了。为什么正向循环删除时会漏数据因为后面的元素往前补位了但循环变量还在往前走。1.2 删除成员、逐个置空、重定义数组三条操作的本质区别我见过的初学者经常把删除成员和另外两种操作搞混一种是“把某个成员变成空值”另一种是“重新定义数组长度”。这三者看起来都像在“移除数据”但结果完全不同。操作方式数组内容变化数组长度变化典型用途删除成员目标位置成员消失后方元素前移长度减1真正移除某条记录给某个下标赋默认值该位置变成0或空文本但位置还在不变先标记无效后续重建数组重定义数组重新分配整个数组的长度按参数变化截断、扩容、清空我给读者定位问题的时候第一步就是问他写的到底是“删除成员”还是“数组[3]0”。如果代码里写的是后者那逻辑上永远是“数组里有个空位”遍历时还得额外判断空位非常别扭。真正的删除成员命令会把空位彻底填掉让数组始终紧凑这样遍历、取数量、按位置访问都干净得多。1.3 想用删除成员先养成声明动态数组的习惯易语言里数组分为固定数组和动态数组。固定数组在声明时就把最大下标定死了比如局部变量 arr, 整数型, , 10表示这个数组固定有10个成员。这种数组想执行删除成员就比较尴尬因为固定数组的长度是写死的删掉一个成员后整个数组的边界会变得不可控。我建议所有需要增删操作的数组都声明成动态数组。声明方法是在变量类型后面的参数里填写0局部变量 数组, 整数型, , 0这个0表示数组初始不固定长度之后可以通过加入成员、插入成员、重定义数组来动态调整。只有动态数组才是真正意义上的“可伸缩数组”才适合做删除成员的操作。如果你现在还在用固定数组存用户列表建议尽早改成动态数组不然后面越写越痛苦。2. 删除指定位置的成员三参数命令随手就能用的边界与保护2.1 最小的删除例子删掉第三个成员后发生了什么先看一个最容易理解的操作按位置删除单个成员。易语言的命令格式是删除成员 (数组变量, 删除位置, 删除数量)三个参数分别是“要操作的数组”“从哪个下标开始删”“连续删几个”。比如.版本 2 .支持库 spec 局部变量 数组, 整数型, , 0 数组 { 10, 20, 30, 40, 50 } 删除成员 (数组, 3, 1) 调试输出 (取数组下标 (数组, 1)) 输出4 调试输出 (数组 [3]) 输出40执行完删除成员 (数组, 3, 1)之后最大下标从5变成4第3个成员从原来的30变成了40。这就是删除指定位置成员最基本的行为数组缩短后面所有元素整体前移。这个命令的朴素用法看起来很简单但真正容易出问题的地方在于“位置越界”。如果你的数组只有4个成员却写删除成员 (数组, 5, 1)程序会直接报错。所以在真实项目里删除位置不能想当然必须先用取数组下标 (数组, 1)拿到当前最大下标再判断要删的位置是否合法。2.2 连续删除一段成员删除数量参数的正确姿势第三个参数删除数量的作用是一口气删除从起始位置开始的连续多个成员。比如数组 { 10, 20, 30, 40, 50, 60 } 删除成员 (数组, 2, 3)执行之后从下标2开始删3个也就是把20、30、40全部删掉数组变成10, 50, 60这个特性和单独循环删三次是完全等价的但效率更高代码也更短。我写批量清理逻辑时只要发现目标位置是连续的就一定用这一个命令处理而不是写循环。不过这里有个很隐蔽的越界情况要格外小心如果数组当前最大下标是5你写删除成员 (数组, 4, 10)起始位置4合法但删除数量10已经超过了实际能删的成员数。易语言对这种情况的处理是直接报错程序中止。所以安全写法是先把起始位置和最大下标做一次减法计算出最多还能删多少个剩余可删数量 取数组下标 (数组, 1) - 删除位置 1 如果 (删除数量 剩余可删数量) 删除数量 剩余可删数量 如果结束2.3 越界判断与空数组删除前必须做的三件事我检查别人的删除相关代码时通常只看三件事有没有判断数组非空有没有判断删除位置在有效范围内有没有处理“删除数量超过了实际数量”的情况。三个条件只要有一个漏掉程序早晚会崩。一个比较完善的按位置删除操作至少应该是这个样子如果 (取数组下标 (数组, 1) 0) 数组非空 如果 (删除位置 ≥ 1 且 删除位置 ≤ 取数组下标 (数组, 1)) 位置合法 删除成员 (数组, 删除位置, 1) 否则 输出提示信息 如果结束 否则 输出提示信息 如果结束这里有一个很多人容易忽略的细节判断位置合法时下界不一定是0。易语言的数组下标默认从1开始所以合法位置是1到取数组下标 (数组, 1)。如果你的程序里通过重定义数组开启了0下标模式那下界判断就要改成0不能照抄。另外数组为空时执行删除成员无论删除位置写几都会出问题。我习惯在删除前先判断最大下标是否大于0这算是一个保命习惯。3. 删除指定值的成员按条件匹配的标准套路与正序循环陷阱3.1 为什么会有“按值删除”的需求按位置删除虽然简单但很多业务场景根本不知道“位置”是多少。比如一个在线用户列表你想把名字叫“小明”的用户踢下线你手里只有“小明”这个文本没有他的下标。再比如学生名单里要删除学号为A001的记录你也没法一眼看出这个学号在数组的第几位。这时候就必须“按值删除”先在数组里遍历查找目标值找到之后记录它的下标然后调用删除成员把它删掉。听起来很简单但一旦要删的是所有匹配项写法上稍不注意就会出经典问题。3.2 正序循环删除漏成员的根因排队补位引起的下标偏移我先给一组最典型的例子数组里有{ 1, 2, 2, 3 }目标是删掉所有的2。如果用正向的计次循环一边遍历一边删大概率会漏掉第二个2。过程是这样的第1轮i1数组[1]1不删。 第2轮i2数组[2]2删除数组变成 { 1, 2, 3 }。 第3轮i3数组[3]3不删。第二个2原本在第3个位置但在第2轮删掉第一个2之后它整体往前挪到了第2个位置。可循环变量i已经往下走到第3轮时检查的是新的第3个位置那个挪上来的2就这样被跳过了。这就是“排队补位”引起的下标偏移。数组删除会让后面的人往前补位正向循环的遍历顺序又是从小往大走两者一叠加紧跟在被删成员后面的元素就永远检查不到。如果你需要删除所有匹配值正向循环几乎必然出错。3.3 倒序删除的完整写法解决这个问题最直接的办法是倒序循环从数组末尾往开头遍历遇到匹配值就删。因为删除的是后面的成员前面的成员根本不会受影响所以不会出现“漏掉”的情况。标准写法如下.版本 2 .支持库 spec 局部变量 数组, 整数型, , 0 局部变量 i, 整数型 局部变量 目标值, 整数型 数组 { 1, 2, 2, 3 } 目标值 2 变量循环首 (取数组下标 (数组, 1), 1, -1, i) 如果真 (数组 [i] 目标值) 删除成员 (数组, i, 1) 如果真结束 变量循环尾 () 调试输出 (取数组下标 (数组, 1)) 输出2剩下 1 和 3你可能会问为什么删除后i不需要回退因为我们是倒序删除删除位置i后面下标比它大的元素才会前移而i前面下标比它小的元素位置一个都没变。下一次循环i自动减1检查的是前面一个元素完全不会撞到“补位”问题。这个套路一定要形成肌肉记忆。以后不管你在易语言里做数组去重、批量删记录、清理无效数据只要涉及“按值删除所有匹配项”第一反应就该是倒序循环加删除成员。3.4 顺带搞定去重删除重复成员的两个关键点数组去重也是“删除指定值”的一个经典应用。思路很简单外层循环固定一个成员内层循环从后面倒序查找相同的值找到就删。因为内层是倒序删所以不会出现下标混乱。核心逻辑这样写变量循环首 (取数组下标 (数组, 1) - 1, 1, -1, i) 从倒数第2个往前固定 变量循环首 (取数组下标 (数组, 1), i 1, -1, j) 从末尾往i后面找 如果真 (数组 [j] 数组 [i]) 删除成员 (数组, j, 1) 如果真结束 变量循环尾 () 变量循环尾 ()这里要注意两层循环都用倒序原因完全不同。外层倒序是为了保证删除靠后的成员时前面已经比较过的固定成员不会移位内层倒序是为了保证删除多个重复项时下标不会偏移导致漏删。两个关键点记住了去重逻辑基本不会写错。4. 多维数组删除成员、彻底清空和截断保留特殊场景的行为边界4.1 二维数组删除成员等于删掉一整行易语言的数组不只有一维二维数组在存放表格类数据时很常见。比如一个二维数组存学生成绩单第一维是学生编号第二维是语文、数学、英语成绩那么删除成员 (成绩数组, 2, 1)的作用就是删掉整个第2名学生的所有成绩也就是“一整行”。删除之后第3行变成新的第2行行数减1但每一行的列数保持不变。你可以这样理解删除成员这个命令永远是针对“最外面那一维”的它对第二维只做整组移除不做单列删除。如果你想把二维数组里某一列删掉需要先用临时数组把剩下列搬出来再重新构造数组。这一步没有捷径。多维数组场景下最容易犯的错是删完第一维的某个成员后继续用原来的第二维下标去访问数据。比如原来有3行删掉第2行后只有2行了你还按第3行去访问必然越界。所以多维数组每次操作后都要重新取一次第一维的最大下标。4.2 彻底清空数组的两种办法与效率对比业务上经常需要把数组里的成员全部清空比如重新加载名单、重置数据源。常见的做法有两个。第一种是循环删除所有成员。这个办法逻辑直观但效率很差。数组有1000个成员就要执行1000次删除成员每次都伴随大量元素搬家属于最笨的清空方式。第二种是直接重定义数组重定义数组 (数组, 假, 1, 0)这条命令会把数组重新定义成一维、最大下标为0的空数组。第二个参数假表示不保留原有内容也就是直接把旧数据全部丢弃。这是我个人最推荐的做法一条命令解决问题而且底层操作非常快。要注意重定义数组清空后数组类型和维度还在只是没有成员了。之后你还可以继续用加入成员往里面塞数据不需要重新声明变量。4.3 用重定义数组实现“保头截尾”和“只留最近N条”有些场景不需要全部清空只需要保留前面一部分。比如一个日志数组积累了1000条你想只保留最近100条那可以直接执行如果 (取数组下标 (日志数组, 1) 100) 重定义数组 (日志数组, 真, 1, 100) 如果结束第二个参数真表示“保留原内容”配合最大下标100使用效果就是保留数组的第1到第100个成员其余全部截断。这比循环删除后面900条要高效太多了。不过要记住一点重定义数组是“从头部保留”没有办法直接“从尾部保留”。如果业务需求是“删掉前100条留下后面的”那就得先用倒序循环删除前面的一部分或者先复制出尾部数据再重新构造数组。我自己的习惯是能通过重定义数组解决的问题绝不写循环写循环之前一定先想清楚到底要保留哪一段。5. 批量删除大量成员为什么越删越慢性能剖析与三种优化思路5.1 删除成员的隐藏成本元素搬家很多人觉得删除成员就是一个很简单的操作实际上它背后有一笔看不见的开销。因为数组是连续存储结构删除中间某个成员后后面所有成员都要往前移动一个位置。这个移动操作需要逐个赋值数量越往后越多。假如有一个一万成员的数组你执行删除成员 (数组, 1, 1)就要把后面9999个成员全部往前挪。再删一次又搬9998个。如果业务里需要删除几百上千个分散成员可能会搬动几十万甚至上百万次数据程序自然卡顿。我有个朋友的批量删除程序原先要跑好几秒把所有逻辑改成“过滤重建”之后瞬间降到几十毫秒差距就是这么大。5.2 把多次分散删除合并成一次连续删除既然删除连续成员比删除分散成员高效那么能合并的一定尽量合并。如果业务里已经知道要删除的下标是连续的直接用一条带删除数量的删除成员命令就对了。比如要删除下标5到10的6个成员千万不要写循环删6次直接删除成员 (数组, 5, 6)如果被删除的位置是几段连续的区间那也分别用几条删除成员命令每段一次。这里有个细节多条删除操作优先从后面的区间删起因为先删前面的区间会改变后面区间的下标。比如要删第2到第4个又要删第10到第12个一定先执行删除第10到第12那个再执行删除第2到第4那个。5.3 顶不住的场景换方案过滤重建而不是原地删除如果待删除成员分散在整个数组里数量还特别多原地删除就不合适了。这时候我推荐换一种思路不删而是把“要留下的成员”挑出来重新组装一个新数组。流程是这样先声明一个空的动态数组然后循环遍历原数组凡是应该保留的成员就加入成员进新数组最后用复制数组把新数组覆盖回原数组。整个过程中没有大量元素搬家只有一次遍历和加入操作性能是线性增长的。.版本 2 局部变量 原数组, 整数型, , 0 局部变量 新数组, 整数型, , 0 局部变量 i, 整数型 计次循环首 (取数组下标 (原数组, 1), i) 如果真 (是否保留 (原数组 [i])) 这里按你的业务条件判断 加入成员 (新数组, 原数组 [i]) 如果真结束 计次循环尾 () 复制数组 (原数组, 新数组)这个思路特别适合“批量删除无效数据”的场景。比如名单里有大量过期记录与其一个个删不如把有效的挑出来重建。代码看上好看了速度也快得多。需要注意的是是否保留这个位置要替换成你真实的筛选条件不要照抄。6. 实战写一个安全删除名单成员的完整流程6.1 需求拆解名单删除到底要处理哪些情况最后用一个贴近真实业务的例子把所有内容串起来。假设我现在维护一个课堂点名程序名单存放在文本型动态数组里每个成员是一个学号。需求是在输入框里输入学号点击删除按钮从名单里把这个学号对应的所有记录全部删除。先拆一下需求这里其实藏着几个边界条件输入学号可能不在名单里可能只出现一次也可能出现多次。注册记录不太可能重复但写程序时最好按“允许重复”来处理不然真出现脏数据时程序就崩了。另外一个隐蔽问题是删除完之后界面要刷新所以删除逻辑执行完必须更新一次最大下标再重新绑定显示控件。6.2 核心代码按学号倒序删除全部匹配项核心部分就是倒序遍历匹配删除.版本 2 .支持库 spec 局部变量 名单, 文本型, , 0 局部变量 目标学号, 文本型 局部变量 i, 整数型 局部变量 删除数量, 整数型 名单 { “A001”, “A002”, “A003”, “A001” } 模拟名单 目标学号 “A001” 删除数量 0 变量循环首 (取数组下标 (名单, 1), 1, -1, i) 如果真 (名单 [i] 目标学号) 删除成员 (名单, i, 1) 删除数量 删除数量 1 如果真结束 变量循环尾 () 调试输出 (“共删除”, 删除数量) 调试输出 (“剩余数量”, 取数组下标 (名单, 1))最后得到的名单里不会再出现 A001而且整个过程不会漏删重复项。我在真实项目里还会加一个提示框如果删除数量等于0说明目标学号压根不在名单里需要给用户一个反馈。6.3 封装通用删除函数以后直接复用为了不在每个窗口里重复写这段逻辑我习惯把“按值删除全部匹配项”封装成一个子程序。子程序接收两个参数一个是数组变量一个是目标值内部完成倒序删除。.子程序 删除数组匹配值 .参数 数据数组, 文本型, 数组 .参数 目标值, 文本型 .局部变量 i, 整数型 变量循环首 (取数组下标 (数据数组, 1), 1, -1, i) 如果真 (数据数组 [i] 目标值) 删除成员 (数据数组, i, 1) 如果真结束 变量循环尾 ()这样封装好了之后任何窗口只要引用这个子程序传一个数组和值就能完成删除。易语言的数组参数默认是传址传递的也就是说子程序内部对数组的修改会直接作用到原数组不需要额外返回。这一点很方便但也提醒我们要注意这类子程序执行完外部的数组长度已经变了调用处的代码不要再用旧的长度去做循环。我个人在实际项目里还会再封装一个“安全删除指定位置”的子程序把数组非空判断、位置越界判断、剩余数量判断全部塞进去。以后所有窗口里的删除操作都走这两个函数再也没出现过下标越界的报错。这样处理完你再去写易语言的列表管理、用户管理、日志清理等功能思路会清晰很多。
阅读完成 · 觉得有帮助?
咨询建站