【问题标题】:How to make a ReadOnlyCollection from a HashSet without copying the elements?如何在不复制元素的情况下从 HashSet 制作 ReadOnlyCollection?
【发布时间】:2019-12-19 23:22:49
【问题描述】:

我有一个私有的HashSet<string>,它是一个只读属性的支持字段,它应该返回一个只读集合,这样调用者就不能修改集合。所以我尝试:

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

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

这不会编译为 ReadOnlyCollection 接受 IList&lt;T&gt; 不是由 HashSet&lt;T&gt; 实现的。是否有另一个包装器可以用来让我免于复制项目?就我的目的而言,只需返回由HashSet&lt;T&gt; 实现的实现ICollection&lt;T&gt;(而不是IList&lt;T&gt;)的东西就足够了。

【问题讨论】:

标签: c# base-class-library


【解决方案1】:

考虑将属性公开为IReadOnlyCollection&lt;&gt; 类型,这将提供HashSet&lt;&gt; 的只读视图。这是一种有效的实现方式,因为属性 getter 不需要底层集合的副本。

这不会阻止某人将该属性转换为 HashSet&lt;&gt; 并对其进行修改。如果您对此感到担忧,请考虑在属性 getter 中使用 return _referencedColumns.ToList(),这将创建您的基础集的副本。

【讨论】:

  • 谢谢。那么是否没有包装器可以节省复制开销并且无法回退?
  • 仅作记录:转换为 IReadOnlyCollection&lt;&gt; 仅适用于 .NET 4.6 及更高版本(请参阅:stackoverflow.com/a/32762752/331281)。
  • 好电话。这仍将使用HashSet 的索引行为,尽管这不再明显。对IReadOnlyCollection.Contains() 的调用带有强烈的线性时间实现的味道,即使情况不一定如此。
  • 框架差异让我感到震惊。在 VS 2017 中,如果您导航到 HashSet 类 defn,它会显示为实现 IReadOnlyCollection 但编译器会抛出错误,所以想知道一段时间发生了什么。我的项目是 4.5
  • 投反对票,因为 IReadOnlyCollection&lt;T&gt; 不包含 Contains() 方法,这似乎是哈希集只读视图的一个非常重要的组成部分。
【解决方案2】:

您可以使用以下装饰器包装哈希集并返回只读的ICollection&lt;T&gt;IsReadOnly 属性返回 true,修改方法抛出 NotSupportedException,如ICollection&lt;T&gt; 的合同中指定的那样):

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&lt;&gt; 会更好吗?
  • 不客气。这取决于消费者。它更喜欢IReadOnlyCollection&lt;T&gt;吗?
  • IReadOnlyCollection&lt;T&gt; 已经足够并且完美地记录了调用者的期望。我应该首先在我的问题中使用它,但我现在不会更改它以保留历史记录。
  • 值得注意的是,ICollection&lt;T&gt; 适用于 .Net 4,而 IReadOnlyCollection&lt;T&gt; 需要 .Net 4.5+。因此,历史实现有时会更有用。
【解决方案3】:

虽然它不是只读的,但微软发布了一个名为System.Collections.Immutable的nuget包,其中包含一个ImmutableHashSet&lt;T&gt;,它实现了IImmutableSet&lt;T&gt;,它扩展了IReadOnlyCollection&lt;T&gt;

快速使用示例:

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 不变性是一种通用设计模式,不仅是为线程安全而设计的,性能影响是,IMO,更多地与使用这些集合而不是集合本身的算法耦合
  • 小心 ImmutableHashSet 是 several times slower 而不是 HashSet!
【解决方案4】:

这很简单,我不知道为什么微软没有提供这个,但我会发布我基于IReadOnlyCollection&lt;T&gt;的实现,以及完整的扩展方法。

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&lt;T&gt; set;) 中,只暴露IReadOnlyCollection 的接口。它不能转换为HashSet,因为它是not-a HashSet。起首巴斯,帕西奥先生!
猜你喜欢
  • 2021-12-03
  • 1970-01-01
  • 2020-01-05
  • 1970-01-01
  • 1970-01-01
  • 2020-12-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多