在派生类中将属性设置为只读

8

我正在覆盖继承类中的一个属性,我希望将其设置为只读。C#编译器不允许我更改访问修饰符,因此它必须保持public。

那么,最好的方法是什么?我应该在set { }中抛出InvalidOperationException吗?


我认为我们需要更多的上下文信息。 - Noldorin
6个回答

8

在派生类中使setter抛出InvalidOperationException违反了里氏替换原则,基本上使得setter的使用取决于基类的类型,从而消除了多态性的价值。

派生类必须遵守其基类的契约。如果在所有情况下都不适合使用setter,则它不应该出现在基类中。

解决这个问题的一种方法是稍微打破继承关系。

class C1 { 
  public virtual int ReadOnlyProperty { get; } 
}
class C2 { 
  public sealed override int ReadOnlyProperty { 
    get { return Property; }
  }
  public int Property {
    get { ... }
    set { ... }
  }
}

你正在遇到的类型在这种情况下可能会继承C1,而其余部分可能会切换为从C2派生。

那么,如果基类有一个IsReadOnly属性并且抛出自己的异常,那就没问题了?这就是NameObjectCollectionBase>NameValueCollection>HttpValueCollection继承的工作方式,它被PageRequest.Param使用。我理解原则背后的推理,但在NameObjectCollectionBase的情况下,它与您的声明相矛盾:如果setter在所有情况下都不合适,则不应该出现在基类中。不幸的是,我正在使用一个没有实现这个功能的.NET类(有充分的理由)。我认为在我的情况下这是有意义的。 - Nelson Rothermel
1
@Nelson,基类拥有IsReadOnly最多只是一种妥协。个人认为这是一个可怕的模式,如果BCL将集合适当地分为可变和只读接口/类型,那么会更好。 - JaredPar
好的,那我来更具体地说明一下。我正在从ASP.NET的ListView中派生出一个类,我们称之为MyCustomList。我想要ListView的所有功能/模板,但是让MyCustomList处理获取数据、分配DataSource/Id以及防止DataSource/Id被更改。我认为抛出异常是一个很好的折衷方案,而不是重新实现所有ListView的功能。 - Nelson Rothermel
也许我应该扩展ObjectDataSource(或类似的东西),但同时也想要一些默认模板。 - Nelson Rothermel
@Nelson,我认为采用“ObjectDataSource”路线更合理。 - JaredPar
我越想越同意你的观点... :) 每次需要付出一点点额外的工作量,但是更加灵活和可扩展。 - Nelson Rothermel

5

您可以隐藏原始实现并返回基础实现:

class Foo
{
    private string _someString;
    public virtual string SomeString 
    {
        get { return _someString; }
        set { _someString = value; }
    }
}

class Bar : Foo
{
    public new string SomeString 
    { 
        get { return base.SomeString; }
        private set { base.SomeString = value; }
    }
}

namespace ConsoleApplication3
{
    class Program
    {
        static void Main( string[] args )
        {
            Foo f = new Foo();
            f.SomeString = "whatever";  // works
            Bar b = new Bar();
            b.SomeString = "Whatever";  // error
        }
    }
}

然而,正如Jared所暗示的那样,这是一种有点奇怪的情况。为什么不从一开始就将基类中的setter设置为私有或受保护呢?


除非你从示例中执行 "Foo f2 = b; f.SomeString = "eeba geeba!";",否则编译器不允许这样做。 - mjfgates
@mjfgates:我猜你的意思是 Foo f = new Bar(); f.SomeString = "blah"?这就是我没有选择这条路的原因。由于我是从标准的ASP.NET Web控件派生而来,所以无法更改基类。 - Nelson Rothermel
不,我的意思是我写的那样。在这种情况下,你正在调用Foo.SomeString,因为SomeString在Bar中不是多态的,它隐藏了Foo的实现。 - Ed S.
@Ed:我指的是mjfgates写的内容(尽管间接地与你所写的有关)。无论如何,我明白这是如何工作的,不想有两个实现,根据你所使用的类型而表现出不同的行为。 - Nelson Rothermel
抱歉,我错过了在手机上的回应。我完全同意这不是最佳选择。这个问题源自最初的请求。如果你做了一个不好的设计选择,你可能会因此直接做出更多不好的设计选择。我根本不想隐藏setter并避免破坏接口。 - Ed S.

1

我认为在这种情况下,更好的解决方案是使用新关键字

public class Aclass {        
    public int ReadOnly { get; set; }
}

public class Bclass : Aclass {        
    public new int ReadOnly { get; private set; }
}

使用新关键字,您可以隐藏基础的get和set访问器。 - Dimitrios

0

在C#中,您不允许隐藏基类的公共成员,因为某人可以将派生类的实例塞入对基类的引用中,并以此访问属性。

如果您想创建一个提供另一个类大部分但不是全部接口的类,则必须使用聚合。不要从“基类”继承,而是通过值在“派生”类中包含一个“基类”,并为您希望公开的“基类”功能提供自己的成员函数。然后,这些函数只需将调用转发到相应的“基类”函数即可。


0
只需使用新关键字定义属性,并在派生类中不提供Setter方法。这将隐藏基类属性,但是正如mjfgates所提到的,仍然可以通过将其分配给基类实例来访问基类属性。这就是多态性。

0

有些情况下,你所描述的模式是合理的。例如,可能有一个抽象类MaybeMutableFoo,从中派生出子类型MutableFooImmutableFoo。这三个类都包括一个IsMutable属性和方法AsMutable()AsNewMutable()AsImmutable()

在这种情况下,如果MaybeMutableFoo的契约明确指定setter只有在IsMutable返回true时才能工作,那么公开读写属性将是完全正确的。一个具有MaybeMutableFoo类型字段的对象,如果它持有一个ImmutableFoo实例,除非必须写入该实例,否则可以完全满足于该实例(因为它知道它是不可变的)。当需要写入时,它会用通过AsMutable()返回的对象替换该字段,然后将其用作可变的foo(它知道它是可变的,因为它刚刚被替换了)。让MaybeMutableFoo包含一个setter将避免在将其引用设置为可变实例后对字段进行任何未来的类型转换。

允许这种模式的最佳方法是避免实现虚拟或抽象属性,而是实现非虚拟属性,其getter和setter链接到虚拟或抽象方法。如果有一个基类

public class MaybeMutableFoo
{
    public string Foo {get {return Foo_get();} set {Foo_set(value);}
    protected abstract string Foo_get();
    protected abstract void Foo_set(string value};
}

那么一个派生类ImmutableFoo可以声明:

    new public string Foo {get {return Foo_get();}

为了使其Foo属性成为只读,而不会干扰对抽象Foo_get()Foo_set()方法进行重写的能力。请注意,Foo的只读替换不会改变属性获取的行为;只读版本与基类属性相同地链接到基类方法。以这种方式做事情将确保更改属性getter的补丁点只有一个,更改setter的补丁点也只有一个,即使属性本身被重新定义,这些补丁点也不会改变。


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