为什么C#不允许调用base.SomeAbstractMethod

时间:2011-03-04 11:55:57

标签: c# oop abstract-class

以下是讨论的一些代码

  abstract class ClassA
  {
    public abstract void StartProcess();
  }

  class ClassB : ClassA
  {
    public override void StartProcess()
    {      
      Console.WriteLine("ClassB: Render");
    }
  }

  class ClassC : ClassA
  {
    public override void StartProcess()
    {
      base.StartProcess();//This is where the compiler complains
      Console.WriteLine("ClassC: Render");
    }
  }

在每个人都跳下我的喉咙之前,我只想说我完全清楚它为什么没有。但有些情况下能够这样做是有意义的,并且可以防止必须将基类的方法声明为虚拟但具有空实现。

来自Delphi背景,我们可以在Delphi中完成这个并在我们的类设计中使用它。如果你错误地在基类上调用抽象方法(在运行时)你就会遇到“抽象错误”。

然后我希望(Delphi)编译器检查我之前! 现在我希望(C#)编译器允许我这样做! 这有多奇怪?

问题: 编译器/ Jitter难道不能忽略这样的调用并发出警告而不是错误吗? 别人看到/感受到这种痛苦吗?

我所拥有的案例,我需要的是以下内容: ClassA是库的一部分(无法控制此类) 生成ClassC(类似于编译ASP.NET页面或编译Razor View的方式。

但是库的用户可以定义ClassB,然后ClassC将从ClassB而不是ClassA(当它生成时)下降。类似于ASP.NET页面通常从System.Web.UI.Page下载的方式,但是如果您已经定义了自己的“基础”页面和应用程序中的其他页面,那么现在从您的基页继承,那么生成的类将从您的基页下降(转而来自System.Web.UI.Page)。

我希望这部分是清楚的。然后看看我提供的代码,我无法获得ClassC的实例来调用ClassB的实现,因为代码gen不知道包含base.StartProcess()。

修改 似乎有些人并没有完全理解我所写的内容。因此,假设您正在编写代码生成部分,该部分生成从ClassA继承的ClassC。好吧,因为该方法是anstract(在ClassA中),所以无法生成调用StartProcess()的代码行(因为编译器不允许它)。因此,如果有人定义了ClassB,代码生成仍然不会调用base.StartProcess()。这实际上是ASP.NET MVC视图中发生的事情。

理想情况下,我希望编译器忽略它。它忽略了很多东西,例如在null引用上调用dispose。

我正在尝试进行讨论而不是传播......

EDIT2 让我们假设我们有一个层次结构,如上面的代码所示,它工作。 我们现在的机会是,基类ClassA可能有一个实现(将来)StartProcess()后代将调用它。今天实现这一目标的唯一方法是将方法定义为无虚体。但那对我来说感觉有些蠢。

4 个答案:

答案 0 :(得分:10)

当宣布base.StartProcess()被宣布为抽象时,怎么可能有意义?无法调用实现,因此编译器禁止它。

我个人喜欢在编译时看到错误,而不是在执行时看到错误或让JITter 忽略一个我特意做的调用。如果它返回了您赋给变量的值,该怎么办?如果该方法不存在,该变量值应该是什么?

如果ClassC将从ClassB派生,那么你就不会遇到问题 - 因为你不会调用抽象基本方法。但是您的代码声明它直接从ClassA派生,而不是ClassB派生。如果生成了ClassC,则应该生成它以从ClassB派生,这样就可以了。

我个人认为编译器在这里做的正确。

编辑:只是为了让它绝对清楚我认为合适的解决方案是:

  • 如果您希望能够从任何派生类调用base.M(),您应该将其设置为带有无操作实现的虚方法,而不是抽象方法。
  • 如果你有一个代码生成器,只有在它生成一个基类具有base.M()实现的类的情况下才能生成对M的调用,那么由代码生成器决定做得对 - 语言不应该让其他人受到影响(通过将错误报告推迟到执行时间,或者更糟糕的是仅通过执行无操作来吞下错误)只是因为一个工具写错了。

我认为 的缺点是将调用抽象基本方法作为执行时错误,使其成为无操作的错误比在问题。

现在,一个有趣的语言功能可能可能在这里有用,这将是一个虚拟方法的想法,它强制覆盖在覆盖之前或之后调用基本实现...以类似的方式如何在派生类中的构造函数总是必须直接或通过另一个构造函数来调用基类中的构造函数。我强烈怀疑这样一个特性的复杂性(返回值会发生什么?如何在语义之前/之后使用指定?异常怎么样?)会超过好处。在简单的类层次结构中,template method pattern可以更简单的方式执行相同的任务。

答案 1 :(得分:1)

我认为让编译器编译这样的代码是没有意义的。

另一方面,我了解你的情况。修复应该在代码生成器上进行:它不应该生成对抽象方法的调用(可以使用反射检查)。如果您无法访问代码生成器的代码,恐怕您没有很多选择......

您可以创建一个派生自A的外观对象,但将所有抽象方法实现为空虚拟方法,并操纵代码生成器以使用它而不是A.

答案 2 :(得分:1)

我明白你的意思了。有时候,不关心基类方法是否抽象可能会很方便。但是,子类已经very coupled到它的父类,以至于编译器确切知道哪些调用有效并相应地发出错误消息。没有虚拟基类。

您可以做的是定义适配器类。一种无操作,只会实现抽象方法什么都不做。如果它们返回值并且您无法确定要返回的 default 值,则可能不可行。您现在将从适配器派生并调用其非抽象方法。

<强>更新

可以通过使用反射解决你的“要求”。而不是:

base.StartProcess();

你会使用这样的东西:

this.BaseCall("StartProcess");

如果它不是抽象的,那么只会在你的基类 上调用StartProcess

这是让它工作的丑陋代码(也考虑参数和默认返回值):

public static class BaseExtensions {
  public static void BaseCall(this object self, string methodName, params object[] parameters) {
    self.BaseCall(methodName, typeof(void), null, parameters);
  }
  public static T BaseCallWithReturn<T>(this object self, string methodName, T defaultReturn = default(T), params object[] parameters) {
    return (T)self.BaseCall(methodName, typeof(T), defaultReturn, parameters);
  }
  private static object BaseCall(this object self, string methodName, Type returnType, object defaultReturn, object[] parameters) {
    var parameterTypes = parameters.Select(p => p.GetType()).ToArray();
    if (self.GetType().BaseType == null) return null;
    var method = self.GetType().BaseType.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, parameterTypes, null);
    if (method == null || method.IsAbstract) return defaultReturn;
    var dm = new DynamicMethod(methodName, returnType, new Type[] { self.GetType() }.Concat(parameterTypes).ToArray(), self.GetType());
    var il = dm.GetILGenerator();
    PushParameters(il, parameterTypes.Length);
    il.Emit(OpCodes.Call, method);
    il.Emit(OpCodes.Ret);
    return dm.Invoke(null, new object[] { self }.Concat(parameters).ToArray());
  }
  private static void PushParameters(ILGenerator il, int n) {
    il.Emit(OpCodes.Ldarg_0);
    for (int i = 0; i < n; ++i) {
      switch (i+1) {
        case 1: il.Emit(OpCodes.Ldarg_1); break;
        case 2: il.Emit(OpCodes.Ldarg_2); break;
        case 3: il.Emit(OpCodes.Ldarg_3); break;
        default: il.Emit(OpCodes.Ldarg_S, i+1); break;
      }
    }
  }
}

值得吗?我会让决定。

答案 3 :(得分:0)

您从ClassC派生ClassA,您希望base.StartProcess实际上做什么?

你真的想从ClassB

派生出来