HashSet <T> 'den asıl öğe nasıl alınır?


86

Neden mümkün olmadığına dair bu soruyu okudum , ancak soruna bir çözüm bulamadım.

.NET'ten bir öğe almak istiyorum HashSet<T>. Bu imzaya sahip bir yöntem arıyorum:

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

Kümeyi böyle bir yöntemle aramak O (1) olacaktır. A'dan bir öğeyi almanın tek yolu HashSet<T>, O (n) olan tüm öğeleri numaralandırmaktır.

Bu soruna kendiminkinden başka bir çözüm bulamadım HashSet<T>veya bir Dictionary<K, V>. Başka bir fikrin var mı?

Not: Öğenin içerip içermediğini
kontrol etmek istemiyorum HashSet<T>. HashSet<T>Güncellemem gerektiğinden (başka bir örnekle değiştirmeden) içinde depolanan öğenin referansını almak istiyorum . Göndereceğim öğe TryGetItemeşit olacaktır (kurucuya ilettiğim karşılaştırma mekanizmasına göre) ancak aynı referans olmayacaktır.


1
Neden İçerir'i kullanıp geçtiğiniz öğeyi girdi olarak iade etmiyorsunuz?
Mathias


2
Anahtar değerine göre bir nesneye bakmanız gerekiyorsa, Dictionary <T> onu depolamak için daha uygun bir koleksiyon olabilir.
ThatBlairGuy

@ThatBlairGuy: Haklısın. Öğelerimi depolamak için dahili bir Sözlük kullanarak kendi Set koleksiyonumu uygulayacağımı düşünüyorum. Anahtar, öğenin HashCode'u olacaktır. Bir HashSet ile yaklaşık olarak aynı performansa sahip olacağım ve koleksiyonumdan bir öğe eklemem / kaldırmam / almam gerektiği her seferinde bir anahtar sağlamak zorunda kalmamı sağlayacak.
Francois C

2
@mathias Çünkü hashset, girdiye eşit olan ama aslında aynı olmayan bir öğe içerebilir. Örneğin, referans türlerinin bir karma kümesine sahip olmak isteyebilirsiniz, ancak eşitlik referansını değil, içeriği karşılaştırmak isteyebilirsiniz.
NounVerber

Yanıtlar:


28

İstediğiniz şey bir yıl önce .NET Core'a eklendi ve yakın zamanda .NET 4.7.2'ye eklendi :

.NET Framework 4.7.2'de standart Koleksiyon türlerine aşağıdaki gibi yeni işlevselliği etkinleştirecek birkaç API ekledik.
- Diğer koleksiyon türlerinde kullanılan Try modeliyle eşleşmesi için SortedSet ve HashSet'e 'TryGetValue' eklenir.

İmza aşağıdaki gibidir (.NET 4.7.2 ve üzerinde bulunur):

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

Not : İlgileniyorsanız, gelecekte ekledikleri ilgili işlev var - HashSet.GetOrAdd (T).


65

Bu aslında koleksiyon setinde büyük bir eksikliktir. Yalnızca anahtar sözlüğüne veya nesne referanslarının alınmasına izin veren bir HashSet'e ihtiyacınız olacaktır. Pek çok insan bunu istedi, neden düzeltilmediğimi aştım.

Üçüncü taraf kitaplıklar olmadan en iyi çözüm, Dictionary<T, T>değerlerle aynı anahtarlarla kullanmaktır , çünkü Dictionary, girişlerini bir karma tablo olarak saklamaktadır. Performans açısından HashSet ile aynıdır, ancak elbette hafızayı boşa harcar (giriş başına bir işaretçi boyutu).

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
Sözlüğünün anahtarlarının, hashset için EqualityComparer'a şu anda koyduğu anahtarlar olmasını öneririm. Maddelerin gerçekten eşit olduğunu söylemediğinizde bir EqualityComparer kullanmanın kirli olduğunu hissediyorum (aksi takdirde, oluşturduğunuz öğeyi karşılaştırma amacıyla kullanabilirsiniz). Anahtarı temsil eden bir sınıf / yapı yapardım. Bu, elbette daha fazla hafıza pahasına gelir.
Ed T

1
Anahtar Değer içinde saklandığı için Dictionary yerine KeyedCollection'dan miras alınan koleksiyonu kullanmanızı öneririm. msdn.microsoft.com/en-us/library/ms132438(v=vs.110).aspx
Erişim Engellendi

11

Bu yöntem, .NET Framework 4.7.2'ye (ve ondan önce .NET Core 2.0'a ) eklenmiştir ; bakın HashSet<T>.TryGetValue. Kaynaktan alıntı :

/// <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
İyi gelince şöyle SortedSet de.
nawfal

4

Dize eşitliği karşılaştırıcısını aşırı yüklemeye ne dersiniz?

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

Ve sonra HashSet'i şu şekilde bildirin:

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

Bu tamamen bellek yönetimi ile ilgilidir - özdeş bir kopya yerine karma sette bulunan asıl öğeyi döndürmek. Dolayısıyla yukarıdaki kodda aynı içeriğe sahip dizeyi buluruz ve sonra buna bir referans döndürürüz. Dizeler için bu, interning'in yaptığına benzer.
mp666

@zumalifeguard @ mp666 bunun olduğu gibi çalışması garanti edilmez. HashSetSpesifik değer dönüştürücüsünü sağlamak için , birisinin somutlaştırmasını gerektirir . En uygun çözüm TryGetValue, özelleşmiş öğenin yeni bir örneğini geçirmek olabilir StringEqualityComparer(aksi takdirde as StringEqualityComparer, .val1özellik erişiminin atılmasına neden olan bir boş değerle sonuçlanabilir ). Bunu yaparken, StringEqualityComparer, HashSetExtension içinde yuvalanmış bir özel sınıf haline gelebilir. Ayrıca, geçersiz kılınmış bir eşitlik karşılaştırıcısı olması durumunda, StringEqualityComparer varsayılanı çağırmalıdır.
Graeme Wicksted

HashSet'inizi şu şekilde bildirmeniz gerekir: HashSet <string> valueCash = new HashSet <string> (new StringEqualityComparer ())
mp666

1
Kirli hack. Nasıl çalıştığını biliyorum ama tembelliği onu bir tür çözüm
haline getiriyor

2

Tamam, bu şekilde yapabilirsin

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

Bu, seçilen nesnenin yeni bir Örneğini almak içindir. Nesnenizi güncellemek için şunları kullanmalısınız:

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

Bu ilginç bir yoldur, sadece saniyeyi bir denemeye kaydırmanız gerekir - böylece listede olmayan bir şeyi arıyorsanız, bir NullReferenceExpection elde edersiniz. Ama doğru yönde atılmış bir adım mı?
Piotr Kula

11
LINQ, koleksiyonu bir foreach döngüsünde dolaşır, yani O (n) arama süresi. Soruna bir çözüm olsa da, ilk etapta bir HashSet kullanma amacını ortadan kaldırıyor.
Niklas Ekman


2

Başka bir Trick InternalIndexOf, HashSet'in dahili işlevine erişerek Yansıma yapabilir . Alan adlarının kodlanmış olduğunu unutmayın, bu nedenle gelecek .NET sürümlerinde bunlar değişirse bu durum bozulacaktır.

Not: Mono kullanıyorsanız, alan adını olarak m_slotsdeğiştirmelisiniz _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;
    }
}

Ölçek:

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

1

SortedSet, eğer bunu kullanmak bir seçenekse, muhtemelen bu durumda O (log n) arama süresine sahip olacaktır. Yine de O (1) değil, ama en azından daha iyi.


1

@ Mp666 yanıtının değiştirilmiş uygulaması, herhangi bir HashSet türü için kullanılabilir ve varsayılan eşitlik karşılaştırıcısının geçersiz kılınmasına izin verir.

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 genişletme yöntemini kullandığınız için Enumerable.Contains, kümenin tüm öğelerini numaralandıracak ve karşılaştıracak ve kümenin karma uygulamasının sağladığı tüm faydaları kaybedecektir. O zaman set.SingleOrDefault(e => set.Comparer.Equals(e, obj))çözümünüzle aynı davranış ve performans özelliklerine sahip olan yazabilirsiniz .
Daniel AA Pelsmaeker

@Virtlink İyi yakalama - Kesinlikle haklısın. Cevabımı değiştireceğim.
Graeme Wicksted

Bununla birlikte, karşılaştırıcınızı dahili olarak kullanan bir HashSet'i sararsanız, işe yarar. Şunun
Daniel AA Pelsmaeker

@Virtlink teşekkür ederim! HashSet'i bir seçenek olarak sarmayı bıraktım, ancak karşılaştırıcılar ve ek çok yönlülük için bir uzatma yöntemi sağladım. Artık iş parçacığı açısından güvenli ve bellek sızdırmayacak ... ama umduğumdan biraz daha fazla kod!
Graeme Wicksted

@Francois Yukarıdaki kodu yazmak daha çok "optimum" bir zaman / bellek çözümü bulmanın bir egzersiziydi; ancak, bu yöntemle gitmenizi önermiyorum. Özel bir IEqualityComparer ile Sözlük <T, T> kullanmak çok daha basit ve geleceğe yöneliktir!
Graeme Wicksted

-2

HashSet, Contains (T) yöntemine sahiptir.

Özel bir karşılaştırma yöntemine ihtiyacınız varsa bir IEqualityComparer belirtebilirsiniz (örneğin, bir kişi nesnesini depolayın, ancak eşitlik karşılaştırması için SSN'yi kullanın).


-11

Ayrıca ToList () yöntemini kullanabilir ve buna bir dizinleyici uygulayabilirsiniz.

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

Bu mantığı uyguladığımda işe yaradığına göre neden olumsuz oy aldığınızdan emin değilim. ISet'in x sayıda değer içerdiği Dictionary <string, ISet <String>> ile başlayan bir yapıdan değerleri çıkarmam gerekiyordu. Bu değerleri almanın en doğrudan yolu, anahtarı ve ISet Değerini çekerek sözlükte dolaşmaktı. Ardından, bireysel değerleri görüntülemek için ISet'te döngü yaptım. Zarif değil ama işe yaradı.
j.hull
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.