如何最好地将List<T>暴露为只读?

7

我多次遇到这个设计问题,但从未找到一个完美的解决方案。

我想要暴露一个集合,在持有者类作用域下可编辑,但在其他公共作用域下只可读。

尝试1:

public class MyClass
{
    private List<object> _myList = new List<object>();

    public IEnumerable<object> MyList { get { return _myList; } }
}

这样做的问题在于外部代码可以将其强制转换回 List 并进行编辑,就像这样:
var x = ((List<object>)MyList);

Trial 2:

public class MyClass
{
    private List<object> _myList = new List<object>();

    public IEnumerable<object> MyList { get { return _myList.ToList(); } }
}

这种方法可以防止外部修改,但会造成多次复制 List 的不必要开销。

试验3:

public class MyClass
{
    private List<object> _myList = new List<object>();
    private ReadOnlyCollection<object> _roList = 
        new ReadOnlyCollection<object>(_myList)

    public IEnumerable<object> MyList { get { return _roList; } }
}

这是标准解决方案,目前我正在使用,但ReadOnlyCollection要慢大约30%:

Debug Trace:
Use normal foreach on the ReadOnlyCollection
Speed(ms): 2520,3203125
Result: 4999999950000000

use<list>.ForEach
Speed(ms): 1446,1796875
Result: 4999999950000000

Use normal foreach on the List
Speed(ms): 1891,2421875
Result: 4999999950000000

有没有一种“完美”的方法来做到这一点?谢谢。
7个回答

7
你尝试过返回枚举器吗?
public class MyClass
{
    private List<object> _myList = new List<object>();

    public IEnumerable<object> MyList { get { yield return _myList.GetEnumerator(); } }
}

这不会创建副本,是只读的,也无法转换回列表。
编辑:这仅适用于yieldreturn。以这种方式进行惰性评估,我不知道这对您是否有影响。

是的。它无法编译,或者我做错了:无法隐式转换类型'System.Collections.Generic.List<object>.Enumerator'为'System.Collections.Generic.IEnumerable<object>'。 - LMB
我现在没有编译器的访问权限,但我以前见过这个工作。我可能语法有误。 - Paul Phillips
请原谅我,如果我再次做错了,但只有当返回类型为IEnumerable<object>或IEnumerable时,它才能编译通过。对吗?或者它可以是强类型的? - LMB
如果你的意思是这个东西必须是 IEnumerable 才能工作,那么就必须是这样。如果你的意思是它是一个 IEnumerable,但不是 object 类型的其他类型,那么也可以。只要底层集合是那种类型即可。 - Paul Phillips

5
你可以使用 List<T>.AsReadOnly 方法来暴露你的列表的一个薄的只读包装器。不会有额外的复制,调用者将即时“看到”在你的方法内部所做的对原始数组的更改:
public ReadOnlyCollection<object> MyList { get { return _myList.AsReadOnly(); } }

我认为这仍然会遇到OP在ReadOnlyCollection中遇到的性能问题。 - Michael Yoon
我刚试了一下。使用List.AsReadOnly上的普通foreach,速度甚至更慢。速度(毫秒):2771.296875。结果:4999999950000000。谢谢! - LMB

3

List<T> 实现了 IReadOnlyList<T> 接口:

public class MyClass
{
    private List<object> _myList = new List<object>();   // Modifiable

    public IReadOnlyList<object> MyList => _myList;      // Read only
}

1
我认为这段代码适用于内部项目。唯一的问题是调用者可能会将 IReadOnlyList<object> 强制转换回常规的 List<object> 并再次修改它。因此,如果该项目是公共共享库,我不建议使用此代码。 - GLJ

1
我通常使用并且非常喜欢的解决方案是以下这个简单的方法:
public IEnumerable<object> MyList { get { return _myList.Select(x => x); } }

然而,它要求您使用支持Linq的.NET版本。
对于使用 foreach 循环遍历它,实际上速度更快:选择 小于1ms,选择 ReadOnlyCollection 小于0.1秒。 对于 ReadOnlyCollection ,我使用了:
public IEnumerable<object> MyROList { get { return new ReadOnlyCollection<object>(_myList); } }

在我的测试中它比较慢。你是怎么做到的?=> 在List上使用普通的foreach,选择(x => x) | 速度(ms): 4216.5234375 | 结果: 4999999950000000 - LMB
我不知道你在 foreach 循环中测试什么。我只是调用一个虚拟的 foreach,如下所示:foreach (var a in foo.MyList) { }。对于 ReadOnlyCollection,情况也一样。我所做的每个循环测试都包含在 StopWatch 中,以计算所需的时间。 - Guillaume
使用另一个答案中提出的 yield 进行相同的测试,结果比我的解决方案稍微快一些。但对于1000万个项目,差异大约为0.01毫秒。 - Guillaume
我在一台x64机器上将1到100,000,000的所有长整型数值相加。 - LMB
确实在求和时速度较慢。我的解决方案需要2417毫秒。ReadOnlyCollection需要1000毫秒。我对1000万个项目进行了236毫秒和94毫秒的计算。对于100万个项目,我得到了27毫秒和10毫秒的计算结果。 - Guillaume

0
为什么不使用 List<T>.AsReadOnly 方法呢?请参考下面的代码片段:
public class MyClass
{
    private List<object> _myList = new List<object>();

    public IList<object> MyList { get { return _myList.AsReadOnly(); } }
}

0

使用ReadOnlyCollection<T>作为包装器 - 它在内部使用您的List<T>集合(使用相同的引用,因此不会复制任何内容,并且对列表所做的任何更改也会反映在ReadOnlyCollection中)。

它比仅提供IEnumerable属性更好,因为它还提供索引和计数信息。


0

2
我猜这和IEnumerable的情况一样。可以将其强制转换回List并进行修改。 - LMB

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