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

lom*_*axx 175 .net c# language-design

我最近一直与提供商合作,我遇到了一个有趣的情况,我希望有一个抽象的静态方法.我读了一些关于这个主题的帖子,这有点意义,但有一个很清楚的解释吗?

ang*_*son 153

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

对静态方法的调用是通过类名完成的,而不是通过对象引用完成的,调用它的IL代码将通过定义它的类的名称调用抽象方法,不一定是您使用的类的名称.

让我举个例子.

使用以下代码:

public class A
{
    public static void Test()
    {
    }
}

public class B : A
{
}
Run Code Online (Sandbox Code Playgroud)

如果你打电话给B.Test,就像这样:

class Program
{
    static void Main(string[] args)
    {
        B.Test();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

.entrypoint
.maxstack 8
L0000: nop 
L0001: call void ConsoleApplication1.A::Test()
L0006: nop 
L0007: ret 
Run Code Online (Sandbox Code Playgroud)

正如你所看到的那样,调用A.Test,因为它是定义它的A类,而不是B.Test,即使你可以用这种方式编写代码.

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

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

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

因此,.NET中不提供虚拟/抽象静态方法.

  • 我发现这个答案非常有用,因为`Test()`的定义在`A`而不是抽象,可能在`B`中定义. (20认同)
  • 通用类型参数有效地表现为不可持久的"类型"变量,并且虚拟静态方法在这种上下文中可能是有用的.例如,如果有一个带有虚拟静态`CreateFromDescription`工厂方法的`Car`类型,那么接受`Car`约束泛型类型`T`的代码可以调用`T.CreateFromDescription`来生成类型为`的汽车. T`.如果定义了这样一个方法的每个类型都拥有一个嵌套类泛型的静态单例实例来保存虚拟"静态"方法,那么这样的构造可以在CLR中得到很好的支持. (5认同)
  • 结合在C#中完成运算符重载的方式,遗憾的是这消除了要求子类为给定的运算符重载提供实现的可能性. (4认同)

Dav*_*ier 43

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

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

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; }
}
Run Code Online (Sandbox Code Playgroud)

如果你调用Base.GetNumber(),将调用哪个方法?返回哪个值?很容易看出,如果不创建对象实例,继承就相当困难.没有继承的抽象方法只是没有主体的方法,因此无法调用.

  • 为什么在世界上Base.GetNumber()会返回除5之外的其他内容?它是基类中的一种方法 - 那里只有一个选项. (53认同)
  • 鉴于你的情况,我会说Base.GetNumber()会返回5; Child1.GetNumber()返回1; Child2.GetNumber()返回2; 你能证明我错了,帮助我理解你的理由吗?谢谢 (31认同)
  • 这根本不是一个有效的答案。正如其他人所说,Base.GetNumber() 必须始终返回 5。孩子 1 和 2 必须分别返回 1 和 2。其他没有任何意义。 (7认同)
  • @ArtemRussakovskii:假设有一个`int DoSomething <T>()其中T:Base {return T.GetNumber();}`.如果`DoSomething <Base>()`可以返回5,而DoSomething <Child2>()`将返回2,这似乎很有用.这种能力将不仅对玩具的例子时有用,而且对于像`类汽车{公共静态虚拟租车生成(PurchaseOrder的PO);}`,其中每类从`Car`派生必须定义它可以建立一个方法给定采购订单的实例. (4认同)
  • 与非静态继承完全相同的"问题". (3认同)

0BL*_*BLU 19

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

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

SharpLab 演示

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()}");
        }
    }
}

Run Code Online (Sandbox Code Playgroud)

由于这是运行时的重大变化,因此生成的 IL 代码看起来也非常干净,这意味着这不仅仅是语法糖。

public static void UseStaticClassMethodSimple<T>() where T : ISomeAbstractInterface {
IL_0000: constrained. !!T
IL_0006: call string StaticAbstractTesting.ISomeAbstractInterface::CallMe()
IL_000b: call void [System.Console]System.Console::WriteLine(string)
IL_0010: ret
}
Run Code Online (Sandbox Code Playgroud)

资源:

  • 目前[计划在 C#11 中正确发布](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-11)。 (2认同)

Chr*_*son 17

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

C#,就像之前的Java和C++一样,不是这样的语言; 所述static关键字被明确地用于表示,该方法是静态结合,而不是动态的/虚拟的.


Con*_*rph 15

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

\n

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

\n
void Catch<TAnimal>() where TAnimal : Animal\n{\n    string scientificName = TAnimal.ScientificName; // abstract static property\n    Console.WriteLine($"Let\'s catch some {scientificName}");\n    \xe2\x80\xa6\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

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

\n

主要思想是将抽象泛型伴随类(此处SpeciesFor<TAnimal>)与应包含静态抽象成员的类(此处Animal)相关联:

\n
public abstract class SpeciesFor<TAnimal> where TAnimal : Animal\n{\n    public static SpeciesFor<TAnimal> Instance { get { \xe2\x80\xa6 } }\n\n    // abstract "static" members\n\n    public abstract string ScientificName { get; }\n    \n    \xe2\x80\xa6\n}\n\npublic abstract class Animal { \xe2\x80\xa6 }\n
Run Code Online (Sandbox Code Playgroud)\n

现在我们想让这个工作:

\n
void Catch<TAnimal>() where TAnimal : Animal\n{\n    string scientificName = SpeciesFor<TAnimal>.Instance.ScientificName;\n    Console.WriteLine($"Let\'s catch some {scientificName}");\n    \xe2\x80\xa6\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
    \n
  1. 我们如何确保 的子类的实现者为该子类Animal提供特定的实例?SpeciesFor<TAnimal>
  2. \n
  3. 物业如何SpeciesFor<TAnimal>.Instance检索此信息?
  4. \n
\n

解决1的方法如下:

\n
public abstract class Animal<TSelf> where TSelf : Animal<TSelf>\n{\n    private Animal(\xe2\x80\xa6) {}\n    \n    public abstract class OfSpecies<TSpecies> : Animal<TSelf>\n        where TSpecies : SpeciesFor<TSelf>, new()\n    {\n        protected OfSpecies(\xe2\x80\xa6) : base(\xe2\x80\xa6) { }\n    }\n    \n    \xe2\x80\xa6\n}\n
Run Code Online (Sandbox Code Playgroud)\n

通过将构造函数设置为Animal<TSelf>private,我们可以确保它的所有子类也是内部类的子类Animal<TSelf>.OfSpecies<TSpecies>。所以这些子类必须指定一个TSpeciesnew()边界的类型。

\n

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

\n
public abstract class SpeciesFor<TAnimal> where TAnimal : Animal<TAnimal>\n{\n    private static SpeciesFor<TAnimal> _instance;\n\n    public static SpeciesFor<TAnimal> Instance => _instance ??= MakeInstance();\n\n    private static SpeciesFor<TAnimal> MakeInstance()\n    {\n        Type t = typeof(TAnimal);\n        while (true)\n        {\n            if (t.IsConstructedGenericType\n                    && t.GetGenericTypeDefinition() == typeof(Animal<>.OfSpecies<>))\n                return (SpeciesFor<TAnimal>)Activator.CreateInstance(t.GenericTypeArguments[1]);\n            t = t.BaseType;\n            if (t == null)\n                throw new InvalidProgramException();\n        }\n    }\n\n    // abstract "static" members\n\n    public abstract string ScientificName { get; }\n    \n    \xe2\x80\xa6\n}\n
Run Code Online (Sandbox Code Playgroud)\n

我们怎么知道里面的反射代码MakeInstance()永远不会抛出异常?正如我们已经说过的, 层次结构中的几乎所有类Animal<TSelf>也是 的子类Animal<TSelf>.OfSpecies<TSpecies>。所以我们知道对于这些类TSpecies必须提供特定的。由于约束,这种类型也必然是可构造的: new()。但这仍然遗漏了像Animal<Something>没有相关物种的抽象类型。现在我们可以说服自己,奇怪的重复模板模式 where TAnimal : Animal<TAnimal>使得无法编写,SpeciesFor<Animal<Something>>.Instance因为 typeAnimal<Something>永远不是 的子类型Animal<Animal<Something>>

\n

等等\xc3\xa0:

\n
public class CatSpecies : SpeciesFor<Cat>\n{\n    // overriden "static" members\n\n    public override string ScientificName => "Felis catus";\n    public override Cat CreateInVivoFromDnaTrappedInAmber() { \xe2\x80\xa6 }\n    public override Cat Clone(Cat a) { \xe2\x80\xa6 }\n    public override Cat Breed(Cat a1, Cat a2) { \xe2\x80\xa6 }\n}\n\npublic class Cat : Animal<Cat>.OfSpecies<CatSpecies>\n{\n    // overriden members\n\n    public override string CuteName { get { \xe2\x80\xa6 } }\n}\n\npublic class DogSpecies : SpeciesFor<Dog>\n{\n    // overriden "static" members\n\n    public override string ScientificName => "Canis lupus familiaris";\n    public override Dog CreateInVivoFromDnaTrappedInAmber() { \xe2\x80\xa6 }\n    public override Dog Clone(Dog a) { \xe2\x80\xa6 }\n    public override Dog Breed(Dog a1, Dog a2) { \xe2\x80\xa6 }\n}\n\npublic class Dog : Animal<Dog>.OfSpecies<DogSpecies>\n{\n    // overriden members\n\n    public override string CuteName { get { \xe2\x80\xa6 } }\n}\n\npublic class Program\n{\n    public static void Main()\n    {\n        ConductCrazyScientificExperimentsWith<Cat>();\n        ConductCrazyScientificExperimentsWith<Dog>();\n        ConductCrazyScientificExperimentsWith<Tyranosaurus>();\n        ConductCrazyScientificExperimentsWith<Wyvern>();\n    }\n    \n    public static void ConductCrazyScientificExperimentsWith<TAnimal>()\n        where TAnimal : Animal<TAnimal>\n    {\n        // Look Ma! No animal instance polymorphism!\n        \n        TAnimal a2039 = SpeciesFor<TAnimal>.Instance.CreateInVivoFromDnaTrappedInAmber();\n        TAnimal a2988 = SpeciesFor<TAnimal>.Instance.CreateInVivoFromDnaTrappedInAmber();\n        TAnimal a0400 = SpeciesFor<TAnimal>.Instance.Clone(a2988);\n        TAnimal a9477 = SpeciesFor<TAnimal>.Instance.Breed(a0400, a2039);\n        TAnimal a9404 = SpeciesFor<TAnimal>.Instance.Breed(a2988, a9477);\n        \n        Console.WriteLine(\n            "The confederation of mad scientists is happy to announce the birth " +\n            $"of {a9404.CuteName}, our new {SpeciesFor<TAnimal>.Instance.ScientificName}.");\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n


Ryt*_*mis 8

为了添加到前面的解释,静态方法调用在编译时绑定到特定方法,而不是排除多态行为.


use*_*801 8

在这种情况下,静态字段和方法肯定需要继承:

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"?
Run Code Online (Sandbox Code Playgroud)

  • 不,只有一个数组'腿'的实例.输出是不确定的,因为你不知道静态构造函数将被调用的顺序(实际上根本不能保证基类静态构造函数会被调用)."需要"是一个相当绝对的术语,"欲望"可能更准确. (3认同)

Fab*_*mes 5

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

我们使用它,所以类可以有一个没有类实例的可用对象列表,例如,我们有一个如下所示的方法:

class function AvailableObjects: string; override;
begin
  Result := 'Object1, Object2';
end; 
Run Code Online (Sandbox Code Playgroud)

它很丑陋但很必要,这样我们可以实例化所需的内容,而不是只是为了搜索可用的对象而实例化所有类.

这是一个简单的示例,但应用程序本身是一个客户端 - 服务器应用程序,它只有一个服务器中的所有类,以及多个不同的客户端,这些客户端可能不需要服务器拥有的所有内容,也永远不需要对象实例.

因此,与为每个客户端安装一个不同的服务器应用程序相比,这更容易维护.

希望这个例子很清楚.