第 34 讲:面向对象编程(六):关于类,其它想说的东西
对于 C# 编程语言来说,因为体系很庞大,所以学起来很麻烦。就算是正派老师也很不容易讲清楚和讲全知识点,因为细节很多以至于很容易就会漏掉一些知识点。当然了,知识点有些是不必要说的,或者不太重要的,所以漏掉就漏掉吧(毕竟你还可以自己查资料去学,毕竟用到才去查也不妨事,这毕竟不是上课和考试,非得背下来)。
本节内容,我们就挑选一些前面没有解答或者回答的问题,以及一些语法处理机制给大家作出额外的补充说明。这一节完成之后,面向对象我们就完成了 1/3 了。下一节内容我们将进入面向对象的第二个大部分:继承。
Part 1 构造器的串联调用
没想到吧。之前我们讲构造器的时候,我们完全没有提到构造器之间的调用。我们只是说了,构造器能够使用 new
关键字了调用,除此之外没别的办法了。
这话其实也没错,但这仅仅是针对于外部而言。所谓的外部,就是在使用类、要得到类的实例的时候,才会这么说。但是,构造器之间是可以有调用的关系的。不过,因为构造器的特殊性,即使构造器 A
调用了 B
,这个 B
也只能在最开始就得调用,而不能手动调整调用位置。
我们使用之前 Person
类的那个例子给大家介绍一下构造器的串联调用。我们假设有这样三个构造器:
我们只要调用第二个构造器的话,我们就得重复书写 Name = name;
这样的代码一次(因为第一个构造器要写一次 Name = name;
,在第二个构造器里又得写一次)。
为了简化调用模型,我们可以这么写代码:
请注意第二个和第三个构造器。我们在参数表列和大括号中间插入了一段代码:: this(参数)
。这个格式就是构造器调用构造器的语法。我们传入 name
之后,编译器当然就知道你要调用的肯定是第一个构造器了,因为只有第一个构造器,才是只需要一个 string
类型的参数;而在第三个构造器里,我们使用 this(name, age)
就意味着我们调用的构造器一定是第二个,因为只有第二个构造器的参数和类型才是和 name
参数以及 age
参数相匹配的。
因为构造器语法的特殊性,我们只能这么书写。比如第二个构造器的话,: this(name) { Age = age; }
这个语法就表示先调用第一个构造器,先给 name
赋值;然后才是给 age
赋值。
这就体现出了重载的好处和用途了。重载只需要区别参数的数据类型和参数的个数,就可以知道到底调用的是哪个方法。总之,我们需要掌握的是这样两个知识点:
构造器之间可以互相调用,使用的是
: this(参数表列)
的语法;构造器即使能够通过这个语法来达到串联调用的过程,但执行必须是在最开始就执行,这是无法改变的。
Part 2 this
引用
在我们之前的语法讲解里,我们是没有提到这个概念的。按照一般教材的书写,this
引用可能早就讲过了。我之所以放在最后,作为不重要的内容来说,是因为它的存在感很低。
考虑一种情况。字段被属性封装后,我们使用“下划线+驼峰命名法”的命名规则来给字段命名。其实,这么取名还有一个好处:避免冗余 this
引用的代码书写。
假设,我们为字段取名的时候,最开头不添加下划线的话,我们还是拿 Person
类为例:
显然,我们这里不一定非得赋值给 Name
属性,因为给 Name
属性赋值后,还是会调用 set
方法,然后给 name
字段赋值。因此我们便可直接通过 name
参数对 name
字段赋值。可问题在于,我们直接写 name = name;
的话,我咋知道谁是谁呢?参数和字段都用的一个名字 name
,巧就巧在大小写都是一样的。这我咋办呢?难道就不能使用这个字段名了吗?
也不是。我们只需要在字段 name
的左边添加语法 this.
来表达“这个赋值的是字段”,就可以了:
前面追加了 this.
的语法,我们把 this
想象成和前面索引器语法差不多的 this[参数]
的类似语法,this
把它当成一个“万能替换变量”就可以了。在具体的时候,替换成具体的变量名即可。这里是因为我们是在对这个实体的 name
字段赋值,而“这个实体”我们是无法从代码表达出来的,于是 C# 用了一个 this
关键字专门表达“我要赋值的是‘这个’对象”。正是因为是“这个”对象,所以我们用的单词是 this,因为这个单词刚好就是“这个”的意思。
同时,所有别的实例成员(比如属性、索引器或者没 static
修饰的方法之类)都可使用 this
关键字,只是系统一般都会推荐你把 this
给删掉,因为没有意义写出来,编译器是知道的(即使不写,本来就是赋值给这个字段、调用这个方法、使用这个属性之类的行为)。比如说,实际上我们可以使用这样的写法:this.Name = name;
来表示我把参数 name
赋值给属性 Name
。但是没有必要写 this.
,我是这个意思。
从另一个角度说,我们使用下划线就避免了书写 this
的问题,因为没人会给参数上追加一个下划线吧。
Part 3 嵌套类
类有一个神奇的地方在于,你可以在类里插入一个嵌套的类,就好像是循环里嵌套循环那样。
我们注意从第 24 行开始的代码。这里包含了一个 private class
。不是说类不能用 private
修饰吗?因为这是嵌套类。给嵌套类修饰 private
意味着这个类型不会暴露给外界的任何地方,仅在 Person
这个大括号范围里随便用。因此,只有嵌套类可以用 private
修饰 class
。
另外,这里带有一个 IsSame
方法,这个方法虽然修饰 public
,但因为类是 private
的,因此外界还是看不到它。而我们在写 ==
的运算符重载的时候,我们使用了 PersonEqualityChecker.IsSame
的静态方法调用的语法,来表达方法是通过嵌套类里访问得到的 IsSame
方法的。
当然,你完全不必这么去设计代码的思维,而是直接把第 28 到第 30 行的代码抄到 ==
的运算符重载的执行代码里,直接不要嵌套类。我这么做只是让你明白啥叫嵌套类。
嵌套类很少用到,一般在设计的时候,我们根本不会去使用嵌套类的语法,因为写起来很丑(多套了一层大括号);而且就算是给这个嵌套类使用 public
的修饰符,外界可以访问了,我们书写这个嵌套类的时候,语法还得带上嵌套类所在的这个类的名字 Person
,即 Person.PersonEqualityChecker.IsSame
。你看看,很丑不说,而且这样写,但看这句话,你也不知道前面这个 Person
到底是命名空间,还是类名。
Part 4 何为良构类型
所谓的良构类型(Well-formed Type),说白了,就是构造良好的类型,也就是说,在设计类的代码的时候,读起来代码很舒服的一种设计方式。
为了避免很多不必要的复杂问题的出现,我们会使用一些轻便、简单的语法来替换正统的复杂语法;一方面简化了代码的书写,另外一方面来说,编译器也确实知道这些代码都做什么,会帮我们去做这些处理过程,所以也不必担心处理逻辑。
第一,类设计的时候强烈建议重载 ==
和 !=
运算符,哪怕你补充在大小比较运算符都行,但这俩是非常建议重载的。因为我们书写比较的时候,肯定是用等号和不等号比较的时候多;但如果不重写的话,==
和 !=
会被默认认为是任何数据类型都可使用的“是否指向同一块内存空间”的逻辑。这显然对于比较数值来说是没有意义的,因此一定要重载它们。
第二,强烈建议在传入引用类型作为参数的时候,验证参数是否为 null
。在调用方法、使用 ==
重载的时候,我们是不是也会使用传入 null
作为比较的代码。如果 null
在不处理的时候,null
本身又是用来表达没有分配内存空间,这你上哪里去比较内部的数值?
所以,我们建议在传入引用类型的时候,都去确认一下 null
。
ReferenceEquals
方法是系统自带的、专门表达“是否指向同一块内存”的方法。如果我们在重写 ==
和 !=
的时候使用比较内存的代码的话,因为 ==
被重载掉了,因此此时再使用 ==
和 !=
的时候就会递归调用自己,导致错误。所以,避免无法判断是否内存一致的话,我们就得用到这个默认的系统方法 ReferenceEquals
了。a && b
就等价于两个参数都是 null
;a ^ b
则等价于两个里有一个是 null
,而另外一个不是;最后剩下的情况就是俩参数都不是 null
了,所以可使用挨个字段比较的过程。
顺带一提,因为
ReferenceEquals
比较的是地址是不是一样,所以你不能把俩值类型的东西放进去,比如ReferenceEquals(3, 3)
。这样你是得不到正确结果的。即使我们知道俩都是 3,一定是相等的,但这个方法执行下来,只可能是false
,因为它俩地址不同,只是存储的数据是一样的。
第三,如果类型本身没有关联的话,使用自定义类型转换器的时候请尽量使用显式转换器。比如说我现在想要实现一个类 A
,然后去允许它直接转 int
。因为这俩在实现上好像没啥关系,所以我们尽量都建议你这么转换使用 explicit operator
而不是 implicit operator
。
第四,能不用 this
引用,就不要写出来。因为写出来是没有意义的,写出来只会导致代码看起来更复杂。
第五,请尽量不要使用嵌套类。原因想必我就不多说了吧。