返回只读对象的最佳实践


11

我对C#中的OOP有“最佳实践”问题(但它适用于所有语言)。

考虑使库类具有要通过属性访问器公开的对象,但是我们不希望公众(使用此库类的人)对其进行更改。

class A
{
    // Note: List is just example, I am interested in objects in general.
    private List<string> _items = new List<string>() { "hello" }

    public List<string> Items
    {
        get
        {
            // Option A (not read-only), can be modified from outside:
            return _items;

            // Option B (sort-of read-only):
            return new List<string>( _items );

            // Option C, must change return type to ReadOnlyCollection<string>
            return new ReadOnlyCollection<string>( _items );
        }
    }
}

显然,最好的方法是“选项C”,但是很少有具有ReadOnly变体的对象(当然,没有用户定义的类具有它)。

如果您是A类的用户,您是否希望更改为

List<string> someList = ( new A() ).Items;

传播到原始(A)对象?还是返回一个克隆(如果它在注释/文档中是这样写的)可以吗?我认为这种克隆方法可能会导致很难跟踪的错误。

我记得在C ++中我们可以返回const对象,并且您只能在其上调用标记为const的方法。我想C#中没有这样的功能/样式?如果不是,为什么不包括呢?我相信这称为const正确性。

但是,我的主要问题还是关于“您会期望什么”或如何处理选项A与B。


2
看一下有关.NET 4.5的新不可变集合的预览的本文:blogs.msdn.com/b/bclteam/archive/2012/12/18/…您已经可以通过NuGet获得它们。
克里斯托夫·克莱斯

Answers:


10

除了@Jesse的答案外,这是集合的最佳解决方案:总体思路是设计A的公共接口,使其仅返回

  • 值类型(如int,double,struct)

  • 不变的对象(例如“字符串”或用户定义的类,它们仅提供只读方法,就像您的类A)

  • (仅在必须时):对象副本,如“选项B”所示

IEnumerable<immutable_type_here> 本身是不可变的,因此这只是上述情况的特例。


19

还要注意,您应该对接口进行编程,并且总的来说,您想从该属性返回的是IEnumerable<string>。A List<string>有点不可以,因为它通常是易变的,我将进一步说,ReadOnlyCollection<string>作为ICollection<string>感觉的实现者,它好像违反了Liskov替代原理。


6
+1,使用IEnumerable<string>恰恰是人们想要的。
布朗

2
在.Net 4.5中,您也可以使用IReadOnlyList<T>
svick

3
其他相关方的注意事项。在考虑属性访问器时:如果您只是通过执行return _list(+隐式强制转换为IEnumerable)来返回IEnumerable <string>而不是List <string>,则可以将该列表重新投射回List <string>并进行更改,更改将传播到主要对象中。您可以返回新的List <string>(_list)(+随后的隐式强制转换为IEnumerable),这将保护内部列表。更多关于这可以在这里找到:stackoverflow.com/questions/4056013/...
NeverStopLearning

1
最好是要求所有需要按索引访问它的集合的接收者都必须准备好将数据复制到一个方便使用的类型中,还是更好地要求返回集合的代码以某种形式提供它?是否可以通过索引访问?除非供应商可能以不方便按索引访问的形式提供它,否则我认为使接收者免于制作自己的冗余副本的价值将超过使供应商不必供应的价值。随机访问形式(例如,只读IList<T>
supercat 2013年

2
我不喜欢IEnumerable的一件事是,您永远不知道它是作为协程运行还是仅作为数据对象运行。如果将其作为协程运行,则可能会导致细微的错误,因此有时应该知道这一点。这就是为什么我倾向于选择ReadOnlyCollection或IReadOnlyList等
史蒂夫Vermeulen的

3

我曾遇到过类似的问题,下面是我的解决方案,但这实际上仅在您也控制库的情况下才有效:


从您的课程开始 A

public class A
{
    private int _n;
    public int N
    {
        get { return _n; }
        set { _n = value; }
    }
}

然后仅从属性的get部分(和任何读取方法)派生出一个接口,并让A实现

public interface IReadOnlyA
{
    public int N { get; }
}
public class A : IReadOnlyA
{
    private int _n;
    public int N
    {
        get { return _n; }
        set { _n = value; }
    }
}

然后,当然,当您要使用只读版本时,简单的转换就足够了。确实,这就是解决方案C所要解决的问题,但是我想我会提供实现它的方法


2
问题是,有可能将其转换回可变版本。并可以通过子类中的新方法来更改其对LSP的违反,由于可以通过基类(在这种情况下为接口)方法观察到的状态。但是,即使它没有强制执行,它也至少可以传达意图。
user470365 2013年

1

对于发现此问题但仍对答案感兴趣的任何人-现在有另一种方法可供选择ImmutableArray<T>。这些都是几点为什么我认为这是更好,然后IEnumerable<T>或者ReadOnlyCollection<T>

  • 它明确指出它是不可变的(表达性API)
  • 它清楚地表明集合已经形成(换句话说,它不是很懒惰地初始化,可以多次遍历它)
  • 如果项目的数量是已知的,它没有隐藏knowlegde喜欢IEnumerable<T>
  • 因为客户不知道该程序的实现是什么,IEnumerable或者IReadOnlyCollect他/她不能假定它永远不会更改(换句话说,不能保证一个集合的项目没有更改,而对该集合的引用保持不变)

另外,如果APi需要通知客户端有关集合中的更改,我会将事件作为单独的成员公开。

请注意,我并不是说IEnumerable<T>总体上是不好的。在将collection作为参数传递或返回延迟初始化的collection时,我仍然会使用它(在这种情况下,隐藏项目计数是有意义的,因为尚不知道)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.