我正在尝试使用C#的Dictionary实现简单的算法:

我的“外部”字典看起来像这样:Dictionary<paramID, Dictionary<string, object>> [其中paramID只是一个包含2个字符串的标识符]

如果键“ x”已在字典中,则将特定条目添加到该记录的字典中;如果它不存在,则将其条目添加到外部字典中,然后将条目添加到内部字典中。

不知何故,当我使用TryGetValue时,它总是返回false,因此它总是在外部Dictionary中创建新条目-产生重复项。

我的代码大致如下所示:

    Dictionary<string, object> tempDict = new Dictionary<string, object>();

    if(outerDict.TryGetValue(new paramID(xKey, xValue), out tempDict))
    {
     tempDict.Add(newKey, newValue);
    }


即使外部词典中有此特定条目,也不会执行if中的block。

我想念什么吗? (如果需要,我可以发布调试器的屏幕截图-如果需要,可以发布其他信息)

最佳答案

如果您没有在paramID类型上重载equals和GetHashCode,并且它是一个类而不是结构,则默认的相等含义将生效,并且每个paramID仅与其自身相等。

您可能想要以下内容:

public class ParamID : IEquatable<ParamID> // IEquatable makes this faster
{
  private readonly string _first; //not necessary, but immutability of keys prevents other possible bugs
  private readonly string _second;
  public ParamID(string first, string second)
  {
    _first = first;
    _second = second;
  }
  public bool Equals(ParamID other)
  {
    //change for case-insensitive, culture-aware, etc.
    return other != null && _first == other._first && _second == other._second;
  }
  public override bool Equals(object other)
  {
    return Equals(other as ParamID);
  }
  public override int GetHashCode()
  {
    //change for case-insensitive, culture-aware, etc.
    int fHash = _first.GetHashCode();
    return ((fHash << 16) | (fHash >> 16)) ^ _second.GetHashCode();
  }
}


对于要求的解释,我将使用不同版本的ParamID,在该版本中,字符串比较不区分大小写和顺序,而不是基于区域性(一种适用于某些计算机可读代码的形式(例如,匹配关键字的形式) -不区分大小写的计算机语言或不区分大小写的标识符(如语言标签),但不能用于人类可读的内容(例如,它不会意识到“ SS”与“ß”是不区分大小写的匹配)。该版本还考虑了{“ A” ,“ B”}匹配{“ B”,“ A”}-也就是说,它不在乎字符串的方式。通过使用不同的规则执行不同的版本,应该可以触及其中的一些设计考虑因素。

让我们从只包含两个状态字段的类开始:

public class ParamID
{
  private readonly string _first; //not necessary, but immutability of keys prevents other possible bugs
  private readonly string _second;
  public ParamID(string first, string second)
  {
    _first = first;
    _second = second;
  }
}


在这一点上,如果我们执行以下操作:

ParamID x = new ParamID("a", "b");
ParamID y = new ParamID("a", "b");
ParamID z = x;
bool a = x == y;//a is false
bool b = z == x;//b is true


因为默认情况下,引用类型仅等于其自身。为什么?首先,有时候这正是我们想要的,其次,如果没有程序员定义平等的工作原理,我们并不一定总是想要其他的东西。

还要注意,如果ParamID是一个结构,则将定义相等性,就像您想要的一样。但是,实现会相当低效,而且如果包含十进制数也可能会出错,因此无论哪种方式,明确地实现相等始终是一个好主意。

我们要赋予这种不同的相等性概念的第一件事是重写IEquatable<ParamID>。这不是严格必要的(并且直到.NET 2.0才存在),但是:


在许多用例中,包括在Dictionary<TKey, TValue>键时,它都会更高效。
以此为起点,很容易进行下一步。


现在,实现平等概念时必须遵循四个规则:


对象仍然必须始终等于其自身。
如果X == Y并且X!= Z,则以后如果这些对象的状态都没有更改,则X == Y并且X!= Z仍然。
如果X == Y并且Y == Z,则X ==Z。
如果X == Y并且Y!= Z,则X!=Z。


大多数时候,您最终会遵循所有这些规则,甚至根本没有考虑它,您只需要检查它们,以确保您在实现中特别陌生和聪明。在某些情况下,我们还可以利用规则1来提高性能:

public class ParamID : IEquatable<ParamID>
{
  private readonly string _first; //not necessary, but immutability of keys prevents other possible bugs
  private readonly string _second;
  public ParamID(string first, string second)
  {
    _first = first;
    _second = second;
  }
  public bool Equals(ParamID other)
  {
    if(other == null)
      return false;
    if(ReferenceEquals(this, other))
      return true;
    if(string.Compare(_first, other._first, StringComparison.InvariantCultureIgnoreCase) == 0 && string.Compare(_second, other._second, StringComparison.InvariantCultureIgnoreCase) == 0)
      return true;
    return string.Compare(_first, other._second, StringComparison.InvariantCultureIgnoreCase) == 0 && string.Compare(_second, other._first, StringComparison.InvariantCultureIgnoreCase) == 0;
  }
}


我们要做的第一件事是查看是否正在将相等与null进行比较。在这种情况下,我们几乎总是希望返回false(并非总是如此,但是异常非常非常罕见,如果您不确定要处理这种异常,则肯定不是),当然我们不想抛出NullReferenceException。

我们要做的下一件事是查看对象是否正在与自身进行比较。这纯粹是一种优化。在这种情况下,这可能会浪费时间,但是对于更复杂的相等性测试来说可能非常有用,因此值得在此指出这一技巧。这利用了身份包含平等的规则,也就是说,任何对象都等于自身(Ayn Rand似乎认为这具有某种深远的意义)。

最后,在处理了这两种特殊情况后,我们得出了平等的实际规则。就像我在上面说的那样,对于不区分大小写的序数比较,我的示例认为两个对象具有相同的两个字符串(以任意顺序)是相等的,因此我有一些代码可以解决这个问题。

(请注意,比较组件的顺序可能会对性能产生影响。不是在这种情况下,而是使用同时包含int和字符串的类,我们将首先比较ints,因为它更快,因此我们也许会发现false的答案,甚至在查看字符串之前)

现在,到此为止,我们已经为重写Equals中定义的object方法奠定了良好的基础:

public override bool Equals(object other)
{
  return (other as ParamID);
}


由于如果asParamIDother会返回ParamID引用,而其他任何内容都为null(包括null,则是我们首先传递的内容),并且由于我们已经处理了与null的比较,都准备好了。

尝试在此时进行编译,您将收到一条警告,提示您已覆盖Equals而不是GetHashCode(如果相反,也是如此)。

字典(以及其他基于哈希的集合,如HashTable和HashSet)使用GetHashCode来确定将密钥放置在内部的位置。它将采用哈希码,以一种与之相关的方式将其重新哈希化为较小的值,然后使用该哈希码将对象放入其内部存储中。

因此,很明显,以下是一个不好的主意,因为ParamID不是在所有字段上都是只读的:

ParamID x = new ParamID("a", "b");
dict.Add(x, 33);
x.First = "c";//x will now likely never be found in dict because its hashcode doesn't match its position!


这意味着以下规则适用于哈希码:


被认为相等的两个对象必须具有相同的哈希码。 (这是一个很难的规则,如果您破坏它,将会有错误)。
虽然我们不能保证唯一性,但返回结果的散布越多越好。 (软规则,做得越好,您的性能就会越好)。
(好吧,2½。)虽然不是严格的规则,但是如果我们对上述第2点采取如此复杂的方法,以至于永远需要返回结果,那么净杂凑效果将比如果我们拥有质量较差的哈希值更糟。因此,如果可以的话,我们也想尽快地做到合理。


尽管有最后一点,但很少值得记住结果。基于散列的集合通常会自己存储值,因此在对象中这样做很浪费。

对于第一个实现,因为我们的相等性方法依赖于字符串相等性的默认方法,所以我们可以使用字符串默认哈希码。对于我的不同版本,我将使用另一种方法,我们将在以后进行探讨:

public override int GetHashCode()
{
  return StringComparer.OrdinalIgnoreCase.GetHashCode(_first) ^ StringComparer.OrdinalIgnoreCase.GetHashCode(_second);
}


让我们将此与第一个版本进行比较。在这两种情况下,我们都获得组成部分的哈希码。如果值是整数,字符或字节,我们本来可以使用这些值本身,但是在这里,我们以为这些部分实现相同逻辑的工作为基础。在第一个版本中,我们使用GetHashCode本身的string,但是由于“ a”与“ A”具有不同的哈希码,因此在这里不起作用,因此我们使用一个生成哈希码的类来忽略这种差异。

两者之间的另一个大区别是,在第一种情况下,我们将更多位与((fHash << 16) | (fHash >> 16))混合使用。这样做的原因是为了避免重复的哈希。我们无法生成一个完美的哈希码,其中每个不同的对象都具有不同的值,因为只有4294967296可能的哈希码值,但是ParamID可能有更多的值(包括null,该哈希码被视为0)。 (在某些情况下,可能会使用完美的哈希值,但它们带来的关注与此处不同。)由于存在这种缺陷,我们不仅要考虑可能的价值,而且还要考虑可能的价值。通常,像在第一个版本中所做的那样移位位可以避免具有相同散列的公共值。我们不希望{“ A”,“ B”}与{“ B”,“ A”}哈希相同。

产生一个故意返回总是为0的可怜的GetHashCode是一个有趣的实验,它将起作用,但是字典不是接近O(1),而是O(n),而字典则是O(n),因此很糟糕!

第二个版本则不这样做,因为它有不同的规则,因此我们实际上要考虑的是相同的值,但要相等地切换,并因此使用相同的哈希码。

另一个很大的不同是使用StringComparer.OrdinalIgnoreCase。这是StringComparer的实例,在其他接口中,该实例实现IEqualityComparer<string>IEqualityComparer。关于IEqualityComparer<T>IEqualityComparer接口有两个有趣的事情。

首先是基于哈希的集合(例如字典)都使用它们,只是除非将一个实例作为实例传递给其构造函数,否则它们将使用DefaultEqualityComparer,该函数将调用我们上面介绍的Equals和GetHashCode方法。

另一个是,它允许我们忽略上面提到的Equals和GetHashCode,并从另一个类提供它们。这有三个优点:


我们可以在“ equals”的定义可能不止一种的情况下使用它们(字符串是经典的情况)。
我们可以由班级作者忽略,而是提供我们自己的。
我们可以使用它们来避免特定的攻击。这种攻击是基于一种情况,即您所提供的输入将被所攻击的代码散列。选择输入是为了有意提供不同但哈希相同的对象。这意味着我们所讨论的避免过早避免的性能很差,并且可能非常严重,以至于成为拒绝服务攻击。通过为哈希码提供具有随机元素的不同IEqualityComparer实现(但对于比较器的每个实例都相同),我们可以每次都充分改变算法以引发攻击。这种用法很少见(它必须完全基于外部输入进行哈希处理,该输入必须足够大才能使糟糕的性能真正受到损害),但是在出现这种情况时至关重要。


最后。如果我们覆盖Equals,那么我们可能也可能不想覆盖==和!=。使它们仅引用身份(有时是我们最关心的身份)可能会很有用,但让它们引用其他语义(“” abc“ ==” ab“ +” c“)可能会很有用。是覆盖的示例)。

综上所述:

引用对象的默认相等性是标识(仅等同于其自身)。

值类型的默认相等性是所有字段的简单比较(但性能较差)。

无论哪种情况,我们都可以更改类的相等性概念,但这必须同时包含Equals和GetHashCode *

我们可以覆盖这一点,并提供另一个平等概念。

字典,HashSet,ConcurrentDictionary等均取决于此。

哈希码表示从对象的所有值到32位数字的映射。

对于我们认为相等的对象,哈希码必须相同。

哈希码必须很好地传播。

*偶然地,匿名类具有与值类型类似的简单比较,但是具有更好的性能,几乎可以与我们关心匿名类型的哈希码的所有情况相匹配。

09-16 23:28