C# 重载与重写
基本定义
构造函数:
构造函数是一种特殊的方法,主要用来创建对象时初始化对象,即为对象成员变量赋初始值,总与new运算符一起使用再创建对象的语句中。特别的一个类中可有多个构造函数,可根据其参数的不同或参数类型的不同开区分它们,即构造函数的重载。
重写:
当一个子类继承一个父类,而子类中的方法与父类中的方法的名称,参数个数,类型都完全一致时,就称子类中的这个方法重写了父类的方法。
重载:
一个类中的方法与另一个方法同名,但是其参数表不同,这种方法称之为重载方法。
实现方法
重写:
通常,派生类继承基类的方法。因此,在调用对象继承方法的时候,调用和执行的是基类的实现。但是,有时需要对派生类中的继承方法有不同的实现。例如,假设动物类存在“跑"的方法,从中派生出马和狗,马和狗的跑得形态是各不相同的,因此同样方法需要两种不同的实现,这就需要"重新编写"基类中的方法。“重写"基类方法就是修改它的实现或者说在派生类中重新编写。
重载:
在一个类中用相同的名称但是不同的参数类型创建一个以上的过程、实例构造函数或属性。
区别
区别\ 名称 | 重载 | 重写 |
---|---|---|
范围 | 同一个类 | 不同的类 |
方法名 | 相同 | 相同 |
参数列表 | 必须不同,与参数列表顺序无关 | 相同 |
修饰符 | 无关 | 大于父类方法 |
抛出父类没有的异常 | 可以 | 不可以 |
返回类型 | 不同 | 相同 |
与面向对象 | 多态 | 继承 |
重载特征:
I.方法名必须相同
II.参数列表必须不相同,与参数列表的顺序无关
III.返回值类型可以不相同
用于在给定了参数列表和一组候选函数成员的情况下,选择一个最佳函数成员来实施调用。
重写特征:
重写就是子类重写父类的方法,在调用的时候,子类的方法会覆盖父类的方法,也就是会调用子类的方法。在父类中的方法必须有修饰符virtual,而在子类的方法中必须指明override;
发生方法重写的两个方法返回值,方法名,参数列表必须完全一致(必须具有相同的方法名和参数列表,返回值类型必须相同或者其子类);
子类抛出的异常不能超过父类相应的方法抛出的异常;
子类方法的访问级别不能低于父类相应方法的访问级别(public,package,protected, private),不能缩小被重写方法的访问权限;
方法体不同。
重写格式:
|
|
重写以后,用父类对象和子类对象访问myMethod()方法,结果都是访问在子类中重新定义的方法,父类的方法相当于被覆盖掉了。
- 子类中为满足自己的需要来重复定义某个方法的不同实现。
- 通过使用override关键字来实现覆写。
- 只有虚方法和抽象方法才能被覆写。
要求(三相同)即相同的方法名称,相同的参数列表,相同的返回值类型。
表达式
Lambda表达式
- lambda表达式是一个匿名函数,用它可以简化代码,常常用做委托,回调;
- lambda表达式都使用运算符 => ,当见到这个符号,基本上是一个lambda表达式;
- lambda运算符的左边时输入参数(), => ,右边时表达式或语句块;
- lambda表达式,是可以访问到外部变量的,可以将此表达式分配给委托类型;
Lambda运算符
为研究Lambda表达式,新建一个SimpleLambdaExpression的控制台应用程序。现在,考虑泛型List类型的FindAll()方法,此方法需要System.Predicate的泛型委托,它用于包装任何接受类型为T的输入参数并且返回布尔值的方法。在Program类型中增加一个方法,叫做TraditionalDelegateSyntax(),它与System.Predicate
类型交互,找出整数List中的偶数。
case1: 使用传统委托方式
|
|
分析:以上代码可以按预期工作,但IsEvenNumber()方法只会在有限的环境中被调用,而且,如果调用FindAll(),就需要完整的方法定义。但如果用匿名方法来代替,代码就简洁许多。考虑下面Program类型的新方法。
Case2: Program类型的新方法
|
|
分析:以上的代码并不是首先创建一个Predicate委托类型,然后再编写一个独立方法,而是使用了一个匿名方法。这个方向是正确的,但是仍然需要使用关键字delegate(或者是一个强类型的Predicate),而且还需要保证输入参数是百分百匹配的。我们认为这样的,定义匿名方法的代码还是有些冗长。可以使用Lambda表达式进一步来简化方法FindAll()的调用,使用新的语法时,底层的委托语法将会消失得无影无踪,请看下面的代码:
Case3:Lambda表达式
|
|
我们将奇怪的语句传递给FindAll方法中,这些语句也就是Lambda表达式,这时候不需要用Predicate(或关键字delegate),而只用一个简单的lambda表达式即可。
在深入Lambda表达式之前,我们需要知道Lambda可以应用于任何匿名方法可以应用的场景,而且比匿名方法更节省编码时间。但实际上,C#编译器只是把表达式翻译成使用委托Predicate的普通匿名方法而已(可以使用isdasm.exe和reflector.ex进行验证),如下面的代码:
|
|
剖析Lambda表达式
Lambda形式可以理解为:Arguments ToProcess => StatementsToProcessThem
List<int> evenNumbers = list.FindAll(i => (i % 2 == 0));
// ‘ i ’是参数列表
// (i % 2 == 0)就是处理“i”的表达式
Lambda表达式的参数可以是显式类型化的也可以是隐式类型化的。现在,表示参数i的数据类型(整型)时隐式类型化的。编译器可以根据整个Lambda表达式的上下文和底层委托推断出i是一个整型。尽管如此,我们也可以显式定义表达式每一个参数的类型,如下用包围数据类型和变量即可:
// 现在,显式的定义参数类型
List<int> evenNumbers = list.FindAll((int i) => (i % 2 == 0));
为保持风格一致,隐式还可以使用括号写成如下:
List<int> evenNumbers = list.FindAll((i) => (i % 2 == 0));
多语句处理参数
我们第一个Lambda表达式是一个求布尔类型值语句,但是我们知道很多委托目标需要执行多条代码执行,C#允许使用一系列的代码语句来定义Lambda表达式。当表达式必须使用多行代码处理参数时,你可以使用花括号限定范围。
|
|
使用Lambda表达式重写CarDelegate示例
推荐使用Lambda表达式是因为它为我们提供了一种简单明了的方式进行匿名函数的定义(由此间接地简化了关于委托的编码操作),以Lambda表达式重写CarDelegate示例,以下展示项目的Program类的简化版本,它使用传统的委托语法响应每一个回调:
|
|
用匿名方法重新写的Main();
|
|
用Lambda表达式重写Main();
|
|
含有多个(或零个)参数的Lambda表达式
以上都是编写的Lambda表达式都只含有一个参数,实际上,Lambda表达式可以处理多个参数或者不提供任何参数,我们创建一个LambdaExpressionMultiplePrams来说明问题。假设SimpleMath有以下更新:
|
|
我们可以看到,委托MathMessage需要两个参数,使用Lambda表达式的Main如下所示:
|
|
这旨在让我们理解Lambda表达式的整体角色和它是如何以“函数方式”,匿名方法和委托状态共同工作的,尽管需要一些时间来适应新的Lambda表达式(=>),不过要始终记住Lambda表达式可简化为:Arguments ToProcess => StatementsToProcessThem 的简单形式,并且在LINQ编程模型中使用了许多Lambda表达式来简化代码。
可空表达式 ?
单问号 ?
?: 单问号用于对 int,double,bool 等无法直接赋值为 null 的数据类型进行 null 的赋值,意思是这个数据类型是 NullAble 类型的。
|
|
在此表达式出现之前,当我们得到一个对象并想使用这个对象,需判断该对象是否为null,否则使用对象时就会抛出NullReferenceException
异常(未将对象引用设置到对象的实例),如下例:
|
|
现在使用可空表达式(?.),作用就是当对象为null时,就不去访问后面点的对象,如下代码:
|
|
?.表达式将声明对象转换成了可为空类型
上面的代码时字符串,那如果时int类型又会是怎样?
int? age = user?.Age;
数据类型后面加一个问号,表示该类型可以是 null。你可以通过该对象的 HasValue
属性做一个判断,表示该对象有值,然后再使用该对象的 Value
属性获取到值。
|
|
再举一个例子:
|
|
总结:
很明显可空表达式减少了一层判断,明显减少了我们的代码量,也提高了我们的效率。但是需要我们选择性使用,不要滥用。
双问号 ??
??: 双问号??可用于在判断一个变量在为null时返回一个指定的值, 具体来讲,??叫做null合并运算符,如果此运算符的左操作数不为 null,则此运算符将返回左操作数;否则返回右操作数。可以用来给变量设置默认值。特别提醒: 记住和空有关的时候,才要去用?? 。如果不会有空的判断,就别用了。因为这个是空的合并运算符。也有人说??是?:的语法糖而已,但是实际上??进行了很大改进,能够更好的支持表达式。
|
|
还有这个例子:
|
|
实际上,??在复合情形中,更好用。
还有如何把第一个表达式,用?和??进行合并。
var flag = tt == null ? 1: tt.Name
C# 可空类型(Nullable)
C#提供了一个特殊的数据类型, nullable 类型(可空类型), 可空类型可以表示其基础值类型正常范围内的值,再加上一个 null 值。
例如,Nullable< Int32 >,读作"可空的 Int32”,可以被赋值为 -2,147,483,648 到 2,147,483,647 之间的任意值,也可以被赋值为 null 值。类似的,Nullable< bool > 变量可以被赋值为 true 或 false 或 null。
在处理数据库和其他包含可能未赋值的元素的数据类型时,将 null 赋值给数值类型或布尔型的功能特别有用。例如,数据库中的布尔型字段可以存储值 true 或 false,或者,该字段也可以未定义。
声明一个nullable类型(可空类型)的语法如下:
<data_type> ? <variable_name> = null;
下面的示例展示可空数据类型的用法:
|
|
正则表达式
基础学习参见菜鸟教程 正则表达式
Partial class说明
partial class基础
C# 2.0就可以将类,结构或接口的定义分拆到两个或多个源文件中,在类声明前添加partial关键字即可。
例如:下面的PartialTest类
|
|
可在不同的源文件中拆写成下面形式:
|
|
另一个文件中写:
|
|
- 什么情况下使用分部类
- 处理大型项目时,使一个类分布于多个独立文件中可以让多位程序员同时对该类进行处理(相当于支持并行处理,很实用);
- 使用自动生成的源时,无需重新创建源文件便可把代码添加到类中。可以观察到Visual Studio在创建Windows窗体,Web窗体都使用此方法。你不用编辑Visual Studio所创建的文件,便可创建使用这些类的代码。换句话说:系统会自动创建一个文件(一般记录的是窗体及窗体中的控件的属性),另一个或几个文件记录的是用户自己编写的代码。这两部分分开可以使结构显得非常清晰,用户只需关注自己负责的那部分就行了(需要的话,这两部分可以互相调用)。等到了编辑运行的时候,系统会自动将这两部分合成一个文件。
- 使用Partial需要注意以下情况
- 使用partial关键字表明可在命名空间内定义该类,结构或接口的其他部分;
- 所有部分都必须使用partial关键字;
- 各个部分必须具有相同的可访问性,如public,private等;
- 如果任意部分声明为抽象的,则整个类型都被视为抽象的;
- 如果将任意部分声明为密封的,则整个类型都被视为密封的;
- 如果任意部分声明继承基类时,则整个类型都将继承该类;
- 各个部分可以指定不同的基接口,最终类型将实现所有分部声明所列出的全部接口;
- 在某一分部定义中声明的任何类、结构或接口成员可供所有其他部分使用;
- 嵌套类型可以是分部的,即使它们所嵌套于的类型本身并不是分部的也如此。如下所示:
|
|
- 使用分部类的一些限制
- 要作为同一类型的各个部分的所有分部类型定义都必须使用partial 进行修饰。如下所示:
|
|
- partial 修饰符只能出现在紧靠关键字class、struct 或interface前面的位置(枚举或其它类型都不能使用partial);
- 要成为同一类型的各个部分的所有分部类型定义都必须在同一程序集和同一模块(.exe 或.dll 文件)中进行定义。分部定义不能跨越多个模块;
- 类名和泛型类型参数在所有的分部类型定义中都必须匹配。泛型类型可以是分部的。每个分部声明都必须以相同的顺序使用相同的参数名。
partial class扩展功能新思路
开闭原则:“对修改封闭,对扩展开放”。在面向对象的系统中,通过类的继承实现扩展。.net中提供的partial class提供了扩展类的新思路。
- 应用场景
可以使用partial class的场景很多。这里分析一个ORM的例子。
系统中有一个Cat类,属性ID、Age、Weight都需要存储到数据库中,一个信息系统中常见的需求。通过读取数据库的结构,可以用工具生成Cat类的代码。并且ORM框架支持了从数据库信息生成Cat对象。
现在的Cat什么动作都没有,客户说,我们需要一个Miaow()的函数。这时就需要对ORM生成的Cat类进行扩展了。
可以肯定地一点是,我们不能修改自动生成的代码,因为这会牵涉到数据库结构与代码同步的问题。解决这个需求有两种方法:继承方式扩展,partial class扩展。
- 继承方式扩展
工具自动生成一个CatBase类,这个类只有属性,嵌入到ORM框架中。既然需要扩展功能,很容易想到对这个基类继承,于是有了Cat类。Cat类如愿以偿地有了Miaow()函数。
以前系统中用的是CatBase的实例,现在创建CatBase实例的地方需要改为创建Cat的实例。这个问题让ORM框架解决吧。
客户的需求实现了,我们自己的代码生成也没有遭到破坏,任务完成。
- partial class扩展
partial class简单地说就是可以将一个类的代码写到两个或多个代码文件中。编译器在编译的过程中将这几个文件组合起来一起编译。一个很酷的技术。
工具生成的Cat类仍然不变。既然需要增加函数,那么在新建一个代码文件,将Miaow()函数写出来就可以。需要做的仅仅是将类的声明由class改为partial class,任务完成。
- 对比分析
两种思路都可以实现需求。孰优孰劣需要仔细分析一下。
实例创建:partial class更加简洁。
系统复杂度:对于系统来说,partial class方式下只存在一个类,而继承方式有两个类。
继承逻辑:从逻辑上讲,Cat并不需要一个基类CatBase,这样做仅仅是因为在代码构建过程中的一个限制。
维护性:两种方式下都会存在两个代码文件,维护成本并没有区别。
可读性:两个Cat文件确实让人费解。
整体上说,使用partial class方式的代码编写会更优雅一些。
“继承”的这种方式比较符合传统的思维习惯,而partial class到底是不是满足开闭原则呢,这点确实不好说。不过在软件构建上,我是一个实用主义者,哪种方式好用就用哪一种。
在ORM的场景中,partial class更加好一些,但有的时候,两个类之间确实就存在继承关系,那么就必须用到继承了。虽然绝大多数情况下,都需要继承方式,但是既然有了partial class技术,我们在做设计时也需要考虑这个思路。
可以看到在VS,Form,Dataset中都使用了partial class方式,原理和这个一样。但是要将这个原理推广到“业务实体”中,可能在理解上需要有所突破。
使用partial class确实会带来可读性的损失,尤其是一个类分布在很多个文件中的时候,所以文件的命名最好是有一个规范来保证。
参考资料
- C#与.net3.5高级程序设计(第4版)
- C# 6.0语法 可空表达式 ?
- C# 基础知识之Partial