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

第 34 讲:面向对象编程(六):关于类,其它想说的东西

2021-05-08 08:23 作者:SunnieShine  | 我要投稿

对于 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 赋值。

这就体现出了重载的好处和用途了。重载只需要区别参数的数据类型和参数的个数,就可以知道到底调用的是哪个方法。总之,我们需要掌握的是这样两个知识点:

  1. 构造器之间可以互相调用,使用的是 : this(参数表列) 的语法;

  2. 构造器即使能够通过这个语法来达到串联调用的过程,但执行必须是在最开始就执行,这是无法改变的。

Part 2 this 引用

在我们之前的语法讲解里,我们是没有提到这个概念的。按照一般教材的书写,this 引用可能早就讲过了。我之所以放在最后,作为不重要的内容来说,是因为它的存在感很低。

考虑一种情况。字段被属性封装后,我们使用“下划线+驼峰命名法”的命名规则来给字段命名。其实,这么取名还有一个好处:避免冗余 this 引用的代码书写。

假设,我们为字段取名的时候,最开头不添加下划线的话,我们还是拿 Person 类为例:

显然,我们这里不一定非得赋值给 Name 属性,因为给 Name 属性赋值后,还是会调用 set 方法,然后给 name 字段赋值。因此我们便可直接通过 name 参数对 name 字段赋值。可问题在于,我们直接写 name = name; 的话,我咋知道谁是谁呢?参数和字段都用的一个名字 name,巧就巧在大小写都是一样的。这我咋办呢?难道就不能使用这个字段名了吗?

也不是。我们只需要在字段 name 的左边添加语法 this. 来表达“这个赋值的是字段”,就可以了:

this.name 前面追加了 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 就等价于两个参数都是 nulla ^ b 则等价于两个里有一个是 null,而另外一个不是;最后剩下的情况就是俩参数都不是 null 了,所以可使用挨个字段比较的过程。

顺带一提,因为 ReferenceEquals 比较的是地址是不是一样,所以你不能把俩值类型的东西放进去,比如 ReferenceEquals(3, 3)。这样你是得不到正确结果的。即使我们知道俩都是 3,一定是相等的,但这个方法执行下来,只可能是 false,因为它俩地址不同,只是存储的数据是一样的。

第三,如果类型本身没有关联的话,使用自定义类型转换器的时候请尽量使用显式转换器。比如说我现在想要实现一个类 A,然后去允许它直接转 int。因为这俩在实现上好像没啥关系,所以我们尽量都建议你这么转换使用 explicit operator 而不是 implicit operator

第四,能不用 this 引用,就不要写出来。因为写出来是没有意义的,写出来只会导致代码看起来更复杂。

第五,请尽量不要使用嵌套类。原因想必我就不多说了吧。


第 34 讲:面向对象编程(六):关于类,其它想说的东西的评论 (共 条)

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