关于C#虚拟和覆盖机制如何在内部工作的主题已经在程序员中被讨论过死...但是在谷歌上半小时之后,我找不到以下问题的答案(见下文):
使用简单的代码:
public class BaseClass { public virtual SayNo() { return "NO!!!"; } } public class SecondClass: BaseClass { public override SayNo() { return "No."; } } public class ThirdClass: SecondClass { public override SayNo() { return "No..."; } } class Program { static void Main() { ThirdClass thirdclass = new ThirdClass(); string a = thirdclass.SayNo(); // this would return "No..." // Question: // Is there a way, not using the "new" keyword and/or the "hide" // mechansim (i.e. not modifying the 3 classes above), can we somehow return // a string from the SecondClass or even the BaseClass only using the // variable "third"? // I know the lines below won't get me to "NO!!!" BaseClass bc = (BaseClass)thirdclass; string b = bc.SayNo(); // this gives me "No..." but how to I get to "NO!!!"? } }
我认为我无法使用最派生的实例(不修改3个类的方法签名)来获取基类或中间派生类的方法。但我想确认并巩固我的理解......
感谢。
答案 0 :(得分:14)
C#无法执行此操作,但 实际上可能在IL中使用call
而不是callvirt
。因此,您可以将Reflection.Emit
与DynamicMethod
结合使用来解决C#的限制。
这是一个非常简单的例子来说明这是如何工作的。如果您真的打算使用它,请将其包装在一个很好的函数中,努力使其适用于不同的委托类型。
delegate string SayNoDelegate(BaseClass instance);
static void Main() {
BaseClass target = new SecondClass();
var method_args = new Type[] { typeof(BaseClass) };
var pull = new DynamicMethod("pull", typeof(string), method_args);
var method = typeof(BaseClass).GetMethod("SayNo", new Type[] {});
var ilgen = pull.GetILGenerator();
ilgen.Emit(OpCodes.Ldarg_0);
ilgen.EmitCall(OpCodes.Call, method, null);
ilgen.Emit(OpCodes.Ret);
var call = (SayNoDelegate)pull.CreateDelegate(typeof(SayNoDelegate));
Console.WriteLine("callvirt, in C#: {0}", target.SayNo());
Console.WriteLine("call, in IL: {0}", call(target));
}
打印:
callvirt, in C#: No.
call, in IL: NO!!!
答案 1 :(得分:7)
如果不对您的样本进行修改并对折扣进行折扣,那么就没有办法了。虚拟系统的目的是强制调用派生的最多,无论什么,CLR擅长其工作。
虽然有几种方法可以解决这个问题。
选项1:您可以将以下方法添加到ThirdClass
public void SayNoBase() {
base.SayNo();
}
这会强制调用SecondClass.SayNo
选项2:这里的主要问题是您想要非虚拟地调用虚拟方法。 C#只提供了一种通过基本修饰符完成此操作的方法。这使得无法以非虚方式调用自己类中的方法。您可以通过将其分解为第二种方法和代理来解决此问题。
public overrides void SayNo() {
SayNoHelper();
}
public void SayNoHelper() {
Console.WriteLine("No");
}
答案 2 :(得分:2)
当然......
BaseClass bc = new BaseClass();
string b = bc.SayNo();
“虚拟”意味着将要执行的实现基于底层对象的ACTUAL类型,而不是它所填充的变量的类型......所以如果实际的对象是一个ThirdClass,这是你将获得的实现,无论你把它投射到什么。如果您想要上面描述的行为,请不要将方法设为虚拟...
如果你想知道“有什么意义?”这是'多态性';这样你就可以将一个集合或一个方法参数声明为某种基类型,并将它包含/传递给派生类型的混合,然而在代码中,即使每个对象都被赋值为一个声明为基类型,对于每一个,将为任何虚方法调用执行的实际实现将是在每个对象的ACTUAL类的类定义中定义的实现...
答案 3 :(得分:2)
在C#中使用base
仅适用于直接基础。您无法访问基础成员。
看起来别人打败了我的答案,可以在IL中做到这一点。
但是,我认为我编写代码的方式有一些优点,所以无论如何我都会发布它。
我做的不同的是使用表达式树,它使您能够使用C#编译器来执行重载解析和泛型参数替换。
那些东西很复杂,如果你能提供帮助,你不想自己复制它。 在您的情况下,代码将如下工作:
var del =
CreateNonVirtualCall<Program, BaseClass, Action<ThirdClass>>
(
x=>x.SayNo()
);
您可能希望将委托存储在只读静态字段中,这样您只需编译一次。
您需要指定3个通用参数:
所有者类型 - 如果您没有使用“CreateNonVirtualCall”,那么您将调用该代码。
基类 - 这是您想要进行非虚拟调用的类
委托类型。这应该表示使用“this”参数的额外参数调用的方法的签名。可以消除这种情况,但在代码方法中需要更多的工作。
该方法采用单个参数,表示调用的lambda。它必须是一个电话,只有一个电话。如果你想扩展代码,你可以支持更复杂的东西。
对于简单,lambda主体仅限于能够访问lambda参数,并且只能将它们直接传递给函数。如果扩展方法体中的代码gen以支持所有表达式类型,则可以删除此限制。这需要一些工作。你可以用回来的代表做任何你想做的事情,所以限制不是太大的交易。
重要的是要注意这段代码并不完美。它可以使用更多的验证,并且由于表达式树的限制,它不能与“ref”或“out”参数一起使用。
我在样本案例中使用void方法,返回值的方法和泛型方法进行了测试,并且它有效。不过,我确信你可以找到一些不起作用的边缘情况。
无论如何,这是IL Gen Code:
public static TDelegate CreateNonVirtCall<TOwner, TBase, TDelegate>(Expression<TDelegate> call) where TDelegate : class
{
if (! typeof(Delegate).IsAssignableFrom(typeof(TDelegate)))
{
throw new InvalidOperationException("TDelegate must be a delegate type.");
}
var body = call.Body as MethodCallExpression;
if (body.NodeType != ExpressionType.Call || body == null)
{
throw new ArgumentException("Expected a call expression", "call");
}
foreach (var arg in body.Arguments)
{
if (arg.NodeType != ExpressionType.Parameter)
{
//to support non lambda parameter arguments, you need to add support for compiling all expression types.
throw new ArgumentException("Expected a constant or parameter argument", "call");
}
}
if (body.Object != null && body.Object.NodeType != ExpressionType.Parameter)
{
//to support a non constant base, you have to implement support for compiling all expression types.
throw new ArgumentException("Expected a constant base expression", "call");
}
var paramMap = new Dictionary<string, int>();
int index = 0;
foreach (var item in call.Parameters)
{
paramMap.Add(item.Name, index++);
}
Type[] parameterTypes;
parameterTypes = call.Parameters.Select(p => p.Type).ToArray();
var m =
new DynamicMethod
(
"$something_unique",
body.Type,
parameterTypes,
typeof(TOwner)
);
var builder = m.GetILGenerator();
var callTarget = body.Method;
if (body.Object != null)
{
var paramIndex = paramMap[((ParameterExpression)body.Object).Name];
builder.Emit(OpCodes.Ldarg, paramIndex);
}
foreach (var item in body.Arguments)
{
var param = (ParameterExpression)item;
builder.Emit(OpCodes.Ldarg, paramMap[param.Name]);
}
builder.EmitCall(OpCodes.Call, FindBaseMethod(typeof(TBase), callTarget), null);
if (body.Type != typeof(void))
{
builder.Emit(OpCodes.Ret);
}
var obj = (object) m.CreateDelegate(typeof (TDelegate));
return obj as TDelegate;
}
答案 4 :(得分:1)
您无法获得覆盖的基本方法。无论你如何投射对象,总是使用实例中的最后一个覆盖。
答案 5 :(得分:0)
如果它支持一个字段,你可以使用反射拉出字段。
即使你使用来自typeof(BaseClass)的反射来拉出methodinfo,你仍然会最终执行被覆盖的方法