前言
新年好,本篇开始进入第三章,《对象和类型》,深刻理解C#的对象,对于使用好.Net类库非常重要。
01
类和结构
从使用角度看,结构和类的区别很小,比如,将结构定义转换为类,只需要将关键字struct改为class即可。创建结构的时候,也同样可以用关键字new。它们的本质区别是,结构是值类型,存储在栈上,而类存储在堆上。
但我还没碰到什么情况下需要使用结构。因为一般的业务代码,在设计时很难提炼出足够可泛化的含义,而如果只是少数情况下采用结构,对性能的提升微乎其微。但我们也可以发现在.Net框架下,还是比较多的使用了结构,这估计是因为:
.Net作为千万程序员都在使用的框架和类库,性能上精益求精就有必要了
其次,我们发现.Net使用结构是成体系的,比如Point,Size, Rectangle等,大量的“形状”都采用了结构,这从对象化的设计角度看就非常有意义了
02
类
类的数据和函数称为类的成员。清晰的理解类的“数据成员”和“函数成员”,对于用好反射功能非常重要。因为反射针对不同的成员类型有不同的功能实现。
数据成员
数据成员是包含类的数据——字段、常量和事件的成员。特别注意的是,“事件”是数据成员。为什么呢?这后面会讲到,事件是一种特殊的委托,而委托实际上是类,所以事件的实例当然是数据。
函数成员
函数成员提供了操作类中数据的某些功能,包括方法、属性、构造函数和终结器(finalizer)、运算符以及索引器。方法、属性、构造函数大家经常碰到,会比较好理解。而对终结器、运算符、索引器来说,它们都是“特殊的方法”。
属性:函数组,即get函数和set函数的组合。
终结器:类似于构造函数,如果构造函数名为“FuncA”,终结器则名为“~FuncA”,类似Java的析构函数finalize()。
运算符:类的运算符可以重载,实现自定义功能,那么运算符归类为函数,也就好理解了。
索引器:将对象以集合的方式访问。
方法 / ref / out
方法的参数传递值得探讨。函数的参数传递有“值传递”和“引用传递”两种方式。在C#中,除非明确带关键字,所有的参数都通过值来传递。这让我们会产生一个疑问,因为在实际编程中,往往会使用引用类型作为参数,而又没有使用ref关键字时,此时传递的到底是值呢还是引用呢?答案是:传递的还是值。值传递时,仍然进行了值的复制,但复制的是“引用”,而不是对象本身。所以,我们也可以推论说:对于传递引用类型的参数,加不加ref,效果都同加ref是一样的。
和ref对应,还有个out关键字。使用out关键字定义的参数,out会强制要求函数中将变量初始化或赋值后再输出。实际上,使用或者不使用参数关键字,对应了实际使用方法中的三种常见场景:
不带关键字:传入参数,不传出
ref关键字:传入参数,再传出
out关键字:不接受参数传入,但传出参数。out和ref一样,传递的是引用。
命名参数
使用命名参数的作用是,书写调用时,参数的书写顺序不再受限制。比如:定义时为:
public void Test(int arg1, int arg2)
调用时可以为:
user.Test(arg2: 1, arg1: 2);
不过我没有用过命名参数,因为实在找不到应用它的场景。但我回想起在之前公司的基于Repository的架构下,倒是能为它找到用武之地。为什么呢?因为在Repository架构下,为遵循架构规则,需要为每一个表建立对外的业务访问接口,而复杂的业务需求又导致接口需要大量参数的方法重载,比如一个方法可能有超过10个参数定义。这时候,在调用方法的时候,万一有一两个参数写错了顺序,就会导致程序的bug。为了避免这种问题,使用命名参数,明确的指定 参数名:值,相当于程序员在写代码时就进行了明确的参数名检查,从而避免bug。但这种问题,是由于不合理的Repository架构导致的,当废弃了Repository架构后,这种应用场景不复存在。
可选参数
可选参数如:public void Test(int arg1, int arg2 = 10)
可选参数必须是方法定义的最后一个参数。
方法重载
书中说到“如果不能使用可选参数,就可以使用方法重载”。这说明了语言设计的倾向性。即原则上应该多使用可选参数,而避免过多的使用重载,毕竟方法重载的“代码复用度”一般不如可选参数。
何为“方法的重载”?方法的重载,即一个方法的几个版本有不同的签名(即,方法名相同,但参数的个数和/或类型不同,注:不包括返回值)。方法重载的参数限制:
两个方法不能仅在返回类型上有区别。
两个方法不能仅根据参数是声明为ref还是out来区分。
属性
属性(Property)的概念是:它是一个方法或一对方法,在客户端代码看来,它(们)是一个字段。这个定义非常有意思。所谓“客户端代码”,就是调用属性的代码。在调用者看来,属性和字段并无区别。区别在于内部,属性可以对其get/set访问器进行代码定制,从而实现更复杂的需求。
因为属性是特殊的方法,那么要实现一个功能时,是应该定义为属性还是定义为方法,这在12月25日的文章中有深入分析。
在有不少老的代码里,属性是这么定义的:
string column1;
public string Column1
{
get
{
return column1;
}
set
{
column1 = value;
}
}
这是因为在.Net2.0时,对属性的定义还没那么方便。虽然它已经比Java的get,set方法好多了,但还是比较繁琐。在更新版本的.Net下,已经可以使用这种方式定义属性:
public int Column1 { get; set; }
这叫做“自动实现的属性”。实际上它只是一个语法糖,本质并没有变。
get,set访问器可以带上修饰符。如果没有带上修饰符,那么它使用的是属性的修饰符。比如可以:
public int Column1 { get; private set; }
public int Column2 { get; }
public int Column3 { private get; set; } //只写属性,不应该出现
内联
因为属性本质是方法,我们也看到实际使用中,大多数属性都是采用“自动实现属性”的,那么为了实现属性的偶尔才使用的“访问限制”功能,而将“字段访问”改为“方法”访问,会增加方法访问的额外开销吧?值得吗?
我们不用担心这个问题。因为.Net编译器已经将属性访问代码编译为“内联代码”而不是“函数调用”代码。
类是对象化编程的基本概念,内容非常多。下一篇将继续讲讲:构造函数、只读字段、匿名类型、结构详解、部分类、静态类、Object类、扩展方法,等。
觉得文章有意义的话,请动动手指,分享给朋友一起来共同学习进步。