【问题标题】:Why can't a static and non-static method share the same signature?为什么静态和非静态方法不能共享相同的签名?
【发布时间】:2014-11-03 16:57:45
【问题描述】:

C# 提供了以下signature characteristics 在函数重载时使用。

我们知道重载只考虑参数;它们的数量和类型,但多态性的目标是根据调用策略提供相同的名称但不同的用法。

如果我有一个类包含两个具有相同名称和签名的方法,而一个是静态的,另一个不是,C# 编译器会抛出错误; “类已经定义了一个名为'foo'的成员,具有相同的参数类型”。对这两种方法的调用将有所不同;一个带有对象名,一个带有类名的静态。因此,跟注策略没有歧义。那为什么会抛出错误呢?

 class Example {

    public void foo() { }
    public static void foo() { }

}

class Program
{
    static void Main(string[] args)
    {

        Example e = new Example();
        e.foo(); 

    }
}

【问题讨论】:

  • 考虑你的void Bar(){foo();} 在你的Example 类中是合法的。您将如何解决 foo() 方法调用?
  • @hamlet-hakobyan this.foo(); / Example.foo();
  • @SepehrFarshid 我的代码中没有this。不合法吗?
  • 这是合法的,我刚才说过,C# 有语法来实际支持它。

标签: c# polymorphism


【解决方案1】:

i) 问题假设 - 获得以下行为:

  1. 能够从类中调用静态方法:例如MyClass.MySpecialMethod()
  2. 还可以从同一类的实例中调用具有相同返回类型、名称和参数的非静态方法:例如instanceOfMyClass.MySpecialMethod()

ii) 上下文:

  1. 重现使用依赖注入的应用程序内部的行为。

当今的大多数编程都使用 DI - 从依赖项的直接实例调用非静态方法几乎是一种反模式(该依赖项之前没有注入 DI)。

iii) 解决方案:

class Program
{
    static void Main(string[] args)
    {
        // instead of class initialization we would have these registrations, e.g.:
        // diContainer.Resolve<IMyApplication>().With<MyDIApplication>();
        // diContainer.Resolve<ITerminator>().With<Terminator>();
        IMyApplication app = new MyDIApplication(new Terminator());

        app.Run();
    }

    public interface IMyApplication { void Run(); }
    public class MyDIApplication : IMyApplication
    {
        private readonly ITerminator terminator;

        public MyDIApplication(ITerminator terminatorDependency)
        {
            this.terminator = terminatorDependency;
        }

        public void Run()
        {
            terminator.Terminate(); // instance method call
            Terminator.Terminate(); // static method call
        }
    }

    public interface ITerminator { void Terminate(); }

    public class Terminator : ITerminator
    {
        public static void Terminate() => Console.WriteLine("Static method call.");
        void ITerminator.Terminate() => Console.WriteLine("Non-static method call.");
    }
}

结论:

是的,两个 Terminate 方法的签名不相同,因为非静态方法是接口的显式实现,与静态方法不冲突,

但实际上,当在依赖注入的上下文中使用此解决方案时,我们真正关心的是结果,而不是管道——即我们设法从一个类中调用了一个静态方法,实际上具有相同的返回, name 和 args 作为注入 DI 的类实例的非静态方法。

【讨论】:

    【解决方案2】:

    同样的问题was asked to Eric Gunnerson,曾在C#语言设计团队工作,他的回答是:

    确实,就编译器而言,这两个函数之间没有歧义。然而,在用户方面存在相当大的混淆可能性。很难在文档中找到正确的方法,而且一旦找到,就很难确定您调用的是正确的版本(即,当您需要实例版本时,您可能不小心调用了静态版本)。

    因此,不允许这样做的原因是设计使然。

    【讨论】:

    • 我意识到这个问题只是在传递一个答案,但老实说,这个答案(甚至来自 C# 设计团队成员)非常薄弱。否则就不会有重载,没有隐藏成员,没有重复名称的接口实现。不会有与类属性同名的参数。导入命名空间不会有“using”关键字,以免混淆具有相同名称的类。我可以继续下去。在所有这些情况下,都会出现警告和/或错误以及解决冲突的方法!在这种情况下可以实施相同的解决方案!
    【解决方案3】:

    出现此错误是因为这是在 C# Language Specification 中定义行为的方式。任何“模棱两可”的用法(或消除歧义的方法)都是无关紧要的,尽管这种推理和极端情况可能导致设计人员没有明确允许这种区分..或者它可能只是一个 C#底层 .NET CLI/CLR 限制的编码1.

    来自 C# 规范中的“3.6 签名和重载”(并与链接的文档一致),格式为项目符号:

    方法的签名由

    组成
    • 方法的名称
    • 类型参数的数量
    • 每个形参的类型和种类(值、引用或输出) ..

    方法修饰符,包括static,在此处被视为方法签名的一部分。

    而且,从“1.6.6 方法”我们有限制和一致的总结:

    方法的签名在声明该方法的类中必须是唯一的。方法的签名由方法的名称、类型参数的数量及其参数的{数量、修饰符和类型}组成。

    此限制适用于(且独立于)考虑多态性的方法之前。

    另外,作为结束说明:实例方法必须是虚拟的或通过接口访问才能在 C# 中运行时多态。 (方法隐藏和方法重载都可以说是编译时多态性的一种形式,但这是另一个话题..)


    1支持这仅仅是由于 .NET CLI/CLR 本身的限制不值得绕过(即出于互操作性原因)。来自ECMA-335中的“I.8.6.1.5 方法签名”:

    一个方法签名由

    组成
    • 调用约定 [CLS 规则 15:“only 调用约定 CLS 支持的是标准托管调用约定"],
    • 泛型参数的个数,如果方法是泛型的,
    • [省略规则]
    • 零个或多个参数签名的列表——方法的每个参数一个—— 而且,
    • 结果值的类型签名(如果已生成)。

    方法签名由方法定义声明。 只能将一个约束添加到一个 除了参数签名之外的方法签名 [CLS 规则 15:“可变参数约束 不是 CLS 的一部分”]:

    • 可以包含可变参数约束以指示超过此点的所有参数都是 可选的。当它出现时,调用约定应该是支持变量的 参数列表。

    因此,C#/CLS 和 ECMA 签名组件之间的交集是方法名称、“泛型参数的数量”和“零个或多个参数签名的列表”。

    【讨论】:

      【解决方案4】:

      我觉得您的问题是“为什么标准选择禁止声明两种仅通过 static 关键字不同的方法?”,因此“因为标准这么说”的答案对我来说似乎不合适。

      现在,问题是,可能有任何原因。标准就是法律,它可以是任意的。如果没有参与语言设计的人的帮助,我们所能做的就是推测原因,试图揭示法律的精神。

      这是我的猜测。我认为这个选择的三个主要原因:

      因为其他语言也是这样说的。

      C++ 和 Java 是 C# 的灵感语言,遵守与这些语言相同的重载规则是有意义的。至于为什么在这些语言中是这样的,我不知道。我在 SO 上发现了一个关于C++ 的类似问题,尽管没有给出关于为什么会这样的答案(除了“标准这么说”之外)。

      因为它会产生需要解决的歧义。

      正如其他人和 OP 所指出的,允许除了 static 关键字之外的相同签名会强制用户以明确的方式调用方法(通过为类名或实例名添加前缀)。这给代码增加了一定程度的复杂性。当然,这已经可以通过字段和参数来完成。然而,有些人不同意这种用法,更愿意为字段选择不同的名称(以 _ 或 m_ 为字段前缀)。

      因为它在 OOP 中没有多大意义。

      这真的是我的理解,所以我可能完全错了(至少@user2864740 认为这个论点是可疑的——参见 cmets),但我觉得静态成员是在 OOP 中引入“函数式编程”的一种方式.它们没有绑定到特定的实例,因此它们不会修改对象的内部状态(如果它们修改了另一个对象的状态,那么它们应该是另一个对象的非静态方法),在某种程度上它们是“纯”的。 因此,我不明白“纯函数”如何在语义上与常规对象方法足够接近,以便它们共享相同的名称。

      【讨论】:

      • 不错!至于Without the help of somebody who participated to the language's design, all we can do is speculate about the reasons this answer from Eric Lippert 可能会有所启发。但是,我真的相信这个特定问题并非如此,我完全同意您的Because it doesn't make a lot of sense 论点。
      • 静态成员与“函数式编程”无关。如果是这种情况,那么每个 C 函数都与“函数式编程”有关。拥有一个静态方法并没有说明它是purity,只是它不能有关联的实例变量(无论如何它可能是不可变的,因此没有“隐藏信息或可能随着程序执行进行而改变的状态”子句,进一步削弱了论点)。限制(不允许具有相同签名的静态非静态方法)也是正交的。
      • @user2864740 虽然我确实同意根据定义,静态方法与纯度无关,但我的主张是,在良好的 OOP 设计中,静态方法应该 是纯粹的,因为如果它们依赖于状态,它们应该是它们修改的对象的非静态方法。 C 缺乏声明实例方法(或根本是“方法”,顺便说一句)的语言结构,因此我们别无选择,只能在设计中同时使用纯函数和非纯函数。我也不是说非静态方法不能是纯粹的。再说一次,我可能是错的,但我看不出你的评论如何证明这一事实。
      • 语言 wrt static没有任何东西能够以某种方式赋予任何额外的“函数式编程”或纯度。用于证明相关利益或暗示这在包含此类结构中发挥作用的此类论点似乎是可疑的。 (关于选择不同的有意义的方法名有一些话要说,但这并不是一个正当的理由。)
      • 在语言设计团队工作的 Eric Gunnerson 表示,它是非法的,因为否则它会引入“相当大的用户混淆可能性”。他的完整答案见我的回答。
      【解决方案5】:

      它抛出错误的原因是可以从非静态方法调用静态方法而无需指定类型名称。在这种情况下,编译器将无法确定调用了哪个方法。

      public class Foo()
      {
         public static void MyMethod() {};
         public void MyMethod() {}
      
         public void SomeOtherMethod()
         {
            MyMethod(); // which method we're calling static or non-static ?
         }
      }
      

      编辑

      刚刚发现这个SO post 与您的案例有关。您可能也想检查一下。

      【讨论】:

      • 但是您可以声明一个与现有字段同名的局部变量。您可以使用this 明确引用该字段。这里将是相同的 - this.MyMethod - 实例; MyMethod 或 Foo.MyMethod - 静态的。
      • @ViktorArsanov 这不是关于你在这里能做什么或不能做什么,而是关于编译器如何解释这段代码。编写这段c#代码是合法的,但是编译器无法理解应该调用哪个方法。
      • 这个答案不正确。想象一下,如果编译器为语言设计团队规定什么是合法的,什么是不合法的,而不是相反。由于解决方案(就像 Viktor 建议的那样)是可能的,正如 Viktor 提到的已经在不同的地方被语言所接受,现在唯一剩下的问题是为什么语言设计团队不允许静态/非静态成员具有相同的签名/名称?有关正确信息,请参阅我的答案。
      【解决方案6】:

      看看这个简单的伪代码:

      class A
      {
        public void B(){...}
        public static void B(){...}
      }
      
      ...
      
      A A = new A();
      A.B();  // <== which one is going to be called?
      

      【讨论】:

      • -1, 不是。class A { void Foo() {} } class B { static void Foo() {} } ... A B = new A(); B.Foo(); // Which one is going to be called? Its A.Foo()! 现在调用 B.Foo() MyNamespace.B.Foo();
      • 编译器从不信任开发者。您是专业的开发人员,不要使用变量名作为类名。你确定没有人会这样做吗?
      • @SepehrFarshid - 这与这个答案显示的完全不同。
      • 但它表明这个例子证明不了。我只是做了同样的事情,并向您展示了编译器的行为方式。在上面的例子中,A.B() 可以调用非静态方法,而Namespace.A.B() 可以调用静态方法。
      猜你喜欢
      • 1970-01-01
      • 2011-12-08
      • 2011-03-02
      • 2012-07-02
      • 1970-01-01
      • 2013-12-20
      • 1970-01-01
      • 2012-12-28
      相关资源
      最近更新 更多