关于c#:为什么不可能覆盖仅getter的属性并添加setter?

关于c#:为什么不可能覆盖仅getter的属性并添加setter?

Why is it impossible to override a getter-only property and add a setter?

为什么不允许以下C#代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
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': cannot override because 'BaseClass.Bar' does not have an overridable set accessor


我认为主要原因是语法太明确,以至于无法以其他任何方式工作。这段代码:

1
public override int MyProperty { get { ... } set { ... } }

很明显,getset都是覆盖。基类中没有set,因此编译器会抱怨。就像您无法覆盖基类中未定义的方法一样,您也无法覆盖setter。

您可能会说编译器应该猜测您的意图,并且仅将重写应用于可以覆盖的方法(在这种情况下,即getter),但这违反了C#设计原则之一-编译器不得猜测您的意图,因为在您不知情的情况下可能会猜错。

我认为以下语法可能会做得很好,但是正如Eric Lippert一直说的那样,即使要实现这样的次要功能,仍然需要付出大量的努力...

1
2
3
4
5
public int MyProperty
{
    override get { ... }
    set { ... }
}

或者,对于自动实现的属性,

1
public int MyProperty { override get; set; }

今天,我偶然发现了同样的问题,我认为有一个很正当的理由想要这个。

首先,我想争论一下,拥有仅获取属性并不一定会转换为只读。我将其解释为"可以从此接口/抽象获取此值",这并不意味着该接口/抽象类的某些实现不需要用户/程序显式设置此值。抽象类用于实现部分所需功能的目的。我绝对没有理由说,继承的类不能在不违反任何合同的情况下添加setter。

以下是我今天需要的简化示例。我最终不得不在我的界面中添加一个setter来解决这个问题。添加设置器而不添加例如SetProp方法的原因是,接口的一个特定实现使用DataContract / DataMember进行Prop的序列化,如果我仅出于此目的添加另一个属性,则将不必要地变得复杂序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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。


这是可能的

tl; dr-如果需要,可以用setter覆盖仅获取方法。基本上就是:

  • 使用相同的名称创建同时具有getsetnew属性。

  • 如果您什么都不做,那么当通过其基类型调用派生类时,仍将调用旧的get方法。要解决此问题,请在旧的get方法上添加使用overrideabstract中间层,以强制其返回新的get方法的结果。

  • 这使我们能够使用get / set覆盖属性,即使它们在基本定义中缺少一个。

    作为奖励,您还可以根据需要更改退货类型。

    • 如果基本定义是仅get,则可以使用派生性更大的返回类型。

    • 如果基本定义是仅set,则可以使用派生较少的返回类型。

    • 如果基本定义已经是get / set,则:

      • 如果将其设为仅set,则可以使用派生更多的返回类型;

      • 如果将其设为仅get,则可以使用派生较少的返回类型。

    在所有情况下,您都可以根据需要保留相同的返回类型。为了简单起见,以下示例使用相同的返回类型。

    情况:仅存在get的属性

    您有一些无法修改的类结构。也许它只是一个类,或者它是一个预先存在的继承树。无论如何,您都想向属性添加set方法,但不能这样做。

    1
    2
    3
    4
    5
    6
    7
    8
    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; } }
    }

    问题:无法使用get / setoverride get

    您想使用get / set属性override,但不会编译。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class C : B
    {
        private int _x;
        public override int X
        {
            get { return _x; }
            set { _x = value; }   //  Won't compile
        }
    }

    解决方案:使用abstract中间层

    虽然不能直接使用get / set属性override,但是可以:

  • 创建具有相同名称的new get / set属性。

  • override旧的get方法和新的get方法的访问器,以确保一致性。

  • 因此,首先编写abstract中间层:

    1
    2
    3
    4
    5
    6
    7
    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; }
    }

    然后,编写不会更早编译的类。这次将编译,因为您实际上并没有真正使用get -only属性。而是使用new关键字替换它。

    1
    2
    3
    4
    5
    6
    7
    8
    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的预期工作。

    1
    2
    3
    4
    5
    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作为其基类之一查看时,一切仍然按预期工作。 AB。但是,它起作用的原因可能不太明显。

    1
    2
    3
    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定义所覆盖,因此它仍然是完全一致的。

    1
    2
    3
    4
    5
    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,仅setget -and- set属性,而不管其在基类中是什么。

  • 更改派生类中方法的返回类型。

  • 主要的缺点是要做更多的编码,并且在继承树中有额外的abstract class。对于带有参数的构造函数,这可能会有些烦人,因为必须将这些参数复制/粘贴到中间层中。


    我同意不能覆盖派生类型中的getter是一种反模式。只读指定缺少实现,而不是纯功能的契约(由最高投票答案表示)。

    我怀疑微软有这个局限性,可能是因为引起了同样的误解,或者可能是因为简化了语法。但是,现在可以将范围应用于单独获取或设置,也许我们可以希望重写也可以。

    最高投票答案表明了一个误解,即只读属性应该比读/写属性更"纯",这是荒谬的。只需查看框架中许多常见的只读属性即可;该值不是常数/纯粹是函数;例如,DateTime.Now是只读的,但除纯函数值之外的任何值。尝试"缓存"只读属性的值(假设下次该属性将返回相同的值)是有风险的。

    无论如何,我都使用以下策略之一来克服此限制;两者都不尽如人意,但会让您在这种语言缺陷之外li行:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
       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;
       }

    问题在于,无论出于何种原因,Microsoft决定应具有三种不同类型的属性:只读,只写和读写,在给定的上下文中只有给定的签名可以存在其中一种。属性只能由相同声明的属性覆盖。要执行您想要的操作,必须创建两个具有相同名称和签名的属性-其中一个是只读的,而其中一个是可读写的。

    就个人而言,我希望可以取消"属性"的整个概念,除了可以将属性式语法用作调用" get"和" set"方法的语法糖外。这不仅可以方便使用"添加集"选项,还可以允许"获取"返回与"集"不同的类型。尽管这种能力不会经常使用,但是有时让" get"方法返回包装对象,而" set"可以接受包装数据或实际数据有时会很有用。


    我可以理解您的所有观点,但实际上,在这种情况下,C#3.0的自动属性变得毫无用处。

    你不能做这样的事情:

    1
    2
    3
    4
    5
    6
    7
    8
    public class ConcreteClass : BaseClass
    {
        public override int Bar
        {
            get;
            private set;
        }
    }

    IMO,C#不应限制这种情况。开发人员有责任相应地使用它。


    您可能可以通过创建新属性来解决该问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public new int Bar
    {            
        get { return 0; }
        set {}        
    }

    int IBase.Bar {
      get { return Bar; }
    }

    因为Baseclass的作者已明确声明Bar必须是只读属性。推导打破合同并使其可读写是没有意义的。

    我在这一点上与Microsoft合作。
    假设我是一位新程序员,被告知要针对Baseclass派生进行编码。我写的东西假定Bar不能被写入(因为Baseclass明确声明它是一个get only属性)。
    现在,根据您的推导,我的代码可能会损坏。例如

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class BarProvider
    { BaseClass _source;
      Bar _currentBar;

      public void setSource(BaseClass b)
      {
        _source = b;
        _currentBar = b.Bar;
      }

      public Bar getBar()
      { return _currentBar;  }
    }

    由于不能按照BaseClass接口设置Bar,BarProvider认为缓存是安全的事情-因为Bar不能修改。但是,如果在派生中可以设置set,则如果有人在外部修改_source对象的Bar属性,则此类可以提供陈旧的值。关键是"保持开放,避免做偷偷摸摸的事情和让人惊讶"

    更新:Ilya Ryzhenkov问:"为什么接口不按照相同的规则运行?"
    嗯..我想到这变得更加泥泞。
    接口是一个合同,上面写着"期望实现具有名为Bar的读取属性"。就我个人而言,如果看到接口,则不太可能将其假设为只读。当我在接口上看到get-only属性时,我将其读取为"任何实现都会公开此属性Bar"……在它单击的基类上,因为" Bar是只读属性"。当然,从技术上讲,您并没有违反合同。因此,您在某种意义上是正确的。.最后,我要说:"使误解变得越来越难"。


    为了解决此问题,请使用以下解决方法:

    1
    2
    3
    4
    5
    6
    7
    8
    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);
    }

    这样,我们可以在只读属性上设置对象值。可能无法在所有情况下都起作用!


    解决方案仅适用于一小部分用例,但是:在C#6.0中,将自动为覆盖的仅吸气剂属性添加"只读"设置器。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public abstract class BaseClass
    {
        public abstract int Bar { get; }
    }

    public class ConcreteClass : BaseClass
    {
        public override int Bar { get; }

        public ConcreteClass(int bar)
        {
            Bar = bar;
        }
    }

    您应该更改问题标题,以详细说明您的问题仅与覆盖抽象属性有关,或者与一般覆盖类的get-only属性有关。

    如果是前者(覆盖抽象属性)

    该代码是无用的。仅基类不能告诉您被迫重写Get-Only属性(也许是接口)。基类提供了通用功能,这些功能可能需要实现类的特定输入。因此,通用功能可能会调用抽象属性或方法。在给定的情况下,常见的功能性方法应要求您重写抽象方法,例如:

    1
    public int GetBar(){}

    但是,如果您对此无能为力,并且基类的功能从其自己的公共属性(怪异)中读取,则只需执行以下操作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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;
        }
    }

    我想指出一下(奇怪的)评论:我要说的一种最佳实践是,一个类不使用其自己的公共属性,而在它们存在时使用其私有/受保护的字段。所以这是一个更好的模式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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; }
    }

    如果是后者(覆盖类的get-only属性)

    每个非抽象属性都有一个setter。否则,它是无用的,并且您不必在意使用它。 Microsoft不必允许您做您想做的事。原因是:安装员以某种形式存在,并且您可以轻松完成想要的Veerryy。

    基类,或可以使用{get;}读取属性的任何类,对该属性都有某种公开的设置器。元数据将如下所示:

    1
    2
    3
    4
    public abstract class BaseClass
    {
        public int Bar { get; }
    }

    但是实现将具有复杂性的两个方面:

    最少复杂度:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public abstract class BaseClass
    {
        private int _bar;
        public int Bar {
            get{
                return _bar;
            }}
        public void SetBar(int value) { _bar = value; }
    }

    最复杂:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    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,因为您不希望基类处理它,无权查看它的处理方式,或者被责成真正地快速处理代码违背你的意愿

    无论是前者还是后者(结论)

    长篇短篇:Microsoft不需要更改任何内容。您可以选择如何设置实现类,以及在没有构造函数的情况下使用全部或全部不使用基类。


    基类中的只读属性指示该属性表示始终可以从类内部确定的值(例如,与对象的(db-)上下文匹配的枚举值)。因此,确定值的责任在于类中。

    添加二传手会在这里引起尴尬的问题:
    如果将值设置为已存在的单个可能值以外的值,则将发生验证错误。

    但是,规则通常会有例外。例如,在一个派生类中,上下文很有可能将可能的枚举值缩小到十分之三(十分之三),但是此对象的用户仍然需要确定哪个是正确的。派生类需要将确定值的责任委托给此对象的用户。
    重要的是要认识到该对象的用户应充分了解此异常,并承担设置正确值的责任。

    在这种情况下,我的解决方案是将属性保留为只读,并向派生类添加新的读写属性以支持异常。
    覆盖原始属性将仅返回新属性的值。
    新属性可以具有适当的名称,以正确指示此异常的上下文。

    这也支持了有效的说法:Gishu提出的"使误解越发严重"。


    因为在IL级别,读/写属性转换为两个方法(getter和setter)。

    覆盖时,您必须继续支持基础接口。如果您可以添加一个setter,那么您将有效地添加一个新方法,就类的接口而言,该方法对于外界仍然是不可见的。

    的确,添加新方法本身并不会破坏兼容性,但是由于它将保持隐藏状态,因此决定禁止这样做是完全合理的。


    因为具有只读属性(没有设置器)的类可能有充分的理由。例如,可能没有任何基础数据存储。允许您创建二传手会破坏班级制定的合同。糟糕的OOP。


    因为那样会破坏封装和实现隐藏的概念。请考虑以下情况:创建类并交付它,然后类的使用者使自己能够设置最初只为其提供吸气剂的属性。它会有效地破坏您可以在实现中依赖的类的所有不变量。


    这并非不可能。您只需在属性中使用" new"关键字。例如,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    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"会破坏基类实现和子类实现之间的契约。当一个类可以具有多个合同(通过接口或基类)时,这是必需的。

    希望这可以帮助


    推荐阅读