非抽象类中的抽象方法

58

我想了解在C#中为什么要设计限制在非抽象类中使用抽象方法。

我知道如果实例化一个类,它没有定义抽象方法,因此无法调用,但是当定义静态方法时,它们也被从实例中排除。为什么不以同样的方式处理抽象方法,在具体类中允许使用并强制派生类来实现这些方法?

在具体类中,抽象方法可以被允许,并且可以强制派生类来实现这些方法,这基本上就是在抽象类中使用抽象方法的做法。


7
希望我不是唯一一个说“咦?”的人。 - Lews Therin
2
你有什么使用场景需要在非抽象类中定义抽象方法? - Vikdor
3
请给出一个使用该概念会有用的类的示例。 - Damien_The_Unbeliever
1
如果你非常想这么做,或许可以考虑将该方法设置为“virtual”,然后抛出一个“NotImplementedException”(或者什么也不做,或者返回默认值)。接着记录下来,继承该类的子类必须实现该方法。但总的来说,我同意其他人的看法,这似乎是在用错误的工具去完成工作。 - Chris Sinclair
我最近遇到的一个情况是从UserControl继承,向具有多个超类的控件添加一些已知成员。由于这个类本身是无用的,所以我将它设置为抽象类,但是超类的设计者拒绝使用它,因为它不能被创建。我尝试将方法设置为抽象的(这样它们必须被重写),但它抛出了主题中提到的编译错误。我最终采用了虚方法,并使用适当的消息抛出NotImplementedException。 - Deanna
显示剩余3条评论
8个回答

57

首先,我认为你提出的问题在逻辑上是没有意义的。如果你有一个抽象方法,这基本上意味着这个方法是未完成的(就像@ChrisSinclair指出的那样)。但这也意味着整个类是未完成的,所以它也必须是abstract

或者换一种说法:如果你在一个不是abstract的类中有一个abstract方法,那么这意味着你有一个无法被调用的方法。但这意味着这个方法是没有用的,你可以将其删除,程序运行结果不会变化。

现在,我将通过使用示例来更加具体地阐述:

Animal[] zoo = new Animal[] { new Monkey(), new Fish(), new Animal() };

foreach (Animal animal in zoo)
    animal.MakeSound();
在这里,Animal是非抽象基类(这就是为什么我可以将其直接放入数组中),MonkeyFish是从Animal派生出来的,而MakeSound()是抽象方法。这段代码应该做什么?你没有清楚地说明,但我可以想象几个选项:
  1. 你不能在一个类型为Animal的变量上调用MakeSound(),只能使用一个以派生类命名的变量来调用它,因此会导致编译错误。

    这不是一个好的解决方案,因为abstract的整个意义在于能够将派生类的实例视为基类,并且仍然获得特定于派生类的行为。如果你需要这样做,只需在每个派生类中放置一个正常的(无abstractvirtualoverride)方法,并且不对基类进行任何操作。

  2. 你不能在运行时类型实际上为Animal的对象上调用MakeSound(),因此这会导致运行时错误(异常)。

    这也不是一个好的解决方案。C#是一种静态类型的语言,因此它试图在编译时捕获像“你不能调用这个方法”这样的错误(显然有例外情况,如反射和dynamic),所以将其变成运行时错误并不符合语言的其他部分。此外,你可以通过在基类中创建一个抛出异常的virtual方法来轻松地实现此操作。

总之,你想要一些没有多大意义、设计不良的东西(一个基类的行为与其派生类不同),而且可以很容易地解决。这些都是不应该实现的特性的迹象。


对我有意义,第一点回答了为什么在“类级别”上有抽象以及为什么抽象类应该受到限制以避免实例化。谢谢Vick。 - Ashish Jain

15

所以,您想允许

class C { abstract void M(); }

编译代码。假设它成功了。接下来,当有人执行该代码时,你希望发生什么?

new C().M();

? 您想要一个运行时错误吗?总的来说,C# 更喜欢编译时错误而不是运行时错误。如果您不喜欢这种哲学,还有其他语言可供选择...


我提出了这个问题,并且给出了静态方法被编译器处理的例子。我的问题是关于抽象方法的编译器设计,为什么他们选择特别针对这些方法使用抽象类? - Ashish Jain
@AshishJain 因为抽象类不能直接实例化。它是“未完成的”,就像接口不能直接实例化一样,因为空方法/属性的作用未定义。它强制希望使用它们的程序员在能够实例化之前继承和实现所有抽象方法。编辑:静态方法是一个_不同的_概念。它们属于_Type_而不是对象实例。抽象方法在_instance_级别上,并且需要被实现(还要注意静态方法需要被实现——不存在“抽象静态”)。 - Chris Sinclair
如果你想要执行时错误,你已经可以做到了:创建一个抛出异常的虚拟方法。 - svick
静态方法是一个不同的情况,因为它们在任何情况下都不在实例上。我不知道你所说的“为什么他们专门选择抽象类来处理这些方法?”的意思。我已经向你展示了如果允许具体类拥有抽象方法可能会出现的潜在问题。你更希望发生什么? - AakashM
@AakashM 如果抽象方法有返回类型而不是void,那就更有趣了。:) 至少void方法可以什么都不做,但返回类型会怎样呢?默认值?是的,我不知道还会发生什么。抽象方法存在是有原因的,其他任何事情都可以由我们建议的“虚拟”来处理。 - Chris Sinclair
显示剩余3条评论

4

我认为你已经解答了自己的问题,抽象方法起初是未定义的,因此该类无法实例化。你说它应该忽略它,但根据定义,当添加一个抽象方法时,你正在说“从这个创建的每个类必须实现这个{抽象方法}”,因此在定义抽象类的类中,抽象方法在那一点上仍然未定义,因此该类也必须是抽象的。


3

抽象类可能包含抽象成员。如果任何方法具有abstract关键字,则只有方法声明,我们不能在同一类中实现。因此,抽象类是不完整的。这就是为什么不能为抽象类创建对象的原因。

非抽象类不能包含抽象成员。

例子:

namespace InterviewPreparation
{
   public abstract class baseclass
    {
        public abstract void method1(); //abstract method
        public abstract void method2(); //abstract method
        public void method3() { }  //Non- abstract method----->It is necessary to implement here.
    }
    class childclass : baseclass
    {
        public override void method1() { }
        public override void method2() { }
    }
    public class Program    //Non Abstract Class
    {
        public static void Main()
        {
            baseclass b = new childclass(); //create instance
            b.method1();
            b.method2();
            b.method3();
        }
    }

}

3
您可以使用“虚拟”方法来达到所需的效果,但是使用虚拟方法可能会导致更多运行时业务逻辑错误,因为开发人员不必在子类中实现该逻辑。
我认为这里有一个合理的观点。抽象方法是完美的解决方案,因为它将“强制”要求在子类中定义方法体。
我遇到了许多情况,其中父类必须(或更有效地)实现某些逻辑,但“仅”子类可以实现其余的逻辑。
因此,如果有机会,我将很高兴混合使用抽象方法和完整方法。
@AakashM,我赞赏C#更喜欢编译时错误。我也是这样想的。任何人都是如此。这是关于跳出常规思维方式。
支持此操作不会影响此操作。
让我们在这里跳出常规思维方式,而不是对大公司的决策欣喜若狂。
C#编译器可以检测并拒绝直接使用抽象类,因为它使用“abstract”关键字。
C#还知道强制任何子类实现任何抽象方法。如何做到的?由于使用“abstract”关键字。
对于任何研究过编程语言内部结构的人来说,这都很容易理解。
那么,为什么C#不能检测在普通类中的方法旁边使用“abstract”关键字并在编译时处理它呢?
原因是需要对其进行“重新设计”,而这种努力不值得支持小需求。
特别是在一个缺乏跳出大公司思维定式的人的行业。

1

目前仍不清楚您为什么需要这样做,但另一种方法是强制派生类提供委托实例。可以像这样:

class MyConcreteClass
{
  readonly Func<int, DateTime, string> methodImpl;

  // constructor requires a delegate instance
  public MyConcreteClass(Func<int, DateTime, string> methodImpl)
  {
    if (methodImpl == null)
      throw new ArgumentNullException();

    this.methodImpl = methodImpl;
  }

  ...
}

(当然,签名string MethodImpl(int, DateTime)只是一个例子。)

否则,我可以推荐其他答案来解释为什么您的愿望可能不会让世界变得更好。


接受替代方案并不是最好的选择,甚至可能存在严重的缺陷。但是为什么抽象类的当前实现方式(为什么不像虚拟方法/属性/索引器等一样保留抽象方法)是最佳的,并且“抽象类”的概念是不可避免的(因为它比其他方式具有优势),请记住接口已经存在? - Ashish Jain

1
以上答案是正确的:拥有抽象方法使类本质上是抽象的。如果您无法实例化类的一部分,则无法实例化类本身。然而,以上答案并没有真正讨论您在此处的选择。

首先,这主要是公共静态方法的问题。如果这些方法不打算公开,则可以有受保护的非抽象方法,在抽象类声明中允许这些方法。因此,您可以将这些静态方法移动到单独的静态类中,而不会有太多问题。

作为替代方案,您可以将这些方法保留在类中,但是除了具有抽象方法之外,还可以声明一个接口。实际上,您有一个多重继承问题,因为您希望派生类从两个概念上不同的对象继承:具有公共静态成员的非抽象父级和具有抽象方法的抽象父级。与其他一些框架不同,C#允许多重继承。相反,C#提供了一个正式的接口声明,旨在填补此目的。此外,抽象方法的整个重点实际上只是强制执行某种概念接口。


0

我有一个与OP试图实现的情况非常相似的场景。在我的情况下,我想要抽象化的方法将是一个protected方法,并且只能被基类知道。因此,“new C().M();”不适用,因为所涉及的方法不是公共的。我想要能够实例化并调用基类上的公共方法(因此它需要是非抽象的),但我需要这些公共方法调用子类中受保护的实现,并且在父类中没有默认实现。某种程度上说,我需要强制后代覆盖该方法。由于依赖注入,我不知道子类是什么。

我的解决方案是遵循规则并使用具体的基类和虚拟的protected方法。对于默认实现,我使用NotImplementedException抛出错误“必须在子类的实现中提供方法名称的实现。”

protected virtual void MyProtectedMethod() 
{ 
  throw new NotImplementedException("The implementation for MyProtectedMethod must be provided in the implementation of the child class."); 
}

通过这种方式,永远不会使用默认实现,并且后代实现的实现者将很快看到他们错过了重要的步骤。


那么你是用运行时异常替换编译器错误吗?如果你的类不是抽象的,那么你在某个时候可能会冒险调用你的类实例上未实现的方法并抛出异常。如果你的意思是抽象的话,那就直接使用抽象关键字,不要试图绕过它。 - Geert Bellekens
1
一个运行时错误是一种合理的权衡,因为框架/语言没有提供其他机制来实现OP所要求的功能。在我的情况下,我相信如果我遇到了运行时错误,在开发过程中会很早就发现它。要么子类从未被使用,要么它一直被使用(核心工具)。我只是提供了一个替代方案,以满足在任何子类中都需要覆盖的具体类中定义方法的需求。 - Jim K
1
Ed,我不清楚我的帖子如何通过提供框架/语言限制的解决方法而没有直接回答OP的问题?如果您认为我的帖子不合适,请直接与我联系。 - Jim K
同样的情况在这里,为什么他们要让类成为抽象类,如果一个方法是抽象的。该类显然不是未完成的,因为未完成的方法是有意义的,因此该类已经完成,但有未完成的方法。如果这有任何意义的话? - Noob

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