修改V2rayN 加载所有节点去重时 不要影响原来的各分组订阅

问题

上一篇修改加载节点列表后自动去重, 最后发现一个问题. 点击 "所有" 以后, 调用 节点去重 功能, 然后再回到单独的分组订阅里面, 有节点被去掉了.

举个例子,
如果 订阅A里面有节点 1 2 3 4 5, 订阅B里面有节点3 4 5 6 7. 那么全部里面是1 2 3 4 5 3 4 5 6 7, 再去重.
再去看订阅A和订阅B里面就会有节点被去掉了, 比如, 订阅B里面节点 3 4 5 没有了, 只剩节点 6 7 了.

说具体一点, 本来订阅节点是这样的, 订阅A里面有节点 1 2 3 4 5, 订阅B里面有节点3 4 5 6 7. 

那么点击 "所有" 之后, 我们会得到 1 2 3 4 5 3 4 5 6 7 . 再去重就会得到这样:

然后再去分别查看 订阅A 和 订阅B, 就成了这样:


原理

查阅代码, 作者是把所有的订阅的节点都放在一个总的节点列表里面 (实际上是放在一个数据库里面), 也记载了 这个节点是属于哪个分组订阅的.

界面上需要显示的时候, 就根据你选择的哪个分组, 还有你设置的服务器过滤器, 从这个总列表中把一部分节点过滤出来.

去重操作时, 那些节点就从这个总列表里被删掉了. 当然之后再选择单独分组的时候, 就加载不出来了.


思路

所以我们的思路就是, 当加载节点自动去重的时候, 不要操作底层的那个总列表. 

上一篇教程的最后, 我们的修改成果是这样:

我们进到 RefreshServers 函数的内部去看看.

Ctrl + F12 或右键菜单 转到实现.

里面大致是这么几部分.


所以我们要做的就是在读取了节点列表 lstModel 之后, 进行去重处理, 再执行后面的步骤去显示.


具体实践

我们怎么做呢? 把 SubSelectedChanged 里面 RefreshServers(); 这个函数调用删除, 换成上面我们分析的这些代码, 然后再在中间插写一段给 lstModel 做节点去重的逻辑吗?

从逻辑上说, 这样是可行的, 也是正确的. 

但是在编程开发的工程实践中, 这是糟糕的, 是 代码中坏味道, 是 代码重复. 以后如果需要修改(不管是增强开发, 还是修改BUG)都会带来 霰弹枪手术 的问题.

抱歉上面这一句话掉个书袋, 我是为了给有兴趣的朋友引个路, 介绍一下好的编程学习资料.

换个说法来解释一下. 首先我们要认识到一点, 没有代码是不会在未来修改的. 一是人不可能不犯错误, 二是程序的功能会发展增强.

如果我们把相同功能的代码到处拷贝, 过两个月你发现有BUG需要修改, 你还记得都拷贝到哪里去了吗? 如果有一个地方没有修改到, 就等于在程序中遗漏了BUG没有修改全. 就算你记得, 有10个地方要改, 如果改动比较大, 不是一两行的事, 你怎么保证每个地方都改正确了呢? 不会有地方手一抖, 脑子一抽, 少写了一行吗?

所以在编程开发的工程实践中, 如果有相同或相似功能的代码段, 我们总是将它们合理的安排到一个函数里面, 需要用到的时候, 以这个函数的名字来调用就可以了.

你可以 Shift + F12 搜索一下 RefreshServers 的所有引用, 看看有多少地方用到它, 如果每个地方都是把这一大段代码复制粘贴, 后果不敢想象.


重构

我们要做的事情是先将 RefreshServers 进行重构.

幸运的是, VS 工具提供了方便的操作.

我们选中第一行的代码, 右键菜单 - 快速操作和重构

然后选择 提取方法

给提取出的函数取个名字, LoadProfilelist

再选中剩下的代码, 同样的 提取方法, 取个名字 DisplayProfilelist

哦, 对了, 编程的世界里, 方法 和 函数 这两个术语常常就是指一段可以重用的代码, 用一个名字来指示. 我个人觉得, 如果不是在某个考试的考卷上答题的话, 你可以认为它们就是同一个东西.


重构的作用就是调整程序的结构, 但是应该不影响程序本身的功能. 所以我们现在就测试一下. 

可以把刚刚添加的调用原来的节点去重函数 RemoveDuplicateServer 先屏蔽掉. 用 // 设置为注释.

保存, 编译, 开始调试(或执行). 

测试, 功能和修改之前完全一样.


还记得我们本来的修改思路吗? 要在获取节点列表和显示节点列表之间插入节点去重的逻辑.

所以现在我们把 RefreshServers 换成我们刚刚重构出来的两个函数 LoadProfilelist 和 DisplayProfilelist.

这一步修改应该不影响功能. 如果你不放心, 那就编译, 执行, 测试一下.


添加 lstModel 节点去重的逻辑

之前我们使用的 RemoveDuplicateServer 会修改底层保存的节点数据, 不能用了, 我们干脆删掉.

我们原计划是在读取节点列表和显示节点列表之间插入一下节点去重的逻辑.

那么这个逻辑应该怎么写呢?

我们可以学习一下原来的节点去重逻辑是怎么写的.

找到 RemoveDuplicateServer 函数的实现, 是这样的.

结合英文单词的意思 猜一猜, 我们应该继续看 DedupServerList 函数的实现. 

Ctrl + F12, 转到实现, 内容逻辑如下:

我们要学习的就是中间去重的逻辑部分.

所以我们这么写

List<ProfileItemModel> lstKeep = new();
foreach (ProfileItemModel item in lstModel)
{
  if (!lstKeep.Exists(i => CompareProfileItem(i, item, false)))
  {
    lstKeep.Add(item);
  }
}

然后再把 lstKeep 用于显示.

编译, 结果报错了.

这个报错的意思是说, VS 不认识 CompareProfileItem, 不知道应该应该去哪里调用代码.


怎么调用其它类里面的函数

这里我们要学习一下怎么去调用这个 CompareProfileItem.

我们发现 我们使用 CompareProfileItem 的代码在 MainWindowViewModel.cs 这个文件里. 而 CompareProfileItem 的实现代码是在 ConfigHandler.cs 这个文件里. 这是两个不同的文件.

当我们看 ConfigHandler.cs 文件的头部, 是这样子的:

所以 CompareProfileItem 函数 是位于 class ConfigHandler 这个类里面.

C# 的语法规定, 我们要用这样的方式去使用:

ConfigHandler.CompareProfileItem

再编译, 还是报错. 说不可访问, 因为保护级别.



public 和 private

我们又要学习新知识了. 查看 CompareProfileItem 的定义.

可以看到, 函数名那一行代码的前面写着 private

这个 private 意思就是 CompareProfileItem 只能在 ConfigHandler 这个类的内部使用. 

我们想在类的外部使用, 就要把 private 修改为 public.


保存, 编译, 没有报错了.


Reality节点仅ShortID不同, 被去重了

经过测试, 当同一个VPS上面开VLESS的Reality节点时, 如果仅设置 ShortID 不同, 应该是当作不同的节点的. 但是在我们的 去重操作中, 被去掉了.

我们来调试一下.


在读取节点列表这一行语句上设置 断点.

还要在显示节点列表这一行语句上设置 断点. 

最终是下面这个样子. 最左边显示了一个红点, 是断点的标志.

调试运行. 用菜单也可以, 用快捷键 F5 也可以, 用快捷按钮也可以.

程序会运行到断点的时候暂停在那里. 我们可以查看运行时的变量, 也可以控制程序的运行行为.

我们在v2rayN的界面上点击一个分组订阅按钮, 这个分组订阅里面包含了 仅ShortID不同的一些Reality节点.

程序暂停在第一个断点. 黄色箭头指向的是, 当前程序正准备执行(还没有执行)的语句.

接下来, 让程序执行一步. 菜单也可以, 快捷键 F10 也可以.

程序状态会变成这样. 

我们来查看程序中的变量. 查看左下角的 局部变量 窗口. 


可以看到 lstModel 右边写着 Count = 9, 意思是说这个列表中有9个元素. (这是我自己构造的9个节点数据, 你自己构造了几个节点, 这里就会是几)

接下来, 调试继续. 菜单也可以, 快捷键 F5 也可以.

程序会继续向前执行, 被下一个断点暂停.

可以看到, 节点去重之后 的列表 lstKeep, Count = 6, 意思是里面只剩6个节点了. (我自己构造的数据, 有3个节点是只修改了 ShortID 的)

问题出在哪里呢? 问题应该出在 CompareProfileItem 函数里面. 我们去看一看.

可以看到, 这个判断条件里面似乎没有 ShortID 的字样?


我们在 CompareProfileItem 函数刚进来的地方设置断点.

让程序再次运行起来, 再点击分组订阅按钮, 让程序运行到被刚刚设置的这个断点暂停.

看 局部变量 窗口. o 和 n 是局部变量, 这种带结构的变量, 左边的箭头是可以展开的.

我们把 o 展开来看, 里面是有 shortId 的.

回想刚刚我们看过的, 判断 两个节点信息是否相同的 CompareProfileItem 函数中, 是没有使用到 shortId 参数的.

所以我们应该把 shortId 的判断添加进去. 

同时我们可以看到, 还有这么多节点参数没有判断, 比如 fingerprint, publicKey, spiderX, ... 所以这次一起添加进去. 不要以后遇到问题再改了.


现在测试起来, 总算是没问题了.

========


Github 答案 https://github.com/crazypeace/v2rayN/tree/lstProfilesClearDuplicated


========

后记

其实, CompareProfileItem 函数要求的参数是 ProfileItem, 而我们丢进去的是 ProfileItemModel, 可以看到类型并不一样. 那为什么编译没有报错呢? 为什么函数能正确执行呢?

这里要用到面向对象编程的知识. 

CompareProfileItem 是从 ProfileItem 继承而来, 你可以理解为在 ProfileItem 已有的数据的基础上添加了一些额外的数据.

所以 CompareProfileItem 函数拿到一个 ProfileItemModel 变量丢进来的时候, 它只管能获取到 ProfileItem 定义的数据就可以了.

评论

The Hot3 in Last 30 Days

无服务器 自建短链服务 Url-Shorten-Worker 完整的部署教程

ClouDNS .asia免费域名 托管到CloudFlare开CDN白嫖Websocket WS通道翻墙 / desec.io