C# HashSet<T> 只读解决方法

15

这是样例代码:

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

这里是一个测试代码:

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

我的问题是:

是否有更好的解决方案来处理“Real_ReadonlyHashSet”属性?

微软公司是否会在HashSet<T>中实现“AsReadOnly”方法?


3
这里有一个ImmutableHashSet - Matthew Mcveigh
这并不难,你可以自己写: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
5个回答

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

如果您没有使用CodeContracts,则第一行甚至都不必要。然而,ReadOnlyCollection<T>仅支持IList<T>,而HashSet<T>则不支持。
我的建议是创建自己的ReadOnlySet<T>类,该类接受ISet<T>并仅通过读取操作ReadOnlyCollection<T>在内部所做的那样更新: 这里是我快速编写的完整的ReadOnlySet<T>以及一个扩展方法,可将ISet<T>实现添加到.AsReadOnly()上。
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; }
    }
}

请问您能否为ImmutableHashSet添加更新?问题下面的评论很容易被忽略。 - webbertee
@webbertee 不可变对象行与只读对象行不同。Immutable 用于并发……我在这方面不是专家,但是将一个对象行替换为另一个对象行可能会导致各种问题,即使它们是微妙的。 - ErikE

7

从.NET 5开始,HashSet<T>类现在实现了IReadOnlySet<T>接口。虽然没有内置的ReadOnlySet<T>包装器,类似于现有的ReadOnlyDictionary<TKey, TValue>用于字典,但实现一个是微不足道的:

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

即将推出的.NET 7还将为IDictionary<TKey,TValue>提供一个新的AsReadOnly扩展方法,因此我们也可以为ISet<T>创建一个:
public static ReadOnlySet<T> AsReadOnly<T>(this ISet<T> set) => new ReadOnlySet<T>(set);

使用示例:

HashSet<Item> items = new();
ReadOnlySet<Item> readOnlyItems = items.AsReadOnly();

1

HashSet从.NET Framework 4.6开始实现IReadOnlyCollection接口;在以前的.NET Framework版本中,HashSet类没有实现这个接口。

在learn.microsoft.com上阅读


2
IReadOnlyCollection<T> 没有公开 Boolean Contains(T item) 方法。 - Dai
1
@Dai 你说得对,但是当前的实现中,Enumerable.Contains() 扩展方法尝试将类型转换为 ICollection,如果成功,则在 ICollection 上调用 Contains 方法(由于 HashSet 实现了 ICollection,因此类型转换成功)- 而此方法的实现将是 HashSet 中的 Contains。https://referencesource.microsoft.com/#System.Core/System/Linq/Enumerable.cs,1365 - Stas Boyarincev
@Dai,现在有更好的方法了。IReadOnlySet<T>自.NET 5以来就存在,并且已经被HashSet<T>立即实现。 - Palec

1

您可以编写自己的 IReadOnlyCollection<T> 实现,它包装了一个 IEnumerable<T> 和一个计数:

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

然后您可以这样声明您的ReadonlyHashSet属性:

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

我认为那会解决这个问题。


我认为最好传递 ICollection<T>,这样你就可以传递 .Contains(,这是 HashSet 最强大的部分。 - Scott Chamberlain
@ScottChamberlain 我一直保持返回类型与 OP 中相同 - 当然,Contains() 不是 IReadOnlyCollection<T> 的成员。我想 OP 想要通过类型而不是属性 (IsReadOnly) 来指示该值是只读的。 - Matthew Watson
啊,我是根据 ReadOnlyCollection<T> 来判断的,它会传递它,而不是 IReadOnlyCollection<T> - Scott Chamberlain
@ScottChamberlain 但是这很奇怪,不是吗?微软在这个领域似乎不太一致... - Matthew Watson

0
在.NET Framework 4.6版本中,HashSet实现了IReadOnlyCollection接口以及ISet接口。link... 看起来它确实是这样的。
你也可以这样做,但可能会降低性能:
var foo = (IReadOnlyCollection<string>) mySet.toList(); 

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接