欢迎光临散文网 会员登陆 & 注册

C# 方法的签名上最多可以由多少个关键字构成?

2023-08-12 22:48 作者:SunnieShine  | 我要投稿

这是一个非常有趣的问题。可能我们在平时使用的过程之中并不会这么去用,但是这对我们了解一些新鲜、罕见的语法有一定的蛇皮帮助。

题目介绍

首先,我们知道,一个方法需要有签名(Signature)一说。它指的是我们在书写方法的时候,那个方法的头部。比如下面的这个代码:

该方法具有两个修饰符:publicstatic。这很好理解。一个方法无需修饰符的时候,它默认会和 C 语言的理解进行一定程度上的兼容和匹配。只不过,static 在 C# 里已经具有一个比较大的变化。

那么,请问一个方法最多可以有多少个修饰符构成呢?这一次我们把问题“广义化”到,返回值类型如果是关键字也算修饰符的一部分。也就是说,这个问题如果改成这样的话,那么刚才的这个 Main 方法我们将算成三个修饰符。而题问的是,一个方法最多可以由多少个修饰符构成。

很明显,这个问题可能在不同的 C# 语言版本里有不一样的答案。我们目前将 C# 12 纳入其中进行计算,那么它应该是多少呢?

开始构造

首先我们需要非常熟悉 C# 的修饰符的用法。C# 的修饰符非常多,我们主要在面向对象里会用到如下的修饰符种类:

  • 访问修饰符(publicprotected 这些)

  • 多态修饰符(newvirtualabstractsealed

  • 静态修饰符(static 这一个)

  • 一些普通的类型关键字(intstring 等)

但是,光靠这些是不够长的。我们举个例子。

我们知道,访问修饰符里有一个不起眼的修饰级别: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>> 也算允许存在的。

但是,这样做太耍赖了。于是问题我们不允许这么干。但是,即使方法声明里不带指针,但我们能不能当修饰符加进去呢?答案是可以的。下面的代码就可以展示出这样的效果:

现在已经达到了惊人的 8 个修饰符了。这是答案了吗?No。

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 的接口静态抽象成员(Static Abstract Members in Interface)刚好就允许了这一点。虽然它本来是用于数字泛型参数化的。

于是,我们找到了最多的关键字的情况,11 个。

其他问题

1、override 不能用构造放进去吗?

很遗憾,做不到。因为接口不能用 override 修饰符。而如果你基于普通类型的话,又无法使用静态抽象成员了,因此没可能同时兼具。

2、还能更多吗?

不能。这是本题的最终解决方案。不过 C# 可能会在以后允许在接口里声明 readonly 修饰的成员来保证对象的 this 指针的内部数值只读。于是,它还能加一个修饰符,成为最多的情况。

与此同时,比较新的 scoped 关键字目前只能修饰在参数和临时变量上,我们无法将其放在别的地方,所以 scoped 关键字在这个题目里用不着。

另外,如果我们将运算符也算方法的话,我们可能会考虑出更多的组合。比如说运算符就可以多一个 checked 修饰符(operator checked 的声明),但它不能改变访问级别,它只能是 public 的,所以此时访问修饰符又只能用一个关键字了。怎么都做不到。


C# 方法的签名上最多可以由多少个关键字构成?的评论 (共 条)

分享到微博请遵守国家法律