热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

使用LINQ加入HashSet<T>需要帮助理解意外行为

如何解决《使用LINQ加入HashSet<T>需要帮助理解意外行为》经验,为你挑选了1个好方法。

我使用C#HastSet和LINQ的Join方法遇到了一些奇怪的行为,我不明白.我已经简化了我正在做的事情,以帮助专注于我所看到的行为.

我有以下内容:

 private HashSet _mySet; // module level

 IEnumerable searchKeys; // parameter.
 // Partial key searches are allowed.

 private IEqualityComparer _coreKeyComparer; // Module level.
 // Compares instances of MyClass and ISearchKey to determine 
 // if they match.

鉴于

    searchKeys和_mySet之间存在1对多的关系.

    MyClass实现接口IPartialKey和ICoreKey.

    ISearchKey继承自IPartialKey和ICoreKey.

    MyClass和ISearchKey实例都覆盖了GetHashCode方法.

    MyClass的哈希码值基于其完整键值,包括其ICoreKey和IPartialKey值以及其他字段.

    MyClass使用的完整密钥不是唯一的.两个不同的MyClass实例可以具有相同的哈希码.

    ISearchKey的哈希码值仅基于其ICoreKey和IPartialKey值.即,ISearchKey哈希码可能与匹配的MyClass实例的哈希码不同.(旁注:在我第一次遇到问题的情况下,ISearchKey的IPartialKey值与MyClass完整键匹配,因此GetHashCode方法将为ISearchKey和MyClass返回相同的值.我包含额外的复杂性以更好地说明基础逻辑我正在做什么.)

    _coreKeyComparer.GetHashCode方法仅使用其ICoreKey值返回匹配ISearchKey和MyClass实例的相同值.

    _coreKeyComparer.Equals方法将参数分别转换为MyClass和ISearchKey,如果它们的IPartialKey值匹配则返回true.(旁注:_coreKeyComparer已经过严格测试并且工作正常.)

我预计两个集合之间的连接应该会产生如下结果:

{searchKey_a, myClass_a1},
{searchKey_a, myClass_a2},
{searchKey_a, myClass_a3},
{searchKey_b, myClass_b1},
{searchKey_b, myClass_b2},
{searchKey_c, myClass_c1},
{searchKey_c, myClass_c2},
{searchKey_c, myClass_c3},
{searchKey_c, myClass_c4},
etc....

ie同一个ISearchKey实例会多次出现,一次为它所连接的每个匹配的MyClass实例.

但是当我从searchKeys到_mySet的连接时:

        var matchedPairs = searchKeys
          .Join(
            _mySet,
            searchKey => searchKey,
            myClass => myClass,
            (searchKey, myClass) => new {searchKey, myClass},
            _coreKeyComparer)
            .ToList();

我只为每个searchKeyClass实例获得一个MyClass实例.即matchedPairs集合看起来像:

    {searchKey_a, myClass_a1},
    {searchKey_b, myClass_b1},
    {searchKey_c, myClass_c1},
etc....

但是,如果我反转连接,请从_mySet转到searchKeys:

   var matchedPairs = _mySet
          .Join(
            searchKeys,
            myClass => myClass,
            searchKey => searchKey,
            (myClass, searchKey) => new {searchKey, myClass},
            _coreKeyComparer)
            .ToList();

我得到了正确的matchedPairs集合.来自_mySet的所有匹配记录与它们匹配的searchKey一起返回.

我查看了文档并检查了多个示例,但没有看到为什么searchKeys-to-_mySet Join给出了错误的答案,而_mySet-to-searchKeys给出了正确/不同的答案.

(旁注:我也尝试了从searchKeys到_myset的GroupJoin并得到了类似的结果.即每个searchKeyClass实例最多找到一个来自_mySet的结果.)

我不明白Join方法应该如何工作,或者Join与HashSet的工作方式不同于List或其他类型的集合.

如果是前者,我需要澄清,所以我不会在将来使用Join时犯错误.

如果是后者,那么这个不同的行为是一个.Net bug,或者这是HashSet的正确行为?

假设行为是正确的,我将非常感谢有人解释这个(意外的)Join/HashSet行为背后的基础逻辑.

为了清楚起见,我已经修复了我的代码,因此它返回了正确的结果,我只想了解为什么我最初得到的结果不正确.



1> Eric Lippert..:

您的错误几乎肯定存在于您未在问题中显示的大量代码中.我的建议是,您将程序简化为产生错误最简单的程序.这样做,要么你会发现你的错误,要么你会产生一个如此简单的程序,你可以在你的问题中发布所有这些,然后我们可以分析它.

假设行为是正确的,我将非常感谢有人解释这个(意外的)Join/HashSet行为背后的基础逻辑.

由于我不知道出乎意料的行为是什么,我不能说为什么会这样.然而,我可以准确地说出了什么Join,也许这会有所帮助.

Join 采取以下措施:

一个"外部"集合 - 接收器Join.

"内部"集合 - 扩展方法的第一个参数

两个关键提取器,从外部和内部集合中提取密钥

一个投影,它接受其键匹配的内部和外部集合的成员,并生成该匹配的结果

比较两个键是否相等的比较操作.

这是如何Join工作的.(这在逻辑上是会发生什么;实际的实现细节有所优化.)

首先,我们迭代"内部"集合,恰好一次.

对于内部集合的每个元素,我们提取它的键,然后我们形成一个多字典,它从键映射到内部集合中所有元素的集合,其中键选择器生成该键.使用提供的比较来比较密钥的相等性.

因此,我们现在有一个从查找TKeyIEnumerable.

其次,我们迭代"外部"集合,恰好一次.

对于外部集合的每个元素,我们提取其密钥,并使用提供的密钥比较再次在该字符串的多字典中查找.

然后,我们对内部集合的每个匹配元素执行嵌套循环,调用外部/内部对上的投影,并生成结果.

也就是说,Join行为类似于伪代码实现:

static IEnumerable Join
  (IEnumerable outer, 
  IEnumerable inner, 
  Func outerKeySelector, 
  Func innerKeySelector, 
  Func resultSelector, 
  IEqualityComparer comparer) 
{
  var lookup = new SomeMultiDictionary(comparer);
  foreach(TInner innerItem in inner)
  {
    TKey innerKey = innerKeySelector(innerItem);
    lookup.Add(innerItem, innerKey);
  }
  foreach (TOuter outerItem in outer) 
  {
    TKey outerKey = outerKeySelector(outerItem);
    foreach(TInner innerItem in lookup[outerKey])
    {
      TResult result = resultSelector(outerItem, innerItem);
      yield return result;
    }
  }
}

一些建议:

替换所有GetHashCode实现以便它们返回0,并运行所有测试.他们应该通过!从中返回零总是合法的GetHashCode.这样做几乎肯定会破坏你的表现,但绝不能破坏你的正确性.如果您处于需要特定非零值的情况GetHashCode,那么您就有一个错误.

测试您的密钥比较以确保它是有效的比较.它必须服从三个平等规则:(1)反身性:一个事物总是等于它自己,(2)对称性:等于AB必须相等,BA(3)传递性:如果A等于BB等于C那么A必须相等C.如果不满足这些规则,那么Join可能表现得很奇怪.

Join用a SelectMany和a 替换你的Where.那是:

from o in outer join i in inner on getOuterKey(o) equals getInnerKey(i) select getResult(o, i)

可以改写为

from o in outer
from i in inner
where keyEquality(getOuterKey(o), getInnerKey(i))
select getResult(o, i)

该查询比连接版本,但它在逻辑上完全相同.再次,运行您的测试.你得到相同的结果吗?如果没有,你的逻辑中有一个错误.

同样,我不能强烈强调你的态度"加入可能在给出哈希表时被打破"是阻止你找到你的bug的原因.加入不破.这个代码在十年内没有改变,它非常简单,当我们第一次写它时它是正确的.更有可能的是,你的复杂而神秘的关键比较逻辑在某处被打破.


@EricLippert,我现在意识到我没有仔细阅读你的初步答案.这个bug出现在我的IEqualityComparer中,我错误地坚持认为是正确的.部分匹配的需要导致传递失败,这意味着当应该返回true时,内部hashset上的循环返回false.规则1:永远不要发誓问题不在特定的代码块中,因为它总是在代码中.我无法找到一种方法来修复比较器以处理与一组searchkeys的部分匹配,因此我重构了代码以避免加入.谢谢你的帮助.
@RBDavidson:进一步阅读:如果您对“人们实施比较错误的方式”这一主题感兴趣,请参阅https://ericlippert.com/2011/01/20/bad-comparisons-part-one/。如果您对主题“人们错误地实现GetHashCode的方式”感兴趣,请参阅https://blogs.msdn.microsoft.com/ericlippert/2011/02/28/guidelines-and-rules-for-gethashcode/
推荐阅读
  • 阿里Treebased Deep Match(TDM) 学习笔记及技术发展回顾
    本文介绍了阿里Treebased Deep Match(TDM)的学习笔记,同时回顾了工业界技术发展的几代演进。从基于统计的启发式规则方法到基于内积模型的向量检索方法,再到引入复杂深度学习模型的下一代匹配技术。文章详细解释了基于统计的启发式规则方法和基于内积模型的向量检索方法的原理和应用,并介绍了TDM的背景和优势。最后,文章提到了向量距离和基于向量聚类的索引结构对于加速匹配效率的作用。本文对于理解TDM的学习过程和了解匹配技术的发展具有重要意义。 ... [详细]
  • 本文介绍了在rhel5.5操作系统下搭建网关+LAMP+postfix+dhcp的步骤和配置方法。通过配置dhcp自动分配ip、实现外网访问公司网站、内网收发邮件、内网上网以及SNAT转换等功能。详细介绍了安装dhcp和配置相关文件的步骤,并提供了相关的命令和配置示例。 ... [详细]
  • baresip android编译、运行教程1语音通话
    本文介绍了如何在安卓平台上编译和运行baresip android,包括下载相关的sdk和ndk,修改ndk路径和输出目录,以及创建一个c++的安卓工程并将目录考到cpp下。详细步骤可参考给出的链接和文档。 ... [详细]
  • JavaScript设计模式之策略模式(Strategy Pattern)的优势及应用
    本文介绍了JavaScript设计模式之策略模式(Strategy Pattern)的定义和优势,策略模式可以避免代码中的多重判断条件,体现了开放-封闭原则。同时,策略模式的应用可以使系统的算法重复利用,避免复制粘贴。然而,策略模式也会增加策略类的数量,违反最少知识原则,需要了解各种策略类才能更好地应用于业务中。本文还以员工年终奖的计算为例,说明了策略模式的应用场景和实现方式。 ... [详细]
  • 使用Ubuntu中的Python获取浏览器历史记录原文: ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • 闭包一直是Java社区中争论不断的话题,很多语言都支持闭包这个语言特性,闭包定义了一个依赖于外部环境的自由变量的函数,这个函数能够访问外部环境的变量。本文以JavaScript的一个闭包为例,介绍了闭包的定义和特性。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • Linux如何安装Mongodb的详细步骤和注意事项
    本文介绍了Linux如何安装Mongodb的详细步骤和注意事项,同时介绍了Mongodb的特点和优势。Mongodb是一个开源的数据库,适用于各种规模的企业和各类应用程序。它具有灵活的数据模式和高性能的数据读写操作,能够提高企业的敏捷性和可扩展性。文章还提供了Mongodb的下载安装包地址。 ... [详细]
  • Ihavethefollowingonhtml我在html上有以下内容<html><head><scriptsrc..3003_Tes ... [详细]
  • 深度学习中的Vision Transformer (ViT)详解
    本文详细介绍了深度学习中的Vision Transformer (ViT)方法。首先介绍了相关工作和ViT的基本原理,包括图像块嵌入、可学习的嵌入、位置嵌入和Transformer编码器等。接着讨论了ViT的张量维度变化、归纳偏置与混合架构、微调及更高分辨率等方面。最后给出了实验结果和相关代码的链接。本文的研究表明,对于CV任务,直接应用纯Transformer架构于图像块序列是可行的,无需依赖于卷积网络。 ... [详细]
  • 本文介绍了在处理不规则数据时如何使用Python自动提取文本中的时间日期,包括使用dateutil.parser模块统一日期字符串格式和使用datefinder模块提取日期。同时,还介绍了一段使用正则表达式的代码,可以支持中文日期和一些特殊的时间识别,例如'2012年12月12日'、'3小时前'、'在2012/12/13哈哈'等。 ... [详细]
  • 本文介绍了RxJava在Android开发中的广泛应用以及其在事件总线(Event Bus)实现中的使用方法。RxJava是一种基于观察者模式的异步java库,可以提高开发效率、降低维护成本。通过RxJava,开发者可以实现事件的异步处理和链式操作。对于已经具备RxJava基础的开发者来说,本文将详细介绍如何利用RxJava实现事件总线,并提供了使用建议。 ... [详细]
  • 统一知识图谱学习和建议:更好地理解用户偏好
    本文介绍了一种将知识图谱纳入推荐系统的方法,以提高推荐的准确性和可解释性。与现有方法不同的是,本方法考虑了知识图谱的不完整性,并在知识图谱中传输关系信息,以更好地理解用户的偏好。通过大量实验,验证了本方法在推荐任务和知识图谱完成任务上的优势。 ... [详细]
  • 本文介绍了Java中Currency类的getInstance()方法,该方法用于检索给定货币代码的该货币的实例。文章详细解释了方法的语法、参数、返回值和异常,并提供了一个示例程序来说明该方法的工作原理。 ... [详细]
author-avatar
博文
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有