【问题标题】:How to hide an inherited property in a class without modifying the inherited class (base class)?如何在不修改继承类(基类)的情况下隐藏类中的继承属性?
【发布时间】:2010-12-24 22:15:53
【问题描述】:

如果我有以下代码示例:

public class ClassBase
{
    public int ID { get; set; }

    public string Name { get; set; }
}

public class ClassA : ClassBase
{
    public int JustNumber { get; set; }

    public ClassA()
    {
        this.ID = 0;
        this.Name = string.Empty;
        this.JustNumber = string.Empty;
    }
}

如何隐藏属性Name(不显示为ClassA成员的成员)而不修改ClassBase

【问题讨论】:

  • 这违反了 OOP 的基本原则之一(多态性以及相关的 SOLID 中的 L)。
  • 特别违反了liskov替换原则(en.wikipedia.org/wiki/Liskov_substitution_principle)
  • 许多答案忽略了一个简单的事实,即基类可能由第三方提供。由于此代码不属于自己,因此无法更改。希望通过隐藏成员来创建基类的更狭窄版本是绝对有效的。继承比使用基础控件作为新控件的组件更好,因为这需要大量额外的工作来公开基础上已经找到的方法。
  • @Mario,如果是这种情况,我强烈建议您创建自己的类实现并使用对象到对象映射器来设置您想要的属性。这样您就可以创建更窄的第三方类。

标签: c# inheritance properties base-class


【解决方案1】:

虽然从技术上讲,该属性不会被隐藏,但强烈反对使用它的一种方法是在其上放置如下属性:

[Browsable(false)]
[Bindable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[EditorBrowsable(EditorBrowsableState.Never)]

这是 System.Windows.Forms 对具有不适合的属性的控件所做的。例如,Text 属性位于 Control 上,但它并不适用于从 Control 继承的每个类。所以在MonthCalendar 中,例如,Text 属性看起来像这样(根据在线参考源):

[Browsable(false),
    EditorBrowsable(EditorBrowsableState.Never),
    Bindable(false), 
    DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public override string Text {
    get { return base.Text; }
    set { base.Text = value; }
}
  • 可浏览 - 成员是否显示在“属性”窗口中
  • EditorBrowsable - 成员是否显示在 Intellisense 下拉列表中

EditorBrowsable(false) 不会阻止您键入该属性,并且如果您使用该属性,您的项目仍将编译。但由于该属性没有出现在 Intellisense 中,因此您可以使用它不会那么明显。

【讨论】:

    【解决方案2】:

    您可以使用Browsable(false)

    [Browsable( false )]
    public override string Name
    {
        get { return base.Name; }
        set { base.Name= value; }
    }
    

    【讨论】:

      【解决方案3】:

      我认为这里的很多人根本不了解继承。需要从基类继承并隐藏其曾经公开的 var 和函数。例如,假设您有一个基本引擎,并且您想制造一个增压的新引擎。好吧,您将使用 99% 的引擎,但您将调整它的一些功能以使其运行得更好,但仍然有一些功能应该只显示给所做的修改,而不是最终用户。因为我们都知道 MS 推出的每一个类都不需要任何修改。

      除了使用新功能来简单地覆盖功能之外,它也是微软在其无限智慧中所做的事情之一……哦,我的意思是错误被认为是不再值得的工具。

      现在实现这一点的最佳方法是多级继承。

      public class classA 
      {
      }
      
      public class B : A 
      {} 
      
      public class C : B 
      {} 
      

      B 类完成你所有的工作,C 类公开你需要公开的内容。

      【讨论】:

        【解决方案4】:

        把它藏起来

         public class ClassBase
        {
            public int ID { get; set; }
            public string Name { get; set; }
        }
        public class ClassA : ClassBase
        {
            public int JustNumber { get; set; }
            private new string Name { get { return base.Name; } set { base.Name = value; } }
            public ClassA()
            {
                this.ID = 0;
                this.Name = string.Empty;
                this.JustNumber = 0;
            }
        }
        

        注意:名称仍将是 ClassBase 的公共成员,鉴于不更改基类的约束,无法阻止它。

        【讨论】:

        • 使用此方法,在ClassA 之外执行类似ClassA.Name 的操作将暴露ClassBase.Name 属性。不可能new 成员获得比最初声明的更受限制的访问权限。
        • 这就是笔记的重点。根据问题,名称不会是 ClassA 的公共成员。对于序列化和反射,这可能很重要。
        • 是的,这是真的。然而值得注意的是,当一个人想要从继承的类中隐藏一个成员时,可能是因为他将成员的值设置为一组预定义值中的一个,因此更改它可能会导致意外行为。
        • 同意,并且考虑到不更改基类的约束,没有办法阻止它。
        • 而不是所有关于为什么不应该这样做的修辞......这是我正在寻找的实际答案。出于序列化目的,我真的需要这个。这提供了我完美需要的解决方案!非常感谢!
        【解决方案5】:

        我完全同意不应从基类中删除属性,但有时派生类可能有不同的更合适的方式来输入值。例如,就我而言,我是从 ItemsControl 继承的。众所周知,ItemsControl 具有 ItemsSource 属性,但我希望我的控件合并来自 2 个源(例如,Person 和 Location)的数据。如果我要让用户使用 ItemsSource 输入数据,我需要分离然后重新组合这些值,因此我创建了 2 个属性来输入数据。但回到最初的问题,这留下了 ItemsSource,我不希望用户使用它,因为我正在用我自己的属性“替换”它。我喜欢 Browsable 和 EditorBrowsable 的想法,但它仍然不妨碍用户使用它。这里的基本点是继承应该保留大部分属性,但是当有一个大型复杂类时(尤其是无法修改原始代码的类),重写所有内容将非常低效。

        【讨论】:

          【解决方案6】:

          我知道这个问题很老了,但你可以做的是像这样覆盖 PostFilterProperties:

           protected override void PostFilterProperties(System.Collections.IDictionary properties)
              {
                  properties.Remove("AccessibleDescription");
                  properties.Remove("AccessibleName");
                  properties.Remove("AccessibleRole");
                  properties.Remove("BackgroundImage");
                  properties.Remove("BackgroundImageLayout");
                  properties.Remove("BorderStyle");
                  properties.Remove("Cursor");
                  properties.Remove("RightToLeft");
                  properties.Remove("UseWaitCursor");
                  properties.Remove("AllowDrop");
                  properties.Remove("AutoValidate");
                  properties.Remove("ContextMenuStrip");
                  properties.Remove("Enabled");
                  properties.Remove("ImeMode");
                  //properties.Remove("TabIndex"); // Don't remove this one or the designer will break
                  properties.Remove("TabStop");
                  //properties.Remove("Visible");
                  properties.Remove("ApplicationSettings");
                  properties.Remove("DataBindings");
                  properties.Remove("Tag");
                  properties.Remove("GenerateMember");
                  properties.Remove("Locked");
                  //properties.Remove("Modifiers");
                  properties.Remove("CausesValidation");
                  properties.Remove("Anchor");
                  properties.Remove("AutoSize");
                  properties.Remove("AutoSizeMode");
                  //properties.Remove("Location");
                  properties.Remove("Dock");
                  properties.Remove("Margin");
                  properties.Remove("MaximumSize");
                  properties.Remove("MinimumSize");
                  properties.Remove("Padding");
                  //properties.Remove("Size");
                  properties.Remove("DockPadding");
                  properties.Remove("AutoScrollMargin");
                  properties.Remove("AutoScrollMinSize");
                  properties.Remove("AutoScroll");
                  properties.Remove("ForeColor");
                  //properties.Remove("BackColor");
                  properties.Remove("Text");
                  //properties.Remove("Font");
              }
          

          【讨论】:

          • 我认为这段代码特定于 winforms 或 ASP 可能是 Xamarin 表单。在你的回答中提及它
          【解决方案7】:

          如果你必须这样做,我认为这是糟糕的设计,特别是如果你能够从头开始设计代码。

          为什么?

          好的设计是让基类共享某个概念所具有的共同属性(虚拟或真实)。示例:C# 中的 System.IO.Stream。

          再往下走,糟糕的设计会增加维护成本,并使实施变得越来越困难。尽量避免这种情况!

          我使用的基本规则:

          • 尽量减少基类中的属性和方法的数量。如果您不希望在继承基类的类中使用某些属性或方法;然后不要把它放在基类中。如果您处于项目的开发阶段;现在总是回到绘图板然后检查设计,因为事情发生了变化!需要时重新设计。当您的项目上线时,在设计后期进行更改的成本将会上升!

            • 如果您使用的是由 3:rd 方实现的基类,请考虑“上一层”而不是使用“NotImplementedException”等“覆盖”。如果没有其他级别,请考虑从头开始设计代码。

            • 始终考虑密封您不希望任何人能够继承它的类。它迫使编码人员在“继承-层次结构”中“上一层”,因此可以避免像“NotImplementedException”这样的“松散的结局”。

          【讨论】:

            【解决方案8】:

            为什么在不需要的时候强制继承? 我认为正确的做法是使用 has-a 而不是 is-a

            public class ClassBase
            {
                public int ID { get; set; }
            
                public string Name { get; set; }
            }
            
            public class ClassA
            {
                private ClassBase _base;
            
                public int ID { get { return this._base.ID; } }
            
                public string JustNumber { get; set; }
            
                public ClassA()
                {
                    this._base = new ClassBase();
                    this._base.ID = 0;
                    this._base.Name = string.Empty;
                    this.JustNumber = string.Empty;
                }
            }
            

            【讨论】:

              【解决方案9】:

              我在这里闻到一股代码味。我认为,如果您要实现该基类的所有功能,则应该只继承该基类。您所做的并不能真正正确地代表面向对象的原则。因此,如果你想从你的基础继承,你应该实现 Name,否则你的继承方式是错误的。您的类 A 应该是您的基类,并且您当前的基类应该从 A 继承,如果这是您想要的,而不是相反。

              但是,不要偏离直接问题太远。如果您确实想无视“规则”并希望继续您选择的道路 - 您可以这样做:

              约定是实现该属性,但在调用该属性时抛出 NotImplementedException - 不过,我也不喜欢这样。但这是我个人的看法,这并不能改变这个惯例仍然有效的事实。

              如果您试图废弃该属性(并且它在基类中声明为虚拟),那么您可以在其上使用 Obsolete 属性:

              [Obsolete("This property has been deprecated and should no longer be used.", true)]
              public override string Name 
              { 
                  get 
                  { 
                      return base.Name; 
                  }
                  set
                  {
                      base.Name = value;
                  }
              }
              

              (编辑: 正如 Brian 在 cmets 中指出的那样,如果有人引用 Name 属性,该属性的第二个参数将导致编译器错误,因此他们甚至无法使用它虽然你已经在派生类中实现了它。)

              或者正如我提到的使用 NotImplementedException:

              public override string Name
              {
                  get
                  {
                      throw new NotImplementedException();
                  }
                  set
                  {
                      throw new NotImplementedException();
                  }
              }
              

              但是,如果属性没有声明为虚拟,那么您可以使用 new 关键字来替换它:

              public new string Name
              {
                  get
                  {
                      throw new NotImplementedException();
                  }
                  set
                  {
                      throw new NotImplementedException();
                  }
              }
              

              您仍然可以使用 Obsolete 属性,就像方法被覆盖一样,或者您可以抛出 NotImplementedException,无论您选择哪种方式。我可能会使用:

              [Obsolete("Don't use this", true)]
              public override string Name { get; set; }
              

              或:

              [Obsolete("Don't use this", true)]
              public new string Name { get; set; }
              

              取决于它是否在基类中被声明为虚拟。

              【讨论】:

              • 过时属性还有第二个参数,指定使用该属性应被视为错误。此时您应该会收到一个编译时错误,这很有帮助。
              • 谢谢布赖恩,我应该提一下这个,很好。
              • 我相信您也可以使用“new”关键字为该属性指定新功能,即使它没有标记为虚拟。这将允许他将属性标记为过时,即使它来自属性不是虚拟的类。
              • 是的,你一定是在我发表评论时添加的。哦,好吧,只是想帮忙。
              • 还应该提到,虽然使用new会隐藏派生类的属性的原始实现,但是当对象被强制转换为基类时,仍然会使用原始实现,这可以导致讨厌的错误
              【解决方案10】:

              你不能,这就是继承的重点:子类必须提供基类的所有方法和属性。

              您可以更改实现以在调用属性时抛出异常(如果它是虚拟的)...

              【讨论】: