**
C#编程–面向对象
**
看完siki学院第二季面向对象的笔记
一.调试和错误处理
**错误(Bug)😗*⽐如变量名字写错,导致编译器⽆法编译==(语法错误),有的时候我们的逻辑在某些⽅⾯有瑕疵,也会产⽣错误,这类错误成为语义错误(逻辑错误)。
在程序出错之后,使⽤VS提供给我们的调试功能,找到错误的原因,修改代码。(调试)==
以及c#中的错误处理技术,对可能发⽣错误的地⽅采取预防措施,并编写弹性代码 来处理可能会发⽣的致命错误。(错误处理)
1. 正常模式下的调试: 正常模式指的是不会影响程序的正常运⾏。
(1)在VS中我们使⽤Console.Write(或者WriteLine)
(向控制台输出变量的值,通过这个 我们可以查看变量的值是否符合我们的预期来调试错误。
(2)在Unity中我们使用Debug.Log("")Debug.LogError("")Debug.LogWarn("")
,向unity的 Console
窗⼝输出信息,帮助我们调试错误。
2.中断(Debug)模式下的调试: 中断模式指我们可以暂停程序的执⾏,然后查看程序中的状态,也可以让程序继续执行。
2.1如何让程序中断? 断点
2.2断点是什么?
断点是源代码中⾃动进⼊中断模式的⼀个标记,当遇到断点的时候,程序会进入中断模式。
2.3如何插⼊断点?
(1)右击代码⾏,选择breakpoint
(断点) -> insert breakpoint
(插⼊断点)
(2)光标定位到代码⾏,选择菜单上的Debug
(调试)->Toggle Breakpoint
(切换断点)
(3)光标定位到代码⾏,按下F9
键,在此按下F9
是取消断点
(4)在需要添加断点的⾏⾸位置,直接单击,再次单击取消断点
3.窗⼝ Breakpoints
我们可以通过 (调试-窗⼝-断点),打开断点窗⼝,这个窗⼝显示了当前项⽬中添加了的所 有的断点,可以在这⾥定位断点的位置,也可以去删除断点。
4.监视变量的内容
在中断模式下查看变量值最简单的⽅式,就是把⿏标指向源代码中的变量名,此时会出现 ⼀个⼯具提示,显示该变量的信息。
4.1中断模式下的窗⼝(左下角),有三个选项卡
(1)错误列表 -程序运⾏中发⽣的所有错误的列表
(2)局部变量 -当前运⾏环境中所有的局部变量的值
(3)监视 -监视某个变量的值的变化 在上⾯的⼏个窗⼝中不但可以观察变量值的变化,还可以直接去修改变量中存储的值
5.调⽤堆栈和即时窗⼝:
5.1在中断模式下,可以在右下⾓看到调⽤堆栈和即时窗⼝
5.2在调⽤堆栈窗⼝下我们可以观察到当前代码执行到哪⼀行了,并且可以看到这个代码的是 被什么语句调⽤的
即时窗⼝,可以在这⾥输⼊⼀些命令,查看变量的值,修改变量的值,可以输⼊表达式查看结果
6.单步执⾏代码:
在中断模式下我们可以单步执行代码,单步执行带有有两种 逐过程和逐语句,他们两个 都是⼀条语句⼀跳语句的执行,区别在于逐过程遇到函数,不会进⼊函数内部,⽽把函数 当成⼀条语句去执行。
7. 错误处理(异常处理)
我们上面讨论了再开发过程中如何查找和修正错误,使这些错误不会再发布的代码中出 现,但有时,我们知道可能会有错误发⽣,但不能100%的肯定他们不会发⽣,此时最好 能预料到错误的发⽣,编写足够健壮的代码以处理这些错误,而不必中断程序的执行。 错误处理就是用于这个目的。
下⾯学习异常和处理他们的方式。
7.1异常: 异常是在运⾏期间代码中产⽣的错误。
⽰例: int[] myArray = {1,2,3,4};
int myEle = myArray[4];//数组下标越界
运⾏到这⾥的时候,会出现异常,这个异常的定义已经在CLR中定义好了。如果我们不去 处理这个异常,那么当异常发⽣的时候,程序会终⽌掉,然后异常后⾯的代码都⽆法执⾏。
7.2异常处理(捕捉异常) try ... catch ... finally
语句
我们处理异常的语法结构如下(包含了三个关键字 try catch finally)
try{ ...
}
catch( <exceptionType>e ){
...
}
finally{
}
其中catch
块可以有0或者多个,finally
可以有0或者1个 但是如果没有catch
块,必须有finally
块,没有finally
块,必须有catch
块,catch
块和finally
块可以同时存在
7.3每个代码块的⽤法
try
块包含了可能出现异常的代码(⼀条或者多条语句) catch
块⽤来捕捉异常,当代码发⽣异常,那么异常的类型和catch
块中的类型⼀样的时 候,就会执行该catch
块,如果catch
块的参数不写,表示发生任何异常都执行这个catch
块
finally
块包含了始终会执行的代码,不管有没有异常产生都会执行
二.面向对象编程
1.1什么是⾯向对象编程?
1.2为什么使⽤⾯向对象编程?
为了让编程更加清晰,把程序中的功能进⾏模块化划分,每个模块提供特定的功能,而且每个模块都是孤立的,这种模块化编程提供了非常大的多样性,大大增加了重⽤代码的机会。
面向对象编程也叫做OOP编程 简单来说⾯向对象编程就是结构化编程,对程序中的变量结构划分,让编程更清晰。
注意:不要从字面意思理解面向对象编程
我们把类创建的变量叫做对象,那么如何创建类呢?
1.3 类是什么东⻄?
类实际上是创建对象的模板,每个对象都包含数据,并提供了处理和访问数据的方法。 类定义了类的每个对象(称为实例)可以包含什么数据和功能。
1.4类的定义
类中的数据和函数称为类的成员
(1)数据成员:数据成员是包含类的数据–字段,常量和事件的成员。
(2)函数成员: 函数成员提供了操作类中数据的某些功能。(方法,属性,构造方法和终结器(析构方法),运算符和索引器)
1.5类的字段和⽅法
(1)字段的声明
访问修饰符 类型 字段名称;
(2)方法的声明
访问修饰符 返回值类型 ⽅法名称(参数){
//⽅法体
}
例如: 定义⼀个类来表示⼀个顾客
class Customer{
public string name;
public string address;
public int age;
public string buyTime;
public void Show(){
Console.WriteLine("名字:"+name);
Console.WriteLine("年龄:"+age);
Console.WriteLine("地址:"+address);
Console.WriteLine("购买时间:"+buyTime);
}
}
在这⾥我们定义了⼀个类,叫做Customer
(顾客),里面包含了四个字段,存储了顾客 的名字,年龄,地址和购买时间,包含了⼀个Show()
⽅法,⽤来输出⾃⾝的⼀些信息 的,在这⾥Customer
就是⼀个新的数据类型
类似于结构体的定义
struct Customer......;
1.6如何利⽤类创建对象
使⽤我们⾃定义的类声明的变量也叫做对象,这个过程也叫做实例化。
ClassName myClass = new ClassName();
其中ClassName
是我们定义的类的名字,myClass
是我们声明的变量(对象)的名字,后 面的new
是⼀个关键字,使⽤new 加上类型名()表⽰对该对象进⾏构造,如果不进⾏构 造的话,这个对象是⽆法使⽤的。
2.1构造函数
我们构造对象的时候,对象的初始化过程是⾃动完成的,但是在初始化对象的过程中有的 时候需要做⼀些额外的⼯作,例如需要初始化对象存储的数据,构造函数就是⽤于初始化数据的函数。
声明基本的构造函数的语法就是声明⼀个和所在类同名的⽅法,但是该方法没有返回类型。
public class MyClass{
public MyClass(){
这个构造函数的函数体
}
}
当我们使⽤new
关键字创建类的时候,就会调⽤构造⽅法。
我们⼀般会使⽤构造⽅法进⾏初始化数据的⼀些操作。
构造函数可以进⾏重载,跟普通函数重载是⼀样的规则
注意: 当我们不写,任何构造函数的时候,编译器会提供给我们⼀个默认的 ⽆参的构造函 数,但是如果我们定义了⼀个或者多个构造函数,编译器就不会再提供默认的构造函数
2.2属性的定义
属性的定义结构:
public int MyIntProp{
get{
// get code
}
set{
//set code
}
}
(1)定义属性需要名字和类型
(2)属性包含两个块 get块和set块
(3)访问属性和访问字段⼀样,当取得属性的值的时候,就会调⽤属性中的get块,所以 get块,类型需要⼀个返回值就是属性的类型;当我们去给属性设置值的时候,就会调⽤ 属性中的set块,我们可以在set块中通过value访问到我们设置的值。
2.3 通过属性来访问字段
我们习惯上把字段设置为私有的,这样外界不能修改字段的值,然后我们可以通过定义属 性来设置和取得字段中的值。
private int age;
public int Age{//习惯上属性⼤写 字段⼩写
set{
if(value<0)return;
age = value;
}
get{
return age;
}
}
(1)设置属性的只读或者只写
private string name;
public string name{
get{
return name;
}
}
属性可以值只提供⼀个set块或者get块
(2)属性的访问修饰符
public string name{
get{ return name;
}
private set{
name = value;
}
}
(3)⾃动实现的属性
public int Age{get;set;}
编译器会⾃动创建private int age
属性
匿名类型
我们创建变量(对象的时候),必须指定类型,其实我们也可以不去指定类型,这个就是 匿名类型,我们可以使⽤var声明⼀个匿名类型。
使⽤var声明的匿名类型,当初始化的时候,这个变量的类型就被确定下来,并且以后不 可以修改。
var var1 = 34;
三.程序内存区域:堆 栈 静态存储区
程序所有的数据,也就是所有的变量,都是存储在内存中的
栈空间⽐较⼩,但是读取速度快
堆空间⽐较⼤,但是读取速度慢
1.栈
1.1栈的特征: 数据只能从栈的顶端插⼊和删除
把数据放⼊栈顶称为⼊栈(push)
从栈顶删除数据称为出栈(pop)
内存中的栈,是由系统管理(.Net框架)
2.堆
2.1堆是⼀块内存区域,与栈不同,堆⾥的内存能够以任意顺序存⼊和移除
3.Garbage Collector垃圾回收器
CLR的GC就是内存管理机制,我们写程序不需要关⼼内存的使⽤,因为这些都是CLR帮 我们做了。
4. 值类型和引用类型
4.1 类型被分为两种:值类型(整数,bool struct char ⼩数)和引⽤类型(string 数组 ⾃定义的类,内置的类)。
4.2 值类型只需要⼀段单独的内存,⽤于存储实际的数据,(单独定义的时候放在栈中) 引⽤类型需要两段内存
第⼀段存储实际的数据,它总是位于堆中
第⼆段是⼀个引⽤,指向数据在堆中的存放位置
注意:
当我们使⽤引⽤类型赋值的 时候,其实是赋值的引⽤类型的引⽤ 如果数组是⼀个值类型的数组,那么数组中直接存储值,如果是⼀个引⽤类型的数组(数组中存储的是引⽤类型),那么数组中存储的是引⽤(内存地址)
四.面向对象-继承
1.继承是什么?
很多类中有相似的数据,⽐如在⼀个游戏中,有Boss
类,⼩怪类Enemy
,这些类他们有 很多相同的属性,也有不同的,这个时候我们可以使⽤继承来让这两个类继承⾃同⼀个类。
1.1继承的类型
1.2实现继承:
表示⼀个类型派⽣于⼀个基类型,它拥有该基类型的所有成员字段和函数。 在实现继承中,派⽣类型采⽤基类型的每个函数的实现代码,除非在派⽣类型的定义中指定重写 某个函数的实现代码。 在需要给现有的类型添加功能,或许多相关的类型共享⼀组重 要的公共功能时,这种类型的继承非常有⽤。
1.3接⼝继承:
表示⼀个类型只继承了函数的签名,没有继承任何实现代码。 在需要指定该类型具有 某些可⽤的特性时,最好使⽤这种类型的继承。
2.多重继承
⼀些语⾔(C++)⽀持所谓的 “多重继承”,即⼀个类派生自多个类。 使⽤多重继承的优点是 有争议的:⼀方面,毫⽆疑问,可 以使⽤多重继承编写⾮常复杂但很紧凑的代码。另⼀方面,使⽤多重实现继承的代码常常很难理解和调试。
如前所述,简化健壮代码的编写⼯作是 开发 C#的重要设计 ⽬标。 因此,C#不⽀持多重实现继承。 ⽽ C#允许类型派生自多个接⼝— — 多重接⼝继承。 这说明,C#类可以派⽣⾃另⼀个类和任意多个接⼝。更准确地说, System.Object
是⼀个公共的基类,所以每个 C#(除了Object类之外)都有⼀个基类,还可以有任意多个基接⼝。
2.1实现继承
如果要声明派生自另⼀个类的⼀个类,就可以使⽤下⾯的语法:
class MyDerivedClass : MyBaseclass
{
// functions and data members here
}
如果类(或 结构)也 派生自接⼝,则⽤逗号分隔列表中的基类和接⼝:
public class MyDerivedClass: MyBaseClass , IInterface1 , IInterface2
{
// etc.
}
2.2this
和base
关键字 this(base)作⽤:
(1)IDE给提示
(2)区分局部变量和字段
this可以访问当前类中定义的字段,属性和⽅法,有没有this都可以访问,有this
可以让IDEVS编译器给出提示,另外当⽅法的参数跟字段重名的时候,使⽤this可以表明访问的是类 中的字段,
base
可以调⽤父类中的公有⽅法和字段,有没有base都可以访问,但是加上 base.IED⼯具会给出提⽰,把所有可以调⽤的字段和⽅法罗列出来⽅便选择
3.虚方法
把⼀个基类函数声明为virtual
,就可以在任何派生类中重写该函数:
class MyBaseClass{
public virtual string VirtualMethod(){
return "Method is called in base class";
}
}
在派生类中重写另外⼀个函数时,要使⽤override
关键字显示声明
class MyDerivedClass:MyBaseClass{
public override string VirtualMethod(){
return "Method is called in derivedclass.";
}
}
我们在子类⾥⾯重写虚函数之后,不管在哪⾥调⽤都是调⽤重写之后的⽅法
4. 隐藏方法
如果签名相同的方法在基类和派生类中都进行了声明,但是该⽅法没有分别声明为virtual
和override
,派生类就会隐藏基类方法。(要使⽤new
关键字进行声明)
5.基类
class MyBaseClass{
public int MyMethod(){
}
}
6.派⽣类(在派生类中把基类同名的⽅法隐藏掉了)
class MyDerivedClass:MyBaseClass{
public new void MyMethod() {
}
}
抽象类 C#允许把类和函数声明为 abstract
。 抽象类不能实例化,抽象类可以包含普通函数和抽象 函数,抽象函数就是只有函数定义没有函数体。 显然,抽象函数本⾝也是虚拟的Virtua
l(只有函数定义,没有函数体实现)。
类是⼀个模板,那么抽象类就是⼀个不完整的模板,我们不能使⽤不完整的模板去构造对象。
abstract class Building{
public abstract decimal CalculateHeatingCost();
}
7.密封类和密封方法
C#允许把类和⽅法声明为 sealed
。 对于类 ,这表示不能继承该类;对于⽅法表⽰不能重写该⽅法。
sealed FinalClass {
// etc
}
7.1 什么时候使用密封类和密封⽅法?
(1)防⽌重写某些类导致代码混乱
(2)商业原因
7.2 派⽣类(⼦类)的构造函数
1.在⼦类中调⽤⽗类的默认构造函数(⽆参)(会先调⽤⽗类的,然后是⼦类的)
public class MyDerivedClass{
public MyDerivedClass():base(){
//do something
}
}
在这里:base()
可以直接不写,因为默认会调用父类中的默认构造函数
2.调⽤有参数的构造函数
public class MyDerivedClass{
public MyDerivedClass(string name):base(name){
//do something
}
}
8.修饰符
修饰符,⽤来类型或者成员的关键字。
修饰符可以指定⽅法的可见性。
(1)public
:同⼀程序集(DLL或EXE)中的任何其他代码或引⽤该程序集的其他程序集都可 以访问该类型或成员。
(2) private
: 只有同⼀类或结构中的代码可以访问该类型或成员。
(3)protected
: 只有同⼀类或结构或者此类的派⽣类中的代码才可以访问该类型或成员。
(4)internal
: 同⼀程序集中的任何代码都可以访问该类型或成员,但的代码不可以。
(5)protected internal
: 在⼀程序集中,protected internal
体现的是internal的性质;在其 他程序集中,protected internal
体现的是protected
的性质。
public
和private修饰字段和⽅法的时候,表⽰该字段或者⽅法能不能通过对象去访问,只 有public的才可以通过对象访问,private
(私有的)只能在类模板内部访问。
protected
保护的,当没有继承的时候,它的作⽤和private
是⼀样的,当有继承的时候, protected
表示可以被子类访问的字段或者⽅法
8.1类的修饰符
public class ...
class ...
前者可以在别的项⽬下访问,后者不⾏
8.2其他修饰符
1.new
隐藏继承的成员
2.abstract
使⽤abstract
修饰的类为抽象类,抽象类只能是其他类的基类,不能与sealed
、static
⼀起 使⽤。
abstract
可以修饰抽象类中的⽅法或属性,此时,⽅法或属性不能包含实现,且访问级别 不能为私有。 抽象类不能被实例化。
3. sealed
使⽤sealed
修饰的类为密封类,密封类⽆法被继承,不能和abstract
、static
⼀起使⽤。 当sealed
⽤于⽅法或属性时,必须始终与override
⼀起使⽤。
4. static
使⽤static修饰的类为静态类,静态类所有成员都必须是静态的,不能与abstract
、sealed
⼀起使⽤。 static
可以修饰⽅法、字段、属性或事件,始终通过类名⽽不是实例名称访问静态成员, 静态字段只有⼀个副本。 静态类不能被实例化。
5.Const
使⽤const关键字来声明某个常量字段或常量局部变量,必须在声明常量时赋初值。 不能与static⼀起使⽤,常量默认是static
的,常量字段只有⼀个副本。
6.readonly
使⽤readonly
关键字来声明只读字段。 只读字段可以在声明或构造函数中初始化,每个类或结构的实例都有⼀个独⽴的副本。 可以与static
⼀起使⽤,声明静态只读字段。 静态只读字段可以在声明或静态构造函数中初始化,静态常量字段只有⼀个副本。
7.virtual
virtual
关键字⽤于修饰⽅法、属性、索引器或事件声明,并使它们可以在派⽣类中被重 写。 默认情况下,⽅法是⾮虚拟的。 不能重写⾮虚⽅法。 virtual
修饰符不能与static
、abstract
、private
或override
修饰符⼀起使⽤。
8.override
要扩展或修改继承的⽅法、属性、索引器或事件的抽象实现或虚实现,必须使⽤override
修饰符。 重写的成员必须是virtual
、abstract
或override
的
9.定义和实现接口
定义⼀个接⼝在语法上跟定义⼀个抽象类完全相同,但不允许提供接⼝中任何成员的实现方式,⼀般情况下,接⼝只能包含⽅法,属性,索引器和事件的声明。
接⼝不能有构造函数,也不能有字段,接⼝也不允许运算符重载。 接⼝定义中不允许声明成员的修饰符,接⼝成员都是公有的
9.1派⽣的接⼝
接⼝可以彼此继承,其⽅式和类的继承⽅式相同
public interface A{
void Method1();
}
public interface B:A{
void Method2();
}
五.集合类 列表List
当我们有很多类型⼀样的数据的时候,前面我们⼀般使⽤数组来进⾏管理,但是这样有个 缺点就是数组的大小是固定的。如果我们很多类型⼀样的数据,比如游戏得分,我们可以 集合类来进⾏管理,比如列表List
,我们可以使⽤列表List很⽅便的添加数据,删除数据 还有其他对数据的操作。
1. 列表List的创建和使用
1.创建列表(列表可以存储任何类型的数据,在创建列表对象的时候首先要指定你要创建的这个列表要存储什么类型的)(泛型)
List scoreList = new List();
new List(){1,2,3}
new List(){"one","two"}
var scoreList = new List();
2.往列表中插入数据
scoreList.Add(12);
scoreList.Add(45);
3.如何取得列表中的数据?
列表中的数据跟数组有点相似,索引从0开始 ,可以通过索引 来访问 scoreList[0] //访问添加到列表中的第⼀个数据
关于列表的更多内容
1 .列表内部数据是使⽤数组进⾏的存储,⼀个空的列表内部会有⼀个⻓度为0的数组,当 给列表中添加元素的时候,列表的容量会扩⼤为4,如果添加第5个的时候,列表的大小会重新设置为8,如果添加第9个元素,列表容量会扩⼤为16,依次增加。
当列表的中的 容量发⽣改变的时候,它会创建⼀个新的数组,使用Array.Copy()
⽅法将旧数组中的元素 复制到新数组中。为了节省时间,如果事先知道要存储的数据的个数,就可以利⽤列表的 构造函数指定列表的容量大小,比如下面的
List intlist = new List(10);
创建了⼀个初始容量为10的列表,当容量不够⽤的时候,每次都会按照原来容量的2倍进⾏扩容。
我们可以通过Capacity
属性获取和设置容量 intList.Capacity = 100;
2.注意容量和列表中元素个数的区别,容量是列表中⽤于存储数据的数组的⻓度通过Capacity
获取,列表中的元素是我们添加进去需要管理的数据,通过Count
获取 .
2.列表的遍历
遍历列表有两种⽅式:
1.for循环,遍历所有的索引,通过索引访问列表中的元素
for(int i=0;i<list.Count;i++){
//循环体list[i]
}
2.foreach遍历
foreach(int temp in list){
//依次取得list中的每⼀个元素赋值给temp,并执⾏循环体 //循环体 temp
}
2.1 操作列表的属性和⽅法
1.Capacity
获取容量大小
2.Add()
⽅法添加元素
3.Insert()
⽅法插⼊元素
4.[index]
访问元素
5.Count
属性访问元素个数
6.RemoveAt()
⽅法移除指定位置的元素
7.IndexOf()
⽅法取得⼀个元素所在列表中的索引位置 LastIndexOf()
上⾯的⽅法是从前往后搜索,这个是从后往前搜索,搜索到满足条件的 就停⽌ 上⾯的两个⽅法,如果没有找到指定元素就返回-1
8.Sort()
对列表中是元素进⾏从小到大排序
3. 泛型
3.1 泛型是什么?
通过参数化类型来实现在同⼀份代码上操作多种数据类型。利⽤“参数化类型”将类型抽象 化,从⽽实现灵活的复⽤。 泛型类定义 定义⼀个泛型类就是指的是,定义⼀个类,这个类中某些字段的类型是不确定的,这些类 型可以在类构造的时候确定下来,
举例: 创建⼀个类处理int类型和double类型的相加
class ClassA<T>{
private T a;
private T b;
public ClassA(T a,T b){
this.a = a ;this.b = b;
}
public T GetSum(){
return a+“”+b;
}
}
3.2泛型⽅法
定义泛型⽅法就是定义⼀个⽅法,这个⽅法的参数的类型可以是不确定的,当调⽤这个⽅ 法的时候再去确定⽅法的参数的类型。
3.3 实现任意类型组拼成字符串的⽅法
public static T GetSum(T a,T b){
return a+""+b;
} GetSum(23,12);
GetSum(23.2,12);
3.4 使用泛型和索引器来实现⼀个我们自己的集合类MyList
有下面的方法和属性
1.Capacity
获取容量大小
2.Add()
方法添加元素
3.Insert()
方法插⼊元素
4.[index]
访问元素(索引器)
5.Count
属性访问元素个数
6.RemoveAt()
方法移除指定位置的元素
7.IndexOf()
方法取得⼀个元素所在列表中的索引位置 LastIndexOf()
上⾯的⽅法是从前往后搜索,这个是从后往前搜索,搜索到满⾜条件的就 停止上⾯的两个方法,如果没有找到指定元素就返回-1
8.Sort()
对列表中是元素进⾏从小到⼤排序
索引器:通过[index]这种形式去访问数据,就是索引器