如何将引用类型属性设置为“只读”

12

我有一个类Bar,其中包含一个私有字段,其引用类型为Foo。 我想在公共属性中公开Foo,但我不希望属性的使用者能够更改Foo。 但是,Bar内部应该可以更改它,即我不能将字段设置为readonly

因此,我需要的是:

      private _Foo;

      public Foo 
      { 
         get { return readonly _Foo; } 
      } 

当然,这不是有效的。我可以返回一个Foo的克隆(假设它实现了ICloneable接口),但这对使用者来说并不明显。我应该将属性名更改为FooCopy吗?或者改成GetCopyOfFoo方法?你认为哪种是最佳实践?谢谢!

8个回答

12

看起来你需要类似于C++中的"const"。但是在C#中,这种语言特性并不存在。如果假设可变成员是公共的,那么没有办法表明消费者不能修改对象的属性,但有可能使用建议的方案返回Foo的克隆,或者可能提供一个视图(例如ReadOnlyCollection针对集合所做的)。当然,如果可以将Foo设置为不可变类型,这会让生活更简单...

请注意,使字段只读和使对象本身不可变之间存在很大的区别。

目前,该类型可以以两种方式更改内容。它可能会执行以下操作:

_Foo = new Foo(...);

或者

_Foo.SomeProperty = newValue;

如果只需要实现第二个目的,该字段可以是只读的,但您仍然面临着获取属性的人能够更改对象的问题。如果只需要实现第一个目的,并且实际上 Foo 已经是不可变的或可以变为不可变的,您只需提供一个只具有“getter”的属性即可。

非常重要的一点是您理解修改字段值(使其引用到另一个实例)和修改字段引用的对象内容之间的区别。


在这个上下文中,视图是什么?是接口还是包装器? - Joel in Gö
......并且绝对同意参考文献区分的价值/内容。不幸的是,我需要同时做两件事。 - Joel in Gö
请看我的评论,我认为他所说的“view”是什么意思。 - RossFabricant
@jon:只是好奇,Jon,为什么我们不能在这里设置私有属性?'public Foo { get { return _Foo; } private set { _Foo = value;} }' 或者我漏掉了什么吗? - ram
@Ram: 我不明白那会有什么帮助。这个想法是暴露出 Foo 的只读视图 - 不仅仅是持有它的变量,而是对象本身。 - Jon Skeet

3

很遗憾,目前在C#中没有简单的解决方案。你可以在接口中提取Foo的“只读部分”,并让你的属性返回它,而不是Foo


不太理想,因为接口仍然可以转换为 Foo。 - RossFabricant
我同意 - 但你至少向调用者透露了你的意图。 - Brian Rasmussen
10
@rossfabricant说:你需要决定是要防范Murphy还是Machiavelli。将对象向下转型为Foo是Machiavellan的做法。你不能设计来防范恶意,只能防范错误。 - Pontus Gagge
1
好的观点,而且由于反射可供调用者使用,无论如何,你很难对抗马基雅维利。但是,在任何情况下,const修饰符都更好。 - Brian Rasmussen
@pontus gagge-非常棒的观点。希望我能给你的评论点赞更多。 - John MacIntyre

2

通常情况下,制作副本、创建ReadOnlyCollection或使Foo成为不可变对象是最佳选择,正如你已经猜测的那样。

如果涉及到更多的工作,我有时候会更喜欢使用方法而不是属性。方法意味着正在发生某些事情,并且在使用API时会引起更多的关注。


1
你收到和返回的Foo对象进行“克隆”是一种正常的做法,称为防御性复制。除非克隆会对用户可见产生某些未预料的副作用,否则绝对没有理由不这样做。这通常是保护类内部私有数据的唯一方法,特别是在C#或Java中,其中没有C++中的const概念。(也就是说,在这两种语言中,必须这样做才能正确创建真正的不可变对象。)
只是为了澄清,可能的副作用包括您的用户(合理地)期望返回原始对象,或者Foo持有的某些资源无法正确克隆。(在这种情况下,它实现IClonable是做什么?!)

1

如果你不想让任何人干扰你的状态...就不要暴露它!正如其他人所说,如果某些东西需要查看你的内部状态,请提供一个不可变的表示。或者,让客户端告诉去做某事(谷歌搜索“tell don't ask”),而不是他们自己去做。


0
我在思考类似的安全问题。可能有一种方法。相当明确但不简短。总体想法很简单。然而,我总是找到一些绕过的方法,所以从未测试过。但你可以尝试一下-也许对你有用。
这只是伪代码,但我希望背后的思路是清楚的。
public delegate void OnlyRuller(string s1, string s2);
public delegate void RullerCoronation(OnlyRuller d);

class Foo {
  private Foo();
  public Foo(RullerCoronation followMyOrders) {
     followMyOrders(SetMe);
  }

  private SetMe(string whatToSet, string whitWhatValue) {
     //lot of unclear but private code
  }
}

所以在创建这个属性的类中,您可以访问SetMe方法,但它仍然是私有的,因此除了创建者Foo之外,看起来是不可变的。

然而,对于大于几个属性的任何内容,这很可能很快变得非常混乱 - 这就是为什么我总是更喜欢其他封装方式的原因。然而,如果您非常重视不允许客户端更改Foo,则这是一个替代方案。

然而,正如我所说,这只是理论。


0
为了澄清Jon Skeet的评论,您可以创建一个视图,即对可变Foo进行不可变包装的类。以下是一个示例:
class Foo{
 public string A{get; set;}
 public string B{get; set;}
 //...
}

class ReadOnlyFoo{
  Foo foo; 
  public string A { get { return foo.A; }}
  public string B { get { return foo.B; }}
}

只有当Foo的属性是值类型时,这才有效。如果Foo具有引用类型属性foo.Obj,则调用者仍然可以随意更改对象(尽管不能将其设置为新对象,请参见John Skeet上面的评论)。因此它并不是真正的不可变的。 - Joel in Gö
你可以为ReadOnlyFoo设置一个属性,该属性返回一个ReadOnlyBar。 - RossFabricant
...等等无穷无尽的循环...看起来开销非常大。 - Joel in Gö
@Joel:这就是你使用深度可变类型的后果。@rossfabricant:是的,这正是我所说的,谢谢 :) - Jon Skeet

0

你可以在C#中手动复制C++ const的行为。

无论Foo是什么,调用者修改它的状态的唯一方式就是通过调用其方法或设置属性。

例如,Foo的类型是FooClass

class FooClass
{
    public void MutateMyStateYouBadBoy() { ... }

    public string Message
    {
        get { ... }
        set { ... }
    }
}

所以在你的情况下,你希望他们可以获取Message属性,但不允许设置它,并且你绝对不希望他们调用该方法。

因此,定义一个描述他们被允许做什么的接口:

interface IFooConst
{
    public string Message
    {
        get { ... }
    }
}

我们省略了可变方法,只保留了属性的getter。

然后将该接口添加到FooClass的基本列表中。

现在,在具有Foo属性的类中,您有一个字段:

private FooClass _foo;

还有一个属性的获取器:

public IFooConst Foo
{
    get { return _foo; }
}

这基本上是手动复制了 C++ 中 const 关键字自动执行的操作。在伪代码 C++ 中,类型为 const Foo & 的引用就像是一个自动生成的类型,只包括那些被标记为 const 成员的 Foo 成员。将其转换为某个理论上未来版本的 C#,你可以这样声明 FooClass
class FooClass
{
    public void MutateMyStateYouBadBoy() { ... }

    public string Message
    {
        get const { ... }
        set { ... }
    }
}

其实我所做的就是将IFooConst中的信息合并回到FooClass中,通过给一个安全成员打上新的const关键字。因此,在某种程度上,添加一个const关键字除了提供一种正式的方法来处理这种模式之外,并没有为语言增加太多东西。
然后,如果您有一个对FooClass对象的const引用:
const FooClass f = GetMeAFooClass();

您只能在f上调用const成员。

请注意,如果FooClass定义为public,则调用者可以将IFooConst强制转换为FooClass。但是他们在C++中也可以这样做 - 这被称为“去除const”,涉及到一个名为const_cast<T>(const T &)的特殊运算符。

还有一个问题,即接口在产品版本之间不太容易演变。如果第三方可能实现您定义的接口(如果他们可以看到它),则您不能在将来的版本中添加新方法而不需要要求其他人重新编译其代码。但是,只有当您正在编写可扩展的库供其他人构建时,才会出现此问题。也许内置的const功能可以解决此问题。


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