为什么不可能覆盖仅 getter 的属性并添加 setter?[关闭]
-
01-07-2019 - |
题
为什么不允许使用以下 C# 代码:
public abstract class BaseClass
{
public abstract int Bar { get;}
}
public class ConcreteClass : BaseClass
{
public override int Bar
{
get { return 0; }
set {}
}
}
CS0546 'ConcreteClass.Bar.set':无法重写,因为“BaseClass.Bar”没有可重写的设置访问器
解决方案
因为 Baseclass 的作者已明确声明 Bar 必须是只读属性。推导打破这个契约并使其可读写是没有意义的。
我在这方面支持微软。
假设我是一名新程序员,被告知要针对基类派生进行编码。我编写了一些假设 Bar 无法写入的内容(因为 Baseclass 明确声明它是仅获取属性)。现在根据你的推导,我的代码可能会崩溃。例如
public class BarProvider
{ BaseClass _source;
Bar _currentBar;
public void setSource(BaseClass b)
{
_source = b;
_currentBar = b.Bar;
}
public Bar getBar()
{ return _currentBar; }
}
由于 Bar 无法按照 BaseClass 接口进行设置,BarProvider 假定缓存是安全的事情 - 因为 Bar 无法修改。但是,如果在派生中可以设置,则如果有人在外部修改 _source 对象的 Bar 属性,则此类可能会提供过时的值。重点是'保持开放,避免做偷偷摸摸的事情和让别人感到惊讶'
更新: Ilya Ryzhenkov 问道:“那么为什么界面不遵循相同的规则呢?”唔..当我思考这个问题时,事情变得更加混乱。
接口是一个约定,其中规定“期望实现具有名为 Bar 的读取属性”。 亲自 如果我看到一个接口,我就不太可能做出只读的假设。当我在接口上看到仅获取属性时,我将其读为“任何实现都会公开此属性 Bar”...在基类上,它单击为“Bar 是只读属性”。当然,从技术上讲,你并没有违反合同。你做得更多。所以从某种意义上来说你是对的..我最后想说的是“尽可能避免误解的出现”。
其他提示
我认为主要原因很简单,语法太明确,无法以任何其他方式工作。这段代码:
public override int MyProperty { get { ... } set { ... } }
非常明确的是 get
和 set
是覆盖。没有 set
在基类中,所以编译器会抱怨。就像您无法重写基类中未定义的方法一样,您也无法重写 setter。
您可能会说编译器应该猜测您的意图,并且仅将覆盖应用于可以覆盖的方法(即本例中为 getter),但这违背了 C# 设计原则之一 - 编译器不能猜测您的意图,因为它可能在您不知情的情况下猜测错误。
我认为下面的语法可能会做得很好,但正如 Eric Lippert 一直说的那样,实现这样的小功能仍然需要大量的努力......
public int MyProperty
{
override get { ... }
set { ... }
}
或者,对于自动实现的属性,
public int MyProperty { override get; set; }
我今天偶然发现了同样的问题,我认为有一个非常充分的理由想要这个。
首先我想说的是,拥有只读属性并不一定意味着只读。我将其解释为“从这个接口/抽象中您可以获得这个值”,这并不意味着该接口/抽象类的某些实现不需要用户/程序显式地设置这个值。抽象类的目的是实现部分所需的功能。我认为继承类绝对没有理由不能在不违反任何契约的情况下添加 setter。
以下是我今天需要的简化示例。我最终不得不在界面中添加一个设置器来解决这个问题。添加 setter 而不是添加 SetProp 方法之类的原因是,该接口的一个特定实现使用 DataContract/DataMember 来序列化 Prop,如果我必须为此目的添加另一个属性,那么这将变得不必要的复杂化的序列化。
interface ITest
{
// Other stuff
string Prop { get; }
}
// Implements other stuff
abstract class ATest : ITest
{
abstract public string Prop { get; }
}
// This implementation of ITest needs the user to set the value of Prop
class BTest : ATest
{
string foo = "BTest";
public override string Prop
{
get { return foo; }
set { foo = value; } // Not allowed. 'BTest.Prop.set': cannot override because 'ATest.Prop' does not have an overridable set accessor
}
}
// This implementation of ITest generates the value for Prop itself
class CTest : ATest
{
string foo = "CTest";
public override string Prop
{
get { return foo; }
// set; // Not needed
}
}
我知道这只是一篇“我的 2 美分”帖子,但我对原始海报的感觉并试图合理化这是一件好事对我来说似乎很奇怪,特别是考虑到直接从界面。
另外,关于使用 new 而不是 override 的提及在这里并不适用,它根本不起作用,即使它起作用,也不会给你想要的结果,即接口所描述的虚拟 getter。
这是可能的
太长了;博士- 如果需要,您可以使用 setter 覆盖仅获取方法。基本上就是:
创建一个
new
属性同时具有get
和一个set
使用相同的名称。如果你不做任何其他事情,那么旧的
get
当通过其基类型调用派生类时,方法仍然会被调用。要解决此问题,请添加abstract
中间层使用override
在旧的get
方法强制它返回新的get
方法的结果。
这使我们能够覆盖属性 get
/set
即使他们的基本定义中缺少一个。
作为奖励,您还可以根据需要更改返回类型。
如果基本定义是
get
-only,那么您可以使用更派生的返回类型。如果基本定义是
set
-only,那么您可以使用派生较少的返回类型。如果基本定义已经是
get
/set
, , 然后:您可以使用更派生的返回类型 如果 你做到了
set
-仅有的;您可以使用派生较少的返回类型 如果 你做到了
get
-仅有的。
在所有情况下,如果需要,您可以保持相同的返回类型。为简单起见,以下示例使用相同的返回类型。
情况:已有的 get
- 仅限财产
您有一些无法修改的类结构。也许它只是一个类,或者它是一个预先存在的继承树。无论哪种情况,您都想添加一个 set
方法到属性,但不能。
public abstract class A // Pre-existing class; can't modify
{
public abstract int X { get; } // You want a setter, but can't add it.
}
public class B : A // Pre-existing class; can't modify
{
public override int X { get { return 0; } }
}
问题:不能 override
这 get
-只有 get
/set
你想要 override
与一个 get
/set
属性,但无法编译。
public class C : B
{
private int _x;
public override int X
{
get { return _x; }
set { _x = value; } // Won't compile
}
}
解决方案:使用 abstract
中间层
虽然你不能直接 override
与一个 get
/set
财产,你 能:
创建一个
new
get
/set
同名的财产。override
老人get
带有新访问器的方法get
方法来保证一致性。
所以,首先你写下 abstract
中间层:
public abstract class C : B
{
// Seal off the old getter. From now on, its only job
// is to alias the new getter in the base classes.
public sealed override int X { get { return this.XGetter; } }
protected abstract int XGetter { get; }
}
然后,您编写之前无法编译的类。这次它会编译,因为你实际上并没有 override
正在 get
- 仅限财产;相反,您将使用替换它 new
关键词。
public class D : C
{
private int _x;
public new virtual int X { get { return this._x; } set { this._x = value; } }
// Ensure base classes (A,B,C) use the new get method.
protected sealed override int XGetter { get { return this.X; } }
}
结果:一切正常!
显然,这按预期工作 D
.
var test = new D();
Print(test.X); // Prints "0", the default value of an int.
test.X = 7;
Print(test.X); // Prints "7", as intended.
查看时一切仍然按预期进行 D
作为其基类之一,例如 A
或者 B
. 。但是,它起作用的原因可能不太明显。
var test = new D() as B;
//test.X = 7; // This won't compile, because test looks like a B,
// and B still doesn't provide a visible setter.
然而,基类定义 get
最终仍然被派生类的定义覆盖 get
, ,所以它仍然是完全一致的。
var test = new D();
Print(test.X); // Prints "0", the default value of an int.
var baseTest = test as A;
Print(test.X); // Prints "7", as intended.
讨论
此方法允许您添加 set
方法 get
-仅限属性。您还可以用它来执行以下操作:
将任何属性更改为
get
-仅有的,set
-仅,或get
-和-set
属性,无论它在基类中是什么。更改派生类中方法的返回类型。
主要缺点是需要编写更多代码并需要额外的工作量 abstract class
在继承树中。对于采用参数的构造函数来说,这可能有点烦人,因为必须将这些参数复制/粘贴到中间层中。
我同意不能重写派生类型中的 getter 是一种反模式。只读指定缺乏实现,而不是纯函数的合同(由最高投票答案暗示)。
我怀疑微软有这个限制,要么是因为宣扬了同样的误解,要么是因为简化了语法;不过,现在范围可以应用于单独的获取或设置,也许我们希望覆盖也可以。
最高投票答案所表明的误解是,只读属性应该在某种程度上比读/写属性更“纯粹”,这是荒谬的。简单看一下框架中很多常见的只读属性;该值不是常数/纯函数;例如,DateTime.Now 是只读的,但绝不是纯函数值。尝试“缓存”只读属性的值并假设下次会返回相同的值是有风险的。
无论如何,我使用了以下策略之一来克服这一限制;两者都不太完美,但可以让你克服这种语言缺陷:
class BaseType
{
public virtual T LastRequest { get {...} }
}
class DerivedTypeStrategy1
{
/// get or set the value returned by the LastRequest property.
public bool T LastRequestValue { get; set; }
public override T LastRequest { get { return LastRequestValue; } }
}
class DerivedTypeStrategy2
{
/// set the value returned by the LastRequest property.
public bool SetLastRequest( T value ) { this._x = value; }
public override T LastRequest { get { return _x; } }
private bool _x;
}
您也许可以通过创建一个新属性来解决这个问题:
public new int Bar
{
get { return 0; }
set {}
}
int IBase.Bar {
get { return Bar; }
}
我可以理解您的所有观点,但实际上,C# 3.0 的自动属性在这种情况下变得毫无用处。
你不能做这样的事情:
public class ConcreteClass : BaseClass
{
public override int Bar
{
get;
private set;
}
}
IMO,C# 不应该限制此类场景。开发人员有责任相应地使用它。
问题是,无论出于何种原因,微软决定应该存在三种不同类型的属性:只读、只写和读写,在给定上下文中只有其中一个可以以给定签名存在;属性只能被相同声明的属性覆盖。要执行您想要的操作,有必要创建两个具有相同名称和签名的属性 - 其中一个是只读的,另一个是读写的。
就我个人而言,我希望“属性”的整个概念可以被废除,除了属性语法可以用作调用“get”和“set”方法的语法糖。这不仅有利于“add set”选项,而且还允许“get”返回与“set”不同的类型。虽然这种能力不会经常使用,但有时让“get”方法返回包装器对象而“set”可以接受包装器或实际数据可能会很有用。
这是使用反射实现此目的的解决方法:
var UpdatedGiftItem = // object value to update;
foreach (var proInfo in UpdatedGiftItem.GetType().GetProperties())
{
var updatedValue = proInfo.GetValue(UpdatedGiftItem, null);
var targetpropInfo = this.GiftItem.GetType().GetProperty(proInfo.Name);
targetpropInfo.SetValue(this.GiftItem, updatedValue,null);
}
这样我们就可以在只读属性上设置对象值。但可能并不适用于所有场景!
您应该将问题标题更改为以下细节:您的问题仅与覆盖抽象属性有关,或者您的问题通常与覆盖类的仅获取属性有关。
如果是前者 (覆盖抽象属性)
那个代码没啥用。单独的基类不应告诉您必须重写 Get-Only 属性(可能是接口)。基类提供可能需要来自实现类的特定输入的通用功能。因此,公共功能可能会调用抽象属性或方法。在给定的情况下,常见的功能方法应该要求您重写抽象方法,例如:
public int GetBar(){}
但是,如果您无法控制这一点,并且基类的功能从其自己的公共属性读取(奇怪),那么只需执行以下操作:
public abstract class BaseClass
{
public abstract int Bar { get; }
}
public class ConcreteClass : BaseClass
{
private int _bar;
public override int Bar
{
get { return _bar; }
}
public void SetBar(int value)
{
_bar = value;
}
}
我想指出(奇怪的)评论:我想说,最佳实践是类不使用自己的公共属性,而是使用其存在的私有/受保护字段。所以这是一个更好的模式:
public abstract class BaseClass {
protected int _bar;
public int Bar { get { return _bar; } }
protected void DoBaseStuff()
{
SetBar();
//Do something with _bar;
}
protected abstract void SetBar();
}
public class ConcreteClass : BaseClass {
protected override void SetBar() { _bar = 5; }
}
如果是后者 (覆盖类的仅获取属性)
每个非抽象属性都有一个 setter。否则它是没有用的,你不应该关心使用它。微软不必允许你做你想做的事。原因是:setter 以某种形式存在,你可以完成你想要的 韦里 容易地。
基类,或者任何可以读取属性的类 {get;}
, , 有 一些 该属性的某种公开设置器。元数据将如下所示:
public abstract class BaseClass
{
public int Bar { get; }
}
但实现的复杂性有两端:
最不复杂:
public abstract class BaseClass
{
private int _bar;
public int Bar {
get{
return _bar;
}}
public void SetBar(int value) { _bar = value; }
}
最复杂:
public abstract class BaseClass
{
private int _foo;
private int _baz;
private int _wtf;
private int _kthx;
private int _lawl;
public int Bar
{
get { return _foo * _baz + _kthx; }
}
public bool TryDoSomethingBaz(MyEnum whatever, int input)
{
switch (whatever)
{
case MyEnum.lol:
_baz = _lawl + input;
return true;
case MyEnum.wtf:
_baz = _wtf * input;
break;
}
return false;
}
public void TryBlowThingsUp(DateTime when)
{
//Some Crazy Madeup Code
_kthx = DaysSinceEaster(when);
}
public int DaysSinceEaster(DateTime when)
{
return 2; //<-- calculations
}
}
public enum MyEnum
{
lol,
wtf,
}
我的观点是,无论哪种方式,你都会暴露出二传手。在你的情况下,你可能想要覆盖 int Bar
因为您不希望基类处理它,无权查看它是如何处理的,或者被指派违背您的意愿快速修改一些代码。
在后者和前者中 (结论)
长话短说:微软没有必要改变任何事情。您可以选择如何设置实现类,并且在没有构造函数的情况下,使用全部基类或不使用基类。
仅适用于一小部分用例的解决方案,但尽管如此:在 C# 6.0 中,会自动为重写的 getter-only 属性添加“readonly”setter。
public abstract class BaseClass
{
public abstract int Bar { get; }
}
public class ConcreteClass : BaseClass
{
public override int Bar { get; }
public ConcreteClass(int bar)
{
Bar = bar;
}
}
因为在 IL 级别,读/写属性会转换为两个(getter 和 setter)方法。
重写时,您必须继续支持底层接口。如果您可以添加一个设置器,那么您将有效地添加一个新方法,就您的类的接口而言,该方法对外界来说是不可见的。
确实,添加新方法本身不会破坏兼容性,但由于它将保持隐藏状态,因此决定禁止这样做是完全有道理的。
因为这会打破封装和实现隐藏的概念。考虑这样的情况:创建一个类,发布它,然后类的使用者使自己能够设置一个您最初仅为其提供 getter 的属性。它会有效地破坏您在实现中可以依赖的类的任何不变量。
因为具有只读属性(无 setter)的类可能有充分的理由。例如,可能没有任何底层数据存储。允许您创建 setter 会破坏类规定的契约。这只是糟糕的 OOP。
基类中的只读属性指示该属性表示始终可以从类内部确定的值(例如与对象的 (db-) 上下文匹配的枚举值)。因此,确定值的责任仍在班级内部。
添加 setter 会在这里导致一个尴尬的问题:如果您将该值设置为除已有的单个可能值以外的任何值,则会发生验证错误。
不过,规则常常有例外。例如,在一个派生类中,上下文很有可能将可能的枚举值缩小到 10 个中的 3 个,但该对象的用户仍然需要决定哪一个是正确的。派生类需要将确定值的责任委托给该对象的用户。重要的是要意识到 是这个对象的用户 应该很清楚这个异常 并承担设置正确值的责任。
在这种情况下,我的解决方案是将属性保留为只读,并向派生类添加新的读写属性以支持异常。覆盖原始属性将仅返回新属性的值。新属性可以有一个正确的名称,正确地指示此异常的上下文。
这也支持了有效的评论:Gishu 的“尽可能避免误解的出现”。
这并非不可能。您只需在您的属性中使用“new”关键字即可。例如,
namespace {
public class Base {
private int _baseProperty = 0;
public virtual int BaseProperty {
get {
return _baseProperty;
}
}
}
public class Test : Base {
private int _testBaseProperty = 5;
public new int BaseProperty {
get {
return _testBaseProperty;
}
set {
_testBaseProperty = value;
}
}
}
}
看来这种方法使讨论双方都满意。使用“new”打破了基类实现和子类实现之间的契约。当一个类可以有多个契约(通过接口或基类)时,这是必要的。
希望这可以帮助