Як отримати фактичний елемент із 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
Чому б не використовувати Contains та не повернути елемент, який ви передали як вхід?
Матіас,


2
Якщо вам потрібно знайти об’єкт на основі значення ключа, тоді Dictionary <T> може бути найбільш підходящою колекцією для його зберігання.
ThatBlairGuy

@ThatBlairGuy: Ви маєте рацію. Я думаю, що я буду реалізовувати власну колекцію Set, використовуючи словник для внутрішнього зберігання своїх предметів. Ключем буде хеш-код елемента. Я буду мати приблизно таку ж продуктивність, як HashSet, і це заощадить мене від необхідності надавати ключ кожного разу, коли мені потрібно додати / видалити / отримати елемент зі своєї колекції.
Франсуа С

2
@mathias Оскільки хеш-комплект може містити елемент, який дорівнює введеному, але насправді не є однаковим. Наприклад, ви можете мати хеш-код посилальних типів, але ви хочете порівняти вміст, а не посилання для рівності.
NounVerber

Відповіді:


25

Те, про що ви просите, було додано до .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);

PS .: Якщо ви зацікавлені, є пов’язана функція, яку вони додають у майбутньому - HashSet.GetOrAdd (T).


65

Це насправді величезний пропуск у наборі колекцій. Вам знадобиться або Словник лише ключів, або HashSet, що дозволяє отримувати посилання на об'єкти. Так багато людей просили про це, чому це не виправляється, мені не під силу.

Без сторонніх бібліотек найкращим обхідним шляхом є використання Dictionary<T, T>ключів, ідентичних значенням, оскільки Словник зберігає свої записи як хеш-таблицю. З точки зору продуктивності це те ж саме, що і 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, я пропоную використовувати колекцію, успадковану від 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 StringEqualityComparerможе призвести до нуля, що спричиняє .val1доступ до майна). Роблячи це, StringEqualityComparer може стати вкладеним приватним класом у HashSetExtension. Далі, у випадку перевизначеного порівняльника рівності, StringEqualityComparer повинен зателефонувати за замовчуванням.
Грем Вікстед,

вам потрібно оголосити свій 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";

Це цікавий спосіб, просто вам потрібно обгорнути другу спробу - так, якщо ви шукаєте щось, чого немає у списку, ви отримаєте NullReferenceExpection. Але це крок у правильному напрямку?
Piotr Kula

11
LINQ проходить колекцію в циклі foreach, тобто час пошуку O (n). Незважаючи на те, що це рішення проблеми, воно в першу чергу перешкоджає меті використання HashSet.
Ніклас Екман,


2

Інший фокус міг би зробити Reflection, InternalIndexOfотримавши доступ до внутрішньої функції HashSet. Майте на увазі, що імена полів є жорстко закодованими, тому, якщо вони зміниться у майбутніх версіях .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)), що має такі самі характеристики поведінки та продуктивності, як і ваше рішення.
Даніель А.А. Пельсмакер,

@Virtlink Хороший улов - Ви абсолютно праві. Я зміню свою відповідь.
Грем Вікстед,

Однак, якби ви обгортали HashSet, який використовує ваш компаратор всередині, він би працював. Ось так
Daniel AA Pelsmaeker

@Virtlink дякую! У підсумку я обернув HashSet як один із варіантів, але надав порівняльники та метод розширення для додаткової універсальності. Тепер він безпечний для потоків і не буде втрачати пам’ять ... але це набагато більше коду, ніж я сподівався!
Грем Вікстед,

@Francois Написання коду вище було скоріше вправою з’ясувати «оптимальне» рішення часу / пам’яті; однак, я не пропоную вам використовувати цей метод. Використання словника <T, T> із користувацьким IEqualityComparer набагато простіший та надійніший!
Грем Вікстед,

-2

HashSet має метод Contains (T) .

Ви можете вказати IEqualityComparer, якщо вам потрібен власний метод порівняння (наприклад, зберігати об’єкт особи, але використовувати SSN для порівняння рівності).


-11

Ви також можете використовувати метод ToList () і застосувати до нього індексатор.

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

Я не впевнений, чому ви набрали голосів, коли я застосував цю логіку. Мені потрібно було витягти значення зі структури, яка починалася зі словника <рядок, ISet <String>>, де ISet містив x кількість значень. Найбільш прямим способом отримати ці значення було прокрутити словник, витягнувши ключ і значення ISet. Потім я прокрутив ISet для відображення окремих значень. Це не елегантно, але вдалося.
j.hull
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.