C# 核心
C# 核心
面向对象编程概念
面向过程编程是一种以过程为中心的编程思想,分析出解决问题所需要的步骤,然后有函数把步骤一步一步实现,使用的时候一个一个依次调用。
面向对象是一种对现实世界理解和抽象的编程方法,把相关的数据和方法组织作为一个整体来看待。从更高的层次来进行程序开发,更贴近事物的自然运行模式。
面向对象优势:提高代码复用率,提高开发效率,提高程序可拓展性,清晰的逻辑关系。
如何学习面向对象?
面向对象关键知识:类
面向对象三大特性:继承+封装+多态
封装:用程序语言来形容对象。
继承:复用封装对象的代码;儿子继承父亲,复用现成代码。
多态:同样行为的不同表现,儿子继承父亲的基因但是又不同的行为表现。
面向对象七大原则:开闭原则、依赖倒转原则、里氏替换原则、单一职责原则、接口隔离原则、合成复用原则、迪米特法则。
面向对象——封装
类和对象
类申明在命名空间内
访问修饰符 class 类名
{
//特征-成员变量
//行为-成员方法
//保护特征-成员属性
//构造函数和析构函数
//索引器
//运算符重载
//静态成员
}
类的命名用帕斯卡命名法:每个首字母都大写。
注意:同一个语句块/命名空间中的不同类不能重名。
什么是(类)对象?
类的申明和类对象(变量 )的申明是两个概念。
类的申明类似枚举和结构体的申明,相当于申明了一个自定义的变量类型。
而类对象是类创建出来的,相当于申明一个指定类的变量。
类创建对象的过程一般称为实例化对象
类对象都是引用类型的。
实例化对象的基本语法
//类名 变量名;
//类名 变量名 = null;(null代表空)
//类名 变量名 = new 类名();
前两种都是在栈上分配了内存,堆上没有分配内存。
第三种会在栈和堆都分配内存。
练习
1.A目前等于多少?
GameObject A = new GameObject();
GameObject B = A;
B = null;
答:A等于之前new的这个地址,不等于空。
2.A和B有什么关系?
GameObject A = new GameObject();
GameObject B = A;
B = new GameObject();
答:A和B没有关系,B已经new了一个新的Object。
成员变量和访问修饰符
成员变量基本规则:
-
1.声明在类语句块中
-
2.用来描述对象特征
-
3.可以是任意变量类型
-
4.数量不做限制
-
5.是否赋值由需求决定
如果要在类中声明一个和自己相同类型的成员变量时,不能对它实例化。
enum E_SexType
{ Man,Women,}
class Person
{
string name;
int age;
Person girlfriend;//=null,但不能new Person();
}
访问修饰符基本规则
public 自己和别人都能访问和使用
private 自己才能访问和使用,不写访问修饰符默认private
protected 自己和子类才能访问和使用
成员变量的使用和初始值
初始值
值类型来说 数字类型 默认值都是0 bool类型 false 引用类型 null
看默认值的小技巧:
Console.WriteLine(default(类型));
就会输出类型默认初始值。
使用
Person p=new Person();
p.age=10;
Console.WrtieLine(p.age);
成员方法
成员方法的声明
基本概念:成员方法(函数)用来表现对象行为。
注意:
1.成员方法不要加static关键字
2.成员方法必须实例化出对象,再通过对象来使用,相当于该对象执行了某个行为。
3.成员方法受到修饰符的影响。
class Person
{
public string name;
public int age;
public Person[] friends;
bool isAdult()
{
return age>=18;
}
public void Speak(String str)
{
isAdult();//私有函数只能在内部使用。
Console.WriteLine("{0}说{1}",name,str);
}
public void AddFriend(Person p)
{
if(friends == null)
{
friends=new Person[] {p};
}
else
{
Person[] newFriends=new Person[friends.Length + 1];
for(int i=0;i<friends.Length;i++)
{
newFriends[i]=friends[i];
}
newFriends[newFriends.Length - 1] = p;
friends=newFriends;
}
}
}
在c#里面通常把变量放在前面,方法放在后面,c++相反,但通常都可以。
成员方法的使用
成员方法必须实例化出对象,再通过对象来使用,相当于该对象执行了某个行为。
class Program
{
static void Main(strring[] args)
{
Console.WriteLine("成员方法");
Person p = new Person();
p.name = "唐老师";
p.age = 18;
p.Sreak("我爱你");
//p.isAdult不可以调用,因为是私有函数
Person p2 = new Person();
p2.name = "火山哥";
p2.age = 19;
p.addFriend(p2);
for(int i = 0;i < p.friends.Lengh;i++)
{
Console.WriteLine(p.friends[i].name);
}
}
}
输出:成员方法
唐老师说我爱你
火山哥
构造、析构、垃圾回收
构造函数
概念:在实例化对象时,会调用的用于初始化的和函数。
写法:没有返回值、函数名和类名相同、一般为public
class Person
{
public string name;
public int age;
//类中允许自己申明无参构造函数,结构体时不允许的
public Person()
{
name = "唐老师";
age = 18;
}
public Person(int age)
{
//this代表当前调用该函数的对象自己
this.age = age;
}
public Person(string name)
{
this.name = name;
}
public Person(int age,string name)
{
this.age = age;
this.name = name;
}
}
注意:如果不实现自己无参构造函数而实现了有参构造函数,会失去默认的无参构造(会报错)。
构造函数的特殊写法
可以通过this 重用构造函数代码
访问修饰符 构造函数名(参数列表):this(参数1,参数2)
public Person(int age,string name):this(age)
{
}
就能够先调用this括号参数那个的构造函数,能使代码复用。
析构函数
当对象被垃圾回收时调用的,主要是用来回收资源或者特殊处理内存的。
unity中基本用不到
垃圾回收机制
-
垃圾回收的过程是在遍历堆(Heap)上动态分配的所有对象。
-
通过识别它们是否被引用来确定哪些对象是垃圾,哪些对象仍要被使用。
-
垃圾就需要被回收释放。
-
注意:
-
垃圾回收值负责堆内存的垃圾回收
-
引用类型都是存在堆中,所以它分配和释放都通过垃圾回收机制来管理。
-
栈(Stack)上的内存是由系统自动管理的
-
值类型在栈中分配内存的,他们有自己的生命周期,不用对他们进行管理,会自动分配和释放。
-
C#中内存回收机制的大概原理
0代内存 1代内存 2代内存
代的概念:代是垃圾回收机制的一种算法(分代算法)。新分配的对象都会被配置在第0代内存中,每次分配都可能会进行垃圾回收以释放内存(0代内存满时)
在一次内存回收过程开始时,垃圾回收器会认为队中全是垃圾,回收垃圾以下两步:
- 1.标记对象 从根(静态字段、方法参数)开始检查引用对象,标记后为可达对象,未标记为不可达对象,不可达对象就被认为是垃圾。
- 2.搬迁对象压缩堆(挂起执行托管代码线程)释放未标记的对象 、搬迁可达对象、修改引用地址
大对象总被认为是第二代内存,目的是减少性能损耗,提高性能。
不会对大对象进行搬迁压缩,85000字节(83kb)以上的对象为大对象。
//手动触发垃圾回收的方法
//一般情况下,我们不会频繁调用(因为会造成卡顿)
//都是在Loading过场景时才调用
GC.Collect();
成员属性
基本概念:用于保护成员变量,为成员属性的获取和赋值添加逻辑处理,解决3P的局限性。
public——内外访问
private——内部访问
protected——内部和子类访问
属性可以让成员变量在外部
只能获取 不能修改 或者 只能修改 不能获取
基本语法:
访问修饰符 属性类型 属性名
{
get{}
set{}
}
class Person
{
private string name;
private int age;
private int money;
private bool sex;
//属性命名一般用帕斯卡命名法
public string Name
{
get
{
//可以在返回之前添加一些逻辑规则
//意味着这个属性可以获取的内容
return name;
}
set
{
//可以在设置之前添加一些逻辑规则
//value 关键字用于表示外部传入的值
name = value;
}
}
}
Person p = new Person();
p.Name="唐老师";
Console.WriteLine(p.name);
成员属性中 get和set前可以加访问修饰符
注意:1.默认不加,会使用属性声明是的访问权限
2.加的访问修饰符要低于属性的访问权限
3.不能让get和set的访问权限都低于属性的权限
get和set可以只有一个
只有一个就没必要加访问修饰符了
一般情况下 只会出现只有 get 的情况(只获取不修改),基本不会出现只有 set(不会只改不能获取)
自动属性
作用:外部能得不能改的特征
如果类中有一个特征只希望外部能得不能改的 又没什么特殊处理,那么可以直接使用自动属性。
public float Height
{
//没有在get和set中写逻辑的需求或想法
get;
set;
}
不过没什么特殊需求就不要用属性了,直接写成员变量写的代码量更少一些。
索引器
基本概念:让对象可以像数组一样通过索引访问其中元素,是程序看起来更直观,更容易编写。
基本语法:
访问修饰符 返回值 this[参数类型 参数名,参数类型 参数名……]
{
内部的写法和规则和成员属性相同
get{}
set{}
}
class Person
{
private string name;
private int age;
private Person[] friends;
private int[,] array;
public int this[int i,int j]
{
get
{
return arraya[i,j];
}
set
{
array[i,j] = value;
}
}
public string this[string str]
{
get
{
switch(str)
{
case "name":
return this.name;
case "age":
return age.ToString;
}
return "";
}
}
public Person this[int index]
{
get
{
//可以写逻辑 根据需求来处理这里的内容
if(friends == null||friends.Length - 1 < index)
{
return null;
}
return friends[index];
}
set
{
//value代表传入的值
//可以写逻辑 根据需求来处理这里的内容
if(friends == null)
{
frienfds = new Person[]{value};
}
else if(index > friends.Length - 1)
{
//自己定了一个规则,如果索引越界,就默认把最后一个朋友顶掉。
friends[friends.Length - 1] = value;
}
else
{
friends[index] = value;
}
}
}
}
Person p = new Person();
p[0] = new Person();
Console.WriteLine(p[0]);
p[0,0] = 10;
静态成员
基本概念:用static修饰的成员变量、方法、属性等成为静态成员。
特点:直接用类名点出使用。
早已出现的静态成员:Console
自定义静态成员
class Test
{
//静态成员变量
static public float PI = 3.1415926;
//成员变量
public int testInt = 100;
//静态成员方法
public static float CalcCircle(float r)
{
//静态函数不能使用非静态成员
//成员变量只能将对象实例化出来后,才能点出来使用,不能无中生有
//不能直接使用非静态成员,否则会报错
//这样的写法才可以
Test t = new Test();
Console.WriteLine(t.testInt);
return PI*r*r;
}
//成员方法
public void TestFun()
{
//非静态函数可以使用静态成员
Console.WriteLine(PI);
Console.WriteLine("123");
}
}
静态成员的使用
//静态成员
Console.WriteLine(Test.PI);
Console.WriteLine(Test.CalcCircle(2));
//普通成员
Test t = new Test();
Console.WriteLine(Test.testInt);
t.TestFun();
静态成员大多都是public,要在外面点出来使用
为什么可以直接点出来使用?
记住!程序不能无中生有!我们要使用的对象,变量,函数都是要在内存中分配内存空间的,之所以要实例化对象,目的就是分配内存空间,在程序中产生有个抽象的对象。
静态成员的特点程序开始运行时就会分配内存空间。所以我们就能直接使用。静态成员和程序共生共死。只要使用了它,直到使用了它,直到程序结束时内存空间才会被释放,所以静态成员就会有自己唯一的一个“内存小房间”,这让静态成员就有了唯一性,在任何地方使用都是用小房间的里的内容,改变了它也是改变了小房间里的内容。
静态成员的作用:1.常用唯一变量的声明 2.方便别人的获取的对象声明
静态方法:常用的唯一的方法声明,比如相同规则的数学计算相关函数。
静态过多会导致频繁GC,导致卡顿。
常量和静态变量
const可以理解为特殊的static
相同点:他们都可以通过类名点出来使用
不同点:
1.const必须初始化,不能修改,static没有这个规则
2.const只能修饰变量,static可以修饰很多
3.const一定时写在访问符后面的,static没有这个要求
静态类
概念:用static修饰的类
特点:只能包含静态成员,不能被实例化
作用:1.将常用的静态成员写在静态类中,方便使用
2.静态类不能被实例化,更能体现工具类的唯一性,比如Console就是一个静态类
static class TestStatic
{
//静态成员变量
public static int testIndex = 0;
public static void Testfun()
{
}
public static int TesyIndex
{
get;
set;
}
}
静态构造函数
概念:在构造函数加上static修饰
特点:
1.静态类和普通类都可以有
2.不能访问修饰符
3.不能有参数
4.只会自动调用一次
作用:在静态构造函数中初始化静态变量
使用
1.静态类中的静态构造函数
static class StaticClass
{
public static int testInt1 = 100;
public static int testInt2 = 100;
static staticClass()
{
Console.WriteLine("静态构造函数");
}
}
Console.WriteLine(StaticClass.testInt1);
Console.WriteLine(StaticClass.testInt2);
输出:静态构造函数
100
100
2.普通类中的静态构造函数
class Test
{
public static int testInt = 200;
static Test()
{
Console.WriteLine("静态构造");
}
public Test()
{
Console.WriteLine("普通构造");
}
}
Console.Write(Test.testInt);
Test t = new Test();
Test t2 = new Test();
输出:
静态构造
200
普通构造
普通构造
拓展方法
概念:为现有非静态 变量类型 添加 新方法
作用:1.提升程序拓展性
2.不需要在对象中重新写方法
3.不需要继承来添加方法
4.为别人封装的类型写额外的方法
特点:1.一定是写在静态类中
2.一定是一个静态函数
3.第一个参数为拓展目标
4.第一个参数用this修饰
//基本语法
//访问修饰符 static 返回值 函数名(this 拓展名 参数名,参数类型 参数名,参数类型 参数名……)
static class Tools
{
//为int拓展了一个成员方法
//成员方法是需要实例化对象后才能使用的
//value代表使用该方法的实例化对象
public static void SpeakValue(this int value)
{
//拓展方法的逻辑
Console.WriteLine("唐老师为int拓展的方法" + value);
}
public static void SpeakStringInfo(this string str,string str2,string str3)
{
Console.WriteLine("唐老师为string拓展的方法");
Console.WriteLine("调用方法的对象" + str);
Console.WriteLine("传的参数" + str2 + str3);
}
}
int i = 10;
i.SpeakValue(); //为int拓展的方法
string str = "000";
str.SpeakStringInfo("唐老师","111");
输出:唐老师为int拓展的方法10
唐老师为string拓展的方法
调用方法的对象000
传的参数唐老师111
为自定义的类型拓展方法
class Test
{
public int i = 10;
public void Fun1()
{
Console.WriteLine("123");
}
public void Fun2()
{
Console.WriteLine("456");
}
}
static class Tools
{
public static void Fun3(this Test t)
{
Console.WriteLine("为test拓展的方法");
}
}
Test t = new Test();
t.Fun3();
输出:为test拓展的方法
如果拓展方法和成员函数重名,会调用成员函数。
静态类是不能为静态类拓展方法的。
可以有返回值和拓展参数。
运算符重载
基本概念:让自定义的类和结构体能够使用运算符。使用关键字operator。
特点:1.一定是一个公共的静态方法
2.返回值写在operator前
3.逻辑处理自定义
作用:让自定义类和结构体对象可以进行运算
注意:1.条件运算符需要成对出现
2.一个符号可以多个重载
3.不能使用ref和out
//基本语法
//public static 返回类型 operator 运算符(参数列表)
class Point
{
public int x;
public int y;
public static Point operator +(Point p1,Point p2)
{
Point p = new Point();
p.x = p1.x + p2.x;
p.y = p1.y + p2.y;
return p;
}
public static Point operator +(Point p1,int value)
{
Point p = new Point();
p.x = p1.x + value;
p.y = p1.y + value;
return p;
}
public static Point operator +(int value,Point p1)
{
Point p = new Point();
p.x = p1.x + value;
p.y = p1.y + value;
return p;
}
}
Point p1 = new Point();
Point p2 = new Point();
p1.x=1;p1.y=1;
p2.x=2;p2.y=2;
Point p3=p1+p2;
可重载的运算符:算数运算符 逻辑运算符 位运算符 条件运算符
不可重载的运算符:逻辑与或(&& ||)索引符[ ] 强转运算符 特殊运算符(点. 三目运算符 ? : 赋值符号 =)
内部类和分布类
内部类
概念:在一个类中再声明一个类
特点:使用时要包裹者点出自己
作用:亲密关系的变现
注意:访问修饰符作用很大
class Person
{
public int age;
public string name;
public Body body;
public class Body
{
Arm leftArm;
Arm rightArm;
//不写public,外部调用不了的
class Arm
{
}
}
}
Person p = new Person();
Person.Body body = new Person.Body();
分部类
概念:把一个类分成几部分申明
关键字:partial
作用:分部描述一个类,增加程序的可拓展性
注意:分布类可以写在多个脚本文件中
分布类的访问修饰符要一致
分布类中不能有重复成员
partial class Student
{
public bool sex;
public string name;
}
partial class Student
{
public int number;
public void Speak(string str)
{
}
}
分部方法
概念:将方法的申明和实现分离
特点:1.不能加访问修饰符 默认私有
2.只能在分部类中申明
3.返回值只能是void
4.可以有参数但不用out关键字
partial class Student
{
public bool sex;
public string name;
partial void Speak();
}
partial class Student
{
public int number;
public void Speak(string str)
{ }
partial void Speak()
{
//实现逻辑
throw new NotImplementedException();
}
}
挺鸡肋的hhhh
面向对象——继承
继承的基本规则
基本概念:一个类A继承一个类B,类A会继承类B的所有成员,A类将拥有B类的所有特征和行为
被继承的类B称为父类、基类、超类、
继承的类A称为子类、派生类。
子类可以有自己的特征和行为
特点:1.单根性:子类只能有一个父亲
2.传递性:子类可以间接继承父类的父类
class Teacher
{
public string name;
public int number
public void SpeakName()
{
Console.WriteLine(name);
}
}
class TeachingTeacher : Teacher
{
public string subject;
public void SpeakSubject()
{
Console.WriteLine(subject + "老师");
}
}
class ChineseTeacher : TeachingTeacher
{
public void Skill()
{
Console.WriteLine("一行白鹭上青天");
}
}
TeachingTeacher tt = new TeachingTeacher();
tt.name - "唐老师";
tt.number = 1;
tt.SpeakName();
tt.subject = "Unity";
tt.SpeakSubject();
ChineseTeacher ct = new ChineseTeacher();
ct.name = "林老师";
ct.number = 2;
ct.subject = "语文";
ct.SpeakName();
ct.SpeakSubject();
ct.Skill();
访问修饰符的影响
public - 公共 内外部访问
private - 私有 内部访问
protected - 保护 内部和子类访问
之后讲命名空间的时候讲:internal - 内部的 只有在同一个程序集的文件中,内部类型或者是成员才可以访问
子类和父类的同名成员
概念:C#中允许子类存在和父类同名的成员,但是极不建议使用。
里氏替换原则
里氏替换原则是面向对象七大原则中最重要的原则
概念:任何父类出现的地方,子类都可以替代
重点:语法表现-父类容器装着子类对象,因为子类对象包含了父类的所有内容
作用:方便进行对象的储存和管理
class GameObject
{}
class PlayerAtk:GameObject
{
public void PlayerAtk()
{
Console.WriteLine(("玩家攻击"));
}
}
class Monster:GameObject
{
public void MonsterAtk()
{
Console.WriteLine("怪物攻击");
}
}
class Boss:GameObject
{
public void BossAtk()
{
Console.WriteLine("Boss攻击");
}
}
//用父类容器装载子类对象
GameObject player = new Player();
GameObject monster = new Monster();
GameObject boss = new Boss();
GameObject[] objects = new GameObject[] {new Player(),new Monster(),new Boss()};
//无法调用player.PlayerAtk()
is和as
基本概念:
is:判断一个对象是否执行类对象
返回值:bool 是为真 不是为假
as:将一个对象转换为指定类对象
返回值:指定类型对象
成功返回执行类型对象,失败返回null
//类对象 is 类名 该语句块 会有一个bool返回值 true和false
//类对象 as 类名 该语句块 会有一个对象返回值 对象和null
if(player is Player)
{
Player p = player as Player
p.PlayerAtk();
//或者可以这么写
//(player as Player).PlayerAtk();
}
for(int i = 0;i < objects.Length;i++)
{
if(object[i] is Player)
{
(object[i] as Player).PlayerAtk();
}
else if(object[i] is Monster)
{
(object[i] as Monster).MonsterAtk();
}
else if(object[i] is Boss)
{
(object[i] as Boss).BossAtk();
}
}
继承中的构造函数
基本概念:当申明一个子类对象时,先执行父类的构造函数,在执行子类的构造函数。
注意:1.父类的无参构造函数很重要
2.子类可以通过base关键字 代表父类 调用父类构造
class GameObject()
{
public GameObject()
{
Console.WriteLine("GameObject的构造函数");
}
}
class Player:GameObject
{
public Player()
{
Console.WriteLine("Player的构造函数");
}
}
class MainPlayer:Player
{
public MainPlayer()
{
Console.WriteLine("MainPlayer的构造函数");
}
}
MainPlayer mp = new MainPlayer();
输出:GameObject的构造函数
Player的构造函数
Mainplayer的构造函数
父类的无参构造
class Father
{
//子类实例化时,默认自动调用的时父类的无参构造 所以如果父类无参构造被顶掉,会报错
//public Father()
//{
//}
public Father(int i)
{
Console.WriteLine("Father的构造");
}
}
class Son:Father
{
//通过base调用指定父类构造
public Son(int i) : base(1)
{
Console.WriteLine("Son的一个参数构造");
}
public Son(int i,string str):this(i)
{
Console.WriteLine("Son的两个参数构造");
}
}
Son s = new Son(1,"123");
输出:Father的构造
Son的一个参数构造
Son的两个参数构造
万物之父
万物之父
关键字:object
概念:object是所有类型的父类/基类,它是一个类(引用类型)
作用:1.可以利用里氏替换原则,用object容器装所有对象
2.可以用来表是不确定类型,作为函数参数类型
Father f = new Son();
if(f is son)
{
(f as son).Speak();
}
//引用类型
object o = new Son();
Son s = new Son();
o = s;
o = f;
if(o is Son)
{
(o as Son).Speak();
}
//值类型
object o2 = 1f;
//用强转
float f1 = (float)o2;
//特殊的string类型
object str = "123123";
string str2 = str.ToString();
//或者string str2 = str as string;
object arr = new int[10];
int[] ar = (int[])arr;
//或者int[] ar = arr as int[];
//看到引用类型都用as就好了
拆箱装箱
发生条件:用object存值类型(装箱),再把object转为值类型(拆箱)。
-
装箱:把值类型用引用类型存储,栈内存会迁移到堆内存中
-
拆箱:把引用类型储存的值类型取出来,堆内存会迁移到栈内存中
-
好处:不确定类型时可以方便参数的储存和传递
-
坏处:存在内存迁移。增加性能消耗
//装箱
object v = 3;
//拆箱
int intValue = (int)v;
static void TestFun( params object[] array)
{
//里面的内容用is和as写各种操作就好了
}
//调用的时候就可以传任意的参数了
TestFun(1,2,3,4f,34.5,new Son());
尽量少用,因为装箱拆箱会造成性能消耗。也不是完全不能用。
密封类(不重要)
基本概念:密封类时使用sealed密封关键字修饰的类
作用:让类无法再被继承 “结扎”
sealed class Father
{
}
//会报错
class Son : Father
{
}
作用:在面向对象程序的设计中,密封类的主要作用就是不允许最底层子类被继承。可以保证程序的规范性和安全性。
目前来说用处不大,以后制作复杂系统或者程序框架时,便能慢慢体会到密封的作用。
意义:加强面向对象程序设计的规范性、结构性、安全性。
面向对象——多态
概念:多态按字面意思就是“多种状态”,让继承同一父类的子类们,在执行相同方法时有不同的表现(状态)。
主要目的:同一父类的对象,执行相同行为(方法)有不同的表现。
解决的问题:
让同一个对象有唯一行为的特征。
class Father
{
public void SpeakName()
{
Console.WriteLine("Father的方法");
}
}
class Son : Father
{
public new void SpeakName()
{
Console.WriteLine("Son的方法");
}
}
Father f = new Son();
f.SpeakName();
//会输出Father的方法
(f as Son).SpeakName();
//会输出Son的方法
这样写破坏了对象的唯一性。
多态的实现
我们目前学过的多态:编译时多态—— 函数重载,开始就写好的
我们将学习的多态:运行时的多态(vob、抽象函数、接口)
vob
v:virtual(虚函数)
o:override(重写)
b:base(父类)
class GameObject
{
public string name;
public GameObject(string name)
{
this.name = name;
}
//虚函数可以被子类重写
public virtual void Atk()
{
Console.WriteLine("游戏对象进行攻击");
}
}
class Player : GameObject
{
public Player(string name):base(name)
{
}
//重写虚函数
public override void Atk()
{
//base的作用:代表父类 可以通过base来保留父类的行为
base.Atk();
Console.WriteLine("玩家对象的攻击");
}
}
GameObject p = new Player();
p.Atk();
//输出:游戏对象进行攻击
//玩家对象进行攻击
抽象类和抽象函数
抽象类
概念:被抽象关键字abstract修饰的类
特点:1.不能被实例化的类
2.可以包含抽象方法
3.继承抽象类必须重写其抽象方法
abstract class Thing
{
//抽象类中 封装函数的所有知识点都可以在其中书写
public string name;
//可以在抽象类中写抽象函数
}
class Water : Thing
{
}
//不能Thing t = new Thing();
//但是可以用里氏替换原则
Thing t = new Water();
抽象函数
又叫纯虚方法,用abstract关键字修饰的方法
特点:1.只能在抽象类中申明
2.没有方法体
3.不能是私有的
4.继承后必须实现 用override重写
abstract class Fruits)
{
public string name;
//抽象方法必须写访问修饰符,因为默认是private,而抽象方法必须在子类重写,所以一定要写public或者protected
protected abstract void Bad(); //不用去实现
public virtual void Test()
{
//可以选择是否写逻辑
}
}
class Apple : Fruits
{
//子类不重写的父类的抽象方法会报错
public override void Bad()
{
}
//子类可以不重写虚函数,不会报错
class SuperApple:Apple
{
//子类的子类就不用必须重写抽象方法了
//虚方法和抽象方法都可以被子类无线的去重写
public override void Bad()
{
}
public override void Test()
{
//可以选择是否写逻辑
}
}
}
如何选择普通类和抽象类?
不需要被实例化的对象,相对比较抽象的类可以使用抽象类
父类中的行为不太需要被实现的,只希望子类去定义具体的规则的,可以选择抽象类如何使用其中的抽象等等来定义规则
作用:整体框架设计时,会使用。让父类更安全
接口
概念:接口是行为的抽象规范,它也是一种自定义类型。
关键字:interface
接口申明的规范
1.不包含成员变量
2.只包含方法、属性、索引器、事件
3,成员不能被实现
4.成员可以不用写访问修饰符,不能是私有的
5.接口不能继承类,但可以继承另一个接口
接口的使用规范
1.类可以继承多个接口
2.类继承接口后,必须实现接口中所有成员
特点
1.它和类的申明类似
2.接口是用来继承的
3.接口不能被实例化,但是可以作为容器储存对象
接口的声明
interface IFly
{
//默认public,不能是private
void Fly(); //不需要写
string Name
{
get;
set;
}
int this[int index]
{
get;
set;
}
//事件
event Action doSomething;
}
接口是抽象行为的“基类”。
接口命名规范:帕斯卡前面加个I
接口的使用
接口是用来继承的
-
1.类可以继承一个类,n个接口
-
2.继承了接口后,必须实现其中的内容,并且必须是public的
-
3.实现的接口函数,可以加v再在子类重写
class Animal
{}
class Person:Animal,IFly
{
//必须加public,不能是protected
public virtual void Fly()
{
//virtual虚函数,可以再被子类继承和重写
}
public string Name
{
get;
set;
}
public int this[int index]
{
get
{
return 0;
}
set;
}
//事件
public event Action doSomething;
}
//接口不能被实例化,但是可以用里氏替换原则
IFly f = new Person();
接口可以继承接口
接口继承接口时 不需要实现
待类继承接口后 类自己去实现所有内容
interface IWalk
{
void Walk();
}
interface IMove:IFly,IWalk
{
}
//类中就得把接口全部实现
class Test:IMove
{
public virtual void Fly()
{
}
public string Name
{
get;
set;
}
public int this[int index]
{
get
{
return 0;
}
set;
}
public event Action doSomething;
}
显示实现接口
当一个类继承两个接口,但是接口中存在着同名方法时,不能写访问修饰符
注意:显示实现接口时,不能写访问修饰符
interface IAtk
{
void Atk();
}
interface ISuperAtk
{
void Atk();
}
class Player:IAtk,ISuperAtk
{
//显示实现接口 就是用 接口名.行为名
void IAtk.Atk()
{
}
void ISuperAtk.Atk()
{
}
public void Atk()
{
}
}
Player p = new Player();
(p as IAtk).Atk();
(p as ISuperAtk).Atk();
p.Atk();
总结:
继承类:是对象间的继承,包括特征行为等等
继承接口:是行为间的继承,继承接口的行为规范,按照规范去实现内容
由于接口也是遵循里氏替换原则,可以用接口容器装对象。那么就可以实现装载各种毫无关系但却有相同行为的对象
注意:
-
1.接口值包含成员方法、属性、索引器、事件,并且都不实现,都没有访问修饰符
-
2.可以继承多个接口,但只能继承一个类
-
3.接口可以继承多个接口,相当于在进行行为合并,待子类继承时再去实现具体的行为
-
4.接口可以被显示继承,主要用于实现不同接口中的同名函数的不同实现
-
5.实现的接口方法,可以加virtual关键字,之后子类再重写
密封方法
基本概念:用密封关键字sealed修饰的重写函数
作用:让虚方法或者抽象方法之后不能再被重写
特点:和override
abstract class Animal
{
public string name;
public acstract void Eat();
public virtual void Speak()
{
Console.WriteLine("叫");
}
}
class Person:Animal
{
//会报错,就不能写了
//public sealed override void Eat()
//{ }
public override void Speak()
{
}
}
class WhitePerson:Person
{
public override void Eat()
{
base.Eat();
}
public override void Speak()
{
base.Speak();
}
}
面向对象关联知识点
命名空间
基本概念:命名空间是用来组织和重用代码的
作用:就像是一个工具包,类就像是一件一件的工具,都是申明在命名空间中的
基本语法:
namespace 命名空间名
{
类
类
}
namespace MyGame
{
class GameObject
{
}
}
namespace MyGame
{
class Player:GameObject
{
}
}
namespace MyGame2
{
class GameObject
{
}
}
using MyGame;
using MyGame2;
//不同命名空间中相互使用,需要引用命名空间或指明出处
MyGame.GameObject g = new MyGame.GameObject();
MyGame2.GameObject g2 = new MyGame2.GameObject();
在不同的命名空间中可以有同名类。
如果有两个命名空间中有同名类,那么在使用的时候需要指明出处。
//命名空间可以包裹命名空间
namespace MyGame
{
namespace UI
{
class Image
{
}
}
namespace Game
{
class Image
{
}
}
}
using MyGame.UI;
MyGame.UI.Image a = new MyGame.UI.Image;
关于修饰类访问修饰符
public——命名空间中的类 默认为public
internal——只能在该程序集中使用
abstract——抽象类
sealed——密封类
partical——分部类
万物之父中的方法
object中的静态方法
//静态方法 Equals 判断两个对象是否相等
//最终的判断权,交给左侧对象的Equals方法,
//不管值类型引用类型都会按照左侧对象Equals方法的规则来进行比较。
Console.WriteLine(Object.Equals(1,1));
//True
Test t1 = new Test();
Test t2 = new Test();
Test t3 = t1;
Console.WriteLine(Object.Equals(t1,t2));
//False
Console.WriteLine(Object.Equals(t1,t3));
//True
//静态方法 ReferenceEquals
//比较两个对象是否是相同的引用,主要是用来比较引用类型的对象。
//值类型对象返回值是指是false
Console.WriteLine(Object.ReferenceEquals(1,1));
//False
Console.WriteLine(Object.ReferenceEquals(t1,t2));
//False
Console.WriteLine(Object.ReferenceEquals(t1,t3));
//True
object中的成员方法
class Test
{
public int i = 1;
public Test2 t2 = new Test2();
public Test Clone()
{
return MemberwiseClone() as Test;
}
}
class Test2
{
public int i = 2;
}
//普通方法GetType
//该方法在反射相关 知识点中是非常重要的方法,之后我们会具体的讲解这里返回的Type类型
//该方法的主要作用就是获取对象运行时的类型Type,
//通过Type结合反射相关知识点可以做很多关于对象的操作。
Test t = new Test();
Test type = t.GetType();
//普通方法MemberwiseClone
//该方法用于获取对象的浅拷贝对象,口语化的意思就是会返回一个新的对象
//但是新对象的引用变量会和老对象中一致。
Test t2 = t.Clone();
Console.WriteLine("克隆对象后");
Console.WriteLine("t.i = " + t.i); //1
Console.WriteLine("t.t2.i = " + t.t2.i); //2
Console.WriteLine("t2.i = " + t2.i); //1
Console.WriteLine("t2.t2.i = " + t2.t2.i); //2
t2.i = 20;
t2.t2.i = 21;
Console.WriteLine("改变克隆体信息后");
Console.WriteLine("t.i = " + t.i); //1
Console.WriteLine("t.t2.i = " + t.t2.i); //21
Console.WriteLine("t2.i = " + t2.i); //20
Console.WriteLine("t2.t2.i = " + t2.t2.i); //21
object中的虚方法
//虚方法Equals
//默认实现还是比较两者是否为同一个引用,即相当于ReferenceEquals
//但是微软在所有值类型的基类SysteValueType中重写了该方法,用来比较值相等。
//我们也可以重写该方法,定义自己的比较相等的规则
//虚方法GetHashCode
//该方法是获取对象的哈希码
// (一种通过算法算出的,表示对象的唯一编码,不同对象哈希码有可能一样,具体值根据哈希算法决定
//我们可以通过重写该函数来自己定义对象的哈希码算法,正常情况下,我们使用的极少,基本不用。
//虚方法Tostring
//该方法用于返回当前对象代表的字符串,我们可以重写它定义我们自己的对象转字符串
//该方法非常常用。当我们调用打印方法时,默认使用的就是对象的Tostring方法后打印
string
1.字符串指定位置获取
//字符串本质是char数组
string str = "唐老师";
Console.WriteLine(str[0]);
//转为char数组
char[] chars = str.ToCharArray();
Console.WriteLine(chars[1]);
for(int i = 0;i < str.Length; i++)
{
Console.WriteLine(str[i]);
}
2.字符串拼接
str = string.Format("{0}{1}",1,3333);
Console.WriteLine(str);
//输出13333
3.正向查找字符串位置
str = "我是唐老师!";
int index = str.IndexOf("唐");
Console.Writeline(index); //输出2
//查找不到返回-1
4.反向查找指定字符串位置
str = "我是唐老师唐老师!";
int index = str.LastIndexOf("唐老师");
Console.Writeline(index); //输出5
5.移除指定位置后的字符
str = "我是唐老师唐老师!";
str.Remove(4);
Console.WriteLine(str); //还是输出我是唐老师唐老师!
str = str.Remove(4);
Console.WriteLine(str); //输出我是唐老
//执行两个参数进行移除
//参数1 开始位置
//参数2 字符个数
str = str.Remove(1,1); //输出我唐老
6.替换指定字符串
str = "我是唐老师唐老师!";
str = str.Replace("唐老师","老炮儿");
Console.WriteLine(str); //输出我是老炮儿老炮儿!
7.大小写转换
str = "fghdsjsjkdre";
str.ToUpper();
Console.WriteLine(str); //小写
str = str.ToUpper();
Console.WriteLine(str); //大写
str.ToLower();
Console.WriteLine(str); //大写
str = str.ToLower();
Console.WriteLine(str); //小写
8.字符串截取
str = "唐老师唐老师";
str = str.SubString(2);
Console.WriteLine(str); //输出师唐老师
//参数1 开始位置
//参数2 字符个数
//不会自动帮你判断是否越界,需要你自己判断越界
str = str.SubString(2,2);
Console.WriteLine(str); //输出老师
※9.字符串切割
str = "1,2,3,4,5,6,7,8";
string[] strs = str.Split(",");
for(int i = 0;i < str.Length; i++)
{
Console.WriteLine(str[i]);
}
//输出 1 2 3 4 5 6 7 8
StringBuilder
string是特殊的引用,每次重新赋值或者拼接时会分配新的内存空间,如果一个字符串经常改变会非常浪费空间。
StringBuilder
C#提供的一个用于处理字符串的公共类
主要解决的问题是
修改字符串而不创建新的对象,需要频繁修改和拼接的字符串可以使用它,提升性能
使用前需要引用命名空间
//初始化
using System.Test
StringBuilder str = new StringBuilder("123123123");
Console.WriteLine(str);
//StringBuilder存在一个容量问题,每次往里面增加时,会自动扩容
//获取容量
Console.WriteLine(str.Capacity); //16
//获取字符常量
Console.WriteLine(str.Length); //9
//增删查改替换
//增
str.Append("4444");
Console.WriteLine(str);
Console.WriteLine(str.Capacity); //16
Console.WriteLine(str.Length); //13
str.AppendFormat("{0}{1}",100,999);
Console.WriteLine(str); //1231234444100999
Console.WriteLine(str.Capacity); //32
Console.WriteLine(str.Length); //19
//插入
str.Insert(0,"唐老狮");
Console.WriteLine(str); //唐老狮1231231234444100999
//删
str.Remove(0,10);
Console.WriteLine(str); //234444100999
//清空
//str.Clear();
//改
str[0] = 'A';
Console.WriteLine(str); //A34444100999
//替换
str.Replace("1","唐");
Console.WriteLine(str); //A34444唐00999
//重新赋值StringBuilder
str.Clear();
str.Append("123123");
Console.WriteLine(str);
//判断StringBuilder是否和某一个字符串相等
if(str.Equals("12312"))
{
Console.WriteLine("相等")
}
结构体和类的区别
区别概述:
结构体和类最大的区别时在存储空间上的,因为结构体是值,类是引用。
因此他们的存储位置一个在栈上,一个在堆上。
值和引用在赋值上的区别
结构体和类在使用上很类似,结构体甚至可以用面向对象的思想来形容一类对象。
结构体具备着面向对象思想中封装的特性,但是它不具备继承和多态的特性,因此大大减少了它的使用频率。
由于结构体不具备继承的特性,所以它不能使用protected保护访问修饰符
细节区别
- 1.结构体是值类型,类是由于类型。
- 2.结构体存在于栈中,类存在于堆中。
- 3.结构体成员不能使用protected访问修饰符,而类可以。
- 4.结构体成员变量申明不能指定初始值,而类可以。 //和c++区别,c++的结构体可以赋初值
- 5.结构体不能申明无参的构造函数,而类可以。
- 6.结构体申明有参构造函数后,无参构造不会被顶掉。
- 7.结构体不能申明析构函数,而类可以。
- 8.结构体不能被继承,而类可以。
- 9.结构体需要在构造函数中初始化所有成员变量,而类随意。
- 10.结构体不能被静态static修饰(不存在静态结构体),而类可以。
- 11.结构体不能再自己内部申明和自己语言的结构体变量,而类可以。
结构体的特别之处
结构体可以继承接口,因为接口时行为的抽象
如何选择结构体和类
- 1.想要使用继承和多态时,直接淘汰结构体,比如玩家、怪物等等
- 2.对象是数据集合是,优先考虑结构体,比如位置、坐标等等
- 3.从值类型和引用类型赋值时的区别上去考虑,比如经常被赋值传递的对象,并且改变赋值对象
抽象类和接口的区别
抽象类和抽象方法
-
abstract修饰的类和方法
-
抽象类不能被实例化
-
抽象方法只能在抽象类中申明 是个纯虚方法,必须再子类中实现
接口
- interface自定义类型
- 是行为的抽象
- 不包含成员变量
- 仅包含方法、属性、索引器、事件、成员都不能实现,建议不写访问修饰符,默认public
相同点
- 1.都可以被继承
- 2.都不能直接实例化
- 3.都可以包含方法申明
- 4.子类必须实现未实现的方法
- 5.都遵循里氏替换原则
区别
- 1.抽象类中可以有构造函数;接口中不能
- 2.抽象类只能被单一继承;继承可以被继承多个
- 3.抽象类中可以有成员变量;接口中不能
- 4.抽象类中可以申明成员方法,虚方法,抽象等等,静态方法;接口中只能申明没有实现的抽象的方法
- 5.抽象方法可以使用访问修饰符;接口中建议不写,默认public
如何选择抽象类和接口
- 表示对象的用抽象类,表示行为拓展的用接口
- 不同对象拥有的共同行为,我们往往可以使用接口来实现
- 举个例子:动物是一类对象,我们自然会选择抽象类;而飞翔是一个行为,我们自然会选择接口。