如何避免公共访问私有字段?

24
例如,让我们声明:

For example let's declare:


private readonly List<string> _strings = new List<string>();
public IEnumerable<string> Strings
{
    get
    {
        return _strings;
    }
}

现在我们可以做以下事情:

((List<string>) obj.Strings).Add("Hacked");

所以我们实际上并没有隐藏List的使用,只是将它隐藏在接口后面。如何在不复制_strings到新集合中的情况下隐藏列表以限制对列表的修改?


4
问题不在于 Strings 将要改变,而在于 Strings 返回的对象本身是可变的 - Eric Lippert
6
Dan的回答很好,但这里有一些微妙之处需要考虑。例如,假设您分发了一个引用,然后在分发引用之后,由于类内部的某个正确操作,列表的内容发生了更改。在您的情况下,这种情况可能吗?如果是这样,那么先前分发的引用是否需要观察到更改?也就是说,它是像不可变立面一样覆盖在可变集合上,还是像快照一样,显示列表的内容如同它过去的状态? - Eric Lippert
当你尝试修改时,似乎会抛出一个异常。 - Sergey Metlov
1
@SergeyMetlov,保持原样,在你的代码第二个版本中,将其从List<T>更改为类似于T[]的东西,然后观察强制转换错误的出现。这教导了编写接口(或公共契约)而不是实现的经验教训 :-) - Adam Houldsworth
1
Eric Lippert曾经讨论过这个问题的许多变化一段时间前 - Raymond Chen
显示剩余4条评论
7个回答

35

有很多方法。

最简单的方法是始终复制私有字段:

return _strings.ToList();

但通常情况下这并不是必需的。不过,如果:

  1. 您知道调用代码通常都需要一个 List<T>,那么最好直接返回它(而不是暴露原始集合)。
  2. 即使原始集合被随后更改,暴露对象保留原始值很重要。

或者,您可以只暴露一个只读包装器:

return _strings.AsReadOnly();

这种选项比第一种更有效,但会暴露对底层集合所做的更改。

这些策略也可以结合使用。如果您需要原始集合的副本(以便对原始集合进行更改不产生影响),但又不希望返回的对象是可变的*,则可以选择:

 // copy AND wrap
return _strings.ToList().AsReadOnly();

您还可以使用 yield

foreach (string s in _strings)
{
    yield return s;
}

这种方法在效率和权衡方面与第一种选项相似,但采用了延迟执行。

显然,在考虑此处的关键问题时,您需要考虑您的方法实际上将如何使用以及您打算使用此属性提供什么功能。

* 虽然说实话,我不确定您为什么会在意。


或者使用 AsEnumerable() 扩展方法而不是 yield return - thecoop
AsEnumerable() 不会复制值。 - Dustin Kingen
1
@thecoop AsEnumerable 只是将源转换为 IEnumerable<T>。没有任何阻止客户端将其强制转换回 List<T> 并添加数据的限制。 - D Stanley
3
你也可以使用 Select(x=>x),这将创建一个新的迭代器,防止它被转换回 List - Servy
啊,是的,我的错。我以为它做了。 - thecoop

7
使用ReadOnlyCollection如何?这应该可以解决特定的问题。
private ReadOnlyCollection<string> foo;
public IEnumerable<string> Foo { get { return foo; } }

OP不想让人们访问列表?ReadOnlyCollection仍然提供对列表的只读访问。 - Mathew Thompson
2
我非常确定那不是问题所在。如果你不想公开某些东西,就将其设为私有。 - Tejas Sharma
我可能错了,希望楼主能提供进一步的澄清。 - Mathew Thompson
现在包含 foo 的类可以向其中添加项目吗?我认为可以,如果是这样的话,那么这个答案是错误的,因为它改变了类的内部行为。 - Nawaz
为什么类的“内部行为”很重要呢?实现细节就是实现细节。另外,一旦创建了一个ReadOnlyCollection,就无法再向其中添加项目。这就是为什么它被称为“只读”的原因。 - Tejas Sharma

5

我只是想给你分享一下我的最近对封装的思考,也许你会觉得合理。

众所周知,封装是非常有用的东西,它有助于应对复杂性。它使您的代码逻辑上更加简洁明了。

正如你所说,对成员的受保护访问可能会被打破,接口用户可以完全访问底层对象。但这有什么问题呢?你的代码不会变得更加复杂或耦合。

事实上,据我所知,通过某些反射使用几乎没有限制,即使是完全私有的成员也是如此。那为什么不试着把它们隐藏起来呢?

我认为没有必要复制一个集合(或任何其他可变对象),上述方法都是错误的。使用你最初的变量即可:

private readonly List<string> _strings = new List<string>();
public IEnumerable<string> Strings
{
    get
    {
        return _strings;
    }
}

“上述的方法都是错误的”——这绝对不是真的,不同的应用有不同的需求。如果你正在开发一个银行应用程序,你肯定要费心确保人们无法随意地向你的集合中写入数据。 - Tejas Sharma
4
@Tejas 我总是有理论上的可能性来修改我记忆中的任何对象,特别是管理的对象。这就是为什么银行应用程序依赖于在线身份验证、交易验证等,而不是客户端应用程序中的特定集合类型。 - astef
事实上,您可以修改内存的能力是无关紧要的。如果您正在编写托管代码并且希望提供对集合的只读访问权限,那似乎是完全合理的事情。我想我们同意有所分歧。 - Tejas Sharma
1
但是,如果IEnumerable(或ICollection)接口访问足够,为什么要为其创建一个特殊的集合呢?复制整个集合甚至更不合理。我的立场并不是原则性的,我只是想理解。 - astef
因为强制转换是完全合法的。我同意你的观点,对于大多数情况,将某些东西公开为“IEnumerable”就足够了。ReadOnlyCollection是为那些想要额外保护免受修改的少数情况而设计的。这只是我的看法。拥有不同的观点是完全合理的。 - Tejas Sharma

3
您可以将列表包装在getter中,如下所示:
private readonly List<string> _strings = new List<string>();
public IEnumerable<string> Strings {
  get {
    return new ReadOnlyCollection<string>(_strings);
  }
}

这样你就可以在类内自由修改私有列表,不用担心外部的更改。

注意: ReadOnlyCollection 并不会复制它包含的列表(如 _strings)。


我不知道,也许他们认为 ReadOnlyCollection 会复制集合? - Charles Lambert
@Tejas - 这是不正确的,它确实会与对象保持同步。 - Charles Lambert
嗯,你说得对。我想它内部引用了实际的集合。不错。 - Tejas Sharma
1
_strings.AsReadOnly() 执行与 new ReadOnlyCollection<string>(_strings) 相同的操作,并且可能更加简洁。 - Sam Harwell
@280Z28 - 我同意你的建议可能会更加简洁。但是,我的版本适用于实现 IList 接口的任何类。而 AsReadOnly 方法仅存在于 List<T> 中。 - Charles Lambert

3

1

既然还没有人提供这个答案,这里有另一种选择:

private readonly List<string> _strings = new List<string>();
public IEnumerable<string> Strings 
{
    get { return _strings.Select(x => x); }
}

这实际上相当于之前提到的 yield return 方法,但更加紧凑。

0

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