Como recuperar o item real do HashSet <T>?

86

Eu li esta pergunta sobre por que isso não é possível, mas não encontrei uma solução para o problema.

Eu gostaria de recuperar um item de um .net HashSet<T>. Estou procurando um método que teria esta assinatura:

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

Pesquisar o conjunto para um item com tal método seria O (1). A única maneira de recuperar um item de a HashSet<T>é enumerar todos os itens que são O (n).

Eu não encontrei nenhuma solução alternativa para este problema além de criar meu próprio HashSet<T>ou usar um Dictionary<K, V>. Alguma outra ideia?

Observação:
não quero verificar se HashSet<T>contém o item. Desejo obter a referência do item armazenado no HashSet<T>porque preciso atualizá-lo (sem substituí-lo por outra instância). O item que eu passaria para TryGetItemseria igual (de acordo com o mecanismo de comparação que passei para o construtor), mas não seria a mesma referência.

François C
fonte
1
Por que não usar Contém e retornar o item que você passou como entrada?
Mathias
2
Se você precisar pesquisar um objeto com base em um valor de chave, o Dicionário <T> pode ser a coleção mais apropriada para armazená-lo.
ThatBlairGuy
@ThatBlairGuy: Você está certo. Acho que implementarei minha própria coleção Set usando um Dicionário internamente para armazenar meus itens. A chave será o HashCode do item. Terei aproximadamente o mesmo desempenho de um HashSet e isso me poupará de ter que fornecer uma chave cada vez que precisar adicionar / remover / obter um item de minha coleção.
François C
2
@mathias Porque o hashset pode conter um item igual à entrada, mas não é realmente o mesmo. Por exemplo, você pode querer ter um conjunto de hash de tipos de referência, mas deseja comparar o conteúdo, não a referência de igualdade.
NounVerber

Respostas:

28

O que você está pedindo foi adicionado ao .NET Core há um ano e recentemente adicionado ao .NET 4.7.2 :

No .NET Framework 4.7.2, adicionamos algumas APIs aos tipos de coleção padrão que permitirão a nova funcionalidade da seguinte maneira.
- 'TryGetValue' é adicionado a SortedSet e HashSet para corresponder ao padrão Try usado em outros tipos de coleção.

A assinatura é a seguinte (encontrada em .NET 4.7.2 e superior):

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

PS .: Caso você esteja interessado, há uma função relacionada que estão adicionando no futuro - HashSet.GetOrAdd (T).

Evdzhan Mustafa
fonte
65

Essa é, na verdade, uma grande omissão no conjunto de coleções. Você precisaria apenas de um Dicionário de chaves ou de um HashSet que permite a recuperação de referências de objeto. Muitas pessoas pediram por isso, por que não consertar está além de mim.

Sem bibliotecas de terceiros, a melhor solução alternativa é usar Dictionary<T, T>chaves idênticas aos valores, já que o Dicionário armazena suas entradas como uma tabela hash. Em termos de desempenho, é igual ao HashSet, mas desperdiça memória, é claro (tamanho de um ponteiro por entrada).

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
JPE
fonte
1
Eu sugeriria que as chaves de seu dicionário deveriam ser as que ele colocou atualmente em seu EqualityComparer para o hashset. Acho que é sujo usar um EqualityComparer quando você não está realmente dizendo que os itens são iguais (caso contrário, você pode apenas usar o item que criou para fins de comparação). Eu faria uma classe / estrutura que representa a chave. Claro que isso custa mais memória.
Ed T
1
Como a chave é armazenada dentro de Value, sugiro usar a coleção herdada de KeyedCollection em vez de Dictionary. msdn.microsoft.com/en-us/library/ms132438(v=vs.110).aspx
Acesso negado em
11

Este método foi adicionado ao .NET Framework 4.7.2 (e ao .NET Core 2.0 antes dele); veja HashSet<T>.TryGetValue. Citando a fonte :

/// <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)
Douglas
fonte
1
Bem como para SortedSet .
nawfal
4

Que tal sobrecarregar o comparador de igualdade da string:

  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;
        }
    }
}

E então declare o HashSet como:

HashSet<string> hs = new HashSet<string>(new StringEqualityComparer());
mp666
fonte
Isso é tudo sobre gerenciamento de memória - retornar o item real que está no hashset em vez de uma cópia idêntica. Assim, no código acima, encontramos a string com o mesmo conteúdo e retornamos uma referência a ela. Para strings, é semelhante ao que o estágio faz.
mp666 de
@zumalifeguard @ mp666 não é garantido que funcione como está. Seria necessário que alguém instanciasse o HashSetpara fornecer o conversor de valor específico. Uma solução ideal seria TryGetValuepassar uma nova instância do especializado StringEqualityComparer(caso contrário, o as StringEqualityComparerpoderia resultar em um nulo fazendo com que o .val1acesso à propriedade fosse lançado). Ao fazer isso, StringEqualityComparer pode se tornar uma classe privada aninhada dentro de HashSetExtension. Além disso, no caso de um comparador de igualdade substituído, o StringEqualityComparer deve chamar o padrão.
Graeme Wicksted,
você precisa declarar seu HashSet como: HashSet <string> valueCash = new HashSet <string> (new StringEqualityComparer ())
mp666
1
Hack sujo. Eu sei como funciona, mas é um tipo de solução preguiçosa apenas para fazer funcionar
M.kazem Akhgary
2

Ok, então, você pode fazer assim

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

Isso é para obter uma nova instância do objeto selecionado. Para atualizar seu objeto, você deve usar:

yourHashSet.Where(w => w.Name.Contains("strin")).FirstOrDefault().MyProperty = "something";
Vulovic Vukasin
fonte
Esta é uma maneira interessante, você só precisa embrulhar o segundo em uma tentativa - de modo que, se você pesquisar algo que não está na lista, obtenha um NullReferenceExpection. Mas é um passo na direção correta?
Piotr Kula
11
LINQ percorre a coleção em um loop foreach, ou seja, o tempo de pesquisa O (n). Embora seja uma solução para o problema, ele meio que anula o propósito de usar um HashSet em primeiro lugar.
Niklas Ekman
2

Outro truque seria fazer reflexão, acessando a função interna InternalIndexOf do HashSet. Lembre-se de que os nomes dos campos são codificados permanentemente, portanto, se esses nomes forem alterados nas próximas versões do .NET, haverá falhas.

Nota: Se você usar Mono, deve alterar o nome do campo de m_slotspara _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;
    }
}

Teste:

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

SortedSet provavelmente teria tempo de pesquisa O (log n) nessa circunstância, se usar isso for uma opção. Ainda não O (1), mas pelo menos melhor.

Erik Dietrich
fonte
1

Implementação modificada da resposta @ mp666 para que possa ser usada para qualquer tipo de HashSet e permite a substituição do comparador de igualdade padrão.

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;
    }
}
Graeme Wicksted
fonte
1
Como você está usando o método de extensão Linq Enumerable.Contains, ele enumerará todos os elementos do conjunto e os comparará, perdendo quaisquer benefícios que a implementação de hash do conjunto oferece. Então você pode simplesmente escrever set.SingleOrDefault(e => set.Comparer.Equals(e, obj)), que tem as mesmas características de comportamento e desempenho de sua solução.
Daniel AA Pelsmaeker
@Virtlink Boa pegada - você está absolutamente certo. Vou modificar minha resposta.
Graeme Wicksted de
No entanto, se você agrupasse um HashSet que usa seu comparador internamente, ele funcionaria. Assim: Utillib / ExtHashSet
Daniel AA Pelsmaeker
@Virtlink obrigado! Acabei envolvendo o HashSet como uma opção, mas fornecendo os comparadores e um método de extensão para versatilidade adicional. Agora é thread-safe e não vaza memória ... mas é um pouco mais código do que eu esperava!
Graeme Wicksted de
@Francois Escrever o código acima foi mais um exercício de descobrir uma solução "ótima" de tempo / memória; entretanto, não sugiro que você siga este método. Usar um Dicionário <T, T> com um IEqualityComparer personalizado é muito mais direto e à prova de futuro!
Graeme Wicksted
-2

HashSet tem um Contains (T) método .

Você pode especificar um IEqualityComparer se precisar de um método de comparação personalizado (por exemplo, armazenar um objeto de pessoa, mas usar o SSN para comparação de igualdade).

Philipp Schmid
fonte
-11

Você também pode usar o método ToList () e aplicar um indexador a ele.

HashSet<string> mySet = new HashSet();
mySet.Add("mykey");
string key = mySet.toList()[0];
Eric
fonte
Não sei por que você teve votos negativos quando apliquei essa lógica e funcionou. Eu precisei extrair valores de uma estrutura que começou com Dictionary <string, ISet <String>> onde o ISet continha x número de valores. A maneira mais direta de obter esses valores era percorrer o dicionário puxando a chave e o valor ISet. Em seguida, percorri o ISet para exibir os valores individuais. Não é elegante, mas funcionou.
j.hull