15

Here is this sample code:

static class Store
{
    private static List<String> strList = new List<string>();
    private static HashSet<String> strHashSet = new HashSet<string>();

    public static List<String> NormalList
    {
        get { return strList; }
    }

    public static HashSet<String> NormalHashSet
    {
        get { return strHashSet; }
    }

    public static IReadOnlyList<String> ReadonlyList
    {
        get { return (IReadOnlyList<String>)strList; }
    }

    public static IReadOnlyCollection<String> ReadonlyHashSet
    {
        get { return (IReadOnlyCollection<String>)strHashSet; }
    }

    public static IReadOnlyList<String> Real_ReadonlyList
    {
        get { return (IReadOnlyList<String>)strList.AsReadOnly(); }
    }

    public static IReadOnlyCollection<String> Real_ReadonlyHashSet
    {
        get
        {
            List<String> tmpList = new List<String>(strHashSet);
            return (IReadOnlyList<String>)(tmpList).AsReadOnly();
        }
    }
}

And here is a test code:

// normal behaviour
// you can modify the list and the hashset

Store.NormalList.Add("some string 1");

Store.NormalHashSet.Add("some string 1");

// tricky behaviour
// you can still modify the list and the hashset

((List<String>)Store.ReadonlyList).Add("some string 2");

((HashSet<String>)Store.ReadonlyHashSet).Add("some string 2");

// expected read-only behaviour
// you can NOT modify

// throws InvalidCastException
((List<String>)Store.Real_ReadonlyList).Add("some string 3");
// throws InvalidCastException
((HashSet<String>)Store.Real_ReadonlyHashSet).Add("some string 3");

My questions are these:

Is there a better solution for the "Real_ReadonlyHashSet" property?

Will Microsoft some day implement the "AsReadOnly" method to the HashSet<T>?

VBTiger
  • 153
  • 1
  • 1
  • 7
  • 3
    There's an [ImmutableHashSet](https://msdn.microsoft.com/en-us/library/dn467171(v=vs.111).aspx) – Matthew Mcveigh Apr 23 '16 at 19:00
  • It's also not that hard to just write it yourself: https://github.com/airbreather/AirBreather.Common/blob/aba09330ae3066cb46ad7e0ee963e00d27e63cb6/Source/AirBreather.Common/AirBreather.Common/Collections/ReadOnlySet.cs https://github.com/airbreather/AirBreather.Common/blob/aba09330ae3066cb46ad7e0ee963e00d27e63cb6/Source/AirBreather.Common/AirBreather.Common/Utilities/EnumerableUtility.cs#L47 – Joe Amenta Apr 23 '16 at 19:06

5 Answers5

15

Here is the entirety of the code of .AsReadOnly()

public ReadOnlyCollection<T> AsReadOnly() {
    Contract.Ensures(Contract.Result<ReadOnlyCollection<T>>() != null);
    return new ReadOnlyCollection<T>(this);
}

The first line is not even necessary if you are not using CodeContracts. However, ReadOnlyCollection<T> only supports IList<T> which HashSet<T> does not support.

What I would do is make your own ReadOnlySet<T> class that takes in a ISet<T> and only passes through the read operations like ReadOnlyCollection<T> does internally.

UPDATE: Here is a fully fleshed out ReadOnlySet<T> I quickly wrote up along with a extension method that adds a .AsReadOnly() on to anything that implements ISet<T>

public static class SetExtensionMethods
{
    public static ReadOnlySet<T> AsReadOnly<T>(this ISet<T> set)
    {
        return new ReadOnlySet<T>(set);
    }
}

public class ReadOnlySet<T> : IReadOnlyCollection<T>, ISet<T>
{
    private readonly ISet<T> _set;
    public ReadOnlySet(ISet<T> set)
    {
        _set = set;
    }

    public IEnumerator<T> GetEnumerator()
    {
        return _set.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return ((IEnumerable) _set).GetEnumerator();
    }

    void ICollection<T>.Add(T item)
    {
        throw new NotSupportedException("Set is a read only set.");
    }

    public void UnionWith(IEnumerable<T> other)
    {
        throw new NotSupportedException("Set is a read only set.");
    }

    public void IntersectWith(IEnumerable<T> other)
    {
        throw new NotSupportedException("Set is a read only set.");
    }

    public void ExceptWith(IEnumerable<T> other)
    {
        throw new NotSupportedException("Set is a read only set.");
    }

    public void SymmetricExceptWith(IEnumerable<T> other)
    {
        throw new NotSupportedException("Set is a read only set.");
    }

    public bool IsSubsetOf(IEnumerable<T> other)
    {
        return _set.IsSubsetOf(other);
    }

    public bool IsSupersetOf(IEnumerable<T> other)
    {
        return _set.IsSupersetOf(other);
    }

    public bool IsProperSupersetOf(IEnumerable<T> other)
    {
        return _set.IsProperSupersetOf(other);
    }

    public bool IsProperSubsetOf(IEnumerable<T> other)
    {
        return _set.IsProperSubsetOf(other);
    }

    public bool Overlaps(IEnumerable<T> other)
    {
        return _set.Overlaps(other);
    }

    public bool SetEquals(IEnumerable<T> other)
    {
        return _set.SetEquals(other);
    }

    public bool Add(T item)
    {
        throw new NotSupportedException("Set is a read only set.");
    }

    public void Clear()
    {
        throw new NotSupportedException("Set is a read only set.");
    }

    public bool Contains(T item)
    {
        return _set.Contains(item);
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        _set.CopyTo(array, arrayIndex);
    }

    public bool Remove(T item)
    {
        throw new NotSupportedException("Set is a read only set.");
    }

    public int Count
    {
        get { return _set.Count; }
    }

    public bool IsReadOnly
    {
        get { return true; }
    }
}
Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431
  • Could you please add an update for ImmutableHashSet? The comment below the Question is easily overlooked. – webbertee Feb 08 '21 at 11:07
  • @webbertee The Immutable line of objects aren't the same as the ReadOnly line of objects. Immutable is for concurrency... I'm no expert here on this, but using one in place of the other *could* result in various problems, even if they are subtle. – ErikE Jul 13 '21 at 02:13
7

Starting from .NET 5, the HashSet<T> class now implements the IReadOnlySet<T> interface. There is no built-in ReadOnlySet<T> wrapper though, analogous to the existing ReadOnlyDictionary<TKey, TValue> class for dictionaries, but implementing one is trivial:

public class ReadOnlySet<T> : IReadOnlySet<T>
{
    private readonly ISet<T> _set;
    public ReadOnlySet(ISet<T> set) { ArgumentNullException.ThrowIfNull(set); _set = set; }

    public int Count => _set.Count;
    public bool Contains(T item) => _set.Contains(item);
    public bool IsProperSubsetOf(IEnumerable<T> other) => _set.IsProperSubsetOf(other);
    public bool IsProperSupersetOf(IEnumerable<T> other) => _set.IsProperSupersetOf(other);
    public bool IsSubsetOf(IEnumerable<T> other) => _set.IsSubsetOf(other);
    public bool IsSupersetOf(IEnumerable<T> other) => _set.IsSupersetOf(other);
    public bool Overlaps(IEnumerable<T> other) => _set.Overlaps(other);
    public bool SetEquals(IEnumerable<T> other) => _set.SetEquals(other);
    public IEnumerator<T> GetEnumerator() => _set.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

The upcoming .NET 7 will also feature a new AsReadOnly extension method for IDictionary<TKey,TValue>s, so let's make one for ISet<T>s too:

public static ReadOnlySet<T> AsReadOnly<T>(this ISet<T> set) => new ReadOnlySet<T>(set);

Usage example:

HashSet<Item> items = new();
ReadOnlySet<Item> readOnlyItems = items.AsReadOnly();
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
1

You could write your own implementation of an IReadOnlyCollection<T> that wraps an IEnumerable<T> and a count:

public sealed class ReadOnlyCollectionFromEnumerable<T>: IReadOnlyCollection<T>
{
    readonly IEnumerable<T> _data;

    public ReadOnlyCollectionFromEnumerable(IEnumerable<T> data, int count)
    {
        _data = data;
        Count = count;
    }

    public IEnumerator<T> GetEnumerator()
    {
        return _data.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public int Count { get; }
}

Then you declare your ReadonlyHashSet property like this:

public static IReadOnlyCollection<String> ReadonlyHashSet
{
    get { return new ReadOnlyCollectionFromEnumerable<string>(strHashSet, strHashSet.Count); }
}

I think that would solve the issue.

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • I think it would be better to pass in `ICollection` that way you can pass along `.Contains(` which is the most powerful part of a HashSet. – Scott Chamberlain Apr 23 '16 at 19:37
  • @ScottChamberlain I was keeping the return type the same as in the OP - of course, `Contains()` isn't a member of `IReadOnlyCollection`. I suppose the OP wanted to indicate that the value was read-only via the type rather than via a property (`IsReadOnly`) – Matthew Watson Apr 23 '16 at 19:48
  • Ah, I was going by `ReadOnlyCollection` which [does pass it along](https://msdn.microsoft.com/en-us/library/ms132478(v=vs.110).aspx), not `IReadOnlyCollection` – Scott Chamberlain Apr 23 '16 at 19:53
  • @ScottChamberlain It is odd though, isn't it? Microsoft don't seem to be very consistent in this area... – Matthew Watson Apr 23 '16 at 19:54
1

HashSet implements the IReadOnlyCollection interface starting with the .NET Framework 4.6; in previous versions of the .NET Framework, the HashSet class did not implement this interface.

Read in learn.microsoft.com

Stas Boyarincev
  • 3,690
  • 23
  • 23
  • 2
    `IReadOnlyCollection` does not expose a `Boolean Contains(T item)` method, however. – Dai Oct 24 '19 at 00:42
  • 1
    @Dai You're right, but current implementation Enumerable.Contains() extension method trying to cast type to ICollection and if successful calls Contains on ICollection (cast to ICollection succeeds because HashSet implements ICollection) - and the implementation of this method will be Contains in HashSet. https://referencesource.microsoft.com/#System.Core/System/Linq/Enumerable.cs,1365 – Stas Boyarincev Oct 24 '19 at 13:01
  • @Dai, there is a better way now. [IReadOnlySet](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.ireadonlyset-1) exists since .NET 5 an it has been immediately implemented by HashSet. – Palec Dec 06 '22 at 18:32
0

In .NET Framework 4.6 release, the HashSet implements IReadOnlyCollection interface along with the ISet interface. link... It does seem to.

You could also do this but maybe take a performance hit:

var foo = (IReadOnlyCollection<string>) mySet.toList(); 
andrew pate
  • 3,833
  • 36
  • 28