C# 基础回顾: 嵌套类(转)
C# 基础回顾: 嵌套类
什么是嵌套类
顾名思义,就是把一个类的定义放在另一个类或结构的内部,例如:
|
1
2
3
4
5
6
|
class OuterClass{ class NestedClass { }} |
上面两个类就是嵌套关系,其中的 NestedClass 类就是嵌套类。
为了便于大家明白我在文中说的是哪个类型,我现在统一管外部的类型称为外部类,而嵌套在内部的类型称为嵌套类
如果你曾经写过 Java。那么你可能听说过嵌套类和内部类(非静态嵌套类),这两者首先是静态(用 static 修饰)与非静态的区别。对于内部类,它除了可以定义在类内部之外,还被允许定义在一个方法的内部,同时也支持使用匿名类型。另外,你在内部类可以像操作当前类一样操作外部类的任何成员。
|
1
2
3
4
5
6
7
8
9
10
11
12
|
class OuterClass { string s; // ... class NestedClass { public NestedClass() { } public string GetOuterString() { return s; } // 可以像访问 NestedClass 中的成员一样直接访问,不需要任何对 OuterClass 的引用 } } |
在 C# 中并没有因为是否是静态而划分成两个概念,MSDN 中只能找到嵌套类(Nested Types[1])的说法,而没有内部类的说法。从实际使用效果讲,C# 中的嵌套类不支持在方法级别进行定义,也不能是匿名类型,更无法像使用当前类一样使用外部类的成员。在 C# 的嵌套类内想要使用外部类的成员,必须将外部类的实例通过构造函数传递进去。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
class OuterClass { string s; // ... class NestedClass { private OuterClass _outer; public NestedClass(OuterClass o) { _outer = o;} public string GetOuterString() { return _outer.s; } // 可以像访问 NestedClass 中的成员一样直接访问,不需要任何对 OuterClass 的引用 } } |
Java 的内部原理和 C# 的实现很类似,只不过它是隐式传递了一个当前实例的引用,对于开发人员不可见而已。因此,可以认为这是一个 Java 的语法糖[2]。
那到底是 C# 的写法好还是 Java 的好,这个就见仁见智了。本人认为如果需要大量操作外部类的成员,使用 Java 的方式要相对简单。但是使用 C# 的方式,对于后期的维护来说应该会更有优势,因为你可以一目了然知道某个成员是属于哪个类的,可以帮你快速定位出问题的类。
这篇文章就如题目所说,是介绍 C# 的。因此本文后续的讨论,如果没有特别注明,均是指 C# 中的嵌套类。
嵌套类的特点
大家都是一个妈生的
对于外部类来说,嵌套类和其它成员无异,外部类会像呵护其它成员一样呵护它。所以它一出身,默认就给打上了 private 的标签,这样外界就无法看到它了。虽然外界无法看到,外部类中的其它成员或其它嵌套类还是可以正常看到它的,毕竟大家都是一个妈生的嘛。
也就是说,嵌套类的访问修饰符对外部类的其它成员是透明的,只对外界有效。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
class OuterClass{ private class NestedClass { } //如果没有任何修饰符,那编译器隐式加上 private class NestedClass2 { public void Action() { NestedClass obj = new NestedClass(); } }}class Program{ private OuterClass.NestedClass obj = new OuterClass.NestedClass(); // 编译失败,提示 “由于 NestedClass 的访问限制,无法访问”} |
因为和其它成员一样,所以它也可以拥有internal, protected, public 等访问修饰符,外界是否能看到嵌套类,需要结合外部类的访问修饰符和嵌套类的访问修饰符。比如外界是 public, 嵌套类也是public,那外界就可以正常访问。访问语法为:OuterClass.NestedClass。
你的就是我的,我的还是我
嵌套类是一个毋庸置疑的小霸王,它可以访问所有外部类的成员(无视它们的访问修饰符,无论是不是 private 的)。但是对于外部类来说,想问嵌套类拿东西,那就得看它愿不愿意了。
也就是说,外部类试图访问嵌套类的成员时,需要受到访问修饰符的限制。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
class OuterClass{ private static int _staticField; private int _instanceField; class InnerClass { private OuterClass _outer; private int _privateField; public InnerClass(OuterClass o) { _outer = o; } public void Action() { _privateField = _outer._instanceField + OuterClass._staticField; // 访问自如 } } public void OuterAction() { InnerClass inner = new InnerClass(this); Console.WriteLine(inner._privateField); // 编译提示这句话出错 }} |
无限嵌套
虽然看着别扭,但是如果你愿意,你完全可以再继续嵌套下去。
|
1
2
3
4
5
6
7
8
9
10
|
internal class OuterClass{ public class NestedClass { public class NestedNestedClass { ... } }} |
可以被继承
与普通的类一样,嵌套类也能被继承。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
class OuterClass{ public class NestedClass { protected int _instanceField; } public void Action() { NestedClass obj = new NestedClass(); obj._instanceField = 1; //因为该字段是 protected,所以这里无法通过编译 }}class Program : OuterClass{ class InheritFromNestedClass : NestedClass { void Action() { _instanceField = 1; //protected 字段可以在派生类中被访问 } }} |
隐藏技能 --- “延迟初始化”
延迟初始化的意思是:只有当需要使用的时候才会初始化,用在嵌套类身上则是指它不会在外部类没有访问嵌套类的任何成员前初始化,只有在用到嵌套类的某个成员时候才会初始化。
在《C#嵌套类型的研究笔记》一文中(以下简称<笔记>),作者提出延迟初始化并不是嵌套类的特性,这点我深表认同。但是文中所给出的例子... 实在有点勉强,而且作者还把这个特性归在了静态构造器的身上,那么就有问题了。
问题一 例子不恰当
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public class Outside{ public Outside() { Console.WriteLine("Outside Inilizlized"); } public void SayIt() { Nested.Run(); }} public class Nested{ static Nested() { Console.WriteLine("Nested initilized"); } public static void Run() { Console.WriteLine("Nested Run"); }} |
在<笔记>中,作者将嵌套类从宿主类中剥离了出来(如上所示),然后尝试运行该代码,发现只有当执行 Outside.SayIt() 的时候才会输出 “Nested initilized”,也就是 Nested 类才会初始化。这个结果完全与嵌套类未剥离的效果是一样的。由此来佐证该文作者的说法(即,导致延迟初始化不是因为嵌套,而是因为静态构造器)是不恰当的,只能证明导致延迟初始化不是因为嵌套类,却无法证明静态构造器与延迟初始化有关。
因为 Nested 类在 SayIt 方法中是第一次被调用,而且也是它第一次出现,那有这样的结果也是可想而知。我完全可以用实例构造器、实例方法来替换静态构造器和静态方法,同样可以得出类似的实验结果。换句话说,作者只证明了 Nested 类在需要用到的时候被初始化了,但却无法证明 Nested 类在没有用到的时候肯定不会被初始化。而后者才是延迟初始化的核心特色。
问题二 静态构造器==延迟?
本质上说,静态构造器所做的事情恰好相反,它在类型第一次被使用前就被调用了,目的是为当前这个类型初始化所有必要的静态资源,然后把这些这些资源存放到托管堆中。既然在使用前就被调用了,那又何来“延迟”一说呢?
如果硬要说 “延迟”,我们得先搞懂,这里所谓的延迟是相对于什么而言的。我们说了这么多静态构造函数,想必大家也应该猜到了:延迟是相较于不使用静态构造函数的情况。简单的说,定义了静态构造函数的类型只有在第一次访问该类型的成员时才会初始化。而没有定义静态构造函数的,则有可能在没有访问类型成员前就执行了。举个栗子,如果有两个类型,其中一个类型 A 定义了静态变量且进行了字段初始化,但没有静态构造函数;而另一个类型B,不止定义和初始化了静态变量,还定义了一个空的静态构造函数。那么此时 Main 函数如果先访问 B 的静态字段,后访问 A 的静态字段,就会发现带有静态构造函数的类型 B 反而在 A 之后才初始化。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
public class ClassA{ public static string StaticString = GetString(); public ClassA() { Console.WriteLine("ClassA Instance Inilizlized"); } public static string GetString() { Console.WriteLine("ClassA Static field Inilizlized "); return "xxxxxxxxxxxxxxxxxxx"; }}public class ClassB{ public static string StaticString = GetString(); public ClassB() { Console.WriteLine("ClassB Instance Inilizlized"); } static ClassB() { } public static string GetString() { Console.WriteLine("ClassB Static field Inilizlized "); return "xxxxxxxxxxxxxxxxxxx"; }}class Program{ static void Main(string[] args) { Console.WriteLine("Start Main"); ClassB.StaticString = ""; ClassA.StaticString = ""; Console.WriteLine("Finish Main"); }} //Output//ClassA Static field Inilizlized//Start Main//ClassB Static field Inilizlized //Finish Main |
这里所说的访问是指代码执行到 TypeA.memberA 时,比如 if(TypeA.memberA == ""),我们认为此时访问了 TypeA 的成员 memberA。关于静态构造器更详细的资料请参考我之前写的《C# 基础回顾: 构造器》。
我认为延迟初始化的确不是嵌套类天生的能力,而是后天养成的。这也是为什么我在延迟初始化这个小标题上加了引号,这里的延迟初始化其实是因为调用的时机比较晚而已,再通过静态构造器的这种方式才让一个类保证了初始化的时机只会在用到的时候才初始化。
嵌套类能提供这个作用是在单例模式中被发现的,所以我就拿单例来举例子,但是本文不会重点介绍单例模式。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class Singleton{ private static readonly Singleton _instance = new Singleton(); // 静态构造器用于保证 _instance 会在类型第一次使用的时候初始化 static Singleton() { } private Singleton() { } public static Singleton Instance { get { return _instance; } }} |
这是对单例模式很简单的实现,上述实现由 CLR 保证线程安全,执行速度快,唯一缺点是无法延迟初始化,在第一次调用这个类型的任何成员时,就会自动执行构造函数然后进行初始化。如果初始化的动作很耗时,或者要占用大量资源,那这个实现可能就会有问题了。
换另一种实现,下面的实现使用了著名的双检索机制,不过这段代码会有一点小问题
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public class Singleton{ private static Singleton _instance; private static readonly object _lck = new object(); private Singleton() { } public static Singleton Instance { get { if (_instance == null) { lock (_lck) { if (_instance == null) { _instance = new Singleton(); //这句话的执行会出现问题 } } } return _instance; } }} |
上面的代码,只有当获取 Instance 实例的时候才会去执行创建一个实例化的动作。因为需要加锁,所以不会有线程安全的问题,但是执行速度慢。
_instance = new Singleton() 这句话可能不会按照你的预期执行。在JAVA 1.5之前,这句话的执行流程是:1、先为_instance在栈上分配空间,2、将地址赋值给 _instance,3、构造器才开始初始化。假如在这个时刻(构造器尚未完成初始化或尚未开始之前)另一个线程 B 访问 _instance,由于第一个 if 判断该对象不为 null,线程 B 就会认为这个对象已经初始化完毕,但试图调用该实例的方法可能会导致 crash。归根结底的原因还是因为 _instance = new Singletone() 这个不是原子操作, 所以另一个线程可能在第一个线程刚刚完成了步骤2时就访问了该实例的某个方法导致错误。
详情参考《Implementing the Singleton Pattern in C#》
如果又希望安全,又希望速度快,同时还想延迟初始化,那就可以利用本文提供的嵌套类来实现:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
public class Singleton{ private Singleton() { } public static Singleton Instance { get { return Nested._instance; } } public static void SomeMethod() { Console.WriteLine("SomeMethod"); } class Nested { static Nested() { Console.WriteLine("Nested.cctor"); } internal static Singleton _instance = new Singleton(); }} |
Nested 类中的显示静态构造器保证了 _instance 会在 Nested 类型第一次被使用的时候初始化,而且由 CLR 来保证线程安全。又因为 Nested 是嵌套类,对于何时使用嵌套类由外部类决定,因此只需要外部类在需要使用的时候再调用嵌套类即可。上面的代码只有在 Insatnce 这个属性里才有对 Nested 类的调用,因此保证了在没有调用 Instance 这个属性的时候永远不会初始化这个实例。
最佳实践
不要将嵌套类定义成 public 的。
嵌套类主要是为了对外部类的各个成分进行更好的逻辑分组,如果一个类可能会被外界使用,那就不应该被定义为嵌套类。
如果一个嵌套类被定义成 public 的,那反而会破坏对象的封装性,因为在嵌套类中,允许使用外部类的私有成员。
不要将嵌套类型定义为接口的成员。 许多语言不支持这样的构造[3]。
参考资源
1、Nested Types[MSDN]
2、C# nested classes are like C++ nested classes, not Java inner classes
3、设计准则:嵌套类型[MSDN]

浙公网安备 33010602011771号