【问题标题】:Why does C# not allow calling base.SomeAbstractMethod为什么 C# 不允许调用 base.Some 抽象方法
【发布时间】:2011-07-08 18:45:22
【问题描述】:

这里有一些代码供讨论

  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#)编译器能让我这样做! 这有多奇怪?

问题: 编译器/抖动不能简单地忽略这样的调用并发出警告而不是错误吗? 其他人是否看到/感觉到这种痛苦?

我需要的情况如下: ClassA 是库的一部分(无法控制此类) 生成 ClassC(有点像 ASP.NET 页面的编译方式或 Razor 视图的编译方式。

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

我希望这部分很清楚。然后查看我提供的代码,我无法让 ClassC 的实例调用 ClassB 的实现,因为代码生成器不知道包含 base.StartProcess()。

编辑 似乎有些人不太明白我写的东西。因此,假设您正在编写生成从 ClassA 继承的 ClassC 的代码生成部分。好吧,由于该方法是 anstract (在 ClassA 中),因此您无法生成调用 StartProcess() 的代码行(因为编译器不允许这样做)。因此,如果有人定义了 ClassB,代码生成仍然不会调用 base.StartProcess()。这实际上是 ASP.NET MVC 视图中发生的情况。

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

我正在尝试进行讨论而不是被传教给...

EDIT2 假设我们有一个如上面代码所示的层次结构并且它有效。 我们现在拥有的机会是基类 ClassA 可以有一个实现(将来) StartProcess() 后代会调用它。今天这样做的唯一方法是定义没有主体的虚拟方法。但这对我来说有点恶心。

【问题讨论】:

  • “但在某些情况下,能够这样做是有意义的”我在等待......
  • 不足为奇——这里的人们总是在展示他们有多聪明。我以为我们可以进行讨论而不是小便比赛……太糟糕了。如果您愿意,我正在编辑我的问题以包含其他信息。
  • @Jackie:在我看来这仍然没有意义。如果您的代码生成器明确声明 ClassC 从 ClassA 派生,那么我看不出 ClassB 将如何参与。我仍然很高兴 C# 编译器的行为方式如此。代码生成器需要知道 ClassB 以使 ClassC 派生自它,并且它可以轻松检查是否有要调用的实现。没问题。
  • @Jackie:您提供了一个代码生成器被破坏的示例。破坏语言并不是补偿损坏的代码生成器的正确方法 - 正确的更改是修复代码生成器或基类。如果代码生成器遇到方法调用错误的情况,那是否会成为 C# 变得不区分大小写的参数?不。如果你想要一个无操作方法,如果没有任何东西覆盖它,那正是空虚方法的意思。抽象方法强制重新实现,这根本不是一回事。
  • @Jackie:如果某些东西没有解决真正的问题,但它引入问题(我认为“当当前在编译时发现编码错误时,将其推迟到执行时") 作为引入一个问题,那么我认为没有太多讨论的必要。您声称您希望 C# 编译器能够像 Delphi 编译器那样工作 - 如果您没有突出的问题,您为什么希望这样做?如果您要提出更改,至少应该有一个具体的示例说明它在哪里有用。我不认为“想象一个损坏的代码生成器”很重要。

标签: c# oop abstract-class


【解决方案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;
      }
    }
  }
}

值得吗?我会让决定。

【讨论】:

  • @Jordão,+1。我想你明白我的意思。但是,如果将来更改基类以包含一个实现(显然保持方法虚拟),那么后代将不会调用该实现,因为他们可以更早地这样做。
  • @Jackie:如果稍后添加一个实现,那么抽象的方法就没有意义了,不是吗?它应该被声明为虚拟的,并从一个无操作实现开始,在这种情况下,派生类调用base.Foo 是可以的。仅仅因为您无法控制特定示例中的抽象类或代码生成器,并不意味着语言设计者做出了错误的选择。
  • @Jackie Kirby:当您更改基类时,无法保证子类会按预期运行。所以无论如何你总是必须重新编译重新测试子类。现在那就是它们是如何耦合的。
  • 组合,而不是继承,会真正解决你的问题。如果你编写一个类,你可以安全地调用它的抽象方法,它们将在运行时通过虚拟方法调度来解决。
  • @Jon,你是说你设计过的每一堂课都没有改变过设计吗? “语言设计师做出了错误的选择”,我没有说或暗示他们做了,这就是你从我的问题中得到的吗?如果该语言忽略了某些事情(当它适合时),那么我想知道这是否可能是其中一种情况。您不必看到运行时错误,但您可能应该看到编译时警告。
【解决方案2】:

base.StartProcess() 被声明为抽象时,它怎么可能有意义?不可能有一个实现可以调用,所以编译器禁止它。

我个人喜欢在编译时看到错误,而不是在执行时看到错误或使 JITter ignore 成为我专门进行的调用。如果它返回一个您分配给变量的值怎么办?如果方法不存在,该变量值应该是什么?

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

我个人认为编译器在这里做的事情是完全正确的。

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

  • 如果您希望能够从任何派生类调用 base.M(),则应将其设为具有无操作实现的虚拟方法,而不是抽象方法。
  • 如果您有一个代码生成器,它应该只在它生成的类的基类具有M 实现的情况下生成对base.M() 的调用,那么代码生成器就可以做到这一点 -该语言不应该仅仅因为一个工具编写不正确而让其他人受苦(通过将错误报告推迟到执行时间,或者更糟的是通过简单地执行无操作来吞下该错误)。

我认为 要么 使其成为执行时错误以调用抽象基方法 使其成为无操作的缺点比中描述的问题更糟糕问题。

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

【讨论】:

  • +1 表示编译器不应该编译垃圾。这不是我们在这里讨论的 Perl。
  • 很好的答案,但我希望找到以下内容:如果您希望 base 方法调用不执行任何操作,那么您需要声明一个基础就不会感到惊讶什么都不做的方法。如果所有抽象方法都被视为具有空主体的虚拟方法,那么在方法上再添加 abstract 标记将毫无意义。
  • @Jon,我喜欢你的“有趣的语言功能”。我也希望有这样的东西。具体来说,基类可以以某种方式更明确地(并且可能控制)派生类的行为方式。模板方法模式不提供这种控制。它允许基类确定一系列“步骤”或方法调用,但决不能强制调用 base.Foo()。所以回到另一点,很想对此进行讨论。当然会有很多事情需要解决,但这就是重点!
  • @Jon:我也认为这个功能太复杂了,不值得。作为替代方案,请查看 traits。它们通常是设计可重用代码的更好构造。例如,在Scala 中,有self-type 的概念,这是当类选择组合它时,特征要求 存在的类型。 不像具有基类型,特征可以调用该自身类型的抽象方法。由于特征是组合的,而不是继承的,它们只是虚拟方法调用。
  • @Jordão:我想我必须了解更多 Scala 才能正确理解这一点 - 但是,使用组合而不是继承也有帮助。
【解决方案3】:

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

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

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

【讨论】:

  • 很高兴您能理解我的观点!我并不是真的在寻找解决我遇到的问题的方法。我以代码生成为例说明存在的事物,以便人们可以关联。
【解决方案4】:

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

你真的是想从ClassB派生出来

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-09-15
    • 2016-12-17
    • 2022-01-19
    • 2017-11-24
    • 2019-06-09
    • 2023-04-05
    • 2021-06-26
    • 1970-01-01
    相关资源
    最近更新 更多