¿Cómo recuperar el artículo real de HashSet <T>?

85

Leí esta pregunta sobre por qué no es posible, pero no he encontrado una solución al problema.

Me gustaría recuperar un elemento de .NET HashSet<T>. Estoy buscando un método que tenga esta 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);

Buscar en el conjunto un artículo con tal método sería O (1). La única forma de recuperar un elemento de a HashSet<T>es enumerar todos los elementos que son O (n).

No he encontrado ninguna solución a este problema que no sea hacer la mía propia HashSet<T>o usar un archivo Dictionary<K, V>. ¿Alguna otra idea?

Nota:
No quiero comprobar si HashSet<T>contiene el artículo. Quiero obtener la referencia al elemento que está almacenado en HashSet<T>porque necesito actualizarlo (sin reemplazarlo por otra instancia). El elemento que le pasaría TryGetItemsería igual (según el mecanismo de comparación que le pasé al constructor) pero no sería la misma referencia.

Francois C
fuente
1
¿Por qué no utilizar Contiene y devolver el artículo que pasó como entrada?
Mathias
2
Si necesita buscar un objeto basado en un valor clave, entonces Dictionary <T> puede ser la colección más apropiada para almacenarlo.
ThatBlairGuy
@ThatBlairGuy: Tienes razón. Creo que implementaré mi propia colección de conjuntos usando un diccionario internamente para almacenar mis artículos. La clave será el HashCode del artículo. Tendré aproximadamente el mismo rendimiento que un HashSet y me evitará tener que proporcionar una clave cada vez que necesite agregar / eliminar / obtener un elemento de mi colección.
Francois C
2
@mathias Porque el hashset puede contener un elemento que es igual a la entrada, pero en realidad no es el mismo. Por ejemplo, es posible que desee tener un conjunto de hash de tipos de referencia, pero desea comparar el contenido, no la referencia para la igualdad.
SustantivoVerber

Respuestas:

25

Lo que está pidiendo se agregó a .NET Core hace un año y recientemente se agregó a .NET 4.7.2 :

En .NET Framework 4.7.2, hemos agregado algunas API a los tipos de colección estándar que permitirán nuevas funciones de la siguiente manera.
- 'TryGetValue' se agrega a SortedSet y HashSet para que coincida con el patrón de prueba utilizado en otros tipos de colección.

La firma es la siguiente (que se encuentra en .NET 4.7.2 y 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);

PD .: En caso de que esté interesado, hay una función relacionada que están agregando en el futuro : HashSet.GetOrAdd (T).

Evdzhan Mustafa
fuente
65

En realidad, esta es una gran omisión en el conjunto de colecciones. Necesitaría solo un Diccionario de claves o un HashSet que permita la recuperación de referencias de objetos. Tanta gente lo ha pedido, no entiendo por qué no se soluciona.

Sin bibliotecas de terceros, la mejor solución es utilizar Dictionary<T, T>claves idénticas a los valores, ya que Dictionary almacena sus entradas como una tabla hash. En cuanto al rendimiento, es lo mismo que el HashSet, pero, por supuesto, desperdicia memoria (tamaño de un puntero 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
fuente
1
Sugeriría que las claves de su diccionario sean las que haya colocado actualmente en su EqualityComparer para el hashset. Siento que está sucio usar un EqualityComparer cuando en realidad no está diciendo que los elementos son iguales (de lo contrario, podría usar el elemento que creó para la comparación). Haría una clase / estructura que represente la clave. Por supuesto, esto tiene el costo de más memoria.
Ed T
1
Dado que la clave se almacena dentro de Value, sugiero usar la colección heredada de KeyedCollection en lugar de Dictionary. msdn.microsoft.com/en-us/library/ms132438(v=vs.110).aspx
Acceso denegado
11

Este método se ha agregado a .NET Framework 4.7.2 (y .NET Core 2.0 antes); ver HashSet<T>.TryGetValue. Citando la fuente :

/// <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
fuente
1
Así como también para SortedSet .
Nawfal
4

¿Qué hay de sobrecargar el comparador de igualdad de cadenas?

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

Y luego declare el HashSet como:

HashSet<string> hs = new HashSet<string>(new StringEqualityComparer());
mp666
fuente
Todo se trata de la gestión de la memoria: devolver el elemento real que está en el hashset en lugar de una copia idéntica. Entonces, en el código anterior, encontramos la cadena con el mismo contenido y luego devolvemos una referencia a esto. Para cuerdas, esto es similar a lo que hace la pasantía.
mp666
@zumalifeguard @ mp666 no se garantiza que esto funcione como está. Requeriría que alguien instanciara el HashSetpara proporcionar el convertidor de valor específico. Una solución óptima sería TryGetValuepasar una nueva instancia de la especializada StringEqualityComparer(de lo contrario, as StringEqualityComparerpodría resultar en un nulo y provocar .val1que se arroje el acceso a la propiedad). Al hacerlo, StringEqualityComparer puede convertirse en una clase privada anidada dentro de HashSetExtension. Además, en el caso de un comparador de igualdad anulado, StringEqualityComparer debería llamar al valor predeterminado.
Graeme Wicksted
debe declarar su HashSet como: HashSet <string> valueCash = new HashSet <string> (new StringEqualityComparer ())
mp666
1
Hack sucio. Sé cómo funciona, pero es perezoso, simplemente haz que funcione, una especie de solución
M.kazem Akhgary
2

Ok, entonces puedes hacerlo así

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

Esto es para obtener una nueva instancia del objeto seleccionado. Para actualizar su objeto, debe usar:

yourHashSet.Where(w => w.Name.Contains("strin")).FirstOrDefault().MyProperty = "something";
Vulovic Vukasin
fuente
Esta es una forma interesante, solo necesita envolver el segundo en un intento, de modo que si busca algo que no está en la lista, obtenga una NullReferenceExpection. ¿Pero es un paso en la dirección correcta?
Piotr Kula
11
LINQ atraviesa la colección en un bucle foreach, es decir, tiempo de búsqueda O (n). Si bien es una solución al problema, en cierto modo frustra el propósito de usar un HashSet en primer lugar.
Niklas Ekman
2

Otro truco haría Reflexión, accediendo a la función interna InternalIndexOfde HashSet. Tenga en cuenta que los nombres de campo están codificados, por lo que si cambian en las próximas versiones de .NET, esto se romperá.

Nota: Si usa Mono, debe cambiar el nombre del campo de 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;
    }
}

Prueba:

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

SortedSet probablemente tendría O (log n) tiempo de búsqueda en esa circunstancia, si usar esa es una opción. Todavía no O (1), pero al menos mejor.

Erik Dietrich
fuente
1

Implementación modificada de la respuesta @ mp666 para que pueda usarse para cualquier tipo de HashSet y permite anular el comparador de igualdad predeterminado.

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
fuente
1
Dado que está utilizando el método de extensión Linq Enumerable.Contains, enumerará todos los elementos del conjunto y los comparará, perdiendo los beneficios que proporciona la implementación de hash del conjunto. Entonces puede simplemente escribir set.SingleOrDefault(e => set.Comparer.Equals(e, obj)), que tiene el mismo comportamiento y características de rendimiento que su solución.
Daniel AA Pelsmaeker
@Virtlink Buena captura: tienes toda la razón. Modificaré mi respuesta.
Graeme Wicksted
Sin embargo, si tuviera que envolver un HashSet que usa su comparador internamente, funcionaría. Así: Utillib / ExtHashSet
Daniel AA Pelsmaeker
@Virtlink ¡gracias! Terminé envolviendo HashSet como una opción, pero proporcioné los comparadores y un método de extensión para una versatilidad adicional. Ahora es seguro para subprocesos y no perderá memoria ... ¡pero es bastante más código de lo que esperaba!
Graeme Wicksted
@Francois Escribir el código anterior fue más un ejercicio de encontrar una solución de tiempo / memoria "óptima"; sin embargo, no te sugiero que sigas este método. Usar un Dictionary <T, T> con un IEqualityComparer personalizado es mucho más sencillo y preparado para el futuro.
Graeme Wicksted
-2

HashSet tiene un método Contains (T) .

Puede especificar un IEqualityComparer si necesita un método de comparación personalizado (por ejemplo, almacenar un objeto de persona, pero usar el SSN para la comparación de igualdad).

Philipp Schmid
fuente
-11

También puede usar el método ToList () y aplicar un indexador a eso.

HashSet<string> mySet = new HashSet();
mySet.Add("mykey");
string key = mySet.toList()[0];
Eric
fuente
No estoy seguro de por qué obtuviste votos negativos porque cuando apliqué esta lógica funcionó. Necesitaba extraer valores de una estructura que comenzaba con Dictionary <string, ISet <String>> donde ISet contenía x número de valores. La forma más directa de obtener esos valores era recorrer el diccionario tirando de la clave y el valor ISet. Luego recorrí el ISet para mostrar los valores individuales. No es elegante, pero funcionó.
j.hull