第 80 讲:C# 2 之分部类型
C# 2 诞生了一种新的语法机制,它让我们的数据类型的书写和使用都变得更为方便。
Part 1 分部类型的概念
考虑一种情况。如果我们在书写代码的时候必须把代码放在同一个类型里面,如果这个类型非常庞大,处理起来相当麻烦的话,那么这个类型经常就会超过几千行甚至上万行代码。如果这样的代码列在一起,就非常不方便我们自己书写代码的时候去查找 API 的使用,以及 API 的执行过程。因此,C# 2 里带来了一种新的机制:分部类型(Partial Type)。
1-1 基本语法
我们可以允许一个类型声明分为多个文件来存储,每一个文件的类型声明都使用同一种数据类型的名字。比如说,假设我有一个数据类型叫做 StringHandler
,那么我可以这么去写代码:
我们定义其中一个文件为主类型文件(Main File),它存储和标识这个数据类型的修饰符,以及一些比较关键的成员。接着,我们可以将一些文件分开独立存储,里面可以实现一些分开的内容,比如嵌套类,等等。每一个需要分离成不同文件的类型的声明上都带上 partial
这个新的关键字,挨着类型修饰符 struct
出现。
这里所说的主类型文件只是逻辑存在的一种说法,假设我这个类型是 public sealed class
修饰的,那么你完全可以将 public
和 sealed
修饰到不同的分部类型的声明上去:
编译器将自动合并所有的修饰符,即最终的修饰符是 public sealed class A
,编译器知道去合并修饰符。不过,这种情况下我们就无法说清楚哪一个文件是主要的了。
不过,我们不建议你这么写代码。我们建议你在书写和使用的时候,尽量把修饰符放在同一个文件里去,将其作为主类型文件。
1-2 一般用途
一般来说,这种用法一般实现和使用在嵌套类型里比较多一些。比如刚才的 StringHandler
类型,假设它用来拼接长字符串的话,它应该就必须具有支持 foreach
循环的迭代器类型。假设这个迭代器类型是我们自己实现的:
Enumerator
StringHandler
类型,因此 partial
用在 StringHandler
上,而不是嵌套类型 Enumerator
上面。
Part 2 常见分部类型里需要注意的问题
2-1 partial
修饰符的位置
另外,目前只有 delegate
和 enum
无法使用分部类型机制,而 class
、struct
和 interface
都可以使用 partial
修饰符来指示类型可以多文件分开存储。至于为什么委托和枚举不行……因为委托声明只需要一句话;而枚举类型里没有任何需求去分离不同的委托字段信息。partial
关键字一定要放在类型修饰符 class
、struct
和 interface
的前面。目前来说,partial
关键字的位置只能在类型修饰符的左边紧挨着,也就是说:
partial public sealed class A
public partial sealed class A
这些都是不对的写法。因为位置不正确。
2-2 重复使用相同的修饰符
C# 允许我们在不同的文件上使用相同的修饰符。也就是说,你多次在不同的分部类型文件上使用同一个修饰符,C# 是允许的。
2-3 分部类型的嵌套
C# 甚至支持分部类型嵌套。换句话说,你可以将嵌套类型分文件存储。不过,当前类型,以及它所处类型,以及所处类型的所处类型(可能还有更多的嵌套级别),全部标记上 partial
即可:
A
、B
和 C
类型分文件存储的时候都标记 partial
请注意文件分离出来后的样子和修饰符的使用情况。
2-4 分部类型的类型继承和接口实现
是的,分部类型和普通的数据类型没有什么区别,只是分离开了罢了。分部类型也能实现接口和从基类型派生的规则。
没有什么区别。C# 也确实允许你随意去完成类型继承机制和接口实现机制,而且你甚至也能多次从同一个基类型派生,以及多次实现同一个接口。编译器它知道你已经完成了对接口的实现,以及对基类型的派生过程,因此它不会强求你必须只写一次的。
2-5 为什么编译器会允许我们多次重复书写相同的东西?
这个,其实是因为编译器生成代码的时候方便灵活处理。编译器有些时候为了方便,就会直接把头部全部读取下来,然后抄一遍。C# 这样的允许就保证了这种机制是可以允许和接受的;试想一下,假设 C# 不允许这种机制,即要求修饰符只能出现一次、不得重复书写,而且里面的接口实现、基类型派生的这些语句全部只能写一次的话,是不是就有点不灵活?
比如说,我有一个类型:
A
2-6 partial
修饰过的类型仍可以不分部
为了语法的通用性和灵活性,C# 允许对不分部的数据类型使用 partial
关键字。也就是说,如果这个类型没有分离到不同的文件的时候,这个类型仍然可以使用 partial
关键字。在这种情况下,partial
有没有都没关系,不影响编译器执行、分析和生成代码。
不过,要注意一点的是,这样修饰的话会带来一些隐患,这一点我们稍后会给出说明。
Part 3 泛型类型的分部
3-1 泛型类型的分部规则
泛型类型比正常的类型声明多了一个泛型参数部分,以及泛型参数约束部分(如果泛型参数需要约束的话)。C# 对泛型参数约束的要求还是和前面都差不多,但是!
因为泛型参数个数不同的同名类型是构成类型重载性的,所以泛型参数必须要在分部为多文件的时候,都全部写出来,不过,泛型约束可以不重复再写一遍;写一遍也行,但必须泛型约束是一样的,不能篡改和变动泛型约束。
比如下面给出的这个 A<T>
类型。下面给出了所有它的分部类型。其中注释掉的部分是错误的写法,原因也都写在后面了。
如果看不懂英语的话,我再解释一下吧。
partial class A<T>
:可以,允许不写泛型约束;partial class A<T> where T : struct
:不可以,因为泛型约束不一样;partial class A
:可以,但由于它和A<T>
类型构成类型重载,所以这个A
完全就是一个新的数据类型;partial class A<T> where T : class
:可以;partial class A<T1>
:不可以,因为泛型参数名字不一样,数据类型A<T>
和A<T1>
也不构成重载;partial class A<T, U>
:可以,但它和A<T>
类型构成类型重载,所以A<T, U>
也是一个全新的数据类型。
因此,请一定要注意问题。
3-2 为什么不建议在任何类型上都加上 partial
关键字?
另外,前文说到“留下隐患”指的就是这里的类型重载性。一个数据类型是不需要任何修饰符的,它将会默认为 internal
修饰的类型。而你使用 partial
关键字但没有注意类型的重载性的话,就完全可能使得这个类型完全和你原来的类型关联不上,导致暗藏的程序问题和书写代码的问题,所以,我们并不建议用户在自己实现类型的时候,随时都加上 partial
关键字;相反,我们只建议你必须分离文件的时候才使用该修饰符。