C# 方法的签名上最多可以由多少个关键字构成?
这是一个非常有趣的问题。可能我们在平时使用的过程之中并不会这么去用,但是这对我们了解一些新鲜、罕见的语法有一定的蛇皮帮助。
题目介绍
首先,我们知道,一个方法需要有签名(Signature)一说。它指的是我们在书写方法的时候,那个方法的头部。比如下面的这个代码:
public
和 static
。这很好理解。一个方法无需修饰符的时候,它默认会和 C 语言的理解进行一定程度上的兼容和匹配。只不过,static
在 C# 里已经具有一个比较大的变化。
那么,请问一个方法最多可以有多少个修饰符构成呢?这一次我们把问题“广义化”到,返回值类型如果是关键字也算修饰符的一部分。也就是说,这个问题如果改成这样的话,那么刚才的这个 Main
方法我们将算成三个修饰符。而题问的是,一个方法最多可以由多少个修饰符构成。
很明显,这个问题可能在不同的 C# 语言版本里有不一样的答案。我们目前将 C# 12 纳入其中进行计算,那么它应该是多少呢?
开始构造
首先我们需要非常熟悉 C# 的修饰符的用法。C# 的修饰符非常多,我们主要在面向对象里会用到如下的修饰符种类:
访问修饰符(
public
、protected
这些)多态修饰符(
new
、virtual
、abstract
和sealed
)静态修饰符(
static
这一个)一些普通的类型关键字(
int
、string
等)
但是,光靠这些是不够长的。我们举个例子。
我们知道,访问修饰符里有一个不起眼的修饰级别:protected internal
。这个级别是组合关键字用法,表示的是该成员在当前程序集里可以随便看到;但是出了程序集之后,你只能在从这个成员的对应类型进行派生了之后才可以看到。那么,它因为用到两个关键字,所以我们纳入进来。
其次是 static
。我们一般习惯把静态的修饰符放在访问修饰符旁边,这样一眼就看得到。
然后是多态修饰符。这次我们知道的是,面向对象不允许我们使用 abstract
在静态成员上,所以多态修饰符非得加进来的话,只能用 new
。
最后算上返回值类型,随便挑一个吧。
这样的话,把 int
算进去就有 5 个了。但这肯定不是极限。
一些不太常用的修饰符
我们再来想一想。这个问题针对的是方法,而方法在 C# 里有一个比较不起眼的特性教分部方法(Partial Method)。这个特性允许我们将方法拆解为声明部分和代码实现部分,存储在两个不同的文件之中。这样做的目的其实是为了便于编译器生成代码的时候,在不改动源代码的时候可为类型进行扩展;也可以往分部方法上标记特性,这样可以达到一些比较方便的生成行为。
但,这个方法是非 void
返回的,而且访问修饰符级别也不是 private
,这意味着我们不能使用分部方法的特性。其实不然。从 C# 9 开始,分部方法允许我们使用它来作用于任何方法。所以,代码可以这样:
很好。这样够了吗?并不够。C# 9 允许的分部方法虽然推广到任意方法之上,但很遗憾的是,因为方法本身带有返回值类型,访问修饰级别也并非原有的 private
,所以可能在使用代码的时候造成副作用。而分部方法有一个非常神奇的规则是,它可以不实现。如果你将分部方法的声明部分写出,哪怕你不去实现它,你在代码里也可以使用该方法。虽然它没有实现,它等于是在运行时没有效果,但它并不影响编译。如果你实现了,你就可以独立在单独的文件里实现,让方法跑起来。这样甚至不用改动原来的文件。
但是,这特性在 C# 9 里,因为访问修饰符、返回值类型等因素,该方法就无法保证实现的安全性。因此,它必须给出实现部分。因此,我们只能将其拆解为两个文件。为了保证该方法签名的稳定性。
这样似乎已经到了 6 个关键字的地步了。其实并不够。我们还有一个杀手锏:extern
关键字。
该关键字可谓在 C 语言里用得特别多。在 C# 里基本就遇不到了。可能你写 P/Invoke(互操作性)的时候会用到它。它表示该成员的实现部分不是 C# 来做的。你往成员的修饰符里加了它之后,成员即使直接以分号结尾也不会影响程序的编译,因为编译器认为你的代码走的是别的语言实现的、或者是 C# 实现的代码,但放在了独立的 dll 文件之中。
因此,加上该修饰符,我们可以达到 7 个修饰符的地步,且不需要给出实现。
unsafe
关键字。
该关键字用来表示该代码段落是不安全的。换言之,该段落的代码用到了 C# 里的指针操作。比如 ->
运算符、void*
以及 C# 9 里才有的函数指针。假设我们将返回值替换为函数指针类型,那么方法声明就有些“耍赖”了——因为函数指针的声明是允许递归的。比如说,delegate* managed<void>
表示返回 void
的、由 C# 程序集里给定的静态方法,指向这样的方法的指针;而返回值替换为函数指针:delegate* managed<delegate* managed<void>>
也算允许存在的。
C# 7 开始允许我们将返回值声明为引用返回,这样的方法将返回一个对象的引用。ref T
表示返回一个 T
类型实例的引用;而 ref readonly T
则表示一个 T
类型的只读引用,即从上面传下来,你无法改动它指向的对象的值。比如对对象进行自增操作,它改变了内部的数值;但你可以用到它的引用来做一些事情。
那么这样的话,就好说多了。我们可以使用 ref readonly int
很好。这样我们就达到了 10 个修饰符的地步。这便是最多的情况。
那么,这是答案吗?No。可是我们已经无法再继续进行推广了啊。static
修饰符意味着方法不能追加 readonly
修饰符来表示方法不改变 this
的内部数值(如果 class C
改成 struct C
的话);而 async
修饰符又不允许我们使用引用,而该方法返回了一个只读引用。这看起来并不能继续了;而 override
则可以用于 class
类型,但这个方法是静态的,你也无法继续进行重写之类的操作。
那我们构造一个?
B
类型里最后的这个实现部分用到了最多的情况,而且用的是实例的实现模式,没有用 static
关键字。这样的目的是为了可以用 override
关键字;而这样也可以使用 sealed override
或者是 abstract override
组合修饰来达成派生的方法重写模式。
另外,该例子的其他修饰符也都用上了,但很遗憾的是,这例子也是 10 个关键字。
那,真的没有办法继续了吗?答案是否定的。
接近答案
让我们发散地想一想。既然静态成员无法抽象,那有没有可能,我们让静态成员抽象起来呢?C# 11 的接口静态抽象成员
其他问题
1、override
不能用构造放进去吗?
很遗憾,做不到。因为接口不能用 override
修饰符。而如果你基于普通类型的话,又无法使用静态抽象成员了,因此没可能同时兼具。
2、还能更多吗?
不能。这是本题的最终解决方案。不过 C# 可能会在以后允许在接口里声明 readonly
修饰的成员来保证对象的 this
指针的内部数值只读。于是,它还能加一个修饰符,成为最多的情况。
与此同时,比较新的 scoped
关键字目前只能修饰在参数和临时变量上,我们无法将其放在别的地方,所以 scoped
关键字在这个题目里用不着。
另外,如果我们将运算符也算方法的话,我们可能会考虑出更多的组合。比如说运算符就可以多一个 checked
修饰符(operator checked
的声明),但它不能改变访问级别,它只能是 public