为啥我不能在 C# 中使用抽象静态方法?

Posted

技术标签:

【中文标题】为啥我不能在 C# 中使用抽象静态方法?【英文标题】:Why can't I have abstract static methods in C#?为什么我不能在 C# 中使用抽象静态方法? 【发布时间】:2010-09-05 10:05:56 【问题描述】:

我最近一直在使用providers,我遇到了一个有趣的情况,我想要一个具有抽象静态方法的抽象类。我阅读了一些关于该主题的帖子,这有点道理,但是有一个很好的清晰解释吗?

【问题讨论】:

请让这些保持开放,以便将来改进。 我认为问题在于 C# 需要另一个关键字,正是这种情况。您需要一个返回值仅取决于调用它的类型的方法。如果所述类型未知,则不能将其称为“静态”。但是一旦类型变得已知,它就会变成静态的。 “未解决的静态”就是这个想法——它还不是静态的,但是一旦我们知道接收类型,它就会是。这是一个非常好的概念,这就是程序员不断要求它的原因。但它并不完全符合设计师对语言的看法。 @WilliamJockusch 接收类型是什么意思?如果我调用 BaseClass.StaticMethod() 那么 BaseClass 是它可以用来做出决定的唯一类型。但是在这个级别它是抽象的,因此无法解析该方法。如果您改为很好地调用 DerivedClass.StaticMethod,则基类是无关紧要的。 在基类中,方法未解析,不能使用。您需要一个派生类型或一个对象(这又将具有派生类型)。您应该能够调用 baseClassObject.Method() 或 DerivedClass.Method()。你不能调用 BaseClass.Method() 因为那没有给你类型。 How to implement virtual static properties?的可能重复 【参考方案1】:

静态方法没有实例化,它们只是在没有对象引用的情况下可用。

通过类名调用静态方法,而不是通过对象引用,调用它的中间语言(IL)代码将通过定义它的类名调用抽象方法,不一定您使用的类的名称。

让我举个例子。

使用以下代码:

public class A

    public static void Test()
    
    


public class B : A


如果你调用 B.Test,像这样:

class Program

    static void Main(string[] args)
    
        B.Test();
    

那么Main方法里面的实际代码如下:

.entrypoint
.maxstack 8
L0000: nop 
L0001: call void ConsoleApplication1.A::Test()
L0006: nop 
L0007: ret 

如您所见,调用是针对 A.Test,因为定义它的是 A 类,而不是 B.Test,即使您可以这样编写代码。

如果你有 类类型,就像在 Delphi 中一样,你可以创建一个引用类型而不是对象的变量,你会更多地使用虚拟方法,因此抽象静态方法(以及构造函数),但它们不可用,因此静态调用在 .NET 中是非虚拟的。

我意识到 IL 设计者可以允许编译代码以调用 B.Test,并在运行时解析调用,但它仍然不是虚拟的,因为您仍然需要编写某种类名在那里。

虚拟方法以及抽象方法仅在您使用一个变量时才有用,该变量在运行时可以包含许多不同类型的对象,因此您想为当前对象调用正确的方法变量。对于静态方法,无论如何您都需要通过类名,因此要调用的确切方法在编译时是已知的,因为它不能也不会改变。

因此,虚拟/抽象静态方法在 .NET 中不可用。

【讨论】:

结合 C# 中的运算符重载方式,不幸的是,这消除了要求子类为给定运算符重载提供实现的可能性。 我不觉得这个答案非常有用,因为Test() 的定义在A 中,而不是抽象的并且可能在B 中定义。\ 通用类型参数有效地表现为不可持久的“类型”变量,虚拟静态方法在这种情况下可能很有用。例如,如果有一个带有虚拟静态 CreateFromDescription 工厂方法的 Car 类型,那么接受 Car 约束的泛型类型 T 的代码可以调用 T.CreateFromDescription 来生成 T 类型的汽车.如果定义这种方法的每个类型都拥有一个嵌套类泛型的静态单例实例,该类泛型拥有虚拟的“静态”方法,那么 CLR 可以很好地支持这样的构造。【参考方案2】:

静态方法不能被继承或覆盖,这就是为什么它们不能是抽象的。由于静态方法是在类的类型而不是实例上定义的,因此必须在该类型上显式调用它们。所以当你想调用一个子类的方法时,你需要使用它的名字来调用它。这使得继承无关紧要。

假设您可以暂时继承静态方法。想象一下这种情况:

public static class Base

    public static virtual int GetNumber()  return 5; 


public static class Child1 : Base

    public static override int GetNumber()  return 1; 


public static class Child2 : Base

    public static override int GetNumber()  return 2; 

如果调用 Base.GetNumber(),会调用哪个方法?返回哪个值?很容易看出,如果不创建对象实例,继承是相当困难的。没有继承的抽象方法只是没有主体的方法,所以不能被调用。

【讨论】:

鉴于您的情况,我会说 Base.GetNumber() 将返回 5; Child1.GetNumber() 返回 1; Child2.GetNumber() 返回 2;你能证明我错了,帮助我理解你的推理吗?谢谢 为什么 Base.GetNumber() 会返回除 5 以外的任何值?它是基类中的一个方法——那里只有一个选项。 @ArtemRussakovskii:假设有int DoSomething<T>() where T:Base return T.GetNumber();。如果DoSomething<Base>() 可以返回五个,而DoSomething<Child2>() 将返回两个,这似乎很有用。这种能力不仅对玩具示例有用,而且对class Car public static virtual Car Build(PurchaseOrder PO); 之类的东西也有用,其中从Car 派生的每个类都必须定义一个可以在给定采购订单的情况下构建实例的方法。 非静态继承存在完全相同的“问题”。 这根本不是一个有效的答案。正如其他人所说, Base.GetNumber() 必须始终返回 5。孩子 1 和 2 必须分别返回 1 和 2。没有其他意义。【参考方案3】:

另一位受访者 (McDowell) 说多态性仅适用于对象实例。那应该是合格的;有些语言确实将类视为“类”或“元类”类型的实例。这些语言确实支持实例和类(静态)方法的多态性。

C# 和之前的 Java 和 C++ 一样,不是这样的语言; static 关键字明确用于表示该方法是静态绑定的,而不是动态/虚拟的。

【讨论】:

【参考方案4】:

这是一种肯定需要继承静态字段和方法的情况:

abstract class Animal

  protected static string[] legs;

  static Animal() 
    legs=new string[0];
  

  public static void printLegs()
  
    foreach (string leg in legs) 
      print(leg);
    
  



class Human: Animal

  static Human() 
    legs=new string[] "left leg", "right leg";
  



class Dog: Animal

  static Dog() 
    legs=new string[] "left foreleg", "right foreleg", "left hindleg", "right hindleg";
  



public static void main() 
  Dog.printLegs();
  Human.printLegs();



//what is the output?
//does each subclass get its own copy of the array "legs"?

【讨论】:

不,数组“legs”只有一个实例。输出是不确定的,因为您不知道将调用静态构造函数的顺序(实际上根本无法保证会调用基类静态构造函数)。 “需要”是一个相当绝对的术语,而“欲望”可能更准确。 legs 应该是一个静态抽象属性。【参考方案5】:

为了补充前面的解释,静态方法调用在编译时绑定到特定方法,从而排除了多态行为。

【讨论】:

C# 是静态类型的;据我了解,对多态方法的调用也在编译时绑定——也就是说,CLR 不会在运行时解析要调用的方法。 那么您认为多态性究竟如何在 CLR 上起作用?您的解释刚刚排除了虚拟方法调度。 这并不是一个真正有用的评论。我邀请(用“我理解”)有用的话语,想也许你可以提供更多的内容 - 看到人们来到这里寻找答案而不是侮辱。虽然,我似乎也犯了同样的错误——我的意思是上面的评论是一个问题:C# 不会在编译时评估这些东西吗? 抱歉,我并没有侮辱的意思(尽管我承认我的反应有点快;-)。我的问题的重点是,如果你有这些类: class Base public virtual void Method(); 类派生:基础公共覆盖无效方法();并这样写: Base instance = new Derived();实例.方法();调用站点上的编译时类型信息是我们有一个 Base 实例,而实际实例是 Derived。因此编译器无法解析要调用的确切方法。相反,它发出一个“callvirt”IL指令,告诉运行时调度.. 谢谢老兄,信息量大!猜猜我已经把潜入 IL 的时间推迟了,祝我好运。【参考方案6】:

我们实际上覆盖了静态方法(在 delphi 中),它有点难看,但它可以很好地满足我们的需求。

我们使用它以便类可以在没有类实例的情况下拥有其可用对象的列表,例如,我们有一个看起来像这样的方法:

class function AvailableObjects: string; override;
begin
  Result := 'Object1, Object2';
end; 

这很丑但很有必要,这样我们就可以实例化需要的东西,而不是为了搜索可用对象而实例化所有类。

这是一个简单的例子,但应用程序本身是一个客户端-服务器应用程序,它在一个服务器中拥有所有可用的类,以及多个不同的客户端,它们可能不需要服务器拥有的所有东西,也永远不需要对象实例。

因此,这比为每个客户端使用不同的服务器应用程序更容易维护。

希望这个例子很清楚。

【讨论】:

【参考方案7】:

这个问题已有 12 年历史,但仍需要给出更好的答案。正如 cmets 中很少有人指出的那样,与所有答案所假装的相反,在 C# 中使用静态抽象方法肯定是有意义的。正如哲学家丹尼尔丹尼特所说,想象力的失败并不是对必然性的洞察。没有意识到 C# 不仅仅是一种 OOP 语言,这是一个常见的错误。对给定概念的纯 OOP 视角会导致受限且在当前情况下被误导的检查。多态性不仅仅是对多态性进行细分:它还包括参数多态性(也称为泛型编程),而 C# 已经支持这一点很长时间了。在这个额外的范例中,抽象类(和大多数类型)不仅用于为实例提供类型。它们也可以用作泛型参数的界限;多年来,某些语言(例如 Haskell,以及最近的 Scala、Rust 或 Swift)的用户已经理解的东西。

在这种情况下,您可能想做这样的事情:

void Catch<TAnimal>() where TAnimal : Animal

    string scientificName = TAnimal.ScientificName; // abstract static property
    Console.WriteLine($"Let's catch some scientificName");
    …

在这里表达可以由子类专门化的静态成员的能力完全有意义

不幸的是,C# 不允许抽象静态成员,但我想提出一种可以很好地模拟它们的模式。这种模式并不完美(它对继承施加了一些限制),但据我所知它是类型安全的。

主要思想是将抽象伴随类(此处为SpeciesFor&lt;TAnimal&gt;)与应包含静态抽象成员的类(此处为Animal)相关联:

public abstract class SpeciesFor<TAnimal> where TAnimal : Animal

    public static SpeciesFor<TAnimal> Instance  get  …  

    // abstract "static" members

    public abstract string ScientificName  get; 
    
    …


public abstract class Animal  … 

现在我们想完成这项工作:

void Catch<TAnimal>() where TAnimal : Animal

    string scientificName = SpeciesFor<TAnimal>.Instance.ScientificName;
    Console.WriteLine($"Let's catch some scientificName");
    …

当然我们有两个问题要解决:

    我们如何确保Animal 子类的实现者向该子类提供SpeciesFor&lt;TAnimal&gt; 的特定实例? SpeciesFor&lt;TAnimal&gt;.Instance 属性如何检索此信息?

这是我们如何解决 1:

public abstract class Animal<TSelf> where TSelf : Animal<TSelf>

    private Animal(…) 
    
    public abstract class OfSpecies<TSpecies> : Animal<TSelf>
        where TSpecies : SpeciesFor<TSelf>, new()
    
        protected OfSpecies(…) : base(…)  
    
    
    …

通过将Animal&lt;TSelf&gt; 的构造函数设为私有,我们确保它的所有子类也是内部类Animal&lt;TSelf&gt;.OfSpecies&lt;TSpecies&gt; 的子类。所以这些子类必须指定一个TSpecies 类型,它有一个new() 边界。

对于 2,我们可以提供以下实现:

public abstract class SpeciesFor<TAnimal> where TAnimal : Animal<TAnimal>

    private static SpeciesFor<TAnimal> _instance;

    public static SpeciesFor<TAnimal> Instance => _instance ??= MakeInstance();

    private static SpeciesFor<TAnimal> MakeInstance()
    
        Type t = typeof(TAnimal);
        while (true)
        
            if (t.IsConstructedGenericType
                    && t.GetGenericTypeDefinition() == typeof(Animal<>.OfSpecies<>))
                return (SpeciesFor<TAnimal>)Activator.CreateInstance(t.GenericTypeArguments[1]);
            t = t.BaseType;
            if (t == null)
                throw new InvalidProgramException();
        
    

    // abstract "static" members

    public abstract string ScientificName  get; 
    
    …

我们如何知道MakeInstance() 中的反射代码永远不会抛出?正如我们已经说过的,Animal&lt;TSelf&gt; 层次结构中的几乎所有类也是Animal&lt;TSelf&gt;.OfSpecies&lt;TSpecies&gt; 的子类。所以我们知道,对于这些类,必须提供特定的TSpecies。由于约束: new(),这种类型也必然是可构造的。但这仍然遗漏了像 Animal&lt;Something&gt; 这样没有关联物种的抽象类型。现在我们可以说服自己奇怪地重复出现的模板模式where TAnimal : Animal&lt;TAnimal&gt; 使得不可能将SpeciesFor&lt;Animal&lt;Something&gt;&gt;.Instance 写成Animal&lt;Something&gt; 类型永远不是Animal&lt;Animal&lt;Something&gt;&gt; 的子类型。

等等:

public class CatSpecies : SpeciesFor<Cat>

    // overriden "static" members

    public override string ScientificName => "Felis catus";
    public override Cat CreateInVivoFromDnaTrappedInAmber()  … 
    public override Cat Clone(Cat a)  … 
    public override Cat Breed(Cat a1, Cat a2)  … 


public class Cat : Animal<Cat>.OfSpecies<CatSpecies>

    // overriden members

    public override string CuteName  get  …  


public class DogSpecies : SpeciesFor<Dog>

    // overriden "static" members

    public override string ScientificName => "Canis lupus familiaris";
    public override Dog CreateInVivoFromDnaTrappedInAmber()  … 
    public override Dog Clone(Dog a)  … 
    public override Dog Breed(Dog a1, Dog a2)  … 


public class Dog : Animal<Dog>.OfSpecies<DogSpecies>

    // overriden members

    public override string CuteName  get  …  


public class Program

    public static void Main()
    
        ConductCrazyScientificExperimentsWith<Cat>();
        ConductCrazyScientificExperimentsWith<Dog>();
        ConductCrazyScientificExperimentsWith<Tyranosaurus>();
        ConductCrazyScientificExperimentsWith<Wyvern>();
    
    
    public static void ConductCrazyScientificExperimentsWith<TAnimal>()
        where TAnimal : Animal<TAnimal>
    
        // Look Ma! No animal instance polymorphism!
        
        TAnimal a2039 = SpeciesFor<TAnimal>.Instance.CreateInVivoFromDnaTrappedInAmber();
        TAnimal a2988 = SpeciesFor<TAnimal>.Instance.CreateInVivoFromDnaTrappedInAmber();
        TAnimal a0400 = SpeciesFor<TAnimal>.Instance.Clone(a2988);
        TAnimal a9477 = SpeciesFor<TAnimal>.Instance.Breed(a0400, a2039);
        TAnimal a9404 = SpeciesFor<TAnimal>.Instance.Breed(a2988, a9477);
        
        Console.WriteLine(
            "The confederation of mad scientists is happy to announce the birth " +
            $"of a9404.CuteName, our new SpeciesFor<TAnimal>.Instance.ScientificName.");
    

这种模式的一个限制是(据我所知)不可能以令人满意的方式扩展类层次结构。例如,我们不能引入与 MammalClass 同伴关联的中介 Mammal 类。另一个是它不适用于比抽象类更灵活的接口中的静态成员。

【讨论】:

【参考方案8】:

使用.NET 6 / C# preview,您可以使用“接口中的静态抽象成员”完全做到这一点。

(在编写代码时编译成功,但某些 IDE 无法突出显示代码)

using System;

namespace StaticAbstractTesting

    public interface ISomeAbstractInterface
    
        public abstract static string CallMe();
    

    public class MyClassA : ISomeAbstractInterface
    
        static string ISomeAbstractInterface.CallMe()
        
            return "You called ClassA";
        
    

    public class MyClassB : ISomeAbstractInterface
    
        static string ISomeAbstractInterface.CallMe()
        
            return "You called ClassB";
        
    

    public class Program
    

        public static void Main(string[] args)
        
            UseStaticClassMethod<MyClassA>();
            UseStaticClassMethod<MyClassB>();
        

        public static void UseStaticClassMethod<T>() where T : ISomeAbstractInterface
        
            Console.WriteLine($"typeof(T).Name.CallMe() result: T.CallMe()");
        
    


资源:

https://docs.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/6.0/static-abstract-interface-methods https://github.com/dotnet/csharplang/issues/4436

【讨论】:

【参考方案9】:

抽象方法是隐式虚拟的。抽象方法需要一个实例,但静态方法没有实例。因此,您可以在抽象类中拥有静态方法,但不能是静态抽象(或抽象静态)。

【讨论】:

-1 虚拟方法不需要实例,除非是设计使然。而且您实际上并没有解决这个问题,而是转移了它。【参考方案10】:

目前在 C# 10 中作为预览功能提供。

【讨论】:

以上是关于为啥我不能在 C# 中使用抽象静态方法?的主要内容,如果未能解决你的问题,请参考以下文章

如何强制继承类在 C# 中实现静态方法?

C#,实现类似方法的“静态抽象”

为啥我不能在接口中声明静态方法?

接口和抽象类

为啥我们不能在(非静态)内部类(Java 16 之前)中使用静态方法?

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