一、面向对象
C#是一种面向对象编程语言。面向对象编程,使用许多代码模块,每个模块都提供特定功能,每个模块都是孤立的,甚至与其他模块完全独立,这种模块化编程方法提供了非常好的多样性,大大增加了重用代码的机会。基于对象,可以根据所要解决的问题建立代码的结构,把宝贵的时间用于考虑需要处理的具体问题,而不是深陷于编写代码的繁杂细节中。
面向对象编程中,一切皆是对象。
1. 类和对象
类是指模板,而对象指的是具体,对象是C#的一个工具,可以用于处理一组类似的事物。类对于对象来说就像是设计蓝图,由类创建新对象时,称为该类的一个实例,这个过程就是实例化。对象可以从类中得到方法。
类的属性和字段,是对象存储数据的方式和手段,同一个类的不同对象在属性和字段中存储了不同的值。属性可以看作一种特殊的方法,一般具有set和get方法,也可以进行字段运算或者其他方法体代码。
方法用于提供访问对象的功能,与属性和字段一样,方法可以是公共或私有,根据需要限制外部代码。
“静态”,静态方法不要求有实例,而非静态方法需要先有一个实例。静态属性、静态字段、静态方法可以看出是全局对象,与实例对象无关。
方法是对象做的事情,字段则是对象知道的事情。
2. 对象的生命周期
构造阶段:对象最初进行实例化,这个初始化过程称为构造阶段,由构造函数完成,每个类都有一个默认的构造函数,也可以进行重载。
析构阶段:在删除一个对象时,常常需要执行一些清理工作,比如释放内存,由析构函数完成。一般情况下,不需要提供析构函数的代码,而是由默认的析构函数自动执行操作。但是,如果在删除对象实例前,需要完成一些重要操作,就应提供特定的析构函数。类的析构函数由~前缀的类名来声明,当进行垃圾回收时,就执行析构函数的代码,释放资源。在.NET中使用的析构函数(由System.Object类提供)叫做Finalize()。
3.接口和抽象类
接口是把公共实例(非静态)方法和属性组合起来,以封装特定功能的一个集合。一旦定义了接口,就可以在类中实现它,类就可以支持接口所指定的所有属性和成员。使用关键字interface定义接口,接口名以I开头。
实现接口的类必须包括接口的所有方法,接口让类信守承诺。可以使用“is”查看一个类是否实现了某个接口,接口可以继承其他接口。is指出一个对象实现了什么,as则告诉编译器如何看待一个对象。
接口不能实例化,但是可以引用,接口引用类似于对象引用。对于一个对象,如果没有任何引用指向它,这个对象就会消失,接口引用可以保持一个对象存活。
抽象类,类似于一个正常的类,可以有字段和方法,也可以继承其他类,但是不能实例化。
接口中的各个方法会自动成为抽象方法,所以在接口中不需要像抽象类中那样使用abstract关键字。只有抽象类中可以有抽象方法,不过抽象方法还可以有具体方法。抽象方法没有方法体。
抽象类就像类和接口之间的一个过渡
1.抽象类就像一个正常的类,可以有字段和方法,而且也可以继承其他类。如果一个方法有声明但没有语句或方法体,这就称为一个抽象方法,继承抽象类的子类必须实现所有的抽象方法。
2.抽象类就像一个接口,抽象类可以包含属性和方法的声明,与接口一样,这些属性和方法必须由继承类实现。抽象类中的抽象方法必须标志为abstract
3.不过抽象类不能实例化,抽象类和具体类最大的区别就是不能使用new创建抽象类的实例。
为什么用抽象类?因为想要提供一些代码,但是任然需要子类填入其余代码
二、三大特性
1.封装
封装是指创建一个对象,它使用私有字段在内部记录状态,另外使用公共属性和方法,使得其他类只能使用它们需要的那部分内部数据。让你的隐私属于你个人。
封装意味着保证类中的一些数据是私有的,只能在这个对象内部访问该字段(实例化对象不能访问)。使用封装来控制对类方法和字段的访问。私有字段和方法只能从类的内部访问。一个对象要得到一个对象私有字段中存储的数据,唯一的办法就是使用返回该数据的公共方法。属性作为一种特殊的方法,使封装更容易,只有在另一个类读写一个属性时才运行。
2.继承
继承一个类或接口继承自另一个类或接口。任何类都可以从另一个类中继承,也就是说这个类拥有它继承的类的所有成员。在OOP中,被继承(也称为派生)的类称为父类或基类。使用继承来避免子类中出现重复代码。子类改变父类行为时,通过覆写(override)进行方法改写。每一个子类都扩展其基类。基类中的虚方法(virtual)需要在子类中进行覆写。子类通过base关键字访问其基类。
C#程序使用继承,因为程序会对真实世界中的实际事物建模,而继承可以模仿实际事物之间的关系。真实世界中的事物往往存在于一个层次体系中,从一般到特点;相应地,程序中的类层次体系也是如此。在类模型中,层次体系结构总较低的类从它上面的类继承。
为什么不能多继承?这是C#的一大限制?
这不是限制,而是保护。如果两个类具有同一方法,但是方法具体实现不同,那么继承这两个类,子类不知道实现哪个基类的方法,C#通过接口来保护,不必处理这种情况,即使实现的多个接口包含同一方法,最后实现还是根据自身的方法体决定。
3.多态
多态表示一个对象可以有多种形态。将一个类的实例用在需要其他类型(如父类或该类实现的接口)的语句或方法中,这就是在使用多态。完成向上强制转换和向下强制转换时就采用了多态原则。比如使用一只鸟来取代更一般的动物,或者将string类型当成object类型使用。
4.面向对象设计的几点原则
(1)单一职责原则
单一职责原则(SRP),就一个类而言,应该仅有一个引起它变化的原因。如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到意想不到的破坏。
(2)开放-封闭原则
开放-封闭原则,是说软件实体(类、模块、函数等等)应该可以扩展,但是不可修改。两个特性,对扩展是开放的,对更改是封闭的。
开放-封闭原则是面向对象设计的核心所在。遵循这个原则可以带来面向对象技术所声称的巨大好处,也就是可维护、可扩展、可复用、灵活性好。开发人员应该仅对程序中呈现出频繁变化的那些部分做出抽象,然而,对于应用程序中的每个部分都刻意地进行抽象同样不是一个好主意。拒绝不成熟的抽象和抽象本身一样重要。
(3)依赖倒转原则
A.高层模块不应该依赖底层模块。两个都应该依赖抽象。
B.抽象不应该依赖细节。细节应该依赖抽象。
说白了,针对接口编程,不用对实现编程。
依赖倒转其实可以说是面向对象设计的标志,用哪种语言来编写程序不重要,如果编写时考虑的都是如何针对抽象编程而不是针对细节编程,即程序中所有的依赖关系都是终止于抽象类或者接口,那就是面向对象的设计,反之那就是过程化的设计了。
(4)里氏代换原则
里氏代换原则(LSP):子类型必须能够替换掉它们的父类型。
一个软件实体如果使用的是一个父类的话,那么一定适用于其子类,而且它察觉不出父类对象和子类对象的区别。也就是说,在软件界面,把父类都替换成它的子类,程序的行为没有改变。由于子类型的可替代性才使得使用父类型的模块在无需修改的情况下就可以扩展。
(5)迪米特法则
迪米特法则(LoD),也叫最少知识原则,如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。
迪米特法则首先强调的前提是在类的结构设计上,每一个类都应当尽量降低成员的访问权限,也就是说,一个类包装好自己的private状态,不需要让别的类知道的字段或行为就不要公开。
迪米特法则其根本思想,是强调了类之间的松耦合。类之间的耦合越弱,越有利于复用,一个处于弱耦合的类被修改,不会对有关系的类造成波及。也就是说,信息的隐藏促进了软件的复用。
三、数据类型
在C#中,数据根据变量的类型以两种方式中一种存储在一个变量中。变量类型分为两种:引用类型和值类型,变量的类型决定了它能存储哪种数据。
主要区别在于:值类型在内存的一个地方存储它们自己和它们的内容。引用类型存储指向内存中其他某个位置(称为堆)的引用,而在另一个位置存储内容。值类型总是包含一个值(未赋值状态也是有一个初始值的),而引用类型可以是null(表示不包含任何值)。
实际上,在使用C#时,不必过多地考虑这个问题。到目前为止,所使用的string(引用类型)与其他简单变量(大多数是值类型)的方式完全相同。只有string和object简单类型是引用类型,但数组也是隐性的引用类型,我们创建的每个类都是引用类型。
对于一个过大的值完成强制类型转换时,C#会自动对其调整,如果将一个设置为365的int变量强制转换为一个byte变量,对于byte来说365太大了,不过并不会因此显示错误,而只会把这个值回转,将得到值109。
C#中保留字不能作为变量名
对象也是一种变量,对象只是程序可以使用的另外一种类型的变量。这么说来,处理对象或是值并没有区别。如果要放在内存中,而且程序需要用到它,都可以使用变量。
代码需要处理内存中的一个对象时,它会使用一个引用(reference)这是一个变量,其类型是所指向对象的类。引用就像标签(lable),代码用它与一个特定的对象交互。如果没有任何引用,对象就会被垃圾回收。要让一个对象留在堆中,必须有引用。如果对象的最后一个引用没有了,对象也将消失。
四、文件读写
流(stream)是.NET Framework为程序读写数据提供的方法。
使用流能做的事情:
1.写流,通过流的Write()方法,可以把你的数据写至一个流。
2.读流,通过流,可以使用Read()方法从文件、网络、内存或任何地方获取数据。
3.改变在流中的位置,大多数流读支持一个Seek()方法,允许查看在流中的哪个位置,从而能够在特定位置插入数据。
Stream是一个抽象类,所以它本身不能实例化。继承IDisposable接口和MarshalByRefObject抽象类
IDisposable接口,执行与释放或重置非托管资源相关的应用程序定义的任务,只有Dispose()一个方法,立即释放所占用的资源,这往往是结束对象处理的最后一步。
程序员经常因为处理文件时没有适当关闭流导致bug,使用using语句包围流的代码块,会自动关闭流。
每一个流都有一个Dispose()方法,它会关闭这个流。所以如果在一个using语句中声明流,它就会始终自行关闭。
using (StreamWriter sw2 = new StreamWriter(@"E:\test.txt", true))
{
sw2.WriteLine("using");
sw2.WriteLine("自动关闭");
}
File类有许多非常有用的静态方法,它们会自动打开一个文件、读、写数据,然后自动将文件关闭。
Stream子类:
FileStream类,允许读写文件
MemoryStream类,允许向内存块读写数据
NetworkStream类,允许向网络上的其他计算机或设备读写数据
GZipStream,允许压缩数据,从而占更少空间,而且更易于下载和存储
CryptoStream类,为文件流提供文件,不过文本已经加密
使用内置File和Directory类处理文件和目录
使用File类:https://msdn.microsoft.com/zh-cn/library/system.io.file.aspx
1.查找文件是否存在
2.读写文件
3.向文件追加文本
4.获得文件的有关信息
使用Directory类:https://msdn.microsoft.com/zh-cn/library/system.io.directory.aspx
1.创建一个新的目录
2.获得一个目录中文件的列表
3.删除一个目录
对象串行化
要在文件中存储对象,确实有一种更容易的方法。这称为串行化(serialization)。
C#串行化一个对象时,它会保存这个对象的全部状态,这样以后就能让一个相同的实例对象在堆中复活。
复活的过程称为逆串行化(deserialize)
// 串行化
private void button2_Click(object sender, EventArgs e)
{
Flobbo f = new Flobbo("blue yellow");
using (Stream output = File.Create("Flobbo_File.txt"))
{
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(output,f);
}
}
//逆串行化
private void button3_Click(object sender, EventArgs e)
{
Flobbo f;
using (Stream input = File.OpenRead("Flobbo_File.txt"))
{
BinaryFormatter formatter = new BinaryFormatter();
f = (Flobbo)formatter.Deserialize(input);
}
this.textEdit1.Text = f.Zap;
}
串行化本质上是保存对象的字段信息。对象串行化时,它引用的所有对象也要串行化,每个对象都会写入到文件,同时提供逆串行化这个对象时C#重构对象所需的全部信息。对象串行化到一个文件时,它们会以二进制格式写入文件。可以是bat、txt文件。
C#将串写入文件时最紧凑的方式就是作为串写入。不过要把一个数字作为串写入就有些浪费了。存储int只需要4个字节,如果C#把一个数字(比如49,635,852)存储为一个8字符的串(加上逗号就是10个字符),这样太浪费空间了!.NET采用一种Unicode的格式对所有串和字符进行编码。编码(Encoding)是指将逻辑数据(如字母H)转换为字节(数字72)。需要这样做是因为,字母、数字、枚举和其他数据最后都会以字节形式存储在磁盘或内存中。
五、异常处理
异常处理:程序抛出一个异常,.NET会生成一个Exception对象。exception,名词。一个人或事物不符合一般规律或者不遵循某个规律。异常是一个对象。这个对象的属性可以告诉你异常的有关信息。.NET之所以要生成异常,是为了在执行抛出异常的语句时能尽可能的为你提供这个异常的有关信息。异常就是要帮助你找出并修正代码中未能达到预期表现的地方。
C#语言包含结构化异常处理语法,用三个关键字可以标记出能够处理异常的代码和指令,如果发生异常,就使用这些指令处理异常。用于这个目的的三个关键字是try、catch和finally,它们都有一个关联的代码块,必须在连续的代码行中使用。其结构如下:
try
{
}
catch (Exception ex)
{
throw ex;
}
finally
{
}
什么时候使用try和catch?
只要是编写有风险的代码,或者可能抛出异常的代码,都应该使用try和catch。关键是要明确哪些代码是有风险的,哪些代码比较安全。通常在用户输入时,容易报异常。
只有try块中的代码抛出异常时才会执行catch块。可以利用这个机会向用户提供有关信息来修正这个问题。如果有些代码始终要运行,可以使用一个finally块,比如关闭流,清空资源,这些代码必须要运行,即使出现了异常也需要运行这些清理代码。finally块始终会运行。catch后面的(Exception)可以指定捕捉的异常,或者不指定任何的异常则会捕捉所有异常。
已处理异常:添加catch块来处理的错误,这个异常被认为是已处理异常。
未处理异常:如果运行时环境没有找到与这个异常匹配的catch块,就会停止程序正在做的工作而产生的一个错误,这个异常称为未处理异常。
一般情况下,你并不希望捕获所有类型的异常。实际上,要尽量避免捕获Exception,而应当捕获特定的异常。但是这样一来,必须确保调用栈中的某个方法确实有一个捕获所有异常的异常处理程序。这会导致用户看到由于“未处理异常”导致的可怕后果——崩溃
对于不同异常可能需要不同的处理才能保证程序继续运行。
对于除0产生的异常,相应的catch块可能要设置某些数字值,从而保存你已经处理的一些数据。
对于null引用异常,如果想要从这个异常中恢复,则可能需要创建一个对象的新实例。
是不是所有错误处理都按照try/catch/finally顺序来完成?
不是的,可以有另外的组合。如果想处理很多不同类型的错误,就可以有多个catch块。也可以根本没有catch块。
Exception对象工作原理
1.一个对象工作时,遇到一些未预料到的情况时,它抛出一个异常
2.try/catch捕获这个异常。在catch块中为这个Exception指定名字ex
3.Exception对象一种存在,直到catch块结束处理。然后ex引用消失,这个对象被垃圾回收。
避免大量问题的一种简便方法:使用using,可以轻松实现try和finally
避免异常:实现IDisposable完成自己的清理。IDisposable只有一个成员,Dispose()方法。这个方法中的所有代码都会在using语句最后执行。using的目的是帮助你确保用using语句创建的每一个对象都会撤销。
你想知道抛出了哪种类型的异常,以便处理那种异常。异常处理远不止打印一个通用的错误消息那么简单。处理异常并不总意味着等同于修正异常。
最糟糕的catch块:注释,应当处理异常,而不是埋藏异常。程序员认为通过使用一个空的catch块,他能把这个异常埋藏起来,但这样做只能让以后查找问题的人更头疼。
关于异常处理的一些简单想法
1.设计代码时要妥善地处理失败
2.要为用户提供有用的错误消息
3.尽可能抛出.NET内置异常。只有当需要提供定制信息时才抛出定制异常。
4.要考虑到try块中的代码可能被短路。
最重要的是,要避免不必要的文件系统错误,只要使用流就应当使用using!!!
妥善的异常处理对用户来说是不可见的。程序绝对不会崩溃,如果出现问题,也能妥善地得到解决,而不是发出一些莫名奇妙的错误信息。
六、事件与委托
没有你的监视,代码自己在做什么
事件类似于异常,因为它们都是由对象引发(抛出),我们可以提供代码来处理事件。但它们有几个重要的区别。最重要的区别是并没有try/catch类似的结构来处理事件,而必须订阅它们。订阅一个事件的含义是提供代码,在事件发生时执行这些代码,它们称为事件处理程序。单个事件可供多个处理程序订阅,在该事件发生时,这些处理程序都会被调用,其中包括引发该事件的对象所在的类中的事件处理程序。
事件处理程序本身是简单的方法,唯一的限制就是必须匹配事件所要求的返回类型和参数。这个限制是事件定义的一部分,由一个委托指定。
基本处理过程:
1.应用程序创建一个可以引发事件的对象
2.应用程序订阅事件
3.引发事件后,通知订阅器,调用事件处理方法
委托是一种可以把引用存储为函数的类型。委托的声明非常类似于函数,但不带函数体,且使用delegate关键字。委托的声明指定了一个返回类型和参数列表。在定义了委托后,就可以声明该委托类型的变量,接着把这个变量初始化与委托有相同返回类型和参数列表的函数引用。之后,就可以使用委托变量调用这个函数,就像该变量是一个函数一样。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace EventAndDelegate_List11
{
//定义一个表示事件参数的对象
public class BallEventArgs : EventArgs
{
public int Trajectory { get; private set; }
public int Distance { get; private set; }
public BallEventArgs(int Trajectory, int Distance)
{
this.Trajectory = Trajectory;
this.Distance = Distance;
}
}
public class Ball
{
//在产生事件的类中定义这个事件
public event EventHandler BallInPlay;
public void OnBallInPlay(BallEventArgs e)
{
if (BallInPlay != null)
BallInPlay(this, e);
}
}
class Fan
{
public Fan(Ball ball)
{
//各个对象订阅事件
ball.BallInPlay += new EventHandler(ball_BallInPlay);
}
//事件处理方法
void ball_BallInPlay(object sender, EventArgs e)
{
if (e is BallEventArgs)
{
BallEventArgs ballEventArgs = e as BallEventArgs;
if ((ballEventArgs.Distance > 400) && ballEventArgs.Trajectory > 30)
FitleBall();
else
Yell();
}
}
private void Yell()
{
MessageBox.Show("Fan:Woo-hoo!");
}
private void FitleBall()
{
MessageBox.Show("Fan:Home run!I'm going for the ball!");
}
}
class Pitcher
{
public Pitcher(Ball ball)
{
//各个对象订阅事件
ball.BallInPlay += new EventHandler(ball_BallInPlay);
}
//事件处理方法
void ball_BallInPlay(object sender, EventArgs e)
{
if (e is BallEventArgs)
{
BallEventArgs ballEventArgs = e as BallEventArgs;
if ((ballEventArgs.Distance < 95) && ballEventArgs.Trajectory < 60)
CatchBall();
else
CoverFirstBase();
}
}
private void CoverFirstBase()
{
MessageBox.Show("Pitcher:I Covered first ball");
}
private void CatchBall()
{
MessageBox.Show("Pitcher:I caught the ball");
}
}
event关键字,通知其他对象存在这样一个事件,以便其他对象订购这个事件
EventHandler告诉其他对象,它们的事件处理程序应当有两个参数,一个名为sender的object,另一个是名为e的EventArgs
以上资料,为《head first C#》、《C#入门经典》读书笔记,作为C#入门的总结,作为自己学习的一个记录。