【问题标题】:Why can't I retrieve an item from a HashSet without enumeration?为什么我不能在没有枚举的情况下从 HashSet 中检索项目?
【发布时间】:2009-09-29 20:41:05
【问题描述】:

我正在寻找 HashSet 设计者的头脑。据我所知,我的问题同时适用于 Java 和 C# HashSet,这让我觉得这一定是有充分理由的,尽管我自己也想不出来。

我在HashSet中插入了一个item后,为什么不枚举就无法检索到那个item,几乎不是一个高效的操作?特别是因为 HashSet 是以一种支持高效检索的方式显式构建的。

让 Remove(x) 和 Contains(x) 返回被删除或包含的实际项目通常对我很有用。这不一定是我传递给 Remove(x) 或 Contains(x) 函数的项目。当然,我想我可以通过 HashMap 实现相同的效果,但是当完全可以使用集合来实现这一点时,为什么还要浪费所有的空间和精力呢?

我理解可能存在一些设计问题,即添加此功能将允许使用与其在框架中的角色或未来角色不一致的 HashSet,但如果是这样,这些设计问题是什么?

编辑

要回答更多问题,这里有更多详细信息:

我在 C# 中使用具有覆盖哈希码、equals 等的不可变引用类型来模拟值类型。假设该类型具有成员 A、B 和 C。Hashcode、equals 等仅取决于 A 和 B。给定一些 A 和 B,我希望能够从哈希集中检索该等价项并得到它的 C。我赢了似乎无法为此使用 HashSet,但我至少想知道这是否有任何充分的理由。伪代码如下:

public sealed class X{
 object A;
 object B;
 object extra;

 public int HashCode(){
  return A.hashCode() + B.hashCode();
 }

 public bool Equals(X obj){
  return obj.A == A && obj.B == B;
 }
}

hashset.insert(new X(1,2, extra1));
hashset.contains(new X(1,2)); //returns true, but I can't retrieve extra

【问题讨论】:

  • “为什么没有枚举就无法检索该项目”你能澄清一下你的意思吗,你的意思是 get(),contains() 在你的情况下是 O(n) 吗?
  • 当然 :) 我的意思是如果没有枚举,我无法检索到我放入集合中的确切引用。 HashSet 没有 get() 运算符,并且 contains() 接受一个参数,该参数的计算结果可能等于您输入的引用,但可能不是您输入的确切引用。希望可以清除它。
  • 在这种情况下,您可以将 equals() 实现为 return this == obj - 即仅检查相同的参考。如果没有对象创建控制,这是要付出高昂的代价。对象创建控制可以单独解决问题。
  • 您错过了哈希不一定是唯一的。它只是和索引工具。
  • 这至少添加到 .NET (v4.7.2)。

标签: c# java hashset


【解决方案1】:

在 .Net 中,您可能正在寻找的是 KeyedCollection http://msdn.microsoft.com/en-us/library/ms132438.aspx

您可以通过一些“通用”的聪明才智来解决每次重新实现这个抽象类的麻烦。 (参见 IKeyedObject`1。)

注意:任何实现 IKeyedObject`1 的数据传输对象都应该有一个重写的 GetHashCode 方法,只需返回 this.Key.GetHashCode();等号也一样...

我的基类库中通常会出现这样的内容:

public class KeyedCollection<TItem> : System.Collections.ObjectModel.KeyedCollection<TItem, TItem>
    where TItem : class
{
    public KeyedCollection() : base()
    {
    }

    public KeyedCollection(IEqualityComparer<TItem> comparer) : base(comparer)
    {
    }

    protected override TItem GetKeyForItem(TItem item)
    {
        return item;
    }
}

public class KeyedObjectCollection<TKey, TItem> : System.Collections.ObjectModel.KeyedCollection<TKey, TItem>
    where TItem : class, IKeyedObject<TKey>
    where TKey : struct
{
    public KeyedCollection() : base()
    {
    }

    protected override TItem GetKeyForItem(TItem item)
    {
        return item.Key;
    }
}

///<summary>
/// I almost always implement this explicitly so the only
/// classes that have access without some rigmarole
/// are generic collections built to be aware that an object
/// is keyed.
///</summary>
public interface IKeyedObject<TKey>
{
    TKey Key { get; }
}

【讨论】:

  • 我不确定KeyedCollection&lt;TItem, TItem&gt; 是否真的有用。
【解决方案2】:

您打算如何从哈希集中检索项目?根据定义,集合没有以任何方式排序,因此没有可用于检索相关对象的索引。

集合,作为一个概念,用于测试包含,即有问题的元素是否在哈希数据集中。如果您希望使用键值或索引从数据源中检索值,我建议您查看 MapList

编辑:基于对原始问题的编辑的附加答案

很快,根据您的新信息,您可能有兴趣将您的数据实现为 Java 枚举,类似于以下内容:

 public enum SoonilsDataType {
      A, B, C;

      // Just an example of what's possible
      public static SoonilsDataType getCompositeValue(SoonilsDataType item1,
           SoonilsDataType item2) {
           if (item1.equals(A) && 
                     item2.equals(B)) {
                return C;
           }
      }
 }

枚举的自动继承 values() 返回枚举的“集合”中所有值的列表,您可以使用与集合相同的方式测试包含性。此外,因为它是一个完整的类,您可以定义新的静态方法来执行复合逻辑(就像我在示例代码中试图暗示的那样)。 Enum 唯一的问题是您不能在运行时添加新实例,这可能不是您想要的(尽管如果集合的数据大小不会在运行时增长,Enum 就是您想要的)。

【讨论】:

  • 查看我对 penpen 的评论以获得您的答案。
  • @Soonil 如果我理解您对 penpen 的评论,您不仅使用对象相等性(存在于集合中)而且还使用您自己设计的标识符(或其他一些数字索引)作为次要等于并且您希望此标识符也用作散列集的索引?如果这是正确的,那么您的工作似乎与哈希集的性质正交,我再次建议您将 Map(可能是 HashMap)作为您的数据源。
  • 当然,彼得,我在问题的末尾添加了一些细节,如果有帮助,请告诉我。
  • Peter,TBH,我不完全理解你对枚举的理解,但我认为我们的理解在某个地方出现了分歧 :) +1 努力!
  • @andresp 我知道这已经很晚了,但是如果您知道在可枚举集合中只有一个元素(包括HashSet),那么扩展方法First() 和/或Single() 是正是你想要/需要的。
【解决方案3】:

如果您在插入对象后更改它,则它的哈希可能已更改(如果 hashCode() 已被覆盖,则尤其可能发生这种情况)。如果散列发生变化,则在集合中查找它会失败,因为您将尝试查找散列在与存储位置不同的位置的对象。

此外,如果要查找不同实例的相等对象,则需要确保已在对象中覆盖了 hashCode 和 equals。

请注意,这一切都适用于 Java - 我假设 C# 也有类似的东西,但由于我使用 C# 已经有好几年了,我会让其他人谈谈它的功能。

【讨论】:

  • 这是正确的,但目前完全有可能通过在将引用添加到集合后挂在引用上来破坏 HashSet(以及具有此不变量的所有组件)。您可以根据需要使用该引用来改变和使不变量无效。 API 已经依赖用户来履行这个契约,返回对象引用不会改变这一点。
【解决方案4】:

我想Set 接口和HashSet 类的设计者希望确保Collection 接口上定义的remove(Object) 方法也适用于Set;此方法返回一个布尔值,表示对象是否已成功删除。如果设计者想要提供 remove(Object) 返回 Set 中已经存在的“相等”对象的功能,这将意味着不同的方法签名。

此外,鉴于被删除的对象在逻辑上等于传递给 remove(Object) 的对象,因此在返回包含的对象时添加的值是有争议的。但是,我自己之前也遇到过这个问题,并且使用了 Map 来解决问题。

请注意,在 Java 中,HashSet 在内部使用HashMap,因此使用HashMap 不会产生额外的存储开销。

【讨论】:

  • 您对 Java 的看法是正确的 :) 唉,C# 不使用 HashMap 来实现 HashSet,如果可能的话,我想保留一些空间/时间优势。
【解决方案5】:

为什么不直接使用HashMap&lt;X,X&gt;?这正是你想要的。每次只需执行.put(x,x),然后您就可以使用.get(x) 获取等于x 的存储元素。

【讨论】:

    【解决方案6】:

    这是图书馆设计师的疏忽。正如我在another answer 中提到的,此方法已添加到.NET Framework 4.7.2(以及之前的.NET Core 2.0);见HashSet&lt;T&gt;.TryGetValue。引用the source

    /// <summary>
    /// Searches the set for a given value and returns the equal value it finds, if any.
    /// </summary>
    /// <param name="equalValue">The value to search for.
    /// </param>
    /// <param name="actualValue">
    /// The value from the set that the search found, or the default value
    /// of <typeparamref name="T"/> when the search yielded no match.</param>
    /// <returns>A value indicating whether the search was successful.</returns>
    /// <remarks>
    /// This can be useful when you want to reuse a previously stored reference instead of 
    /// a newly constructed one (so that more sharing of references can occur) or to look up
    /// a value that has more complete data than the value you currently have, although their
    /// comparer functions indicate they are equal.
    /// </remarks>
    public bool TryGetValue(T equalValue, out T actualValue)
    

    【讨论】:

      【解决方案7】:

      在我看来,您实际上是在寻找 Map&lt;X,Y&gt;,其中 Y 是 extra1 的类型。


      (下面是咆哮)

      equals 和 hashCode 方法定义了有意义的对象相等性。 HashSet 类假定如果两个对象相等,如Object.equals(Object) 所定义,那么这两个对象之间没有区别。

      我想说的是,如果object extra 有意义,那么您的设计并不理想。

      【讨论】:

      • 同意你的咆哮 :) 地图可以工作,但我已经决定反对额外的开销,并且可能会推出我自己的设置(因为这不是生产代码,呵呵)+1跨度>
      【解决方案8】:

      已解决。希望找到一个元素对我来说似乎完全有效,因为用于搜索的代表可能与找到的元素不同。如果元素包含键和值信息,并且自定义相等比较器仅比较键部分,则尤其如此。请参阅代码示例。该代码包含一个比较器,该比较器实现了自定义搜索,用于捕获找到的元素。这需要一个比较器的实例。清除对找到的元素的引用。通过包含执行搜索。访问找到的元素。共享比较器实例时要注意多线程问题。

      using System;
      using System.Collections.Generic;
      
      namespace ConsoleApplication1 {
      
      class Box
      {
          public int Id;
          public string Name;
          public Box(int id, string name)
          {
              Id = id;
              Name = name;
          }
      }
      
      class BoxEq: IEqualityComparer<Box>
      {
          public Box Element;
      
          public bool Equals(Box element, Box representative)
          {
              bool found = element.Id == representative.Id;
              if (found)
              {
                  Element = element;
              }
              return found;
          }
      
          public int GetHashCode(Box box)
          {
              return box.Id.GetHashCode();
          }
      }
      
      class Program
      {
          static void Main()
          {
              var boxEq = new BoxEq();
              var hashSet = new HashSet<Box>(boxEq);
              hashSet.Add(new Box(3, "Element 3"));
              var box5 = new Box(5, "Element 5");
              hashSet.Add(box5);
              var representative = new Box(5, "Representative 5");
              boxEq.Element = null;
              Console.WriteLine("Contains {0}: {1}", representative.Id, hashSet.Contains(representative));
              Console.WriteLine("Found id: {0}, name: {1}", boxEq.Element.Id, boxEq.Element.Name);
              Console.WriteLine("Press enter");
              Console.ReadLine();
          }
      }
      
      } // namespace
      

      【讨论】:

        【解决方案9】:

        那些语言中的集合对象大多被设计为值集合,而不是可变对象。他们使用等号检查放入其中的对象是否唯一。这就是为什么 contains 和 remove 返回布尔值,而不是对象:它们检查或删除您传递给它们的值。

        实际上,如果您对集合执行 contains(X),并期望得到不同的对象 Y,这意味着 X 和 Y 相等(即 X.equals(Y) => true),但有点不同,这似乎是错误的。

        【讨论】:

        • 集合根据您指定的比较方法是唯一的。仅仅因为我希望在我的集合中使用比较方法 A 并不意味着当我考虑相同的对象时比较方法 B 没有价值。在可变性上解决您的 cmets,请参阅我对 aperkins 的回答。
        • 即使对象是不可变的,返回存储的实例可能仍然有很大的实用性。除其他外,假设一个人有大量不可变的嵌套数据结构(例如,从 XML 文档中解析),并且希望用对共享数据结构的引用替换对相同但非共享数据结构的引用。带有可以返回传入项的查找操作的HashMap&lt;T&gt; 将是理想的。
        【解决方案10】:

        我收到了一个关于使用 Map 的有趣建议,方法是让我自己的对象将自己定义为 KeyValuePairs。虽然是一个很好的概念,但不幸的是 KeyValuePair 不是一个接口(为什么不呢?),而是一个结构,它把这个计划从空中发射出去。最后我会推出我自己的 Set,因为我的约束允许我有这个选项。

        【讨论】:

          【解决方案11】:

          简答;因为这些项目不能保证是不可变的。

          我遇到了您描述的确切问题,其中 HashCode 基于成员类中的固定字段,但该类包含可以在不更改哈希的情况下更新的附加信息。

          我的解决方案是实现一个基于 ICollection 的通用 MyHashSet,但包裹在 Dictionary> 以提供所需的查找效率,其中 int 键是 T 的 HashCode。但是,这表明如果成员对象的 HashCode 可以更改,那么字典查找后跟列表中项目的相等比较将永远找不到更改的项目。没有强制成员不可变的机制,因此唯一的解决方案是枚举批次。

          【讨论】:

            【解决方案12】:

            在想同样的事情之后,并且能够很好地查看源代码:

            来源:http://referencesource.microsoft.com/#System.Core/System/Collections/Generic/HashSet.cs

            集合是唯一项(对象或值)的集合。在 .net 实现中,如果比较器的 Equals 方法为这两个项目返回 true,则该项目与另一个项目相同(不是唯一的)。如果这两个项目具有相同的哈希码,则不会。因此,检查项目是否存在是一个两步过程。首先使用 hashset 来最小化要完成的项目的数量,然后是压缩本身。

            如果您希望检索一个项目,您必须能够为检索函数提供一个唯一标识符。你可能知道你想要的项目的哈希码。但这还不够。因为多个项目可以具有相同的哈希值。您还需要提供项目本身,以便可以调用 Equal 方法。很明显,如果您拥有该物品,就没有理由得到它。

            可以创建一个数据结构,要求没有两个唯一项返回相同的哈希码。而不是你可以从中得到一个项目。添加*会更快,如果您知道哈希,则可以检索。如果将两个不相等但返回相同哈希的项目放入其中,则第一个将被覆盖。据我所知,这种类型在 .net 中不存在,不,这与字典不同。

            *假设 GetHash 方法是相同的。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2011-02-11
              • 1970-01-01
              • 2012-12-02
              • 2021-04-07
              • 2011-11-09
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多