类的定义
- 面向对象编程,结构化编程
- 类,定义了每一个对象
- 类,就是创造对象的模板,数据+功能
- 类的成员,数据成员+函数成员
4.1 数据成员:是包含类的数据(字段+常量和事件的成员)
4.2 函数成员:提供了操作类中数据的某些功能(方法+属性+构造方法+终结器(析构方法)+运算符+索引器) - 类的字段和方法
5.1 字段,就是数据成员(访问修饰符,类型,字段名称)
5.2 方法,就是函数成员(访问修饰符,返回值类型,方法名称) - 求平方是double类型,一般会把他转换为float类型,强行转换
- 类中,编程规范上,一般把字段设置成privte,那就只能在单一的类中使用,所以需要用个Set+this.去重新设置,以保安全
构造过程
- 我们构造对象的时候,对象的初始化过程是自动完成的,但是在初始化对象的过程中有的时候需要做一些额外的工作,例如需要初始化对象存储的数据,构造函数就是用于初始化数据的函数。
...
public class MyClass{
public MyClass(){
这个构造函数的函数体
}
}
...
当我们使用new关键字创建类的时候,就会调用构造方法。我们一般会是用构造方法进行初始化数据的一些操作。构造函数可以进行重载,跟普通函数重载是一样的。
注意:
当我们不写,任何构造函数的时候,编译器会提供给我们一个默认的无参的构造函数,但是如果我们定义了一个或者多个构造函数,编译器就不会再提供默认的构造函数。
属性的定义
- 演示
...
public int MyIntProperty
{
get
{
Console.WriteLine("属性中的get块被调用");
return 100;
}
set
{
Console.WriteLine("属性中的set块被调用");
Console.WriteLine("在set块中访问value的值是:" + value);
}
}
...
set值:当给属性设置值的时候,会调用set方法
get值:当给属性取值的时候,会调用get方法
两者不要求同时存在,如果没有get块,那就不能通过属性取值了。 - get+set的作用:
1)通过set方法,在设置值之前,做一些校验的工作。
2)可以设置只读和只写,在set或get那,用private / public
- Public string Name{get;set;}
匿名类型
- 用引用来添加其他库的类别
- 只是要public一次
- 当var,匿名类型被赋值了以后,那他的形式就被确定下来了,不能再变
堆和栈 程序运行时的内存区域
我们把内存分为堆空间和栈空间
1)栈空间比较小,但读取速度快
2)堆空间比较大,但读取速度慢栈的特征(先入后出)
1)数据只能从栈的顶端插入,和删除
2)把数据放入栈顶,称为入栈(push)
3)把栈顶删除数据称为出栈(pop)堆的特征
1)堆是一块内存区域,与栈不同,堆里的内存能够以任意顺序存入和移除.NET当中的GC Garbage Collection 垃圾回收器
CLR的CG就是内存管理机制,我们写程序不需要关心内存的使用,因为这些都是CLR帮我们做了
过程:
1)程序在堆里保存了三个对象
2)后来的程序中,其中的一个对象不再被程序使用
3)垃圾收集器发现无主对象并释放他
4)垃圾收集之后,被释放对象的内存可以被重用
值类型和引用类型,在内存中的存储
类型被分为两种,值类型(整数+bool+struct+char+小数)+引用类型(string,数组,自定义的类,内置的类)
值类型,只需要一段内存,用于存储实际的数据(单独定义的时候,放在栈中)
引用类型,需要两段内存,第一段存储实际数据,他总是在堆中。第二段是一个引用,指向数据在堆中的存放位置(指向第一段内存)
当我们使用了引用类型赋值的时候,其实是赋值的是引用类型。赋值的话,其实是调换内存地址的指向
-
//如果数组是一个值类型的数组,那么数组中直接存储数值。
//如果是一个引用类型的数组,那么数组中存储的是引用地址。
...
static void Test1()
{
int i = 34;
int j = 34;
int temp = 34;
char c = 'a';
bool b = true;
}
...
...
static void Test2()
{
int i = 34;
int j = 234;
string name = "siki";
}
...
...
static void Test3()
{
string name = "siki";
string name2 = "taikr";
name = name2;
name = "google";
Console.WriteLine(name + ":" + name2);
}
...
...
static void Test4()
{
Vector3 v = new Vector3();
v.x = 100;
v.y = 100;
v.z = 100;
Vector3 v2 = new Vector3();
v2.x = 200;
v2.y = 200;
v2.z = 200;
v2 = v;
v2.x = 300;
Console.WriteLine(v.x);
}
class Vector3
{
public float x, y, z;
}
...
...
static void Test5()
{
Vector3[] vArray = new Vector3[] { new Vector3(), new Vector3(), new Vector3() };
//如果数组是一个值类型的数组,那么数组中直接存储数值。
//如果是一个引用类型的数组,那么数组中存储的是引用地址。
Vector3 v1 = vArray[0];
vArray[0].x = 100;
v1.x = 200;
Console.WriteLine(vArray[0].x);
}
...
继承
实现继承:
表示一个类型派生于一个基类型,他拥有该基类型的所有成员字段和函数。在实现继承中,派生类型采用基类型的每个函数的实现代码,除非在派生类型的定义中指定重写某个函数的实现代码。在需要给现有的类型添加功能或许多相关类型共享一组重要公共功能时,这种类型的继承非常有用。
接口继承:
表示一个类型只继承了函数的签名,没有继承任何实现代码,在需要指定该类型具有某些可用的特性时,最好使用这种类型的继承。
多重继承:
使用多重继承的有点是有争议的:一方面,毫无疑问,可以使用多重继承编写非常复杂、但很紧凑的代码。另一方面,使用多重实现继承的代码常常很难理解和调试。但是,C#的主要设计目标是简化健壮代码,所以C#不支持多重实现继承。
但是,C#允许类型派生自多个接口,多重接口继承。这说明,C#类可以派生自另一个类和任意多个接口。更准确地说,system.object是一个公共的基类,所以每个C#都有一个基类,还有可以有任意多个基接口。
...
class Program
{
static void Main(string[] args)
{
//Boss boss = new Boss();
//boss.AI();//继承父类里面所有的数据成员和函数成员,都会继承到子类里面
//boss.Attack();
//Enemy enemy;
//enemy = new Boss();//父类声明的对象,可以用子类去构造.不可以反过来
////enemy虽然使用父类进行了声明,但是使用了子类构造,所以本质上是一个子类类型,我们可以强制类型转换,转换成子类类型
//Boss boss = (Boss)enemy;
//boss.Attack();
Enemy enemy = new Enemy();
Boss boss = (Boss)enemy;//这样会错误,因为一个对象是什么类型的,主要看他是什么构造来定。这里的enemy使用了父类的构造函数,所以只有弗雷中的字段和方法,不能被强制转换成子类
//可以使用父类去声明对象,用子类去实例化
Console.ReadKey();
}
}
class Enemy
{
private float hp;
private float speed;
public float HP
{
get { return hp; }
set { hp = value; }
}
public float Speed
{
get { return speed; }
set { speed = value; }
}
public void AI()
{
Console.WriteLine("这里是Enemy1的公有AI方法");
}
public void Move()
{
Console.WriteLine("这里是Enemy1的公有move方法");
}
}
class Boss: Enemy
{
public void Attack()
{
AI();
Move();
HP = 100;//父类里面公有的数据和方法,才能被访问
Console.WriteLine("Boss正在进行攻击");
}
}
...
虚方法
- 把一个基类函数声明为virtual,就可以在任何派生类中重写该函数
- 在派生类中,重写另外一个函数时,要使用override关键字显示声明
- 我们在子类里面重写虚函数之后,不管在哪里调用都是用重写之后的方法(只有当通过子类的去构造的时候,才会去调用子类的内容)
- 虚方法重不重写都没关系,如果重写了,就用新的,如果不重写,就继续调用虚方法
隐藏方法
- 如果签名相同的方法在基类和派生类中都进行了声明,但是该方法没有分别声明为virtual和overri,派生类就会隐藏基类方法(要使用new方法进行声明)
- 隐藏方法:如果使用子类声明的对象,调用隐藏方法会调用子类的,如果使用父类声明对象,那么就会调用父类中的隐藏方法
- 一般少用隐藏方法,会引起混乱
this base关键字
- this的作用就是,可以去调用当前类的各种方法(基类+当前自己的),不用其实也可以,但是有this,会好找很多。
- base只能访问父类里面的方法
详细定义内容:
- this可以访问当前类中定义的字段、属性和方法,有没有this都可以访问,有this可以让编译器给出提示,另外当方法的参数跟字段重名的时候,使用this可以表明访问的是类中的字段。this可以调用父类中的方法和字段,有没有this都可以方法,但是加上base.IDE工具会给出提示,可以把所有的都显示出来
抽象类
- C#允许把类和函数声明为abstract,抽象类不能实例化,抽象类可以包含普通函数和抽象函数,抽象函数就是只有函数定义没有函数体。显然,抽象函数本身也是虚拟的virtual(只有函数定义,没有函数体实现)
- 类是一个模板,那么抽象类就是一个不完整的模板,我们不能使用不完整的模板去构造对象
- 可以用抽象类去声明对象,但是不用他去构造
- 当我们继承了一个抽象类的时候,必须去实现抽象方法
密封类和密封方法
- C#允许把类和方法声明为sealed,对于类,这表示不能继承该类;对于方法表示不能重写该方法
- 什么时候会使用密封类和密封方法?
答:防止重写某些类导致代码混乱,或,商业原因
...
class DerivedClass:BaseClass//密封类无法被继承
{
public sealed override void Move()//我们可以把重写的方法声明为密封方法,表示该方法不能被重写
{
base.Move();
}
}
//sealed class BaseClass//这里声明了一个密封类
class BaseClass
{
public virtual void Move()
{
}
}
...
派生类的构造函数
- 在子类中调用父类的默认构造函数(无参)(会先调用父类的,再调用子类的)
- 调用有参数的构造函数
...
class Program
{
static void Main(string[] args)
{
//DerivedClass o1 = new DerivedClass();
DerivedClass o2 = new DerivedClass(1, 2);
Console.ReadKey();
}
}
class BaseClass
{
private int x;
public BaseClass()
{
Console.WriteLine("base class 构造函数");
}
public BaseClass(int x)
{
this.x = x;
Console.WriteLine("y赋值完成");
}
}
class DerivedClass:BaseClass
{
private int y;
public DerivedClass()
//调用父类中无参的构造函数,当我们没有在子类的构造函数中显示声明调用父类的构造函数,默认会调用父类中的无参构造函数
{
Console.WriteLine("这个是DerivedClass无参的构造函数");
}
public DerivedClass(int x, int y) : base(x)
{
this.y = y;
Console.WriteLine("x赋值完成");
}
}
...
关于访问修饰符
- class的修饰符,只有在class前面声明了public,才能去其他类中访问
- new:成员用相同的签名隐藏继承的成员
- static:成员不作用于类的具体实例
- virtual:成员可以由派生类重写
- abstract:虚拟成员定义了成员的签名,但没有提供实现代码
- override:成员重写了继承的虚拟或抽象成员
- sealed:对于类,不能继承自密封类,对于属性和方法,成员重写已继承的虚拟成员,但任何派生类中的人和成员都不能重写该成员,该修饰符必须与override一起使用
- extern:成员在外用另一种语言实现
- protected:(可以应用于类型和内嵌类型的所有成员)只能派生的类型能访问该项。当没有继承的时候,protected和private是一样的。但是如果是在继承函数里面,继承还是是可以访问到projected的。
- static可以修饰字段/方法,修饰字段的时候,表示这个字段是静态的数据,叫做静态字段或静态属性,修饰方法的时候,叫做静态函数。使用static修饰的成员,只能通过类名访问。
- 当我们构造对象的时候,对象中只包含了普通字段,不包含静态字段(只能通过类名去访问,通过类名去赋值和取值)
定义和实现接口
- 定义一个接口在语法上,跟定义一个抽象类完全相同,但不允许提供接口中人和成员的实现方式,一般情况下,接口只能包含方法、属性、索引器和事件的声明
- 接口不能有构造函数,也不能有字段,接口也不允许运算符重载
- 接口定义中不允许声明成员的修饰符,接口成员都是公有的
- 接口也是可以相互继承的,只是在继承过程中,可能会有方法的层级嵌套,这个都需要去声明
列表List的创建和使用
- 集合类:列表List
- 当我们有很多类型一样的数据时候,前面我们一般使用数组来进行管理,但是这样有一个缺点就是数组的大小是固定的。如果我们很多类型一样的数据,比如游戏得分,我们可以集合类来进行管理,比如列表List,我们可以使用List很方便地添加数据,删除数据还有其他对数据的操作。
- 类型总结:
*创建列表:列表可以存储任何类型的数据,在创建列表对象的时候,首先要指定你要创建的这个列表要存储什么类型(泛型)
List<int> scoreList = new List<int>();
new List<int>() { 1, 2, 3 };
new List<string>() { "one", "two" };
var newScoreList = new List<int>();
*往列表里注入数据
scoreList.Add(12);
scoreLis.Add(45);
*如何取得列表中的数据?列表中的数据跟数组有点相似,索引从0开始,可以通过索引来访问
scoreList[0];//访问添加到列表中的第一个数据
- 关于列表的更多内容
1)列表初始容量为4,一旦发现超出范围,就会扩大为8,然后扩大为16...依次增加
2)直接设置个值,就可以减少数组的创建和删除,可以提高性能
3)可以通过Capacity属性获取和设置容量,intList.Capacity=100;
4)注意容量和列表中元素个数的区别,容量是列表中用于存储数据的数组长度通过capacity获取,列表中的元素是我们添加进去需要管理的数据,通过count获取
列表的遍历
var scoreList = new List<int>();
scoreList.Add(34);
scoreList.Add(334);
scoreList.Add(3344);
scoreList.Add(344);
scoreList.Add(32344);
scoreList.Add(134);
scoreList.Add(334);
//for (int i = 0; i < scoreList.Count; i++)
//{
// Console.Write(scoreList[i] + " ");
//}
foreach (var temp in scoreList)
{
Console.Write(temp + " ");
}
Console.ReadKey();
操作列表的属性和方法
- 主要操作方法
Capacity:获取容量大小
Add():添加元素
Insert():插入元素
[Index]:访问元素
Count:访问元素个数
RemoveAt():移除指定位置的元素
IndexOf():取得一个元素所在列表的索引位置(从前往后搜)
LastIndexOf():从后往前搜
Sort():对列表中元素进行从小到大排序 - 向指定索引位置插入元素,不可以超出元素个数
泛型类的定义
- 泛型:
通过参数化类型来实现在同一份代码上,操作多种数据类型。利用“参数化类型”将类型抽象化,从而实现灵活的复用 - 泛型类的定义
定义一个泛型类指的是,定义一个类,这个类某些字段的类型是不确定的,这些类型可以在类构造的时候确定下来。举例:
1)创建一个类处理int类型和double类型的相加:
class ClassA<T>//T代表一个数据类型,当使用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;
}
泛型方法
- 定义泛型方法就是定义一个方法,这个方法的参数可以是不确定的,可以当调用这个方法的时候,再去确定方法参数的类型。
可以实现,任意类型组拼成字符串的方法
public static T GetSum<T>(T a, T b){
return a+""+b;
}
GetSum<int>(23,12); GetSum<double>(23.2,12);