Come recuperare l'elemento effettivo da HashSet <T>?


86

Ho letto questa domanda sul perché non è possibile, ma non ho trovato una soluzione al problema.

Vorrei recuperare un elemento da un .NET HashSet<T>. Sto cercando un metodo che abbia questa firma:

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

La ricerca del set per un elemento con un tale metodo sarebbe O (1). L'unico modo per recuperare un elemento da a HashSet<T>è enumerare tutti gli elementi che è O (n).

Non ho trovato alcuna soluzione a questo problema se non crearne uno mio HashSet<T>o utilizzare un file Dictionary<K, V>. Qualche altra idea?

Nota:
non voglio controllare se HashSet<T>contiene l'elemento. Voglio ottenere il riferimento all'elemento memorizzato in HashSet<T>perché ho bisogno di aggiornarlo (senza sostituirlo con un'altra istanza). L'oggetto a cui passerei TryGetItemsarebbe uguale (secondo il meccanismo di confronto che ho passato al costruttore) ma non sarebbe lo stesso riferimento.


1
Perché non utilizzare Contiene e restituire l'elemento che hai passato come input?
Mathias


2
Se è necessario cercare un oggetto in base a un valore chiave, Dictionary <T> potrebbe essere la raccolta più appropriata in cui archiviarlo.
ThatBlairGuy

@ThatBlairGuy: Hai ragione. Penso che implementerò la mia raccolta di set utilizzando un dizionario internamente per memorizzare i miei articoli. La chiave sarà l'HashCode dell'elemento. Avrò circa le stesse prestazioni di un HashSet e mi eviterà di dover fornire una chiave ogni volta che devo aggiungere / rimuovere / ottenere un elemento dalla mia raccolta.
Francois C

2
@mathias Perché l'hashset potrebbe contenere un elemento che è uguale all'input, ma in realtà non è lo stesso. Ad esempio, potresti voler avere un hashset di tipi di riferimento ma vuoi confrontare il contenuto, non il riferimento per l'uguaglianza.
NounVerber

Risposte:


28

Quello che stai chiedendo è stato aggiunto a .NET Core un anno fa ed è stato aggiunto di recente a .NET 4.7.2 :

In .NET Framework 4.7.2 abbiamo aggiunto alcune API ai tipi di raccolta standard che abiliteranno nuove funzionalità come segue.
- "TryGetValue" viene aggiunto a SortedSet e HashSet in modo che corrisponda al modello Try utilizzato in altri tipi di raccolta.

La firma è la seguente (disponibile in .NET 4.7.2 e versioni successive):

    //
    // 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 .: Nel caso tu sia interessato, c'è una funzione correlata che aggiungeranno in futuro : HashSet.GetOrAdd (T).


65

Questa è in realtà un'enorme omissione nel set di raccolte. Avresti bisogno di un dizionario solo delle chiavi o di un HashSet che consenta il recupero dei riferimenti agli oggetti. Così tante persone l'hanno chiesto, perché non viene risolto è al di là di me.

Senza librerie di terze parti, la soluzione migliore è usare Dictionary<T, T>con chiavi identiche ai valori, poiché Dictionary memorizza le sue voci come una tabella hash. Dal punto di vista delle prestazioni è lo stesso di HashSet, ma ovviamente spreca memoria (dimensione di un puntatore per voce).

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
Suggerirei che le chiavi del suo dizionario dovrebbero essere quelle che ha attualmente inserito nel suo EqualityComparer per l'hashset. Penso che sia sporco usare un EqualityComparer quando non stai davvero dicendo che gli elementi sono uguali (altrimenti potresti semplicemente usare l'elemento che hai creato ai fini del confronto). Creerei una classe / struttura che rappresenta la chiave. Questo ovviamente ha il prezzo di più memoria.
Ed T

1
Poiché la chiave è archiviata all'interno di Value, suggerisco di utilizzare la raccolta ereditata da KeyedCollection invece di Dictionary. msdn.microsoft.com/en-us/library/ms132438(v=vs.110).aspx
Accesso negato

11

Questo metodo è stato aggiunto a .NET Framework 4.7.2 (e .NET Core 2.0 prima); vedere HashSet<T>.TryGetValue. Citando la 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)

1
Così come anche per SortedSet .
nawfal

4

Che dire del sovraccarico dell'operatore di confronto di uguaglianza di stringhe:

  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 quindi dichiara l'HashSet come:

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

Si tratta di gestione della memoria: restituire l'elemento effettivo che si trova nell'hashset anziché una copia identica. Quindi nel codice sopra troviamo la stringa con lo stesso contenuto e quindi restituiamo un riferimento a questo. Per le stringhe questo è simile a quello che fa lo stage.
mp666

@zumalifeguard @ mp666 non è garantito che funzioni così come sono. Sarebbe necessario che qualcuno istanziasse il HashSetper fornire il convertitore di valore specifico. Una soluzione ottimale sarebbe quella TryGetValuedi passare in una nuova istanza di specializzato StringEqualityComparer(altrimenti il as StringEqualityComparerrisultato potrebbe essere un null che causa il .val1lancio dell'accesso alla proprietà). In tal modo, StringEqualityComparer può diventare una classe privata annidata all'interno di HashSetExtension. Inoltre, in caso di un operatore di confronto di uguaglianza sottoposto a override, StringEqualityComparer dovrebbe chiamare il valore predefinito.
Graeme Wicksted

devi dichiarare il tuo HashSet come: HashSet <string> valueCash = new HashSet <string> (new StringEqualityComparer ())
mp666

1
Trucco sporco. So come funziona ma è pigro solo farlo funzionare tipo di soluzione
M.kazem Akhgary

2

Ok, quindi puoi farlo in questo modo

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

Questo per ottenere una nuova istanza dell'oggetto selezionato. Per aggiornare il tuo oggetto, dovresti usare:

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

Questo è un modo interessante, devi solo racchiudere il secondo in una prova, in modo che se stai cercando qualcosa che non è nell'elenco otterrai una NullReferenceExpection. Ma è un passo nella giusta direzione?
Piotr Kula

11
LINQ attraversa la raccolta in un ciclo foreach, ovvero tempo di ricerca O (n). Sebbene sia una soluzione al problema, in un certo senso sconfigge lo scopo dell'utilizzo di un HashSet in primo luogo.
Niklas Ekman


2

Un altro trucco potrebbe fare Reflection, accedendo alla funzione interna InternalIndexOfdi HashSet. Tieni presente che i nomi dei campi sono hardcoded, quindi se questi cambiano nelle prossime versioni di .NET questo si interromperà.

Nota: se utilizzi Mono, devi modificare il nome del campo da m_slotsa _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;
    }
}

Test:

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

1

SortedSet avrebbe probabilmente un tempo di ricerca O (log n) in quella circostanza, se l'utilizzo di questa è un'opzione. Ancora non O (1), ma almeno meglio.


1

Implementazione modificata della risposta @ mp666 in modo che possa essere utilizzata per qualsiasi tipo di HashSet e consente di sovrascrivere l'operatore di confronto di uguaglianza predefinito.

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
Dal momento che stai utilizzando il metodo di estensione Linq Enumerable.Contains, enumererà tutti gli elementi del set e li confronterà, perdendo qualsiasi vantaggio fornito dall'implementazione hash del set. Quindi potresti anche scrivere set.SingleOrDefault(e => set.Comparer.Equals(e, obj)), che ha le stesse caratteristiche di comportamento e prestazioni della tua soluzione.
Daniel AA Pelsmaeker

@Virtlink Buona presa - Hai assolutamente ragione. Modificherò la mia risposta.
Graeme Wicksted

Tuttavia, se dovessi racchiudere un HashSet che utilizza internamente il tuo comparatore, funzionerebbe. Come questo: Utillib / ExtHashSet
Daniel AA Pelsmaeker

@Virtlink grazie! Ho finito per avvolgere HashSet come un'opzione, ma fornendo i comparatori e un metodo di estensione per una maggiore versatilità. Ora è thread-safe e non perde memoria ... ma è un po 'più di codice di quanto avessi sperato!
Graeme Wicksted

@Francois Scrivere il codice sopra era più un esercizio per trovare una soluzione tempo / memoria "ottimale"; tuttavia, non ti suggerisco di seguire questo metodo. Usare un Dictionary <T, T> con un IEqualityComparer personalizzato è molto più semplice ea prova di futuro!
Graeme Wicksted

-2

HashSet ha un metodo Contains (T) .

È possibile specificare un IEqualityComparer se è necessario un metodo di confronto personalizzato (ad esempio, memorizzare un oggetto persona, ma utilizzare il SSN per il confronto di uguaglianza).


-11

Puoi anche usare il metodo ToList () e applicare un indicizzatore a quello.

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

Non sono sicuro del motivo per cui hai perso voti perché quando ho applicato questa logica ha funzionato. Avevo bisogno di estrarre i valori da una struttura che iniziava con Dictionary <string, ISet <String>> dove ISet conteneva x numero di valori. Il modo più diretto per ottenere quei valori era scorrere il dizionario estraendo la chiave e il valore ISet. Quindi ho eseguito il ciclo di ISet per visualizzare i singoli valori. Non è elegante, ma ha funzionato.
j.hull
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.