重写方法上的 C # 可选参数

好像进去了。NETFramework 重写方法时,可选参数存在问题。以下代码的输出结果如下: “ bbb” “啊” 。但我期望的输出是: “ bbb” “ bbb” .有没有解决办法。我知道它可以通过方法重载来解决,但是想知道为什么会这样。此外,代码在 Mono 中运行良好。

class Program
{
class AAA
{
public virtual void MyMethod(string s = "aaa")
{
Console.WriteLine(s);
}


public virtual void MyMethod2()
{
MyMethod();
}
}


class BBB : AAA
{
public override void MyMethod(string s = "bbb")
{
base.MyMethod(s);
}


public override void MyMethod2()
{
MyMethod();
}
}


static void Main(string[] args)
{
BBB asd = new BBB();
asd.MyMethod();
asd.MyMethod2();
}
}
27790 次浏览

你有没有试过:

 public override void MyMethod2()
{
this.MyMethod();
}

所以你实际上告诉你的程序使用重写的方法。

您可以通过调用:

this.MyMethod();

(in MyMethod2())

它是否是一个错误是棘手的; 但它看起来并不一致。ReSharper 警告你不要改变默认值,如果这有帮助的话; 当然,ReSharper 还有告诉你 this.是多余的,并且提供给你移除它... 这改变了行为-所以 ReSharper 也不是完美的。

看起来 可以确实有编译错误的资格,我承认。我需要仔细看看 真的以确定... 当你需要他的时候,埃里克在哪里,嗯?


编辑:

这里的关键点是语言规范; 让我们看看7.5.3:

For example, the set of candidates for a method invocation does not include methods marked override (§7.4), and methods in a base class are not candidates if any method in a derived class is applicable (§7.6.5.1).

(实际上7.4明确省略了 override方法)

这里有些冲突。它声明,如果在派生类中有一个适用的方法,就不使用 基地方法——这将导致我们使用 派生的方法,但同时,它说标记为 override的方法不被考虑。

但是,7.5。1.1接着说:

对于在类中定义的虚方法和索引器,参数列表是从函数成员最具体的声明或重写中选择的,从接收器的静态类型开始,并在其基类中搜索。

然后7.5.1.2解释了在调用时如何计算这些值:

在函数成员调用(7.5.4)的运行时处理过程中,参数列表的表达式或变量引用按照从左到右的顺序进行计算,如下所示:

...(snip)...

当从具有相应可选参数的函数成员中省略参数时,将隐式传递函数成员声明的默认参数。因为这些参数总是不变的,所以它们的计算不会影响剩余参数的计算顺序。

这显式地强调了它正在查看参数列表,该列表以前在7.5.1.1中定义为来自 最具体的声明或覆盖。这是7.5.1.2中引用的“方法声明”,这似乎是合理的,因此传递的值应该从派生最多的类型到静态类型。

这意味着: csc 有一个 bug,它应该使用 派生的版本(“ bbb bbb”) ,除非它被限制(通过 base.,或强制转换为基类型)只能查看基方法声明(7.6.8)。

这可能是由于模糊性和编译器给予基类/超类优先权。下面修改 BBB 类的代码,添加 this关键字,得到输出‘ BBB BBB’:

class BBB : AAA
{
public override void MyMethod(string s = "bbb")
{
base.MyMethod(s);
}


public override void MyMethod2()
{
this.MyMethod(); //added this keyword here
}
}

它所暗示的一点是,无论何时,当您将类的当前实例的属性或方法作为 最佳实践调用时,都应该始终使用 this关键字。

如果 base 和 child 方法中的这种模糊性没有引起编译器警告(如果没有错误的话) ,我会担心,但是如果它引起了,那么我认为它是看不见的。

==================================================================

编辑: 考虑下面这些链接的样本摘录:

Http://geekswithblogs.net/blackrabbitcoder/archive/2011/07/28/c.net-little-pitfalls-default-parameters-are-compile-time-substitutions.aspx

Http://geekswithblogs.net/blackrabbitcoder/archive/2010/06/17/c-optional-parameters——-pros-and-pitfalls.aspx

Pitfall: Optional parameter values are compile-time There is one thing and one thing only to keep in mind when using optional parameters. If you keep this one thing in mind, chances are you may well understand and avoid any potential pitfalls with their usage: 有一件事是这样的: 可选参数是编译时,语法糖!

陷阱: 小心继承和接口实现中的默认参数

现在,第二个潜在的陷阱与继承和接口实现有关:

   1: public interface ITag
2: {
3:     void WriteTag(string tagName = "ITag");
4: }
5:
6: public class BaseTag : ITag
7: {
8:     public virtual void WriteTag(string tagName = "BaseTag") { Console.WriteLine(tagName); }
9: }
10:
11: public class SubTag : BaseTag
12: {
13:     public override void WriteTag(string tagName = "SubTag") { Console.WriteLine(tagName); }
14: }
15:
16: public static class Program
17: {
18:     public static void Main()
19:     {
20:         SubTag subTag = new SubTag();
21:         BaseTag subByBaseTag = subTag;
22:         ITag subByInterfaceTag = subTag;
23:
24:         // what happens here?
25:         subTag.WriteTag();
26:         subByBaseTag.WriteTag();
27:         subByInterfaceTag.WriteTag();
28:     }
29: }

会发生什么呢? 嗯,即使在每种情况下的对象是 SubTag,其标记是“ SubTag”,您将得到:

第1集: 子标签 第2集: BaseTag 第3集: ITAG

但是记住要确保:

不要在现有的默认参数集中插入新的默认参数,这可能会导致不可预测的行为,这可能不一定会抛出语法错误-添加到列表的末尾或创建新方法。 Be extremely careful how you use default parameters in inheritance hierarchies and interfaces – choose the most appropriate level to add the defaults based on expected usage.

==========================================================================

我认为这是因为这些默认值在编译时是固定的。如果您使用反射器,您将在 BBB 中看到 MyMethod2的以下内容。

public override void MyMethod2()
{
this.MyMethod("aaa");
}

One thing worth noting here, is that the overridden version is called each time. Change the override to:

public override void MyMethod(string s = "bbb")
{
Console.Write("derived: ");
base.MyMethod(s);
}

结果是:

derived: bbb
derived: aaa

类中的方法可以执行下列一到两项操作:

  1. 它定义了一个接口供其他代码调用。
  2. 它定义了调用时要执行的实现。

It may not do both, as an abstract method does only the former.

Within BBB the call MyMethod() calls a method 定义 in AAA.

因为在 BBB中存在重写,调用该方法将导致调用 BBB中的实现。

Now, the definition in AAA informs calling code of two things (well, a few others too that don't matter here).

  1. 签名 void MyMethod(string)
  2. (对于那些支持它的语言)单个参数的默认值是 "aaa",因此在编译形式为 MyMethod()的代码时,如果找不到与 MyMethod()相匹配的方法,你可以调用‘ MyMethod (“ aaa”)来替换它。

So, that's what the call in BBB does: The compiler sees a call to MyMethod(), doesn't find a method MyMethod() but does find a method MyMethod(string). It also sees that at the place where it is defined there's a default value of "aaa", so at compile time it changes this to a call to MyMethod("aaa").

From within BBB, AAA is considered the place where AAA's methods are defined, even if overridden in BBB, so that they can be over-ridden.

在运行时,使用参数“ aaa”调用 MyMethod(string)。因为有一个被覆盖的表单,即被调用的表单,但是不用“ bbb”调用它,因为该值与运行时实现无关,而是与编译时定义有关。

添加 this.会更改所检查的定义,从而更改调用中使用的参数。

Edit: Why this seems more intuitive to me.

就个人而言,因为我说的是直觉,它只能是个人的,我发现这更直观的原因如下:

如果我在编写 BBB代码,那么不管是调用还是重写 MyMethod(string),我都会认为它是“做 AAA的事情”——它是 BBB代码在“做 AAA的事情”,但是它仍然在做 AAA的事情。因此,无论是调用还是重写,我都会意识到这样一个事实,即定义 MyMethod(string)的是 AAA

如果我调用使用 BBB的代码,我会想到“使用 BBB的东西”。我可能不太清楚最初在 AAA中定义了哪些内容,我可能认为这仅仅是一个实现细节(如果我没有在附近使用 AAA接口的话)。

编译器的行为符合我的直觉,这就是为什么当我第一次阅读这个问题时,我觉得 Mono 有一个 bug。经过考虑,我看不出其中一个如何比另一个更好地完成指定的行为。

尽管如此,在个人层面上,我从来不会在抽象、虚拟或重写方法中使用可选参数,如果重写别人的方法,我会匹配他们的方法。

The behaviour is definitely very strange; it is not clear to me if it is in fact a bug in the compiler, but it might be.

昨晚校园里下了很大的雪,西雅图不太擅长处理雪。我的车今天早上没有开,所以我不能进办公室去比较 C # 4,C # 5和 Roslyn 对这个案子的看法,如果他们不同意的话。一旦我回到办公室并能够使用正确的调试工具,我将在本周晚些时候尝试发布一个分析。

这看起来像一个错误给我。我相信它 很好地指定, 它的行为方式应该与调用 具有显式 this前缀的。

我已经简化了这个示例,只使用 单身虚拟机 method, and show both which implementation is called and 参数值是什么:

using System;


class Base
{
public virtual void M(string text = "base-default")
{
Console.WriteLine("Base.M: {0}", text);
}
}


class Derived : Base
{
public override void M(string text = "derived-default")
{
Console.WriteLine("Derived.M: {0}", text);
}


public void RunTests()
{
M();      // Prints Derived.M: base-default
this.M(); // Prints Derived.M: derived-default
base.M(); // Prints Base.M: base-default
}
}


class Test
{
static void Main()
{
Derived d = new Derived();
d.RunTests();
}
}

So all we need to worry about are the three calls within RunTests. 规范中前两个调用的重要部分是 section 7.5.1.1,其中讨论了在查找相应参数时使用的参数列表:

对于在类中定义的虚方法和索引器,参数 列表是从最具体的声明或覆盖中选择的 函数成员的静态类型开始 receiver, and searching through its base classes.

第7.5.1.2节:

当从具有相应可选参数的函数成员中省略参数时,将隐式传递函数成员声明的默认参数。

“对应的可选参数”是将7.5.2与7.5.1.1联系起来的位。

对于 M()this.M(),该参数列表应为 the one in Derived as static type of the receiver is Derived, 实际上,您可以看出编译器处理 that as the parameter list earlier in the compilation, as if you 的 Derived.M()中的参数 强制性的、 调用失败-所以 M()调用 需要这个参数 a default value in Derived, but then ignores it!

事实上,情况会变得更糟: 如果为 参数,但是在 Base中必须调用 M()最终使用 null作为参数值, 我认为这证明了这是一个 bug: null值不能出现 从任何有效的地方。(它是 null,因为这是默认的 值; 它始终只使用默认值 for the parameter type.)

规范的7.6.8部分处理 base.M () ,它说 除了 作为非虚拟行为外,还考虑了表达式 因此它对于基本方法是完全正确的 用于确定有效参数列表。这意味着 最后一行是正确的。

Just to make things easier for anyone who wants to see the really odd bug described above, where a value not specified anywhere is used:

using System;


class Base
{
public virtual void M(int x)
{
// This isn't called
}
}


class Derived : Base
{
public override void M(int x = 5)
{
Console.WriteLine("Derived.M: {0}", x);
}


public void RunTests()
{
M();      // Prints Derived.M: 0
}


static void Main()
{
new Derived().RunTests();
}
}

不管怎样,都需要一个解决方案

我肯定会认为它是一个 bug,要么是因为结果是错误的,要么是因为结果是预期的,那么编译器不应该让你声明它为“覆盖”,或者至少提供一个警告。

我建议您向 Microsoft 报告此事

但这是对还是错呢?

然而,关于这是否是预期的行为,我们首先分析两种观点。

考虑到我们有以下守则:

void myfunc(int optional = 5){ /* Some code here*/ } //Function implementation
myfunc(); //Call using the default arguments

有两种实施方式:

  1. 该可选参数被视为重载函数,结果如下:

    void myfunc(int optional){ /* Some code here*/ } //Function implementation
    void myfunc(){ myfunc(5); } //Default arguments implementation
    myfunc(); //Call using the default arguments
    
  2. That the default value is embedded in the caller, thus resulting in the following code:

    void myfunc(int optional){ /* Some code here*/ } //Function implementation
    myfunc(5); //Call and embed default arguments
    

There are many differences between the two approaches, but we will first take a look on how the .Net framework interprets it.

  1. In .Net you can only override a method with a method that contains the same number of arguments, but you cannot override with a method containing more arguments, even if they are all optional (which would result in a call haveing the same signature as the overridden method), say for example you have:

    class bassClass{ public virtual void someMethod()}
    class subClass :bassClass{ public override void someMethod()} //Legal
    //The following is illegal, although it would be called as someMethod();
    //class subClass:bassClass{ public override void someMethod(int optional = 5)}
    
  2. You can overload a method with default arguments with another method with no arguments, (this has disastrous implications as I will discuss in a moments), so the folloing code is legal:

    void myfunc(int optional = 5){ /* Some code here*/ } //Function with default
    void myfunc(){ /* Some code here*/ } //No arguments
    myfunc(); //Call which one?, the one with no arguments!
    
  3. when using reflection one must always provide a default value.

All of which are enough to prove that .Net took the second implementation, so the behavior that the OP saw is right, at least according to .Net.

Problems With the .Net Approach

However there are real problems with the .Net approach.

  1. Consistency

    • As in the OP's problem when overriding the default value in an inherited method, then results might be unpredictable

    • When the original implantation of the default value is changed, and since the callers don't have to get recompiled, we might end up with default values that are no longer valid

    • Reflection requires you to provide the default value, which the caller doesn't have to know
  2. Breaking code

    • When we have a function with default arguments and latter we add a function with no arguments, all calls will now route to the new function, thus breaking all existing code, without any notification or warning!

    • Similar will happen, if we later take away the function with no arguments, then all calls will automatically route to the function with the default arguments, again with no notification or warning! although this might not be the intention of the programmer

    • Furthermore it does not have to be regular instance method, an extension method will do the same problems, since an extension method with no parameters will take precedence over an instance method with default parameters!

Summary: STAY AWAY FROM OPTIONAL ARGUMENTS, AND USE INSTEAD OVERLOADS (AS THE .NET FRAMEWORK ITSELF DOES)

大体上同意@Marc Gravell 的观点。

然而,我想提到的是,这个问题在 C + + 世界(http://www.devx.com/tips/Tip/12737)中已经存在很久了,答案看起来像是“不像虚函数,它在运行时解析,默认参数是静态解析的,也就是说,在编译时。”因此,这种 C # 编译器的行为似乎是出于一致性而被有意接受的,尽管它出人意料。