为什么我不能在c#中有抽象的静态方法?

我最近一直在使用供应商,我遇到了一个有趣的情况,我想有一个抽象类,它有一个抽象静态方法。我读了一些关于这个话题的帖子,它有点说得通,但有一个清晰的解释吗?

124788 次浏览

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

假设您可以暂时继承静态方法。想象一下这个场景:

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(),将调用哪个方法?返回哪个值?很容易看出,如果不创建对象的实例,继承是相当困难的。没有继承的抽象方法只是没有主体的方法,因此不能被调用。

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

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

对静态方法的调用是通过类名完成的,而不是通过对象引用,调用它的中间语言(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.t test,并在运行时解析调用,但它仍然不会是虚的,因为您仍然必须在那里编写某种类名。

虚方法和抽象方法只有在使用一个在运行时可以包含许多不同类型对象的变量时才有用,因此您希望为变量中的当前对象调用正确的方法。对于静态方法,无论如何都需要遍历类名,因此要调用的确切方法在编译时就知道了,因为它不能也不会改变。

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

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

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

我们实际上重写了静态方法(在delphi中),这有点丑,但它很好地满足了我们的需求。

我们使用它,所以类可以有一个可用对象的列表,而不需要类实例,例如,我们有一个方法看起来像这样:

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

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

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

因此,这比为每个客户机使用一个不同的服务器应用程序要容易得多。

希望这个例子很清楚。

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

这里有一种情况,肯定需要继承静态字段和方法:

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"?

这个问题已经12年了,但它仍然需要一个更好的答案。正如评论中很少有人注意到的那样,与所有回答相反,在c#中使用静态抽象方法肯定是有意义的。正如哲学家丹尼尔·丹尼特(Daniel Dennett)所说,想象力的失败并不是对必然性的洞察。有一个常见的错误是没有意识到c#不仅仅是一种面向对象语言。对给定概念的纯面向对象的观点会导致一种受限的、在当前情况下被误导的检查。多态性不仅仅是关于多态性的子绑定:它还包括参数多态性(又名泛型编程),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<TAnimal>)与一个应该包含静态抽象成员的类(这里是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}");
…
}

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

  1. 如何确保Animal的子类的实现者为这个子类提供了一个特定的SpeciesFor<TAnimal>实例?
  2. 属性SpeciesFor<TAnimal>.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<TSelf>的构造函数设为私有,可以确保它的所有子类也是内部类Animal<TSelf>.OfSpecies<TSpecies>的子类。所以这些子类必须指定具有new()绑定的TSpecies类型。

对于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<TSelf>层次结构中的类也是Animal<TSelf>.OfSpecies<TSpecies>的子类。所以我们知道,对于这些类,必须提供特定的TSpecies。由于约束: new(),此类型也必须是可构造的。但这仍然忽略了像Animal<Something>这样没有关联物种的抽象类型。现在我们可以说服自己,Animal<TSelf>0 where TAnimal : Animal<TAnimal>使得不可能将SpeciesFor<Animal<Something>>.Instance写成类型Animal<Something>从来不是Animal<Animal<Something>>的子类型。

像这样:

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。另一个原因是它不适用于接口中的静态成员,而接口比抽象类更灵活。

.NET 6 / C# 10/next/preview中,你可以通过“接口中的静态抽象成员”来做到这一点。

(在编写代码时编译成功,但一些ide有突出显示代码的问题)

< a href = " https://sharplab。io / # v2: EYLgZgpghgLgrgJwgZwLQGUawJYGMCCwyMCUuMAshALbAQLICSAdizPWGSgDQwjYAbAD4ABAEwBGALAAoWcyjUUABy4ACTDgJESZGABUUMbMwDmsgN6y1NtSIDMak + wSdcENY3QB7JYWKk5GwcXNa2VjK2UXaOUDqBMHYSAGxJAAxqAMJQAgJUABQAlADcYTYAvrJlMXZiahQAnpkCUMjI + Gognj5 + 8 xrbrqgr4dvriinp3b4q / rpBzC5uEAB02bkFhaMjw9HRIgDsagBEAJrecGq4OQIQACZZLW34R6U70ZVvH9UOtfVNj8gAEKdKa9AL9BYhdzVCK7GzjVLjDJeaazBIDJara4bLY2WFwsaHU7nS7XO4PVpAl64tQfKJfN4 / cRJfYwqpvMaOBF2AAs9SgJnySIA2gBdNRQBCmZCbDnbAlRACqyAgmmMuGalKoMAAFt5bgAeRqap4APiKrwVtmVqqw6pNyG1esNxoBgPNJRpDIJTImIj5NrVeAdTv1Bv0HrUAHcdfQPPoQSiwXMYBihgT8VbxgBOfIAEiOFhgDWUEG8YHy+ kkywacooiousesieu1ehkhabhw1bz9e28i3cuujp65rv2t6uuxsnrlddw5gy3g1amukmzn15ot03dmwqapqakjuqduablvn4anysxk6jymo + xfurnqabsqgbizuzabbxzy1aakaktgyhv2ycetdugkok /AB9NIMLSLpcG8eDdEQ24IIAQiI/ rknsndmosbdrkmoc0t0qxibmuxln3bijyhcaqbanybykcibeojdqfjxjetuyvxlwxdkg8g5rwkzpk7iqaadqqbb2dvrchqknjzstis73qtidk6qdbjss9z2snnk37dzrwjolba +cogA=" rel="noreferrer">SharpLab Demo

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


由于这是运行时中的一个主要更改,因此产生的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
}

资源:

它目前作为预览特性在c# 10中可用。