Search code examples
c#listhashsetgeneric-collectionsreadonly-collection

C# HashSet<T> read-only workaround


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>?


Solution

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