HashSet <T>에서 실제 항목을 검색하는 방법은 무엇입니까?


85

왜 불가능한 지에 대한 이 질문을 읽었 지만 문제에 대한 해결책을 찾지 못했습니다.

.NET에서 항목을 검색하고 싶습니다 HashSet<T>. 이 서명이있는 방법을 찾고 있습니다.

/// <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)이됩니다. a에서 항목을 검색하는 유일한 방법 HashSet<T>은 O (n) 인 모든 항목을 열거하는 것입니다.

본인은이 문제 다음 내 자신을 만들기 위해 어떤 해결 방법을 찾을 수있다 HashSet<T>또는를 사용합니다 Dictionary<K, V>. 다른 생각은 없나요?

참고 : 에 항목
HashSet<T>포함되어 있는지 확인하고 싶지 않습니다 . 항목 HashSet<T>을 업데이트해야하므로에 저장된 항목에 대한 참조를 가져오고 싶습니다 (다른 인스턴스로 대체하지 않음). 내가 전달할 항목은 TryGetItem동일하지만 (생성자에게 전달한 비교 메커니즘에 따라) 동일한 참조가 아닙니다.


1
포함을 사용하고 입력으로 전달한 항목을 반환하는 이유는 무엇입니까?
Mathias 2011 년


2
당신이 키 값을 기준으로 오브젝트를 조회해야하는 경우, 다음 사전 <T>는에 저장하기 위해 더 적절한 수집을 할 수있다.
ThatBlairGuy

@ThatBlairGuy : 당신이 맞아요. 내 항목을 저장하기 위해 내부적으로 Dictionary를 사용하여 자체 Set 컬렉션을 구현할 것이라고 생각합니다. 키는 항목의 HashCode입니다. 나는 HashSet과 거의 동일한 성능을 갖게 될 것이며 컬렉션에서 항목을 추가 / 제거 / 가져올 때마다 키를 제공 할 필요가 없습니다.
Francois C

2
@mathias 해시 세트에 입력과 동일한 항목이 포함될 수 있지만 실제로는 동일하지 않기 때문입니다. 예를 들어 참조 유형의 해시 세트를 원하지만 동일성에 대한 참조가 아닌 콘텐츠를 비교하려고 할 수 있습니다.
NounVerber

답변:


25

요청하신 내용은 1 년 전에 .NET Core 에 추가 되었으며 최근 .NET 4.7.2에 추가되었습니다 .

.NET Framework 4.7.2에서는 다음과 같이 새로운 기능을 활성화하는 몇 가지 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);

추신 : 관심이 있으시면 앞으로 추가 할 관련 기능이 있습니다 -HashSet.GetOrAdd (T).


65

이것은 실제로 컬렉션 세트에서 큰 누락입니다. 키 사전 또는 객체 참조 검색을 허용하는 HashSet이 필요합니다. 너무 많은 사람들이 그것을 요구 해왔고, 왜 그것이 고쳐지지 않는지는 저를 넘어선 것입니다.

타사 라이브러리가 없으면 Dictionary<T, T>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

1
그의 사전에 대한 키는 그가 현재 해시 세트에 대해 EqualityComparer에 배치 한 모든 것이되어야한다고 제안합니다. 항목이 실제로 동일하다고 말하지 않을 때 EqualityComparer를 사용하는 것이 더럽다고 생각합니다 (그렇지 않으면 비교 목적으로 만든 항목을 사용할 수 있습니다). 키를 나타내는 클래스 / 구조체를 만들 것입니다. 이것은 물론 더 많은 메모리 비용이 발생합니다.
Ed T

1
키가 Value 내에 저장되기 때문에 Dictionary 대신 KeyedCollection에서 상속 된 컬렉션을 사용하는 것이 좋습니다. msdn.microsoft.com/en-us/library/ms132438(v=vs.110).aspx
액세스가 거부

11

이 메서드는 .NET Framework 4.7.2 (및 이전 .NET Core 2.0 )에 추가되었습니다. 참조하십시오 HashSet<T>.TryGetValue. 출처 인용 :

/// <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)

1
잘위한만큼 의 SortedSet 뿐만 아니라.
nawfal

4

문자열 같음 비교자를 오버로드하는 것은 어떻습니까?

  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());

이것은 메모리 관리에 관한 것입니다. 동일한 복사본이 아닌 해시 세트에있는 실제 항목을 반환합니다. 따라서 위 코드에서 내용이 동일한 문자열을 찾은 다음 이에 대한 참조를 반환합니다. 문자열의 경우 이것은 인턴이하는 것과 유사합니다.
mp666

@zumalifeguard @ mp666 이것은있는 그대로 작동하는 것을 보장하지 않습니다. HashSet특정 값 변환기를 제공하려면 인스턴스화하는 사람이 필요합니다 . 최적의 솔루션은 TryGetValue전문화 된 새 인스턴스를 전달하는 것입니다 StringEqualityComparer(그렇지 않으면 as StringEqualityComparernull로 인해 .val1속성 액세스가 throw 될 수 있음). 이렇게하면 StringEqualityComparer가 HashSetExtension 내에서 중첩 된 개인 클래스가 될 수 있습니다. 또한 재정의 된 같음 비교 자의 경우 StringEqualityComparer가 기본값을 호출해야합니다.
Graeme Wicksted

HashSet를 다음과 같이 선언해야합니다. HashSet <string> valueCash = new HashSet <string> (new StringEqualityComparer ())
mp666

1
더러운 해킹. 나는 그것이 작동하지만 게으른 그냥 종류의 솔루션으로 작동하도록 방법을 알고
M.kazem Akhgary

2

자, 이렇게 할 수 있습니다

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

선택한 개체의 새 인스턴스를 가져 오는 것입니다. 개체를 업데이트하려면 다음을 사용해야합니다.

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

이것은 흥미로운 방법입니다. 두 번째를 try-로 래핑하면 목록에없는 것을 검색하면 NullReferenceExpection을 얻을 수 있습니다. 그러나 올바른 방향으로 나아가는 단계입니까?
Piotr Kula

11
LINQ는 foreach 루프, 즉 O (n) 조회 시간에서 컬렉션을 탐색합니다. 문제에 대한 해결책이기는하지만, 처음에는 HashSet을 사용하는 목적에 어긋납니다.
Niklas Ekman 2016 년


2

또 다른 트릭은 InternalIndexOfHashSet 의 내부 기능에 액세스하여 Reflection을 수행 합니다. 필드 이름은 하드 코딩되므로 향후 .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);
}

1

SortedSet은 옵션을 사용하는 경우 해당 상황에서 O (log n) 조회 시간을 가질 수 있습니다. 여전히 O (1)은 아니지만 적어도 더 좋습니다.


1

@ mp666 응답의 구현을 수정하여 모든 유형의 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;
    }
}

1
Linq 확장 메서드를 사용하고 있기 때문에 Enumerable.Contains집합의 모든 요소를 ​​열거하고 비교하여 집합의 해시 구현이 제공하는 이점을 잃게됩니다. 그런 다음 set.SingleOrDefault(e => set.Comparer.Equals(e, obj))솔루션과 동일한 동작 및 성능 특성을 가진을 작성 하는 것이 좋습니다.
Daniel AA Pelsmaeker

@Virtlink Good catch-당신이 절대적으로 옳습니다. 내 대답을 수정하겠습니다.
Graeme Wicksted

그러나 내부적으로 비교기를 사용하는 HashSet을 래핑하면 작동합니다. 이와 같이 : Utillib / ExtHashSet
Daniel AA Pelsmaeker

@Virtlink 감사합니다! 나는 HashSet을 하나의 옵션으로 래핑했지만 추가 다 기능성을 위해 비교 자와 확장 방법을 제공했습니다. 이제 스레드로부터 안전하고 메모리가 누출되지 않습니다 ...하지만 제가 기대했던 것보다 훨씬 더 많은 코드입니다!
Graeme Wicksted

@Francois 위의 코드를 작성하는 것은 "최적의"시간 / 메모리 솔루션을 찾는 연습에 더 가깝습니다. 그러나이 방법을 사용하지 않는 것이 좋습니다. 사용자 지정 IEqualityComparer와 함께 Dictionary <T, T>를 사용하는 것은 훨씬 더 간단하고 미래를 보장합니다!
Graeme Wicksted

-2

HashSet에는 Contains (T) 메서드가 있습니다.

사용자 지정 비교 방법이 필요한 경우 IEqualityComparer 를 지정할 수 있습니다 (예 : 사람 개체를 저장하지만 동일성 비교를 위해 SSN 사용).


-11

ToList () 메서드를 사용하여 인덱서를 적용 할 수도 있습니다.

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

이 논리를 적용했을 때 왜 반대표를 받았는지 모르겠습니다. ISet에 x 개의 값이 포함 된 Dictionary <string, ISet <String >>로 시작하는 구조에서 값을 추출해야했습니다. 이러한 값을 얻는 가장 직접적인 방법은 키와 ISet 값을 가져 오는 사전을 반복하는 것입니다. 그런 다음 ISet을 반복하여 개별 값을 표시했습니다. 우아하지는 않지만 효과가있었습니다.
j.hull
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.