为什么C#不允许静态方法实现接口?

475

C#为什么要这样设计?

据我所知,接口只描述行为,并用于描述实现接口的类必须实现某些行为的契约义务。

如果类希望在共享方法中实现该行为,为什么不应该呢?

这里是我想象中的一个例子:

// These items will be displayed in a list on the screen.
public interface IListItem {
  string ScreenName();
  ...
}

public class Animal: IListItem {
    // All animals will be called "Animal".
    public static string ScreenName() {
        return "Animal";
    }
....
}

public class Person: IListItem {

    private string name;

    // All persons will be called by their individual names.
    public string ScreenName() {
        return name;
    }

    ....

 }

6
好的,Java 8有这个功能(https://dev59.com/IX7aa4cB1Zd3GeqPvdii)。 - liang
1
看看如何将静态行为与继承或接口实现相结合:https://dev59.com/qWvXa4cB1Zd3GeqPH1Kx#13567309 - Olivier Jacot-Descombes
1
IListItem.ScreenName() => ScreenName()(使用C# 7语法)将通过调用静态方法显式实现接口方法。但是,当你加入继承时,情况会变得很棘手(你必须重新实现接口)。 - Jeroen Mostert
3
告诉大家一个好消息,等待已经结束了!C# 8.0现在有了静态接口方法:https://dotnetfiddle.net/Lrzy6y(虽然它们的工作方式与原帖作者想要的有点不同——你不必实现它们) - Jamie Twells
1
2022年开始的答案请参见:链接;C# 11支持接口上的静态抽象。 - minus one
28个回答

232

假设您正在问为什么不能这样做:

public interface IFoo {
    void Bar();
}

public class Foo: IFoo {
    public static void Bar() {}
}
这在语义上对我来说毫无意义。接口中指定的方法应该用于指定与对象交互的契约。静态方法不允许您与对象交互-如果您发现自己处于可以将实现变为静态的位置,则可能需要问自己该方法是否真正属于接口。
要实现您的示例,我会给Animal一个const属性,这仍然允许从静态上下文访问它,并在实现中返回该值。
public class Animal: IListItem {
    /* Can be tough to come up with a different, yet meaningful name!
     * A different casing convention, like Java has, would help here.
     */
    public const string AnimalScreenName = "Animal";
    public string ScreenName(){ return AnimalScreenName; }
}

对于更为复杂的情况,您可以始终声明另一个静态方法并委托给它。在尝试提供示例时,我想不出任何理由会同时在静态和实例上下文中执行非平凡的操作,因此我将为您省略FooBar blob,并将其视为可能不是一个好主意的指示。


7
一个完美的例子!但我不确定我理解你的推理。当然编译器可以被设计成查看静态成员,实例有一个地址表来实现它们的方法,难道静态方法不能被包含在这个表中吗? - Kramii
12
有一个情况下这可能会很有用。例如,我希望所有实现者实现一个接受XElement参数的GetInstance方法。我既不能在接口中将其定义为静态方法,也不能要求接口具有构造函数签名。 - oleks
25
许多人在双方都表达了自己的观点:从“这没有意义”到“这是个漏洞,但我希望你能够解决它。”(我认为有合理的使用情形,这也是我来到这里的原因。)作为一种解决方法,实例方法可以简单地委托给一个静态方法。 - harpo
5
你还可以将该方法作为扩展方法实现到基础接口上,如下所示:public static object MethodName(this IBaseClass base),放在一个静态类中。但不足之处是,与接口继承不同,这种方法不能强制/允许个别继承者对方法进行重写。 - Troy Alford
8
使用泛型会更加合理。例如:void Something<T>() where T : ISomeInterface { new T().DoSomething1(); T.DoSomething2(); } - Francisco Ryan Tolmasky I
显示剩余8条评论

182
我的(简化的)技术原因是静态方法不在vtable中,并且调用站点在编译时选择。这就是您无法具有覆盖或虚拟静态成员的相同原因。要了解更多详细信息,您需要CS研究生或编译器专家 - 我都不是。

对于政治原因,我将引用Eric Lippert(他是编译器专家,拥有滑铁卢大学(LinkedIn来源)的数学,计算机科学和应用数学学士学位):

…静态方法的核心设计原则,赋予它们名称的原则…是可以始终在编译时确定将调用哪个方法。也就是说,该方法可以仅通过对代码的静态分析来解析。

请注意,Lippert确实为所谓的类型方法留下了余地:

也就是说,与类型关联的方法(如静态方法),它不需要非空“this”参数(不像实例或虚拟方法),但被调用的方法将取决于T的构造类型(不像静态方法,必须在编译时确定)。

但尚未证明其有用性。


5
太好了,这就是我想要写的答案——我只是不知道具体实现细节。 - Chris Marasti-Georg
6
好的回答。我想要这个“类型方法”!它在许多场合都很有用(考虑一个类型/类的元数据)。 - Philip Daubmeier
26
这是正确答案。你将一个接口传给某人,他们需要知道如何调用方法。接口只是一个“虚拟方法表”。你的静态类没有这个。调用者无法知道如何调用方法。(在阅读这个答案之前,我认为C#只是过于追求严谨。现在我意识到这是一个技术限制,是由“接口的定义”所决定的。)其他人可能会说它是不良设计。但这并不是不良设计——而是一个技术上的限制。 - Ian Boyd
3
完全可以为静态类生成带有关联虚表的对象。观察Scala如何处理object并允许它们实现接口即可。 - Sebastian Graf
3
一个虚函数表(vtable)可以简单地指向静态实现,就像 Func<> 一样,但不仅包含一个指针,而是包含接口所需的所有方法的指针。在虚函数表中使用 static 类似于静态派发,为了“面向对象”的缘故,这种做法人为地限制了语言的功能。 - Sebastian Graf
显示剩余12条评论

103

这里大多数答案都没有抓住重点。多态不仅可以在实例之间使用,还可以在类型之间使用。当我们使用泛型时,通常会需要这样做。

假设我们在泛型方法中有一个类型参数,我们需要对其进行某些操作。我们不想实例化它,因为我们不知道构造函数。

例如:

Repository GetRepository<T>()
{
  //need to call T.IsQueryable, but can't!!!
  //need to call T.RowCount
  //need to call T.DoSomeStaticMath(int param)
}

...
var r = GetRepository<Customer>()

很不幸,我只能提供一些“丑陋”的替代方案:

  • 使用反射

    这种方法很丑陋,打破了接口和多态的设计思想。

  • 创建完全独立的工厂类

    这可能会大大增加代码的复杂性。例如,如果我们正在尝试对领域对象进行建模,则每个对象都需要另一个仓库类。

  • 实例化,然后调用所需的接口方法

    即使我们控制用作通用参数的类的源代码,这也可能难以实现。原因是,例如,我们可能需要实例仅处于“连接到数据库”的已知状态。

示例:

public class Customer 
{
  //create new customer
  public Customer(Transaction t) { ... }

  //open existing customer
  public Customer(Transaction t, int id) { ... }

  void SomeOtherMethod() 
  { 
    //do work...
  }
}

为了使用实例化来解决静态接口问题,我们需要执行以下步骤:
public class Customer: IDoSomeStaticMath
{
  //create new customer
  public Customer(Transaction t) { ... }

  //open existing customer
  public Customer(Transaction t, int id) { ... }

  //dummy instance
  public Customer() { IsDummy = true; }

  int DoSomeStaticMath(int a) { }

  void SomeOtherMethod() 
  { 
    if(!IsDummy) 
    {
      //do work...
    }
  }
}

这显然很丑,而且还不必要地增加了所有其他方法的代码复杂度。显然,这也不是一个优雅的解决方案!


39
+1表示赞同“这里的大多数回答似乎都错过了整个重点”,令人难以置信的是,似乎几乎所有的回答都回避了问题的核心,而沉迷于大多无用的言辞中…… - user610650
5
@Chris 这里有一个具体的例子让我再次遇到了这个限制。我想为类添加一个"IResettable"接口,以表示它们在静态变量中缓存某些数据,可以通过网站管理员重置(例如,订单类别列表、一组增值税率、从外部API检索的类别列表),以减少数据库和外部API请求。这显然会使用一个静态方法"reset"。这使得我可以自动检测哪些类可以被重置。我仍然可以做到这一点,但是该方法没有得到强制执行或在IDE中自动添加,只能靠希望。 - mattmanser
8
@Chris 我不同意,这太过繁琐了。当增加更多的架构是“最好”的解决方案时,这通常意味着语言存在缺陷。还记得所有的模式吗?自从C#引入泛型和匿名方法后,没人再谈论它们了。 - mattmanser
3
你能否使用类似于 "where T : IQueryable, T : IDoSomeStaticMath" 的语法呢? - Roger Willcocks
2
@ChrisMarasti-Georg:一个有点不太干净但有趣的方法是使用这个小构造:public abstract class DBObject<T> where T : DBObject<T>, new(),然后让所有DB类都继承自DBObject<T>。然后你可以在抽象超类中将按键检索设置为具有T返回类型的静态函数,使该函数创建一个新的T对象,然后调用受保护的String GetRetrieveByKeyQuery()(在超类上定义为抽象)来获取要执行的实际查询。虽然这可能有点偏题。 - Nyerguds
显示剩余9条评论

20
我知道这是一个老问题,但很有趣。这个示例并不是最好的。如果您展示一个使用案例,它会更清晰:
string DoSomething<T>() where T:ISomeFunction
{
  if (T.someFunction())
    ...
}
仅仅能够让静态方法实现接口并不能达到您想要的效果;需要的是将静态成员作为接口的一部分。我可以想象许多用例,特别是当涉及到创建东西时。我可以提供两种可能有用的方法:
  1. 创建一个静态泛型类,其类型参数将是您将传递给上面DoSomething的类型。每个此类的变化都将具有一个或多个静态成员,其中包含与该类型相关的内容。可以通过使感兴趣的每个类调用“注册信息”例程来提供此信息,也可以在运行类变体的静态构造函数时使用Reflection获取信息。我相信后一种方法是像Comparer<T>.Default()这样的东西所使用的方法。
  2. 对于每个感兴趣的类T,定义一个实现IGetWhateverClassInfo<T>的类或结构,并满足“new”约束条件。该类实际上不会包含任何字段,但将具有返回具有类型信息的静态字段的静态属性。将该类或结构的类型传递给相关的泛型例程,该例程将能够创建一个实例并使用它来获取有关其他类的信息。如果您使用类来完成此目的,则应按上面指示定义一个静态泛型类,以避免每次都构造新的描述符对象实例。如果您使用结构,则实例化成本应为零,但每个不同的结构类型都需要对DoSomething例程进行不同的扩展。
这些方法都不是很吸引人。另一方面,我希望如果CLR中存在提供这种功能的机制,.NET将允许指定参数化的“new”约束(因为知道类是否具有具有特定签名的构造函数似乎在难度上与知道它是否具有具有特定签名的静态方法相当)。

16

我猜是近视眼。

最初设计时,接口只打算与类的实例一起使用。

IMyInterface val = GetObjectImplementingIMyInterface();
val.SomeThingDefinedinInterface();

只有将接口作为泛型的约束引入后,将静态方法添加到接口中才具有实际用途。

(回复评论:) 我认为现在更改需要更改CLR,这会导致与现有程序集不兼容。


是在泛型的背景下,我第一次遇到了这个问题,但我想知道在其他情况下包括接口中的静态方法是否也有用?有没有理由不能改变事物? - Kramii
我也遇到了这个问题,当我在实现一个泛型类时,它需要参数类型以便在创建自身时传入一些参数。由于 new() 不能接受任何参数,你解决这个问题了吗,Kramii? - Tom
1
@Kramii:静态API的契约。我不需要对象实例,只需要一个特定签名的保证,例如IMatrixMultiplier或ICustomSerializer。将Funcs/Actions/Delegates作为类成员可以解决问题,但在我看来,有时这似乎有些过度设计,并且可能会让没有经验的人试图扩展API感到困惑。 - lightw8

15

在接口代表“合同”的情况下,静态类实现接口似乎是相当合理的。

上述论点似乎都忽略了这个关于合同的观点。


3
我完全同意这个简单而有效的答案。在“静态接口”中有趣的地方在于它将代表一个约定。也许它不应该被称为“静态接口”,但我们仍然缺少一个结构。例如,请查看.NET关于ICustomMarshaler接口的官方文档。它要求实现它的类“添加一个名为GetInstance的静态方法,该方法接受一个字符串参数,并具有ICustomMarshaler的返回类型”。 这确实看起来像是一个用简单英语定义的“静态接口”,虽然我更喜欢用C#来表达它... - Simon Mourier
@SimonMourier 那份文档本应该写得更清楚一些,但是你误解了它。要求GetInstance静态方法的不是ICustomMarshaler接口,而是[MarshalAs]代码属性。他们使用工厂模式来允许属性获取所附加的marshaler实例。不幸的是,他们完全忘记在MarshalAs文档页面上包含GetInstance要求的说明(它仅显示使用内置marshaling实现的示例)。 - Scott Gartner
@ScottGartner - 不明白你的意思。https://msdn.microsoft.com/en-us/library/system.runtime.interopservices.icustommarshaler.aspx清楚地说明了: “除了实现ICustomMarshaler接口之外,自定义封送程序必须实现一个名为GetInstance的静态方法,该方法接受一个字符串作为参数,并具有ICustomMarshaler类型的返回类型。公共语言运行时的COM互操作层将调用此静态方法来实例化自定义封送程序的实例。” 这绝对是一个静态的契约定义。 - Simon Mourier

14

接口规定对象的行为。

静态方法不规定对象的行为,但会以某种方式影响对象的行为。


73
抱歉,我不确定那是否正确!接口并没有规定行为。接口定义了一组命名操作。两个类可以实现接口方法以完全不同的方式进行行为。因此,接口根本不规定行为,它只是由实现它的类来定义。 - Scott Langham
1
希望你不认为我太挑剔了,但我认为这是任何学习面向对象编程的人都应该理解的重要区别。 - Scott Langham
4
接口应该规定一个契约,其中包括行为和呈现。这就是为什么改变接口调用的行为是不可取的,因为二者都应该是固定的。如果你有一个接口,其中调用的行为不同(例如IList.Add执行了删除操作),那就不正确了。 - Jeff Yates
15
是的,如果定义一个与名称不一致的方法来行事,你会感到头脑扭曲。但是,如果有IAlertService.GetAssistance(),它的行为可以是闪光灯闪烁、响起警报声,或用棍子戳眼睛。 - Scott Langham
1
从非特定编程语言的角度来阐述:接口是抽象类型。类型是描述实例或值的东西。静态类从理论上讲并不是真正的类型,因为它们无法被实例化。因此,静态类应该/不能具有接口。 - awdz9nld
显示剩余4条评论

10

接口的目的是允许多态性,可以传递任何已定义为实现该定义接口的类的实例... 保证在多态调用中,代码能够找到您正在调用的方法。因此,允许静态方法实现接口是没有意义的。

那么你如何调用它呢?


public interface MyInterface { void MyMethod(); }
public class MyClass: MyInterface
{
    public static void MyMethod() { //Do Something; }
}

 // inside of some other class ...  
 // How would you call the method on the interface ???
    MyClass.MyMethod();  // this calls the method normally 
                         // not through the interface...

    // This next fails you can't cast a classname to a different type... 
    // Only instances can be Cast to a different type...
    MyInterface myItf = MyClass as MyInterface;  

1
其他编程语言(例如Java)允许从对象实例调用静态方法,尽管您会收到警告,应该从静态上下文中调用它们。 - Chris Marasti-Georg
在 .Net 中,允许从实例调用静态方法。这是一件不同的事情。如果一个静态方法实现了一个接口,你可以在没有实例的情况下调用它。这就是不合理的。请记住,你不能在接口中放置实现,它必须在类中。因此,如果定义了五个不同的类来实现该接口,并且每个类都有这个静态方法的不同实现,编译器会使用哪个呢? - Charles Bretana
1
在泛型的情况下,是传入类型的泛型。我认为在静态方法中使用接口有很多用处(如果你愿意,让我们称它们为“静态接口”,并允许类定义静态接口和实例接口)。所以,如果我有Whatever<T>() where T:IMyStaticInterface,我可以调用Whatever<MyClass>()并在Whatever<T>()实现内部调用T.MyStaticMethod()而无需实例。要调用的方法将在运行时确定。您可以通过反射来完成这项工作,但没有“契约”强制执行。 - Jcl
“多态”这个术语的含义远不止通过vtable分发虚函数调用。即使函数重载也是一种多态,例如在C++中的模板元编程允许以无法在C#中实现的方式使用此功能(并且即使在仍然静态分派的情况下也可以这样做)。将所有int.TryParseshort.TryParse等方法组合成通用的IParse.TryParse,就可以在通用类型T where T:IParse上调用T.TryParse。在C#中是不可能的,除非使用hackery,但在C++(以及其他一些语言)中可以实现同等的效果。 - dialer

6

实际上,确实如此。

截至2022年中期,C#的当前版本已经完全支持所谓的静态抽象成员:

interface INumber<T>
{
    static abstract T Zero { get; }
}

struct Fraction : INumber<Fraction>
{
    public static Fraction Zero { get; } = new Fraction();

    public long Numerator;
    public ulong Denominator;

    ....
}

请注意,根据您的Visual Studio版本和已安装的.NET SDK版本,您可能需要更新其中至少一个(或两个都要更新),或者您需要启用预览功能(请参见在Visual Studio中使用预览功能和预览语言)。
更多信息请查看:

4
关于非泛型环境中使用的静态方法,我同意不允许它们在接口中定义。因为无论如何,如果你有一个对接口的引用,你也无法调用它们。然而,在泛型环境下使用接口而不是多态环境造成了语言设计的根本性缺陷。在这种情况下,接口实际上并不是接口,而是一种约束条件。由于C#没有约束条件的概念,除了在接口中定义,所以会导致缺少重要功能。举个例子:
T SumElements<T>(T initVal, T[] values)
{
    foreach (var v in values)
    {
        initVal += v;
    }
}

这里不存在多态,泛型使用对象的实际类型并调用+=运算符,但由于无法确定该运算符是否存在,因此失败了。简单的解决方案是在约束中指定它;但问题在于运算符是静态的,静态方法不能在接口中,而(这就是问题所在)约束表示为接口。
C#需要一个真正的约束类型,所有接口都将成为约束,但不是所有约束都将是接口,然后您可以这样做:
constraint CHasPlusEquals
{
    static CHasPlusEquals operator + (CHasPlusEquals a, CHasPlusEquals b);
}

T SumElements<T>(T initVal, T[] values) where T : CHasPlusEquals
{
    foreach (var v in values)
    {
        initVal += v;
    }
}

关于实现所有数字类型的IArithmetic,已经有很多讨论了,但是有人担心效率问题,因为约束并不是一种多态构造,所以一个CArithmetic约束可以解决这个问题。


1
请记住,这不是模板,而是泛型,与C++模板不同。 - John Saunders
@JohnSaunders:泛型不是模板,我不知道它们如何能够合理地与静态绑定的运算符一起工作,但在泛型上下文中,有许多情况下可以指定 T 应该具有 静态 成员(例如生成 T 实例的工厂)。即使没有运行时更改,我认为可以定义约定和辅助方法,以便语言可以以高效且可互操作的方式实现这种东西作为语法糖。如果对于每个具有虚拟静态方法的接口都有一个辅助类... - supercat
如果将T限制为IFoo<T>,则T.CreateThing(paramString)将变为IFoo_Helper<T>.CreateThing(paramString)。在实现IFoo<Fred>的类Fred中定义static Fred IFoo.CreateThing(string st)会标记该方法,以便IFoo_Helper方法可以通过反射找到它,第一次需要时[后续访问将通过缓存的委托进行]。 - supercat
@supercat:我想我确实见过一些类似这样的例子。唯一阻止某个特定方法成为“静态”的是它实现了接口的成员。 - John Saunders
1
@JohnSaunders:问题不在于该方法实现了一个接口,而是编译器无法在没有对象实例的情况下选择要调用的虚拟方法。解决方案是通过调用通用静态类来分派“静态接口”调用,而不是使用虚拟分派(这将无法工作)。基于类型的通用分派不需要具有实例,只需要具有类型即可。 - supercat
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接