开发者

Why does C# not allow calling base.SomeAbstractMethod

开发者 https://www.devze.com 2023-02-14 15:47 出处:网络
Here is some code for the discussion abstract class ClassA { public abstract void StartProcess(); } class ClassB : ClassA

Here is some code for the discussion

  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");
    }
  }

Before everyone jumps down my throat, let me just say that I'm fully aware of the why it does not. But there are c开发者_运维知识库ases where being able to do so would make sense and prevent having to declare the base class's method as virtual but with an empty implementation.

Coming from Delphi background, we could do this in Delphi and used it in our class design. If you made the mistake of calling the abstract method on the base class (at run time) you've got an "Abstract Error".

Then I wished the (Delphi) complier check me before! Now I wish the (C#) complier would let me do this! How weird is that?

The Questions: Could not the complier/Jitter simply ignore such a call and issue a warning instead of error? Do others see/feel this pain?

The case I have, where I need this is the following: ClassA is part of a library (no control over this class) ClassC is generated (kind of like how an ASP.NET page gets compiled or a Razor View gets compiled.

But a user of the library can define a ClassB and then ClassC will descend from ClassB instead of ClassA (when it gets generated). Similar to how ASP.NET pages normally descend from System.Web.UI.Page but if you've defined your own "base" page and other pages in your app now descendant from your base page then the generated class descends from your base page (which is turn descends from System.Web.UI.Page).

I hope that part is clear. Then looking at the code I've presented, I can't get instances of ClassC to call into the implementation of ClassB because the code gen doesn't know to include base.StartProcess().

EDIT It seem that some people didn't quite get what I've written. So let's say you were writing the code generation part that generates ClassC that descends from ClassA. Well, since the method is anstract (in ClassA) you can't generate the line of code that calls into StartProcess() (because the complier won't allow it). As a result, if anyone define a ClassB, the code generation still won't call base.StartProcess(). This is in fact what happens in ASP.NET MVC views.

Ideally I'd like the compiler to ignore it. It ignore many things like calling dispose on a null reference for example.

I'm trying to have a discussion rather be preached to...

EDIT2 Let's assume we have a hierarchy as shown in the code above and it worked. The opportunity we have now is that the base class, ClassA could have an implementation (in the future) for StartProcess() descendants would call into it. The only way to do this today is to define the method virtual with no body. But that feels a bit icky to me.


How could it possibly make sense to call base.StartProcess() when that's been declared to be abstract? There can't possibly be an implementation to call, so the compiler prohibits it.

Personally I like seeing errors at compile-time instead of either seeing an error at execution time or making the JITter ignore a call which I've specifically made. What if it returned a value which you assigned to a variable? What should that variable value be if the method doesn't exist?

If ClassC is going to derive from ClassB, then you won't get the problem - because you won't be calling into an abstract base method. But your code declares that it derives directly from ClassA instead, not ClassB. If ClassC is generated, it should be generated to derive from ClassB instead, which would be fine.

Personally I think the compiler is doing exactly the right thing here.

EDIT: Just to make it absolutely clear what I believe the appropriate solutions are:

  • If you want to be able to call base.M() from any derived class, you should make it a virtual method with a no-op implementation, instead of an abstract method.
  • If you have a code generator which should generate a call to base.M() only in the situations where it's generating a class whose base class has an implementation of M, then it's up to the code generator to get that right - the language shouldn't make everyone else suffer (by deferring error reporting to execution time, or worse still swallowing that error by simply performing a no-op) just because one tool has been written incorrectly.

I think the downsides of either making it an execution-time error to call an abstract base method or making it a no-op are worse than the issues described in the question.

Now an interesting language feature which could potentially be useful here would be the idea of a virtual method which forced overrides to call the base implementation either before or after the override... in a similar way to how a constructor in a derived class always has to call a constructor in the base class either directly or via another constructor. I strongly suspect that the complexity of such a feature (what would happen to the return value? How would use specify before/after semantics? What about exceptions?) would outweigh the benefits. In simple class hierarchies, the template method pattern can perform the same duty in a simpler way.


I don't think it would make sense to let the compiler compile such code.

On the other side I understand the situation in which you are. The fix should be made on the code generator: it should not generate calls to abstract methods (can be checked using reflection). If you do not have access to the code of the code generator I am afraid you do not have many options...

You could create a facade object that is derived from A but implements all abstract methods as empty virtual ones and manipulate the code generator to use it instead of A.


I see what you mean. Sometimes it might be convenient to just don't care whether a base class method is abstract or not. But, a subclass is already very coupled to its parent class, so much that the compiler knows exactly what calls are valid and issues error messages accordingly. There're no virtual base classes.

What you can do is define an adapter class. Kind of a no-op that will just implement the abstract methods to do nothing. It might not be feasible if they return values and you can't decide what default value to return. You would now derive from the adapter and call its non-abstract methods.

UPDATE

You can solve your "requirement" by using reflection. Instead of this:

base.StartProcess();

You'd use something like this:

this.BaseCall("StartProcess");

This would call StartProcess on your base class only if it's not abstract.

Here's the ugly code to make it work (which also considers parameters and default return values):

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;
      }
    }
  }
}

Is it worth it? I'll let you decide.


You are deriving ClassC from ClassA, what would you expect base.StartProcess to actually do?

Do you really mean to derive from ClassB

0

精彩评论

暂无评论...
验证码 换一张
取 消