如何从HashSet创建一个ReadOnlyCollection而无需复制元素?

27

我有一个私有的HashSet<string>,它是只读属性的后备字段,该属性应返回一个只读集合,以便调用者无法修改集合。因此,我尝试了以下内容:

public class MyClass
{
    private readonly HashSet<string> _referencedColumns;

    public ICollection<string> ReferencedColumns { 
        get { return new ReadOnlyCollection<string>(_referencedColumns); }
    }

这段代码无法编译,因为ReadOnlyCollection只接受实现了IList<T>的对象,而HashSet<T>并没有实现该接口。是否有其他包装器可用以避免复制项目?对于我的目的来说,只需返回实现ICollection<T>(而不是IList<T>)的某些内容即可,而HashSet<T>已经实现了该接口。


如果重要的是调用方不能修改返回值,请查看Immutability and ReadOnlyCollection<T>,或者参考这个问题 - stuartd
4个回答

30

考虑将属性的类型暴露为 IReadOnlyCollection<>,这将提供一个 HashSet<> 的只读视图。这是一种高效的实现方式,因为属性获取器不需要复制底层集合。

这并不会阻止其他人将该属性转换为 HashSet<> 并对其进行修改。如果您担心这种情况,请在属性获取器中考虑使用 return _referencedColumns.ToList(),以创建底层集合的副本。


1
谢谢。那么,没有可以节省复制开销并且无法转换回来的包装器吗? - Dejan
10
仅供参考:将对象转换为 IReadOnlyCollection<> 只适用于 .NET 4.6 及以上版本(参见:https://dev59.com/cVwY5IYBdhLWcg3wU2XA#32762752)。 - Dejan
1
好的调用。这仍然使用HashSet的索引行为,尽管这已经不再明显了。对IReadOnlyCollection.Contains()的调用强烈暗示了线性时间实现,即使这不一定是情况。 - Timo
7
еӣ дёәIReadOnlyCollection<T>жІЎжңүеҢ…еҗ«Contains()ж–№жі•пјҢжүҖд»Ҙдјҡиў«DownvoteгҖӮиҝҷзңӢиө·жқҘеғҸжҳҜдёҖдёӘеҸӘиҜ»е“ҲеёҢйӣҶзҡ„йҮҚиҰҒз»„жҲҗйғЁеҲҶгҖӮ - Walt D
1
@WaltD IEnumerable<T> 的扩展方法首先尝试将可枚举对象转换为 ICollection<T>。因此,它将使用 HashSet<T> 中的 Contains 方法。 - Alex
显示剩余6条评论

7

虽然它不是只读的,但Microsoft发布了一个名为System.Collections.Immutable的nuget包,其中包含一个实现了IImmutableSet<T>ImmutableHashSet<T>,该接口扩展了IReadOnlyCollection<T>

快速使用示例:

public class TrackedPropertiesBuilder : ITrackedPropertiesBuilder
{
    private ImmutableHashSet<string>.Builder trackedPropertiesBuilder;

    public TrackedPropertiesBuilder()
    {
        this.trackedPropertiesBuilder = ImmutableHashSet.CreateBuilder<string>();
    }

    public ITrackedPropertiesBuilder Add(string propertyName)
    {
        this.trackedPropertiesBuilder.Add(propertyName);
        return this;
    }

    public IImmutableSet<string> Build() 
        => this.trackedPropertiesBuilder.ToImmutable();
}

我唯一关注的是System.Collections.Immutable下的集合(我相信)旨在实现线程安全,而这类类在不需要线程安全的情况下可能会有负面的性能影响。 - ErikE
2
@ErikE 不可变性是一种通用的设计模式,不仅仅是为了线程安全而设计的,我认为性能影响更多地与使用这些集合的算法不当相关,而不是与集合本身相关。 - Uwy
2
请注意,ImmutableHashSet 比 HashSet 慢慢好几倍 - Kirsan

6
您可以使用以下装饰器包装哈希集,并返回一个只读的ICollection<T>IsReadOnly属性返回true,修改方法按照ICollection<T>的协定抛出NotSupportedException):
public class MyReadOnlyCollection<T> : ICollection<T>
{
    private readonly ICollection<T> decoratedCollection;

    public MyReadOnlyCollection(ICollection<T> decorated_collection)
    {
        decoratedCollection = decorated_collection;
    }

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

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

    public void Add(T item)
    {
        throw new NotSupportedException();
    }

    public void Clear()
    {
        throw new NotSupportedException();
    }

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

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

    public bool Remove(T item)
    {
        throw new NotSupportedException();
    }

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

    public bool IsReadOnly
    {
        get { return true; }
    }
}

你可以像这样使用它:

public class MyClass
{
    private readonly HashSet<string> _referencedColumns;

    public ICollection<string> ReferencedColumns { 
        get { return new MyReadOnlyCollection<string>(_referencedColumns); }
    }
    //...

请注意,此解决方案不会对HashSet进行快照,而是将保留对HashSet的引用。这意味着返回的集合将包含一个活动版本的HashSet,即如果HashSet被更改,则在更改之前获取只读集合的使用者将能够看到更改。

谢谢!当然,我可以自己写。我在想BCL是否有什么更好的解决方案。顺便问一下,如果你的MyReadOnlyCollection实现了IReadOnlyCollection<>,会不会更好? - Dejan
不客气。这取决于消费者。它是否更喜欢IReadOnlyCollection<T> - Yacoub Massad
IReadOnlyCollection<T>足够并且完美地向调用者说明了可以期望的内容。我本应该在一开始就使用它来提问,但为了保留历史记录,我现在不会更改。 - Dejan
值得注意的是,ICollection<T>适用于.Net 4,而IReadOnlyCollection<T>需要.Net 4.5+。因此,历史实现有时可能更有用。 - tobriand

6

这很简单,我不知道为什么Microsoft没有提供这个功能,但我会根据 IReadOnlyCollection<T> 提供我的实现,并提供扩展方法以完整性。

public class MyClass {
    private readonly HashSet<string> _referencedColumns;

    public IReadonlyHashSet<string> ReferencedColumns => _referencedColumns.AsReadOnly();
}

/// <summary>Represents hash set which don't allow for items addition.</summary>
/// <typeparam name="T">Type of items int he set.</typeparam>
public interface IReadonlyHashSet<T> : IReadOnlyCollection<T> {
    /// <summary>Returns true if the set contains given item.</summary>
    public bool Contains(T i);
}

/// <summary>Wrapper for a <see cref="HashSet{T}"/> which allows only for lookup.</summary>
/// <typeparam name="T">Type of items in the set.</typeparam>
public class ReadonlyHashSet<T> : IReadonlyHashSet<T> {
    /// <inheritdoc/>
    public int Count => set.Count;
    private HashSet<T> set;

    /// <summary>Creates new wrapper instance for given hash set.</summary>
    public ReadonlyHashSet(HashSet<T> set) => this.set = set;

    /// <inheritdoc/>
    public bool Contains(T i) => set.Contains(i);

    /// <inheritdoc/>
    public IEnumerator<T> GetEnumerator() => set.GetEnumerator();
    /// <inheritdoc/>
    IEnumerator IEnumerable.GetEnumerator() => set.GetEnumerator();
}

/// <summary>Extension methods for the <see cref="HashSet{T}"/> class.</summary>
public static class HasSetExtensions {
    /// <summary>Returns read-only wrapper for the set.</summary>
    public static ReadonlyHashSet<T> AsReadOnly<T>(this HashSet<T> s)
        => new ReadonlyHashSet<T>(s);
}

这与Bas的答案非常相似,但不同之处在于它不仅仅将HashSet强制转换为可以向上转换回原始集合的IReadOnlyCollection,而是实际上将源set封装/包装在一个新的ReadonlyHashSet类(private HashSet<T> set;)中,仅公开IReadOnlyCollection的接口。它不能被转换为HashSet,因为它不是HashSet。Chapeau bas,Pawcio先生! - Paul-Sebastian Manole

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