【问题标题】:How to retrieve actual item from HashSet<T>?如何从 HashSet<T> 中检索实际项目?
【发布时间】:2011-12-07 07:17:44
【问题描述】:

我已阅读 this question 了解为什么这是不可能的,但还没有找到解决问题的方法。

我想从 .NET HashSet&lt;T&gt; 中检索项目。我正在寻找一种具有此签名的方法:

/// <summary>
/// Determines if this set contains an item equal to <paramref name="item"/>, 
/// according to the comparison mechanism that was used when the set was created. 
/// The set is not changed. If the set does contain an item equal to 
/// <paramref name="item"/>, then the item from the set is returned.
/// </summary>
bool TryGetItem<T>(T item, out T foundItem);

用这种方法在集合中搜索一个项目将是 O(1)。从HashSet&lt;T&gt; 中检索项目的唯一方法是枚举所有 O(n) 的项目。

除了创建自己的HashSet&lt;T&gt; 或使用Dictionary&lt;K, V&gt; 之外,我没有找到任何解决此问题的方法。还有什么想法吗?

注意:
我不想检查 HashSet&lt;T&gt; 是否包含该项目。我想获取对存储在HashSet&lt;T&gt; 中的项目的引用,因为我需要更新它(而不用另一个实例替换它)。我将传递给TryGetItem 的项目将是相等的(根据我传递给构造函数的比较机制),但它不会是相同的引用。

【问题讨论】:

  • 为什么不使用 Contains 并返回您作为输入传递的项目?
  • 如果您需要根据键值查找对象,那么 Dictionary 可能是更合适的集合来存储它。
  • @ThatBlairGuy:你是对的。我想我会在内部使用 Dictionary 来实现我自己的 Set 集合来存储我的项目。键将是项目的 HashCode。我将具有与 HashSet 大致相同的性能,并且每次我需要从我的集合中添加/删除/获取项目时都不必提供密钥。
  • @mathias 因为哈希集可能包含一个等于输入的项目,但实际上并不相同。例如,您可能想要一个引用类型的哈希集,但您想要比较内容,而不是相等的引用。

标签: c# .net hashset


【解决方案1】:

另一个技巧是通过访问 HashSet 的内部函数 InternalIndexOf 来进行反射。请记住,字段名称是硬编码的,因此如果在即将发布的 .NET 版本中发生这些更改,这将会中断。

注意:如果您使用 Mono,您应该将字段名称从 m_slots 更改为 _slots

internal static class HashSetExtensions<T>
{
    public delegate bool GetValue(HashSet<T> source, T equalValue, out T actualValue);

    public static GetValue TryGetValue { get; }

    static HashSetExtensions() {
        var targetExp = Expression.Parameter(typeof(HashSet<T>), "target");
        var itemExp   = Expression.Parameter(typeof(T), "item");
        var actualValueExp = Expression.Parameter(typeof(T).MakeByRefType(), "actualValueExp");

        var indexVar = Expression.Variable(typeof(int), "index");
        // ReSharper disable once AssignNullToNotNullAttribute
        var indexExp = Expression.Call(targetExp, typeof(HashSet<T>).GetMethod("InternalIndexOf", BindingFlags.NonPublic | BindingFlags.Instance), itemExp);

        var truePart = Expression.Block(
            Expression.Assign(
                actualValueExp, Expression.Field(
                    Expression.ArrayAccess(
                        // ReSharper disable once AssignNullToNotNullAttribute
                        Expression.Field(targetExp, typeof(HashSet<T>).GetField("m_slots", BindingFlags.NonPublic | BindingFlags.Instance)), indexVar),
                    "value")),
            Expression.Constant(true));

        var falsePart = Expression.Constant(false);

        var block = Expression.Block(
            new[] { indexVar },
            Expression.Assign(indexVar, indexExp),
            Expression.Condition(
                Expression.GreaterThanOrEqual(indexVar, Expression.Constant(0)),
                truePart,
                falsePart));

        TryGetValue = Expression.Lambda<GetValue>(block, targetExp, itemExp, actualValueExp).Compile();
    }
}

public static class Extensions
{
    public static bool TryGetValue2<T>(this HashSet<T> source, T equalValue,  out T actualValue) {
        if (source.Count > 0) {
            if (HashSetExtensions<T>.TryGetValue(source, equalValue, out actualValue)) {
                return true;
            }
        }
        actualValue = default;
        return false;
    }
}

测试:

var x = new HashSet<int> { 1, 2, 3 };
if (x.TryGetValue2(1, out var value)) {
    Console.WriteLine(value);
}

【讨论】:

    【解决方案2】:

    您要的内容已添加到.NET Core a year ago,并且是recently added to .NET 4.7.2

    在 .NET Framework 4.7.2 中,我们向标准 Collection 类型添加了一些 API,它们将启用以下新功能。
    - 将‘TryGetValue’添加到 SortedSet 和 HashSet 以匹配其他集合类型中使用的 Try 模式。

    签名如下(在.NET 4.7.2及以上版本中找到):

        //
        // Summary:
        //     Searches the set for a given value and returns the equal value it finds, if any.
        //
        // Parameters:
        //   equalValue:
        //     The value to search for.
        //
        //   actualValue:
        //     The value from the set that the search found, or the default value of T when
        //     the search yielded no match.
        //
        // Returns:
        //     A value indicating whether the search was successful.
        public bool TryGetValue(T equalValue, out T actualValue);
    

    P.S.:如果你有兴趣,这里有related function they're adding in the future - HashSet.GetOrAdd(T)。

    【讨论】:

      【解决方案3】:

      此方法已添加到.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)
      

      【讨论】:

      【解决方案4】:

      现在 .NET Core 2.0 有这个确切的方法。

      HashSet.TryGetValue(T, T) Method

      【讨论】:

        【解决方案5】:

        修改了@mp666 answer 的实现,使其可用于任何类型的 HashSet 并允许覆盖默认的相等比较器。

        public interface IRetainingComparer<T> : IEqualityComparer<T>
        {
            T Key { get; }
            void ClearKeyCache();
        }
        
        /// <summary>
        /// An <see cref="IEqualityComparer{T}"/> that retains the last key that successfully passed <see cref="IEqualityComparer{T}.Equals(T,T)"/>.
        /// This class relies on the fact that <see cref="HashSet{T}"/> calls the <see cref="IEqualityComparer{T}.Equals(T,T)"/> with the first parameter
        /// being an existing element and the second parameter being the one passed to the initiating call to <see cref="HashSet{T}"/> (eg. <see cref="HashSet{T}.Contains(T)"/>).
        /// </summary>
        /// <typeparam name="T">The type of object being compared.</typeparam>
        /// <remarks>This class is thread-safe but may should not be used with any sort of parallel access (PLINQ).</remarks>
        public class RetainingEqualityComparerObject<T> : IRetainingComparer<T> where T : class
        {
            private readonly IEqualityComparer<T> _comparer;
        
            [ThreadStatic]
            private static WeakReference<T> _retained;
        
            public RetainingEqualityComparerObject(IEqualityComparer<T> comparer)
            {
                _comparer = comparer;
            }
        
            /// <summary>
            /// The retained instance on side 'a' of the <see cref="Equals"/> call which successfully met the equality requirement agains side 'b'.
            /// </summary>
            /// <remarks>Uses a <see cref="WeakReference{T}"/> so unintended memory leaks are not encountered.</remarks>
            public T Key
            {
                get
                {
                    T retained;
                    return _retained == null ? null : _retained.TryGetTarget(out retained) ? retained : null;
                }
            }
        
        
            /// <summary>
            /// Sets the retained <see cref="Key"/> to the default value.
            /// </summary>
            /// <remarks>This should be called prior to performing an operation that calls <see cref="Equals"/>.</remarks>
            public void ClearKeyCache()
            {
                _retained = _retained ?? new WeakReference<T>(null);
                _retained.SetTarget(null);
            }
        
            /// <summary>
            /// Test two objects of type <see cref="T"/> for equality retaining the object if successful.
            /// </summary>
            /// <param name="a">An instance of <see cref="T"/>.</param>
            /// <param name="b">A second instance of <see cref="T"/> to compare against <paramref name="a"/>.</param>
            /// <returns>True if <paramref name="a"/> and <paramref name="b"/> are equal, false otherwise.</returns>
            public bool Equals(T a, T b)
            {
                if (!_comparer.Equals(a, b))
                {
                    return false;
                }
        
                _retained = _retained ?? new WeakReference<T>(null);
                _retained.SetTarget(a);
                return true;
            }
        
            /// <summary>
            /// Gets the hash code value of an instance of <see cref="T"/>.
            /// </summary>
            /// <param name="o">The instance of <see cref="T"/> to obtain a hash code from.</param>
            /// <returns>The hash code value from <paramref name="o"/>.</returns>
            public int GetHashCode(T o)
            {
                return _comparer.GetHashCode(o);
            }
        }
        
        /// <summary>
        /// An <see cref="IEqualityComparer{T}"/> that retains the last key that successfully passed <see cref="IEqualityComparer{T}.Equals(T,T)"/>.
        /// This class relies on the fact that <see cref="HashSet{T}"/> calls the <see cref="IEqualityComparer{T}.Equals(T,T)"/> with the first parameter
        /// being an existing element and the second parameter being the one passed to the initiating call to <see cref="HashSet{T}"/> (eg. <see cref="HashSet{T}.Contains(T)"/>).
        /// </summary>
        /// <typeparam name="T">The type of object being compared.</typeparam>
        /// <remarks>This class is thread-safe but may should not be used with any sort of parallel access (PLINQ).</remarks>
        public class RetainingEqualityComparerStruct<T> : IRetainingComparer<T> where T : struct 
        {
            private readonly IEqualityComparer<T> _comparer;
        
            [ThreadStatic]
            private static T _retained;
        
            public RetainingEqualityComparerStruct(IEqualityComparer<T> comparer)
            {
                _comparer = comparer;
            }
        
            /// <summary>
            /// The retained instance on side 'a' of the <see cref="Equals"/> call which successfully met the equality requirement agains side 'b'.
            /// </summary>
            public T Key => _retained;
        
        
            /// <summary>
            /// Sets the retained <see cref="Key"/> to the default value.
            /// </summary>
            /// <remarks>This should be called prior to performing an operation that calls <see cref="Equals"/>.</remarks>
            public void ClearKeyCache()
            {
                _retained = default(T);
            }
        
            /// <summary>
            /// Test two objects of type <see cref="T"/> for equality retaining the object if successful.
            /// </summary>
            /// <param name="a">An instance of <see cref="T"/>.</param>
            /// <param name="b">A second instance of <see cref="T"/> to compare against <paramref name="a"/>.</param>
            /// <returns>True if <paramref name="a"/> and <paramref name="b"/> are equal, false otherwise.</returns>
            public bool Equals(T a, T b)
            {
                if (!_comparer.Equals(a, b))
                {
                    return false;
                }
        
                _retained = a;
                return true;
            }
        
            /// <summary>
            /// Gets the hash code value of an instance of <see cref="T"/>.
            /// </summary>
            /// <param name="o">The instance of <see cref="T"/> to obtain a hash code from.</param>
            /// <returns>The hash code value from <paramref name="o"/>.</returns>
            public int GetHashCode(T o)
            {
                return _comparer.GetHashCode(o);
            }
        }
        
        /// <summary>
        /// Provides TryGetValue{T} functionality similar to that of <see cref="IDictionary{TKey,TValue}"/>'s implementation.
        /// </summary>
        public class ExtendedHashSet<T> : HashSet<T>
        {
            /// <summary>
            /// This class is guaranteed to wrap the <see cref="IEqualityComparer{T}"/> with one of the <see cref="IRetainingComparer{T}"/>
            /// implementations so this property gives convenient access to the interfaced comparer.
            /// </summary>
            private IRetainingComparer<T> RetainingComparer => (IRetainingComparer<T>)Comparer;
        
            /// <summary>
            /// Creates either a <see cref="RetainingEqualityComparerStruct{T}"/> or <see cref="RetainingEqualityComparerObject{T}"/>
            /// depending on if <see cref="T"/> is a reference type or a value type.
            /// </summary>
            /// <param name="comparer">(optional) The <see cref="IEqualityComparer{T}"/> to wrap. This will be set to <see cref="EqualityComparer{T}.Default"/> if none provided.</param>
            /// <returns>An instance of <see cref="IRetainingComparer{T}"/>.</returns>
            private static IRetainingComparer<T> Create(IEqualityComparer<T> comparer = null)
            {
                return (IRetainingComparer<T>) (typeof(T).IsValueType ? 
                    Activator.CreateInstance(typeof(RetainingEqualityComparerStruct<>)
                        .MakeGenericType(typeof(T)), comparer ?? EqualityComparer<T>.Default)
                    :
                    Activator.CreateInstance(typeof(RetainingEqualityComparerObject<>)
                        .MakeGenericType(typeof(T)), comparer ?? EqualityComparer<T>.Default));
            }
        
            public ExtendedHashSet() : base(Create())
            {
            }
        
            public ExtendedHashSet(IEqualityComparer<T> comparer) : base(Create(comparer))
            {
            }
        
            public ExtendedHashSet(IEnumerable<T> collection) : base(collection, Create())
            {
            }
        
            public ExtendedHashSet(IEnumerable<T> collection, IEqualityComparer<T> comparer) : base(collection, Create(comparer))
            {
            }
        
            /// <summary>
            /// Attempts to find a key in the <see cref="HashSet{T}"/> and, if found, places the instance in <paramref name="original"/>.
            /// </summary>
            /// <param name="value">The key used to search the <see cref="HashSet{T}"/>.</param>
            /// <param name="original">
            /// The matched instance from the <see cref="HashSet{T}"/> which is not neccessarily the same as <paramref name="value"/>.
            /// This will be set to null for reference types or default(T) for value types when no match found.
            /// </param>
            /// <returns>True if a key in the <see cref="HashSet{T}"/> matched <paramref name="value"/>, False if no match found.</returns>
            public bool TryGetValue(T value, out T original)
            {
                var comparer = RetainingComparer;
                comparer.ClearKeyCache();
        
                if (Contains(value))
                {
                    original = comparer.Key;
                    return true;
                }
        
                original = default(T);
                return false;
            }
        }
        
        public static class HashSetExtensions
        {
            /// <summary>
            /// Attempts to find a key in the <see cref="HashSet{T}"/> and, if found, places the instance in <paramref name="original"/>.
            /// </summary>
            /// <param name="hashSet">The instance of <see cref="HashSet{T}"/> extended.</param>
            /// <param name="value">The key used to search the <see cref="HashSet{T}"/>.</param>
            /// <param name="original">
            /// The matched instance from the <see cref="HashSet{T}"/> which is not neccessarily the same as <paramref name="value"/>.
            /// This will be set to null for reference types or default(T) for value types when no match found.
            /// </param>
            /// <returns>True if a key in the <see cref="HashSet{T}"/> matched <paramref name="value"/>, False if no match found.</returns>
            /// <exception cref="ArgumentNullException">If <paramref name="hashSet"/> is null.</exception>
            /// <exception cref="ArgumentException">
            /// If <paramref name="hashSet"/> does not have a <see cref="HashSet{T}.Comparer"/> of type <see cref="IRetainingComparer{T}"/>.
            /// </exception>
            public static bool TryGetValue<T>(this HashSet<T> hashSet, T value, out T original)
            {
                if (hashSet == null)
                {
                    throw new ArgumentNullException(nameof(hashSet));
                }
        
                if (hashSet.Comparer.GetType().IsInstanceOfType(typeof(IRetainingComparer<T>)))
                {
                    throw new ArgumentException($"HashSet must have an equality comparer of type '{nameof(IRetainingComparer<T>)}' to use this functionality", nameof(hashSet));
                }
        
                var comparer = (IRetainingComparer<T>)hashSet.Comparer;
                comparer.ClearKeyCache();
        
                if (hashSet.Contains(value))
                {
                    original = comparer.Key;
                    return true;
                }
        
                original = default(T);
                return false;
            }
        }
        

        【讨论】:

        • 由于您使用的是 Linq 扩展方法Enumerable.Contains,它将枚举集合中的所有元素并比较它们,从而失去集合的哈希实现提供的任何好处。那你还不如直接写set.SingleOrDefault(e =&gt; set.Comparer.Equals(e, obj)),它的行为和性能特征和你的解决方案一样。
        • @Virtlink 很好——你说得对。我会修改我的答案。
        • 但是,如果您要包装一个在内部使用比较器的 HashSet,它会起作用。像这样:Utillib/ExtHashSet
        • @Virtlink 谢谢!我最终将 HashSet 包装为一个选项,但提供了比较器和扩展方法以增加通用性。它现在是线程安全的,不会泄漏内存......但它的代码比我希望的要多得多!
        • @Francois 编写上面的代码更像是找出“最佳”时间/内存解决方案的练习;但是,我不建议您使用这种方法。将 Dictionary 与自定义 IEqualityComparer 一起使用更加直接且面向未来!
        【解决方案6】:

        重载字符串相等比较器怎么样:

          class StringEqualityComparer : IEqualityComparer<String>
        {
            public string val1;
            public bool Equals(String s1, String s2)
            {
                if (!s1.Equals(s2)) return false;
                val1 = s1;
                return true;
            }
        
            public int GetHashCode(String s)
            {
                return s.GetHashCode();
            }
        }
        public static class HashSetExtension
        {
            public static bool TryGetValue(this HashSet<string> hs, string value, out string valout)
            {
                if (hs.Contains(value))
                {
                    valout=(hs.Comparer as StringEqualityComparer).val1;
                    return true;
                }
                else
                {
                    valout = null;
                    return false;
                }
            }
        }
        

        然后将HashSet声明为:

        HashSet<string> hs = new HashSet<string>(new StringEqualityComparer());
        

        【讨论】:

        • 这都是关于内存管理的——返回哈希集中的实际项目,而不是相同的副本。所以在上面的代码中我们找到了相同内容的字符串,然后返回对这个的引用。对于字符串,这类似于实习所做的。
        • @zumalifeguard @mp666 这不能保证按原样工作。这将需要有人实例化HashSet 以提供特定的值转换器。最佳解决方案是TryGetValue 传入专用StringEqualityComparer 的新实例(否则as StringEqualityComparer 可能导致空值导致.val1 属性访问被抛出)。这样做时,StringEqualityComparer 可以成为 HashSetExtension 中的嵌套私有类。此外,在覆盖相等比较器的情况下, StringEqualityComparer 应该调用默认值。
        • 您需要将您的 HashSet 声明为: HashSet valueCash = new HashSet(new StringEqualityComparer())
        • 肮脏的黑客。我知道它是如何工作的,但它的懒惰只是让它成为一种解决方案
        【解决方案7】:

        好的,所以,你可以这样做

        YourObject x = yourHashSet.Where(w => w.Name.Contains("strin")).FirstOrDefault();
        

        这是获取所选对象的新实例。为了更新您的对象,您应该使用:

        yourHashSet.Where(w => w.Name.Contains("strin")).FirstOrDefault().MyProperty = "something";
        

        【讨论】:

        • 这是一种有趣的方式,只需要将第二个包装在尝试中 - 这样如果您搜索不在列表中的内容,您将获得 NullReferenceExpection 。但这是朝着正确方向迈出的一步?
        • LINQ 在 foreach 循环中遍历集合,即 O(n) 查找时间。虽然它是解决问题的方法,但它首先违背了使用 HashSet 的目的。
        【解决方案8】:

        您也可以使用 ToList() 方法并对其应用索引器。

        HashSet<string> mySet = new HashSet();
        mySet.Add("mykey");
        string key = mySet.toList()[0];
        

        【讨论】:

        • 我不知道为什么当我应用这个逻辑它起作用时你会否决票。我需要从以 Dictionary> 开头的结构中提取值,其中 ISet 包含 x 个值。获取这些值的最直接方法是遍历字典,拉取键和 ISet 值。然后我遍历 ISet 以显示各个值。它并不优雅,但很有效。
        • 因为有人会使用 HashSet 来实现 O(1) 复杂度,而 ToList() 使得这个方法 O(n)
        【解决方案9】:

        这实际上是集合集合中的一个巨大遗漏。您将需要仅包含键的字典或允许检索对象引用的 HashSet。这么多人问,为什么不修好,我想不通。

        如果没有第三方库,最好的解决方法是使用 Dictionary&lt;T, T&gt; 与值相同的键,因为 Dictionary 将其条目存储为哈希表。在性能方面它与 HashSet 相同,但它当然会浪费内存(每个条目的指针大小)。

        Dictionary<T, T> myHashedCollection;
        ...
        if(myHashedCollection.ContainsKey[item])
            item = myHashedCollection[item]; //replace duplicate
        else
            myHashedCollection.Add(item, item); //add previously unknown item
        ...
        //work with unique item
        

        【讨论】:

        • 我建议他的字典的键应该是他当前放置在他的 EqualityComparer 中的哈希集的任何内容。我觉得使用 EqualityComparer 当你不是真的说项目相等时使用它是肮脏的(否则你可以只使用你创建的项目来进行比较)。我会创建一个代表密钥的类/结构。当然,这是以更多内存为代价的。
        • 由于 key 存储在 Value 中,我建议使用从 KeyedCollection 继承的集合而不是 Dictionary。 msdn.microsoft.com/en-us/library/ms132438(v=vs.110).aspx
        【解决方案10】:

        SortedSet 在这种情况下可能会有 O(log n) 查找时间,如果使用它是一个选项的话。仍然不是 O(1),但至少更好。

        【讨论】:

          【解决方案11】:

          HashSet 有一个 Contains(T) 方法。

          如果您需要自定义比较方法(例如,存储人员对象,但使用 SSN 进行相等比较),您可以指定 IEqualityComparer

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2020-08-04
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2020-09-14
            • 1970-01-01
            相关资源
            最近更新 更多