深入理解C# Unity List集合去除重复项 Distinct

本文围绕C#集合中去除重复项展开,先尝试用引入Linq的Distinct方法,但集合存类时无效。经多方查找,找到单独写比较类继承IEqualityComparer并实现相关方法的方案。还探讨了为何要同时实现Equals()和GetHashCode(),以及GetHashCode()的实现原则,最终解决问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

C#集合中如何去除重复项?
于是你百度了一波,找到以下解决方案。
第一种:两次循环遍历

List<int> repeatList= new List<int>() { 1, 2, 3, 4, 5, 3, 3, 2, 1};
for (int i = 0; i < repeatList.Count; i++)  //外循环是循环的次数
 {
     for (int j = repeatList.Count - 1 ; j > i; j--)  //内循环是 外循环一次比较的次数
     {

         if (repeatList[i] == repeatList[j])
         {
             repeatList.RemoveAt(j);
         }

     }
 }

第二种:叫你引入Linq,然后Distinct

List<int> repeatList= new List<int>() { 1, 2, 3, 4, 5, 3, 3, 2, 1};
var removeRepeatList = repeatList.Distinct();
foreach (var item in removeRepeatList)
{
  //去除重复项后
}

第二种好像很高端,代码也少。就用第二种啦。唉呀,但是案例里面集合存的int值,现在项目里面集合存的是类怎么弄?应该是一样的吧,然后一整狂撸代码。

//需去重复的类
public class Person
{
    private string firstName;
    private string lastName;

    public string FirstName { get { return firstName; } set { firstName = value; } }
    public string LastName { get { return lastName; } set { lastName = value; } }

    public Person() { }

    public Person(string firstName, string lastName)
    {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public override string ToString()
    {
        return string.Format(firstName + lastName);
    } 
}

using System.Collections.Generic;

namespace Utils
{
	//方便打印集合的工具类
    public class LogHelper
    {
        public static string List2Str<T>(IEnumerable<T> list)
        {
            string str = "";
            string suffix = "-";
            foreach (var item in list)
            {
                str += item + suffix;
            }
            //移除掉最后末尾那个"-"
            str = str.Remove(str.Length-1, 1);

            return str;
        }
    }
}

List<Person> persons = new List<Person>();
 persons.Add(new Person("张", "三"));
 persons.Add(new Person("李", "四"));
 persons.Add(new Person("王", "五"));

 persons.Add(new Person("张", "三"));
 persons.Add(new Person("张", "三"));
 Debug.Log("去掉重复前: " + LogHelper.List2Str(persons));
 var persionsRemoveRepeatList = persons.Distinct();
 Debug.Log("去掉重复后: " + LogHelper.List2Str(persionsRemoveRepeatList));

然后噼里啪啦运行,纳尼?竟然没得啥子卵用。代码写错啦,然后一阵检查,没错啊。方式不对?然后一阵百度,找到的都是些去除int/string类型集合的,心里骂了一句娘,垃圾百度。然后想起可以去MSDN查啊,唉,你这沙雕。
自定义类的集合直接Distinct()无效

MSDN怎么说?
在这里插入图片描述
唉呀,我的乖乖,看不懂看不懂。o_o …
然后又是一阵搜索,找到了方案
方案是单独再写一个比较类,其继承自IEqualityComparer,然后实现Equals()和GetHashCode()方法。
这简单。

using System.Collections.Generic;

class PersonCompare : IEqualityComparer<Person>
{
    public bool Equals(Person x, Person y)
    {
        if((x.FirstName == y.FirstName)  && (x.LastName == y.LastName))
        {
            return true;
        }
        return false;
    }

    public int GetHashCode(Person obj)
    {
        return obj.GetHashCode();
    }
}

噫,好像哪里不对,为何要同时实现Equals()和GetHashCode(),GetHashCode()内部如何实现?先不管,先跑起来看一下到。激动人心的时刻来了!

Debug.Log("去掉重复前: " + LogHelper.List2Str(persons));
var persionsRemoveRepeatList = persons.Distinct(new PersonCompare());
Debug.Log("去掉重复后: " + LogHelper.List2Str(persionsRemoveRepeatList));

纳尼!还是不行,一定是哪里有错了!绝壁不是我代码的问题!(哈哈,不是你傻逼代码的问题就是你傻逼的问题)
在这里插入图片描述
然后你机智的想到了去比较类里面答应输出一波。

using System.Collections.Generic;

class PersonCompare : IEqualityComparer<Person>
{
    public bool Equals(Person x, Person y)
    {
        UnityEngine.Debug.Log("比较相等 x:" + x + " y:" + y);

        if ((x.FirstName == y.FirstName)  && (x.LastName == y.LastName))
        {
            UnityEngine.Debug.Log("比较相等....");
            return true;
        }
        return false;
    }

    public int GetHashCode(Person obj)
    {
        UnityEngine.Debug.Log("GetHashCode: " + obj.ToString());
        return obj.GetHashCode();
    }
}

然后惊奇的发现只执行了PersonCompare里的GetHashCode(),压根没执行Equals()方法。
没执行Equals()
这是怎么回事?好像理解不了了。对了,不是还有个问题没解决得嘛。
为何要同时实现Equals()和GetHashCode(),GetHashCode()内部如何实现?
然后你如饥似渴的查找原因。
直到你发现了这儿。
在这里插入图片描述
原来Distinct()内部会去先检查HashCode是否相等,若不相等压根儿就不会去执行Equals()判断两个元素相等。
那么问题来了,什么是HashCode?其实就是根据对象的地址算出来的一个int数字,即对象的哈希码值,代表了该对象在内存中的存储位置。因为代表内存地址,也就是说每个对象的HashCode都不一样。也就是说我们重写的GetHashCode()不对,不能这样写?

public int GetHashCode(Person obj)
{
    return obj.GetHashCode();
}

那该如何实现GetHashCode()呢?
具体原则如下,特别是第四条,是我查了好多文章才看到一大佬说的。
第一,如果两个对象相等,它们必须产生相同的散列码,即GetHashCode()返回的值必须一样;
第二,对于任意对象obj,obj.GetHashCode()必须是一个实例不变式,无论在obj上调用什么方法,obj.GetHashCode必须返回同样的值;
第三,散列函数应该在所有整数中产生一个随机的分布,这样才能获得效率的提升
前三点是基本原则,第四点是实战原则。
第四对象中用作Equals()方法比较标准的Filed(成员变量/类属性)都应该用来计算HashCode();

综上,我们修改HaseCode()方法。

public int GetHashCode(Person obj)
    {
        //UnityEngine.Debug.Log("GetHashCode: " + obj.ToString());
        return obj.FirstName.GetHashCode() ^ obj.LastName.GetHashCode();
    }

然后运行!
在这里插入图片描述
成功啦!怎么没得好鸡动呢,嘻嘻。
要解决一个看似简单的问题,可能需要各个方面的知识,如果某一方面知识欠缺,可能就会卡在那儿。所以要不断地积累知识,知道得越多,解决问题的能力就越强,看待问题的眼光也会比之前高出一大截。不断地去记录和总结自己所学的所想的,假以时日,会怎么样?我们拭目以待。
项目上传到GitHub了
下次再会。

### 关于 Unity 中 AABB 包围盒树的实现 在游戏开发领域,轴对齐包围盒(Axis-Aligned Bounding Box, AABB)是一种常见的碰撞检测优化技术。通过构建一棵 AABBTrees 来存储场景中的对象,可以显著提高碰撞查询效率。 以下是基于 C#Unity 实现代码示例: #### 构建 AABB 树节点结构 ```csharp public class TreeNode { public Bounds bounds; // 使用 Unity 的 Bounds 类表示 AABB public GameObject obj; public TreeNode leftChild; public TreeNode rightChild; public bool IsLeaf() => leftChild == null && rightChild == null; } ``` 此部分定义了一个简单的二叉树节点类 `TreeNode`,其中包含了用于描述边界框的对象 `Bounds` 和关联的游戏物体 `GameObject`[^1]。 #### 插入方法 为了向树中插入新对象并更新其对应的 AABB 节点范围: ```csharp public void Insert(TreeNode root, GameObject newObject) { if (root.IsLeaf()) { var newBounds = CombineBounds(root.bounds, GetBounds(newObject)); root.obj = null; // 将叶子转换为内部节点 root.leftChild = new TreeNode { bounds = root.bounds, obj = root.obj }; root.rightChild = new TreeNode { bounds = GetBounds(newObject), obj = newObject }; root.bounds = newBounds; } else { float costLeft = CalculateCostForInsertion(root.leftChild, newObject); float costRight = CalculateCostForInsertion(root.rightChild, newObject); if (costLeft < costRight) { Insert(root.leftChild, newObject); } else { Insert(root.rightChild, newObject); } root.bounds.Encapsulate(GetBounds(newObject)); // 更新父节点的边界 } } private Bounds CombineBounds(Bounds b1, Bounds b2) { return new Bounds((b1.center + b2.center) / 2f, Vector3.Max(b1.extents, b2.extents)); } private Bounds GetBounds(GameObject go) { Renderer renderer = go.GetComponent<Renderer>(); if (renderer != null) { return renderer.bounds; } throw new InvalidOperationException("Game object must have a Renderer component."); } private float CalculateCostForInsertion(TreeNode node, GameObject newObject) { Bounds combinedBounds = CombineBounds(node.bounds, GetBounds(newObject)); return combinedBounds.size.sqrMagnitude * node.bounds.Intersects(combinedBounds ? 0 : 1; } ``` 上述代码实现了动态调整树形结构的功能,并考虑了性能开销最小化的原则[^2]。 #### 查询重叠区域的方法 当需要测试某个给定的 AABB 是否与其他任何对象发生交集时,可采用如下递归算法: ```csharp public List<GameObject> QueryOverlappingObjects(TreeNode root, Bounds queryBox) { List<GameObject> result = new List<GameObject>(); if (!queryBox.Intersects(root.bounds)) { return result; // 如果当前节点不相交,则无需继续遍历子节点 } if (root.IsLeaf()) { if (root.obj != null && queryBox.Intersects(GetBounds(root.obj))) { result.Add(root.obj); // 添加符合条件的结果到列表中 } } else { result.AddRange(QueryOverlappingObjects(root.leftChild, queryBox)); result.AddRange(QueryOverlappingObjects(root.rightChild, queryBox)); } return result.Distinct().ToList(); // 去除重复项返回最终集合 } ``` 以上逻辑能够高效筛选出所有可能与指定盒子存在交叉关系的目标实体。 --- ### 性能考量与纹理格式分析 对于大规模复杂模型而言,在 GPU 上渲染过程中可能会遇到内存带宽瓶颈问题。因此建议尝试不同类型的法线贴图压缩方案来平衡画质损失程度同运行速度之间的取舍情况。例如选用 R16G16_FLOAT 这种较低精度的数据类型或许能够在保持视觉效果接近原样的前提下获得更好的帧率表现。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值