接上一篇《C#基础之类型和成员基础以及常量、字段、属性》html
C#中的方法分为两类,一种是属于对象(类型的实例)的,称之为实例方法,另外一种是属于类型的,称之为静态方法(用static关键字定义)。你们都是作开发的,这两个也没啥好说的。编程
惟一的建议就是:你的静态方法最好是线程安全的(这点是提及容易作起难啊……)。数组
构造器是一种特殊的方法,CLR中的构造器分为两种:一种是实例构造器;另外一种是类型构造器。和其余方法不一样,构造器不能被继承,因此在构造器前应用virtual/new/override/sealed和abstract是没有意义的,同时构造器也不能有返回值。 安全
实例构造器用来初始化类型的实例(也就是对象)的初始状态。 网络
对于引用类型,若是咱们没有显式定义实例构造器,C#编译器默认会生成一个无参实例构造器,这个构造器什么也不作,只是简单调用一下父类的无参实例构造器。这里应该意识到,若是咱们定义的类的基类没有定义无参构造器,那么咱们的派生类就必须显式调用一个基类构造器。ide
class MyBase { public MyBase(string name) { } } class MyClass : MyBase { }
上面的代码会报“MyBase不包含采用0个参数的构造函数”的错误,必须显式调用一个基类的构造器:函数
class MyBase { public MyBase(string name) { } } class MyClass : MyBase { public MyClass(string name) : base(name) { } }
一个类型能够定义多个实例构造器,只要这些构造器有不一样的方法签名便可。以下MyBase类,我定义了三个构造器:工具
class MyBase { public MyBase() //无参构造器 : this(string.Empty) { } public MyBase(string name) //一个参数的构造器 : this(name, 0) { } public MyBase(string name, int age) //两个参数的构造器 { } }
除了实例构造器,C#语言还提供了一种初始化字段的简便语法,称为“内联初始化”:性能
从编译后生成的IL代码能够看出,内联初始化本质是在全部实例构造器中,生成一段字段初始化代码的方式来实现的。注意这里一个潜在的代码膨胀问题,若是咱们定义了多个实例构造器,那么在每一个实例构造器开头处,都会生成这样的初始化代码。在有多个实例构造器的类型定义中,应尽可能减小这种内联初始化,能够经过建立一个构造器来初始化这些字段,而后让其余构造器经过this关键字来调用这个构造器。学习
对于值类型,C#不会对值类型生成默认的无参构造器,但CLR老是容许值类型的实例化。即对于如下的值类型定义,虽然咱们没有定义任何构造器,C#也没有为咱们生成默认无参构造器,但它老是能够经过new实例化的(值类型的字段被初始化为0或null)。
struct MyStruct { public int x, y; } MyStruct ms = new MyStruct(); //老是能够实例化
咱们能够为值类型定义有参构造器(C#不容许值类型定义无参构造器),但在内部必须初始化值类型的全部字段。
struct MyStruct { public int x, y; public string z; public MyStruct(int a) //异常:在控制返回到调用以前,字段y、z必须彻底赋值。 { x = a; } }
像上面这样为值类型定义一个有参构造器时,编译器会报“在控制返回到调用以前,字段y、z必须彻底赋值”的错误。为了修正这个问题,能够采用下面的语法为值类型字段初始化。
struct MyStruct { public int x, y; public string z; public MyStruct(int a) //在控制返回到调用以前,字段y、z必须彻底赋值。 { this = new MyStruct(); //this表明值类型实例自己,用new初始化值类型全部字段为0或null。 //this = default(MyStruct); //这种方式书上没提,但我认为这样也能够。 x = a; } }
类型构造器(静态构造器)用来初始化类型的初始状态,而且有且只能定义一个,且没有参数。类型构造器老是私有的,C#会自动把它标记为private,事实上C#禁止开发人员对类型构造器应用任何访问修饰符。
CLR在第一次使用一类型时,若是该类型定义了类型构造器,CLR便会以线程安全的方式调用它。这里应该意识到对类型构造器的调用,因为CLR要作大量检查与判断和线程同步,因此性能上会有所损失。
类型构造器的一般用来初始化类型中的静态字段,C#一样提供了一种内联初始化的语法:
从编译器生成的IL可知,静态字段的内联初始化其实是在类型构造器生成初始化代码完成的,并且首先生成的是内联初始化代码,而后才是类型构造器方法内部显式包含的代码。
注意:虽然值类型能定义类型构造器,但永远都不要那么作。由于CLR有时不会调用值类型的类型构造器。
这两个概念都是针对于类型的继承层次结构中来讲的,若是没有了继承,它们是毫无心义的。这也意味着它们的可访问性至少是protected,即对派生类是可见的。
抽象方法是只定义了方法名称、签名和返回值类型,而没有定义任何方法实现的一种方法。C#中用abstract定义,抽象方法所在的类确定是抽象类。因为抽象方法没有定义方法实现,因此它是没有意义的,必须在派生类中提供方法的实现(若是派生类没有提供,那么它必须仍然定义成抽象类)。
abstract class MyBase { //静态方法 public static void Test0() { /*方法实现*/ } //实例方法 public void Test1() { /*方法实现*/ } //抽象方法 public abstract void Test2(); //虚方法 protected virtual void Test3() { /*方法实现*/} }
在C#中用virtual定义的方法是虚方法,它看上去只是比定义一个普通实例方法多了一个virtual关键字。虚方法老是容许在派生类中重写,但不强求,这正在它和抽象方法的区别。也能够逻辑上把虚方法想象成提供了默认实现的抽象方法,由于提供了默认实现,因此不强求派生类中重写。
抽象方法编译后被标记为abstract virtual instance(抽象虚实例方法),虚方法编译后被标记为virtual instance(虚实例方法)。
抽象方法和虚方法共同的特色都是能够在派生类中重写,在C#中用override关键字来重写一个方法。在VS中,若是咱们在类中输入override关键字加空格,便会显示出全部基类中的虚成员(方法、属性、事件等)。由于抽象方法编译后是抽象和虚的,因此也会显示在列表中。
重写后的方法仍然是virtual的(但再也不是抽象的)
virtual方法是能够被派生类重写的,若是不但愿重写后的方法被接下来的派生类(即派生自MyClass的类)重写,能够在override前应用sealed关键字,将方法标记为封闭的。
以下图中,我将MyClass中的Test3标记为sealed后,MyClass的派生类中,VS列出的可重写的成员中便没有Test3了。
固然,还能够对类应用sealed关键字,这样整个类都不能被继承了!类都不能被继承了,类里包含的全部虚方法更不谈重写了。
主要是partial关键字(也能够应用于类、结构和接口),能够将一个方法定义到多个文件中。
一般有这么一种场情:咱们每每利用代码生成工具生成一些模板化的代码,但又须要对某些细节进行定制,虽然能够经过虚方法重写来实现,但这样作存在两点问题:
这时候,就能够利用分部方法来实现。让代码生成器生成一个分部类(注意这个类能够是密封的),把实现细节抽象成一个方法定义。像下面这样:
//工具生成部分 sealed partial class XXOO { //声明一个分部方法 partial void PrepareSomething(string boy, string girl); public void DoSomething() { //调用分部方法(若是没有提供实现,编译后这句会被优化掉) PrepareSomething("", ""); /*其余逻辑*/ } }
若是咱们没有提供分部方法的实现,那么编译后,整个方法的定义和全部对此方法的调用都会被优化(删除)掉,这样可让代码更少更快!也正由于这一点(编译后分部方法可能不存在),因此分部方法不能定义任何修改符,也不能定义返回值!
固然用分部方法主要仍是为了提供实现细节,咱们甚至能够在不一样的文件中来定义这个类(在VS中输入partial加空格,便会列出当前分部类中的还未提供实现的分部方法):
//自定义的部分 sealed partial class XXOO { //提供具体的实现细节 partial void PrepareSomething(string boy, string girl) { /*提供实现细节的代码*/ } }
咱们再来看看提供分部方法的实现代码后,编译器生成了什么:
关于分部方法有几点要小注意一下:
这两个方法涉及CLR的垃圾回收部分,这里只是从方法层面上谈谈这两个方法。咱们知道,C#是托管语言,咱们写的程序最终托管给CLR,CLR有强大的自动垃圾回收机制来帮助咱们回收内存资源。但注意CLR自动回收的仅是内存资源,有些类除了要利用内存资源外,还须要利用一些其余的系统资源(好比文件、网络链接、套接字、互斥体等),因此CLR提供了一种机制来释放这些资源,这即是Finalize方法(终结器)。
这里的Finalilze方法并非指直接在类中定义一个Finalize方法(虽然能够定义,但永远不要这么作!),而是指用析构语法来定义的一种方法,即“~类名()”的方式定义的方法,该方法编译后,会生成名为Finalize的方法。CLR会在决定回收包含Finalize方法的对象以前用一个特殊的线程调用Finalize方法来释放一些资源(这个具体的过程待往后写到CLR垃圾回收部分慢慢聊)。
下图简单演示了一下如何定义一个终结器,咱们用定义析构函数的语法来定义了一个方法(注意这个方法没有参数和任何修饰符),编译后,编译器为咱们生成一个名为Finalize的protected virtual方法。且在方法内部生成一个try块包装原方法内的代码,生成一个finally块来调用基类的Finalize方法。
虽然定义Finalize方法的语法和C++的析构函数语法同样,但CLR书上说二者原理仍是彻底不一样,因此不能称为析构器(个人理解C++中的析构函数应该是释放对象所用的资源包括内存资源,调用后对象便被清理干净了;而C#中的Finalize方法只是释放对象所用的系统资源,调用后对象仍然存活,直到CLR将其回收,不知道这么理解对不对啊,请指点!)。
虽然Finalize方法颇有用,能确释放一些资源。但有一点要注意,就是它的调用是由CLR决定的,因此调用时间咱们没法保证。因此咱们须要一种机制来显式地释放资源,这即是Dispose模式。.Net里提供了IDisposable接口(包含惟一一个Dispose方法),咱们只要实现该接口即表明咱们的类实现了Dispose模式。在Dispose方法内部,咱们关闭对象所用到的系统资源。这样咱们在代码中,就能够显式调用Dispose方法来释放资源,而不是被动地交给CLR去释放,《CLR Via C#》书中建议全部实现终结器的类都同时实现Dispose模式。以下面的类,实现终结器的同时还实现Dispose模式(先无论实现细节是否合理):
class MyResource: IDisposable { private Mutex mutex; //构造器 public MyFinalization() { mutex = new Mutex(); } //终结器 ~MyFinalization() { mutex = null; } //实现IDisposable接口 public void Dispose() { mutex = null; } }
这样在咱们使用完MyResource对象后,就能够经过调用Dispose方法释放资源。
MyResource resource = new MyResource(); // //…使用资源… // resource.Dispose(); //调用Disopse释放对象所用的资源
对于实现Dispose模式的类型,C#还提供了using语句来简化咱们的编码。
using (MyResource resource = new MyResource()) { // //…使用资源… // }
上面的代码等价于
MyResource resource = new MyResource(); try { // //…使用资源… // } finally { if (resource != null) (resource as IDisposable).Dispose(); }
扩展方法使咱们可以向现有类型“添加”方法,而无需建立新的派生类型、从新编译或以其余方式修改原始类型。 扩展方法是一种特殊的静态方法,但能够像被扩展类型上的实例方法同样进行调用,同时它能够获得VS智能提示的良好支持(咱们能够像使用对象实例方法同样,点出扩展方法)。
定义一个扩展方法,有如下几点要求:
static class ExtensionMethods //静态类名 无所谓 { public static bool IsNullOrEmpty(this string s) //扩展方法 { return string.IsNullOrWhiteSpace(s); } }
上面示例为string类型对象定义了一个名为IsNullOrEmpty的方法,只要咱们的代码中引入扩展方法全部静态类ExtensionMethods 的命名空间,就能够直接在代码中,像使用string类型原生方法同样使用它了。
string name = "heku"; name.IsNormalized(); //Sytem.String类型原生方法 name.IsNullOrEmpty(); //扩展方法
关于扩展方法,还有如下几点要注意:
扩展方法延伸阅读:鹤冲天 http://www.cnblogs.com/ldp615/archive/2009/08/07/1541404.html
传递参数就是赋值操做,咱们能够把方法参数当作方法定义的一些变量,传参就是对这些变量进行赋值的过程。赋值过程就是拷贝线程栈内容的过程,值类型的栈内容保存的就是值实例自己,而引用类型栈内容保存的是引用实例在堆上的地址。因此这里的区别主要是值类型与引用类型内存分配上的区别,具体可参考《C#基础之基本类型》。因此在传参后,方法的值类型参数拥有原始值的复制(一个副本),对其的更改不影响原始值,由于它们根本就不是一块内存!方法的引用类型参数拥有与原始值相同的地址,它们指向同一块堆内存,因此对引用类型参数的更改会影响原始值。以下示例,分别定义了一个值类型val和引用类型refObj,在调用Work方法后,值类型val未被修改,引用类型refObj被修改了。
class Program { static void Main(string[] args) { DoWork dw = new DoWork(); int val = 555; RefType refObj = new RefType { Id = 1, Name = "Heku" }; Console.WriteLine(val); Console.WriteLine("Id={0},Name={1}\n", refObj.Id, refObj.Name); //********输出******** //555 //Id=1,Name=Heku dw.Work(val, refObj); Console.WriteLine(val); Console.WriteLine("Id={0},Name={1}\n", refObj.Id, refObj.Name); //********输出******** //555 //Id=2,Name=Heku修改后 Console.ReadKey(); } } //一个引用类型 class RefType { public int Id { get; set; } public string Name { get; set; } } class DoWork { //修改值类型a 和 引用类型 b public void Work(int a, RefType b) { a++; b.Id++; b.Name = b.Name + "修改后"; } }
咱们定义一个有不少参数的方法后,那么全部调用处都要准备好这些参数才能调用此方法。但每每有些时候,咱们调用时只关心其中的部分参数,一般咱们是经过重载来定义几个参数比较少的方法,内部补全其余参数再调用参数最多的那个方法。但这是纯体力活,并且也不能重载出全部可能的参数组合状况。所以C#提供一种机制,能够在定义方法的同时,给参数指定默认值,这样在方法调用处,若是没有给参数提供值,就会采用默认值,拥有默认值的参数就称为可选参数。
//参数isToUpper由于有了默认值true, //参数other由于有了默认值0, //因此参数isToUpper和other在调用时能够不提供,故称为 可选参数 public string ToUpperOrLower(string message, bool isToUpper = true, int other = 0) { if (isToUpper) return message.ToUpper(); else return message.ToLower(); }
参数isToUpper和other由于提供了默认值,因此咱们能够仅提供message的值,来调用方法:
//调用 没有传可选参数 string result = dw.ToUpperOrLower("HeKu");
若是咱们想为第二个可选参数other显式提供一个值,那么按参数只能一对一按顺序匹配的规则,咱们不得不指定isToUpper的值,这很不爽,因此命名参数的登场了!咱们能够在调用时,用“参数名:参数值”的语法给参数提供值,这种语法的做用是要求参数的匹配方式不要按参数顺序,而是根据提供的名称。像下面这样(没有用命名参数语法的参数仍是按参数顺序匹配,如第一个参数”Heku”):
//为第二个可选参数 显示提供一个值 string result = dw.ToUpperOrLower("HeKu", other: 25);
在定义方法参数时,还有几点要小注意一下:
若是咱们要设计一个方法,来计算全部输入数字的总和。按以往咱们会这么实现(不要关注方法内部实现是否合理):
//计算任意个数字和 public int sum(int[] numbers) { int sum = 0; foreach (int item in numbers) { sum += item; } return sum; }
由于输入的数字个数是未知的,这里用一个数组来接收这些数字。在调用时,咱们不得不先初始化一个数组,而后再调用方法。为了简化这种编程方式,能够在numbers参数定义前,应用params关键字。
//计算任意个数字和 public int sum(params int[] numbers) { int sum = 0; foreach (int item in numbers) { sum += item; } return sum; }
如今就能够直接用这种直观的方式调用sum了:
sum(1, 2, 3);
固然也能够用传统的方式来调用:
int[] numbers = new int[] { 1, 2, 3 }; sum(numbers);
可变数量的参数,有几点要小注意一下:
默认状况下,CLR中的全部方法参数都是传值(线程栈的内容)的,但能够经过在参数应用ref或out关键字来改变这一默认方式。这两个关键字惟一的区别就是,使用ref标识的参数要在传递以前初始化,而使用out标识的参数不须要。
应用了ref或out关键字的参数在传递时,传递的是线程栈内容的引用(地址),注意这里不是堆的地址(以引用方式传参并非说将参数转换成引用类型来传递)。下面看一个例子:
public void Update(ref int a,ref object b) { a++; b = null; }
上面定义了一个方法,接收一个值类型参数,一个引用类型参数。并要求参数以引用的方式传递(加了ref关键字)。下面开始调用:
int a = 100; object o = new object(); object c = o;
Update(ref a,ref c);
Console.WriteLine(a); Console.WriteLine(o == null); Console.WriteLine(c == null);
会输出什么?你想到了吗?答案是:
101 False True
上面咱们讲过,以引用的方式传参传递的是栈的地址。值类型自己的值就是分配在栈上,因此以引用方式传参的值类型就像以传值方式传递的引用类型(比较绕,好好想一下),最终的效果就是Update的第一个参数指向了变量a的栈,因此在方法内部的更改也直接影响到了变量a。对第二个参数,我特别事先定义了两个变量o和c,让它们都指向堆上同一块内存空间,而后把变量c的栈地址传给了方法的第二个参数,在方法内部将第二个参数设为null,实际上就是把c的栈内容设为了null(这点我是根据现象推出来的,究竟是不是这样?请大牛指点!),但这丝毫没有影响到堆上的对象和变量o!因此最终的结果就是a被修改为101,o没变,c被修改成null。若是把第二个参数的ref去掉,结果会是什么样呢?这个请你们本身think一下吧~本丝又敲了两天键盘,眼睛好累啊~
又一个周末,终于敲完了这篇读书笔记性质的总结。给本身列的提纲中“操做符重载方法、转换操做符方法”这一部分因为本身未作过多了解,故未写进来(待往后有机会再补进来吧)。
各位园友同行,本丝也是学习中的菜鸟一枚,若是某些知识点我理解有误,请你们指出!感谢!