一、面向对象基本概念
万物皆对象,用程序来抽象(形容)对象,用面向对象的思想来编程
用中文去形容一类对象,把一类对象的共同点提取出来,然后用程序语言把它翻译过来,带着对象的概念在程序中使用它们
面向对象三大特性,封装、继承、多态
面向对象七大原则,开闭原则,依赖倒转原则,里氏替换原则,单一职责原则,接口隔离原则,合成复用原则,迪米特法则
二、面向对象三大特性——封装
1. 类和对象
1.1 什么是类
具有相同特征,具有相同行为,一类事物的抽象。类是对象的模板,可以通过类创建出对象。类的关键词 class
1.2 类声明在哪里
类一般声明在 namespace 语句块中
1.3 类声明的语法
访问修饰符 class 类名
{
//特征——成员变量(字段)
//行为——成员方法
//保护特征——成员属性
//构造函数和析构函数
//索引器
//运算符重载
//静态成员
}
1.4 什么是(类)对象
类的声明和类对象(变量)声明是两个概念。类的声明类似枚举和结构体的声明,类的声明相当于声明了一个自定义变量类型;而对象是类创建出来的,相当于声明一个指定类的变量,类创建对象的过程一般称为实例化对象,类对象都是引用类型的。
1.5 实例化对象基本语法
类名 变量名; //栈上开辟空间,堆上不开辟空间
类名 变量名=null; //null代表空,栈上开辟空间,堆上不开辟空间
类名 变量名=new 类名(); //栈上堆上开辟空间,这里的括号是启用构造函数
2. 成员变量和访问修饰符
2.1 成员变量的声明
基本规则:
- 声明在类语句块中
- 用来描述对象的特征
- 可以是任意变量类型
- 数量不做限制
- 是否赋值根据需求来定
class Person
{
string name="zhangsan"; //与结构体不同,成员变量可以赋初值
int age;
Person girlFriend; //与结构体不同,类中可以有与自己类名相同的成员变量,但是不能对其进行实例化
Person[] friends;
Pet pet=new Pet(); //与自己类名不同的成员变量,可以对其进行实例化
}
2.2 访问修饰符
- public:共有的 所有的类都可以访问
- private:私有的 当前类内部可访问
- protected:受保护的 当前类以及继承它的子类可访问
- internal:内部的 只限于本项目内访问,其他的不能访问
- protected internal:内部保护访问 只能是本项目内部或子类访问 其他类不能访问
2.3 成员变量的使用和初始值
对于值类型来说,数字类型默认值都是0,bool类型默认值 false
对于引用类型来说,默认值是null
可以通过 Console.WriteLine(default(int)); 的方式查看默认值
3. 成员方法
成员方法(函数)用来表现对象行为
3.1 成员方法的声明
基本规则:
- 声明在类语句块中
- 是用来描述对象的行为
- 规则和方法声明规则相同
- 受到访问修饰符规则影响
- 返回值参数不做限制
- 方法数量不做限制
注意:
- 成员方法不要加 static 关键字
- 成员方法必须实例化出对象,再通过对象来使用,相当于该对象执行的某个行为
- 成员方法受到访问修饰符影响
3.2 成员方法的使用
成员方法必须实例化出对象,再通过对象来使用,相当于该对象执行的某个行为
4. 构造、析构、垃圾回收
在实例化对象时会调用的用于初始化的函数,如果不写默认存在一个无参构造函数
4.1 构造函数的声明
基本规则:
- 类是允许声明无参构造函数,而结构体不允许
- 没有返回值
- 函数名和类名必须相同
- 没有特殊需求时,一般都是 public 的
- 构造函数可以被重载
- this 代表当前调用该函数的对象自己
注意:
如果不自己实现无参构造函数而实现了有参构造函数,会失去默认无参构造函数
4.2 构造函数特殊写法
可以通过 this 重用构造函数代码:
访问修饰符 构造函数名 (参数列表): this(参数1, 参数2...) 在调用这个构造函数时,会先调用this中的构造函数,并将自己参数列表中的参数传给this中的参数使用
public Person()
{ }
public Person(string name)
{
this.name = name;
}
//若调用了这个构造函数,先执行this中的构造函数,再执行自己。若需要传参,则是将自己的name1作为参数传入this中的构造函数
public Person(int age1, string name1) : this(name1)
{
this.age=age1;
}
//this中的参数也可以是常数,不是必须为变量
public Person():this(18)
{}
4.3 析构函数
当引用类型的堆内存被回收时会调用该函数。对于需要手动管理内存的语言(比如 C\C++),需要在析构函数中做一些内存回收处理,但是 C# 中存在自动垃圾回收机制GC。所以我们几乎不怎么使用析构函数,除非你想在某一个对象被垃圾回收时做一些特殊处理。
~类名()
{}
4.4 垃圾回收机制
垃圾回收,英文简称GC。垃圾回收的过程是在遍历堆上动态分配的所有对象,通过识别他们是否被引用,来确定哪些对象是垃圾,哪些对象仍要被使用。所谓的垃圾,就是没有被任何变量、对象引用的内容。垃圾就需要被回收释放。
垃圾回收有很多种算法,比如:引用计数、标记清除、标记整理、复制集合
注意:
GC只负责堆内存的垃圾回收,引用类型都是存在堆中的,所以它的分配和释放都通过垃圾回收机制来管理。栈上的内存是由系统自动管理的,值类型是在栈中分配内存的,他们有自己的生命周期,不用对他们进行管理,会自动分配和释放。
C#中内存回收机制的大概原理:
零代内存、一代内存、二代内存
代的概念:代是垃圾回收机制使用的一种算法(分代算法)。新分配的对象都会被配置在第零代内存中,每次分配都可能会进行垃圾回收,以释放内存(零代内存满时)。
在一次内存回收过程开始时,垃圾回收器会认为堆中全是垃圾,会进行以下两步:
- 标记对象,从根(静态字段、方法参数)开始检查引用对象,标记后为可达对象,未标记为不可达对象,不可达对象就认为是垃圾。
- 搬迁对象压缩堆(挂起执行托管代码线程)释放未标记的对象,搬迁可达对象,修改引用地址。
大对象总被认为是第二代内存,目的是减少性能损耗,提高性能,不会对大对象进行搬迁压缩(85000字节以上的对象为大对象)。
GC.Collect(); //手动触发垃圾回收的方法,可能会造成卡顿
一般情况下,我们不会频繁调用,都是在 loading 过场景时才调用。
5. 成员属性
用于保护成员变量;为成员属性的获取和赋值添加逻辑处理;解决访问限制的局限性,属性可以让成员变量在外部只能获取不能修改,或者只能修改不能获取
5.1 基本语法
访问修饰符 属性类型 属性名
{
get{}
set{}
}
5.2 使用
class Person
{
private string name;
public string Name
{
get { return name; }
set { name = value; }
}
}
5.3 成员属性中 get 和 set 前可以加访问修饰符
- 默认不加,会使用属性声明时的访问限制
- 加的访问修饰符要低于属性的访问权限
- 不能让 get 和 set 的访问权限都低于属性的权限
public string Name
{
get { return name; } //get和set不能同时为private,会报错
set { name = value; } //当Name为private,那get和set中就不能有public
}
5.4 get 和 set 可以只有一个
只有一个时,没有必要在get或set前面加访问修饰符
一般情况只会出现只有get的情况,基本不会出现只有set的情况
5.5 自动属性
外部能得不能改的特征,如果类中有一个特性,是只希望外部能得到但不能改的,又没有什么特殊处理,那么可以直接使用自动属性,初始值在构造函数中处理
public string Name //前面不用声明私有字段
{
//没有再get和set中写逻辑的需求或想法
get;
private set;
}
6. 索引器
让对象可以像数组一样通过索引访问其中某一元素,使程序看起来更直观,更容易编写。
通常适用于在类中有数组变量时使用,可以方便的访问和进行逻辑处理
6.1 基本语法
访问修饰符 返回值 this[参数类型 参数名,参数类型 参数名....]
{
内部的写法和规则和属性相同
get{}
set{}
}
6.2 使用
class Person
{
private string name;
private int age;
private Person[] friends;
public Person this[int index] //这里的索引器在调用时是直接通过索引调用friends数组字段,返回值为Person
{
get
{
return friends[index];
}
set
{
//value代表传入的值
friends[index] = value;
}
}
}
class Program
{
static void Main(string[] args)
{
Person p = new Person();
p[0] = new Person(); //此时数组不用写成p.friends[0]。通过类中的索引器可以省略一些代码
Console.WriteLine(p[0]);
}
}
6.3 索引器可以写逻辑
同属性
6.4 索引器可以重载
重载的概念是函数名相同,参数类型、数量、顺序不同
class Person
{
private string name;
private int age;
private Person[] friends;
private int[,] array;
public int this[int i,int j] //相同的函数名可以理解为this,返回值为int
{
get //也可以只写一个get或set
{
return array[i,j];
}
set
{
array[i,j] = value;
}
}
public Person this[int index]
{
get
{
return friends[index];
}
set
{
//value代表传入的值
friends[index] = value;
}
}
}
public string this[string str]
{
get
{
switch(str)
{
case "name":
return this.name;
case "age":
return this.age.ToString();
}
return "";
}
}
7. 静态成员
静态关键字 static,用 static 修饰的成员变量、方法、属性等称为静态成员。静态成员的特点是直接用类名点出使用
7.1 自定义静态成员
class Test
{
static public int age;
static public string name;
static public float PI=3.14f;
static public float CalcCircle(float r)
{
return PI*r*r;
}
}
7.2 静态成员的使用
Console.WriteLine(Test.PI);
Console.WriteLine(Test.CalcCircle(2));
7.3 为什么可以直接点出来使用
程序中不能无中生有,我们要使用的对象、变量、函数都是要在内存中分配内存空间的,之所以要实例化对象,目的就是分配内存空间,在程序中产生一个抽象的对象。
静态成员的特点,程序开始运行时就会分配内存空间,所以我们就能直接使用。静态成员和程序同生共死。只要使用了它,直到程序结束时,内存空间才会被释放。所以一个静态成员就会有自己唯一的一个内存小房间,这让静态成员就有了唯一性,在任何地方使用都使用的小房间里的内容,改变了它也是改变小房间里的内容。
7.4 静态函数中不能使用非静态成员
成员变量只能将对象实例化出来后才能点出来使用,不能无中生有。不能直接使用非静态成员,否则会报错
class Test
{
static public float PI = 3.14f;
public int testInt = 100;
static public void Print(float r)
{
Test t=new Test(); //若一定要使用,需要在静态方法中实例化一个对象
Console.WriteLine(t.testInt);
}
}
7.5 非静态函数可以使用静态成员
7.6 静态成员对于我们的作用
静态变量:
- 常用唯一变量的声明
- 方便别人获取的对象声明
静态方法:
常用的唯一的方法声明,比如:相同规则的数学计算相关函数
7.7 常量和静态变量
const(常量)可以理解为特殊的 static(静态)。
相同点:他们都可以通过类名点出来使用
不同点:
- const 必须初始化,不能修改;static没有这个规则
- const 只能修饰变量,static 可以修饰很多
- const 一定是写在访问修饰符后面的,static 没有这个要求
7.8 单例模式
用静态成员相关知识实现:一个类对象,在整个应用程序的生命周期中,有且仅有一个该对象的存在。不能在外部实例化,直接通过该类类名就能够得到唯一的对象。
class Test
{
private static Test instance=new Test(); //直接给静态成员一个实例化对象
public static Test Instance{get{return instance;}} //外部只能得到不能修改
private Test() //外部不能实例化
{
}
public int testInt = 10;
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine(Test.Instance.testInt); //单例模式,通过静态成员的调用再调用成员变量
}
}
8. 静态类和静态构造函数
8.1 静态类
用 static 修饰的类,只能包含静态成员,不能被实例化
作用:
- 将常用的静态成员写在静态类中,方便使用
- 静态类不能被实例化,更能体现工具类的唯一性
8.2 静态构造函数
在构造函数上加 static 修饰
特点:
- 静态类和普通类都可以有
- 不能使用访问修饰符
- 不能有参数
- 只会自动调用一次
作用:
在静态构造函数中初始化静态变量
使用:
静态类中的静态构造函数
static class StaticClass
{
static public int testInt = 100;
static public int testInt2 = 100;
static StaticClass()
{
Console.WriteLine("静态构造函数");
}
}
静态构造函数会在第一次使用该静态类的成员之前,自动调用一次。
普通类中的静态构造函数
class Test
{
static public int testInt = 100;
static Test()
{
Console.WriteLine("静态构造");
}
public Test()
{
Console.WriteLine("普通构造");
}
}
静态构造函数会在第一次使用该普通类的成员之前,自动调用一次
普通构造函数会在调用构造函数的时候调用
9. 拓展方法
为现有非静态 变量类型 添加 新方法
作用:
- 提升程序拓展性
- 不需要在对象中重新写方法
- 不需要继承来添加方法
- 为别人封装的类型写额外的方法
特点:
- 一定是写在静态类中
- 一定是个静态函数
- 第一个参数为拓展目标
- 第一个参数用 this 修饰
9.1 基本语法
访问修饰符 static 返回值 方法名(this 拓展类名 参数名,参数类型 参数名....)
9.2 实例
static class Tool
{
//为int拓展了一个成员方法
//成员方法 是需要 实例化对象后才能使用的
//value 代表使用该方法的 实例化对象
public static void SpeakValue(this int value)
{
Console.WriteLine("拓展的方法" + value);
}
public static void SpeakStringInfo(this string value,int num,string str)
{
Console.WriteLine("字符串" + value + "与" + num + str + "拼写");
}
}
9.3 使用
int a = 9;
a.SpeakValue(); //首先实例化对象,然后用实例化对象调用方法
string str1 = "qger14tw";
int num = 1;
string str = "123";
str1.SpeakStringInfo(num, str);
9.4 为自定义的类型拓展方法
class Test1
{
}
static class Tool
{
public static void test1Fun(this Test1 t)
{
Console.WriteLine("拓展方法");
}
}
注意:如果拓展方法和类中原有方法重名,则拓展方法不会起作用
10. 运算符重载
让自定义类和结构体能够使用运算符。关键词 operator
特点:
- 一定是一个公共的静态方法
- 返回值写在 operator 前
- 逻辑处理自定义
作用
让自定义类和结构体对象可以进行运算
注意
- 条件运算符需要成对实现
- 一个符号可以多个重在
- 不能使用 ref 和 out
10.1 基本语法
public static 返回类型 operator 运算符(参数列表)
10.2 实例
class Point
{
public int x;
public int y;
public Point()
{ }
public Point(int a, int b)
{
this.x = a;
this.y = b;
}
public static Point operator +(Point p1, Point p2) //因为是Point类型相加,至少有一个Point类型
{
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 p2) //因为是Point类型相加,至少有一个Point类型
{
Point p = new Point();
p.x = p1.x + p2;
p.y = p1.y + p2;
return p;
}
public static Point operator +(int p1, Point p2) //因为是Point类型相加,至少有一个Point类型
{
Point p = new Point();
p.x = p1 + p2.x;
p.y = p1 + p2.y;
return p;
}
}
10.3 使用
static void Main(string[] args)
{
Point p1=new Point(1,1);
Point p2 = new Point(2,2);
Point p3 = p1 + p2;
Point p4 = p1 + 3;
}
10.4 可重载和不可重载的运算符
算数运算符:+ - * / % ++ -- 注意符号需要两个参数还是一个参数
逻辑运算符:!
位运算符:| & ^ ~ << >>
条件运算符:< > <= >= == != 返回值一般是布尔值,也可以是其他的;相关符号必须配对实现
不可重载的运算符:逻辑与(&&)逻辑或(||) 索引符[] 强转运算符() 特殊运算符:点. 三目运算符?: 赋值符号=
11. 内部类和分布类
11.1 内部类
class Person
{
public int age;
public string name;
public Body body;
public class Body
{
Arm leftArm;
Arm rightArm;
public class Arm{}
}
}
static void Main(string[] args)
{
Person p=new Person();
Person.Body body=new Person.Body();
}
11.2 分部类
把一个类分成几部分声明,关键词 partial
作用:
- 分部描述一个类
- 增加程序的拓展性
注意
- 分部类可以写在多个脚本文件中
- 分部类的访问修饰符要一致
- 分部类中不能有重复成员
public partial class Student
{
public int id;
}
public partial class Student
{
public string name;
}
11.3 分部方法
将方法的声明和实现分离
特点:
- 不能加访问修饰符,默认私有
- 只能在分部类中声明
- 返回值只能是void
- 可以有参数但不用 out 关键字
public partial class Student
{
public int id;
partial void Speak();
}
public partial class Student
{
public string name;
partial void Speak()
{ //实现逻辑 }
}
三、面向对象三大特性——继承
1. 继承的基本概念
一个类A继承一个类B,类A将会继承类B的所有成员,类A将拥有类B的所有特征和行为
被继承的类称为:父类、基类、超类
继承的类称为:子类、派生类
子类可以有自己的特征和行为
特点:
- 单根性,子类只能有一个父类
- 传递性,子类可以间接继承父类的父类
1.1 基本语法
class 类名:被继承的类名
{}
1.2 实例
public class Teacher
{
public string name;
public int number;
public void SpeakName()
{
Console.WriteLine(this.name);
}
}
public class TeachingTeacher : Teacher //继承了Teacher里的字段和方法
{
public string subject;
public void SpeakSubject()
{
Console.WriteLine(this.subject+"老师");
}
}
public class ChineseTeacher : TeachingTeacher //继承了TeachingTeacher和Teacher里的字段和方法
{
public void Skill()
{
Console.WriteLine("一行白鹭上青天");
}
}
1.3 访问修饰符的影响
private:私有访问,父类用private修饰的字段或方法,虽然子类中也会含有,但是不能直接访问。
protected:受保护的 当前类以及继承它的子类可访问
1.4 子类和父类同名成员
C#中允许子类存在和父类同名的成员,但是极不建议使用
public class Teacher
{
public string name;
public int number;
public void SpeakName()
{
Console.WriteLine(this.name);
}
}
public class TeachingTeacher : Teacher //继承了Teacher里的字段和方法
{
public string subject;
public void SpeakSubject()
{
Console.WriteLine(this.subject+"老师");
}
}
public class ChineseTeacher : TeachingTeacher //继承了TeachingTeacher和Teacher里的字段和方法
{
public new string name; //不加new会报警告,告诉覆盖父类的name;最好前面加上new以明确会覆盖父类的name
public void Skill()
{
Console.WriteLine("一行白鹭上青天");
}
}
2. 里氏替换原则
任何父类出现的地方子类都可以替代(父类容器装子类对象)。语法表现——父类容器装子类对象,因为子类对象包含了父类的所有内容
作用:方便进行对象存储和管理
2.1 基本实现
public class Player : GameObject
{
public void PlayerAtk()
{
Console.WriteLine("玩家攻击");
}
}
public class Monster : GameObject
{
public void MonsterAtk()
{
Console.WriteLine("怪物攻击");
}
}
public class Boss : GameObject
{
public void BossAtk()
{
Console.WriteLine("Boss攻击");
}
}
internal class Program
{
static void Main(string[] args)
{
GameObject player = new Player();
GameObject monster = new Monster();
GameObject bosss = new Boss();
GameObject[] objects = new GameObject[] { new Player(), new Monster(), new Boss() };
}
}
但此时被父类装载的子类不能够调用子类自己的方法
2.2 is和as
is:判断一个对象是否是执行的对象,返回值:bool,是为真,不是为假。
as:将一个对象转化为指定类型对象,返回值:指定类型对象,成功返回指定类型对象,失败返回null
可以解决上述被父类装载的子类不能够调用子类自己的方法的问题
2.3 基本语法
类对象 is 类名 该语句块会有一个bool返回值 true和false
类对象 as 类名 该语句块会有一个对象返回值 对象和null
3. 继承中的构造函数
当声明一个子类对象时,先执行父类的构造函数,再执行子类的构造函数
注意:
- 父类的无参构造很重要。
- 子类可以通过base关键字 代表父类 调用父类构造
3.1 继承中构造函数的执行顺序
父类的父类的构造->...父类构造->...子类构造
首先会调用父类的构造函数,然后调用子类构造函数
3.2 父类的无参构造函数很重要
public class Father
{
public Father(int a)
{
Console.WriteLine(a);
}
}
//子类实例化时默认自动调用的是父类的无参构造。所以如果父类无参构造函数被顶掉,会报错
public class Son : Father
{ }
不管子类是否有重载构造函数,只要父类的无参构造函数被顶掉,就会报错
3.3 通过base调用指定父类构造
可以通过 base 调用指定父类的构造函数,来解除报错情况
public class Father
{
public Father(int a)
{
Console.WriteLine(a);
}
}
//子类实例化时默认自动调用的是父类的无参构造。所以如果父类无参构造函数被顶掉,会报错
public class Son : Father
{
public Son(int a):base(a)
{}
}
所以子类在调用构造函数的时候,一定会调用父类的某一构造函数,默认调用父类无参构造函数
4. 万物之父和拆箱装箱
4.1 万物之父
关键字 object。object 是所有类型的基类,它是一个类(引用类型)
作用:
- 可以利用里氏替换原则,用 object 容器装所有对象
- 可以用来表示不确定类型作为函数参数类型
4.2 万物之父的使用
//引用类型
object o = new Son();
//用 is as 来判断和转换即可
if (o is Son)
{
(o as Son).Speak();
}
//值类型
object o2 = 1f;
//用强转
float f1 = (float)o2;
//特殊的 string 类型
object str = "1234";
string str2=str as string; //方法一
string str3=str.ToString(); //方法二
4.3 拆箱装箱
发生条件:用 object 存值类型(装箱),再把 object 转为值类型(拆箱)
装箱:把值类型用引用类型存储,栈内存会迁移到堆内存中
拆箱:把引用类型存储的值类型取出来,堆内存会迁移到栈内存中
好处:不确定类型时,可以方便参数的存储和传递
坏处:存在内存迁移,增加性能消耗
5. 密封类
密封类是使用 sealed 密封关键字修饰的类
作用:让类无法再被继承
5.1 实例
sealed class 类名
{}
5.2 作用
在面向对象程序的设计中,密封类的主要作用就是不允许最底层子类被继承
可以保证程序的规范性、安全性
四、面向对象三大特征——多态
1. 多态vob
按字面的意思就是多种状态,让继承同一父类的子类们,在执行相同方法时有不同的表现(状态)。
主要目的:同一父类的对象,执行相同行为(方法)有不同的表现
解决的问题:让同一个对象有唯一行为的特征
1.1 解决的问题
目前已经学过的多态:编译时的多态——函数重载,开始就写好的
将学习的多态:运行时的多态(vob、抽象函数、接口)
v:virtual(虚函数)
o:override(重写)
b:base(父类)
virtual关键字只能用于修饰基类的方法(普通方法)、属性、索引器或事件声明中。被virtual关键字修饰的方法、属性、索引器或事件就说明是要重写的,也就是要修改的,必须在派生类中重写,因为virtual只能在基类中使用。
1.2 多态的实现
public class GameObject
{
public string name;
public GameObject(string name)
{
this.name= name;
}
//虚函数可以被子类重写
public virtual void Atk()
{
Console.WriteLine("游戏对象进行攻击");
}
}
public class Player : GameObject
{
public Player(string name) : base(name)
{ }
public override void Atk()
{
base.Atk(); //base 的作用:代表父类,可以通过 base 来保留父类的行为
Console.WriteLine("玩家攻击");
}
}
internal class Program
{
static void Main(string[] args)
{
GameObject p = new Player("玩家1");
p.Atk(); //调用的是玩家中的方法;相比之下若只用new覆写Player中的方法,调用的会是父类GameObject的方法
}
}
2. 抽象类和抽象函数
2.1 抽象类
被抽象关键字abstract 修饰的类。一般基类都是抽象概念,常用抽象类来表示
特点:
- 不能被实例化的类
- 可以包含抽象方法
- 继承抽象类必须重写其他抽象方法
- 抽象类中封装的所有知识点都可以在其中书写,还可以在抽象类中写抽象函数
public abstract class Thing
{ }
public class Water : Thing
{ }
internal class Program
{
static void Main(string[] args)
{
//虽然不能被实例化,但是遵循里氏替换原则,用父类容器装子类
Thing t = new Water();
}
}
2.2 抽象函数
又叫纯虚方法,用 abstract 关键字修饰的方法。
特点:
- 只能在抽象类中声明
- 没有方法体
- 不能是私有的
- 继承后必须实现,用 override 重写
public abstract class Fruits
{
public string name;
//抽象方法,是一定不能有函数体的
public abstract void Bad();
}
public class Apple : Fruits
{
public override void Bad()
{
}
}
虚方法与抽象方法对比:
- 都和 override 配对,都可以被子类重写,虚方法和抽象方法都可以被子类的子类无限去重写
- 抽象方法必须写在抽象类中,虚方法可以写在任何类中
- 虚方法可以选择是否写逻辑,抽象方法不能写逻辑
- 继承的子类可以选择是否改写父类虚方法中的逻辑,但是必须改写父类抽象方法的逻辑
如何选择普通类还是抽象类?
- 不希望被实例化的对象、相对比较抽象的类,可以使用抽象类
- 父类中的行为不太需要被实现的,只希望子类去定义具体的规则,可以选择抽象类,然后使用其中的抽象方法来定义规则
3. 接口
接口是行为的抽象规范,它也是一种自定义的类型。关键字 interface
特点:
- 它和类的声明类似
- 接口是用来继承的
- 接口不能被实例化,但是可以作为容器存储对象
3.1 接口的声明
- 不包含成员变量
- 只包含方法、属性、索引器、事件
- 成员不能被实现
- 成员可以不用写访问修饰符,不能是私有的
- 接口不能继成类,但是可以继承另一个接口
接口是抽象行为的基类
interface 接口名 //接口名规范,帕斯卡前加一个I
{
}
interface IFly
{
void Fly();
string Name //属性里也是不能有语句块的
{
get;
set;
}
int this[int index]
{
get;
set;
}
event Action doSomething;
}
3.2 接口的使用
- 类可以继承多个接口
- 类继承接口后,必须实现接口中所有成员。当接口中成员是public或省略时(常用),类中对应成员必须是 public 的;当接口中当接口中成员是protected时,类中对应成员必须显式实现
- 实现的接口函数可以加 virtual 再在子类重写(当该类作为父类被子类继承时,子类可以继续重写该方法)
- 接口也遵循里氏替换原则
public class Person : Animal, IFly //一个子类只能有一个父类,但是可以有多个接口
{
public int this[int index]
{
get
{
return 0;
}
set
{ }
}
public string Name
{ get; set; }
public event Action doSomething;
public virtual void Fly() //当有子类继承 person 类时,子类可以继续重写该方法
{
}
}
3.3 接口可以继承接口
接口继承接口时,不需要实现
待类继承接口后,类自己去实现所有内容
interface IFly
{
void Fly();
}
interface IWalk
{
void Walk();
}
interface IMove:IFly,IWalk
{}
class Person:IMove
{
public void Fly()
{Console.WriteLine("不会飞");}
public void Walk()
{Console.WriteLine("可以走");}
}
internal class Program
{
static void Main(string[] args)
{
IFly ifly = new Person();
ifly.Fly(); //被装在IFly接口里,只能调用该接口的方法
IMove im = new Person();
im.Fly();
im.Walk(); //被装在IMove接口里,只能调用该接口的方法
IWalk iw= new Person();
iw.Walk(); //被装在IWalk接口里,只能调用该接口的方法
}
}
3.4 显式实现接口
当一个类继承两个接口,但是两个接口中存在同名方法时
注意:显式实现接口时不能写访问修饰符
显示实现接口就是用 接口名.行为名 去实现
interface IAtk
{
void Atk();
}
interface ISuperAtk
{
void Atk();
}
public class Player : IAtk, ISuperAtk
{
//显示实现接口就是用 接口名.行为名 去实现
void IAtk.Atk()
{
}
void ISuperAtk.Atk()
{
}
}
优点:可以区分出两种接口方法的不同
缺点:子类类型对象本身不能够调用同名方法(上述 Player p=new Player(); p不能够调用同名方法Atk,必须先as转成对应接口再调用,如(p as IAtk).Atk();)。
总结:
- 继承类:是对象间的继承,包括特征、行为等等
- 继承接口:是行为间的继承,继承接口的行为规范,按照规范去实现内容
- 由于接口也是遵循里氏替换原则,所以可以用接口容器装对象,那么就可以实现装载各种毫无关系,但是却有相同行为的对象
注意:
- 接口值包含成员方法、属性、索引器、事件,并且都不实现,都没有访问修饰符(一般不建议使用 protected)
- 可以继承多个接口,但是只能继承一个类
- 接口可以继承接口,相当于在进行行为合并,待子类继承时再去实现具体的行为
- 接口可以被显式实现,主要用于实现不同接口中的同名函数的不同表现
- 实现的接口方法可以加 virtual 关键字,之后子类再重写
4. 密封方法
就是用密封关键字 sealed 修饰的重写函数
作用:让虚方法或者抽象方法之后不能再被子类重写
特点:和 override 一起出现
4.1 实例
public abstract class Animal
{
public string name;
public abstract void Eat();
public virtual void Speak()
{ Console.WriteLine("叫"); }
}
public class Person : Animal
{
public sealed override void Eat() //子类在继承 person 的时候,不能再重写该方法
{
;
}
public override void Speak()
{
base.Speak();
}
}
五、面向对象相关
1. 命名空间
命名空间是用来组织和重用代码的
作用:就像是一个工具包,类就像是一件一件的工具,都是声明在命名空间中的
1.1 命名空间的使用
namespace 命名空间名
{
类
类
}
命名空间可以同名写多个,可以一个文件写多个同名命名空间,可以多个文件写多个同名命名空间
1.2 不同命名空间中互相使用,需要引用命名空间或指名出处
方法一: using 命名空间;
方法二:命名空间.类名 //指明出处
1.3 不同命名空间中允许有同名类
若使用了两个命名空间的同名类,则需要指明出处的方式指明所使用的类:命名空间.类名
1.4 命名空间可以包裹命名空间
类似内部类
1.5 关于修饰类的访问修饰符
- public:共有的 所有的类都可以访问
- internal:内部的 只限于本项目内访问,其他的不能访问
2. 万物之父中的方法
万物之父 object,所有类型的基类,是一个引用类型。可以利用里氏替换原则,装载一切对象存在装箱拆箱
2.1 object中的静态方法
静态方法 Equals,判断两个对象是否相等,最终的判断权交给左侧对方的 Equals 方法,不管值类型引用类型,都会按照左侧对象 Equals 方法的规则来进行比较
静态方法 ReferenceEquals,比较两个对象是否是相同的引用(是否指向的是相同的内存地址),主要是用来比较引用类型的对象,值类型对象返回值始终是 false
2.2 object中的成员方法
普通方法GetType,该方法的主要作用就是获取对象运行时的类型Type,通过 Type 结合反射相关知识,可以做很多关于对象的操作。
普通方法MenberwiseClone,该方法用于获取对象的浅拷贝对象,口语化的意思就是会返回一个新的对象,但是新对象中的引用变量会和老对象中的引用变量指向的是一个地址(因为是保护访问修饰类型方法,不能够在外部直接调用,可以在类内部封装该方法,再在外面调用)。
class Test
{
public int i = 1;
public Test2 t2 = new Test2();
public Test Clone()
{
return MemberwiseClone() as Test;
}
}
class Test2
{}
2.3 object中的虚方法
虚方法Equals,默认实现还是比较两者是否为同一个引用,即相当于ReferenceEquals。但是微软在所有值类型的基类System.ValueType中重写了该方法,用来比较值相等(也可以用来比较 string 类型值是否相等)。我们也可以重写该方法,定义自己的比较相等的规则
虚方法GetHashCode,该方法是获取对象的哈希玛(一种通过算法算出的,表示对象的唯一编码,不同对象哈希玛有可能一样,具体值根据哈希玛算法决定)。我们可以通过重启该函数来自己定义对象的哈希码算法。正常情况下,我们使用的极少,基本不用。
虚方法ToString,该方法用于返回当前对象代表的字符串,我们可以重写它,定义我们自己的对象转字符串规则。该方法非常常用。
3. string
注意 string的很多方法在调用后都需要重新用变量接收
3.1 字符串指定位置获取
字符串本质是char数组
string str="zhangsan";
Console.WriteLine(str[0]);
转换为char数组
char[] chars = str.ToCharArray();
3.2 字符串拼接
str = string.Format("{0}{1}",1,22);
3.3 正向查找字符位置
从前往后找第一个出现查找词的位置,没找到会返回-1
str = "zhangsan";
int i = str.IndexOf("s");
3.4 反向查找指定字符位置
从后往前找第一个出现查找词的位置,没找到会返回-1
str = "zhangsan";
int i = str.LastIndexOf("a");
3.5 移除指定位置后的字符
输入的位置也移出
str = str.Remove(4);
str = str.Remove(4,1); //两个参数,第一个参数是起始位置,第2个参数是移除个数
3.6 替换指定字符串
str = "123423";
str = str.Replace("23","99"); //两处的23都会被替换
3.7 大小写转换
str = str.ToUpper();
str = str.ToLower();
3.8 字符串截取
截取从指定位置开始之后的字符串,前面的不要
str = str.Substring(2);
str = str.Substring(2,3); //两个参数,第一个参数是起始位置,第2个参数是截取个数
//注意超出个数会报错
3.9 字符串切割
str = "1,2,3,4,5";
string[] strs = str.Split(',');
4. StringBuilder
注意 StringBuilder的很多方法在调用后可以直接修改本体
string 是特殊的引用,每次重新赋值或者拼接时会分配新的内存空间。如果一个字符串经常改变,会非常浪费空间。
StringBuilder是C#提供的一个用于处理字符串的公共类,主要解决的问题是:修改字符串而不创建新的对象,需要频繁修改和拼接的字符串可以使用它,可以提升性能。使用前需要引用命名空间System.Text。StringBuilder每次声明空间时一般会多声明一些空间
4.1 初始化 直接指明内容
StringBuilder str = new StringBuilder("123",100); //第一个参数为添入内容,第2个参数为声明空间(第2个参数可省略)
Console.WriteLine(str);
4.2 容量
StringBuilder存在一个容量的问题,每次往里增加时会自动扩容
获得容量
Console.WriteLine(str.Capacity);
获得字符串长度
Console.WriteLine(str.Length);
4.3 增删查改替换
//增
str.Append("123"); //参数为后面增加内容
str.AppendFormat("{0}{1}",111,222);
//插入
str.Insert(1,"111"); //第1个参数是插入位置,第2个参数是插入内容
//删
str.Remove(0,10);
//清空
str.Clear();
//查
int i = str[0];
//改
str[0] = 'A';
//替换
str.Replace("23","99"); //第一个参数是被替换的字符,第2个参数是替换后的字符
//重新赋值
str.Clear();
str.Append("");
//比较是否相等
if(str.Equals("123123"))
{}
如何优化内存?以现学的知识来分析:如何节约内存?如何尽量少的 GC?内存优化更多提的是堆内存的优化
少 new 对象,少产生垃圾,合理使用 static,合理使用 string 和 stringbuilder
5. 结构体和类的区别
5.1 区别概述
结构体和类最大的区别是在存储空间上的,因为结构体是值,类是引用,因此它们的存储位置一个在栈上,一个在堆上。
结构体和类在使用上很类似,结构体甚至可以用面向对象的思想来形容一类对象。结构体具备着面向对象思想中封装的特性,但是它不具备继承和多态的特性,因此大大减少了它的使用频率。由于结构体不具备继承的特性,所以它不能够使用 protected 保护访问修饰符。
5.2 细节区别
- 结构体是值类型,类是引用类型
- 结构体存在栈中,类存在堆中
- 结构体成员不能使用 protected 访问修饰符,而类可以
- 结构体成员变量声明不能指定初始值,而类可以
- 结构体不能声明无参的构造函数,而类可以
- 结构体声明有参构造函数后,无参构造不会被顶掉
- 结构体不能声明析构函数,而类可以
- 结构体不能被继承,而类可以
- 结构体需要在构造函数中初始化所有变量成员,而类随意
- 结构体不能被静态static 修饰,不存在静态结构体(但是可以存在静态成员)。而类可以
- 结构体不能在自己内部声明和自己一样的结构体变量,而类可以
5.3 结构体的特别之处
结构体可以继承接口,因为接口是行为的抽象
5.4 如何选择结构体和类
想要用继承和多态时,直接淘汰结构体
对象是数据集合时,优先考虑结构体
从值类型和引用类型赋值时的区别上去考虑,比如经常被赋值传递的对象,并且改变赋值对象时,原对象不想跟着变化时,就用结构体。
6. 抽象类和接口的区别
6.1 相同点
- 都可以被继承
- 都不能直接实例化
- 都可以包含方法声明
- 子类必须实现未实现的方法
- 都遵循里氏替换原则
6.2 区别
- 抽象类中可以有构造函数,接口中不能
- 抽象类只能被单一继承,接口可以被继承多个
- 抽象类中可以有成员变量,接口中不能
- 抽象类中可以声明成员方法、虚方法、抽象方法、静态方法;接口中只能声明没有实现的抽象方法
- 抽象类方法可以使用访问修饰符;接口中建议不写,默认 public
6.3 如何选择抽象类和接口
表示对象的用抽象类表示,行为拓展的用接口。
不同对象拥有的共同行为,我们往往可以使用接口来实现。
六、多个脚本文件
sln解决方案文件,cs脚本文件,exe可执行文件
1. 新建脚本文件
可以手动在文件夹中创建 txt 文件,再把 txt 后缀改成 cs
也可以在编译器右侧右键点击项目,添加中有一个新建项
接口、类、结构体一个声明就对应一个脚本
2. 在文件夹中新建脚本文件
在编译器右侧右键点击项目,有一个添加新建文件夹
七、UML类图
UML统一建模语言。是一种为面向对象系统的产品进行说明、可视化和编制文档的一种标准语言,是非专利的第三代建模和规约语言。UML是面向对象设计的建模工具,独立于任何具体程序设计语言。他的最终目标是直接能通过图形,就把业务逻辑完成
UML类图是UML其中很小的一部分。我们学习它的目的是帮助我们进行面向对象程序开发时,理清对象关系,养成面向对象编程习惯
八、七大原则
七大原则要实现的目标是:高内聚、低耦合
8.1 单一职责原则
SRP,类被修改的几率很大,因此应该专注于单一的功能。如果把多个功能放在同一个类中,功能之间就形成了联系,改变其中一个功能,就可能终止另一个功能
8.2 开闭原则
OCP,对拓展开发,对修改关闭。
拓展开发:模块的行为可以被拓展,从而满足新的要求
修改关闭:不允许修改模块的源代码,或者尽量使修改最小化
继承就是最典型的开闭原则的体现,可以通过添加新的子类和重写父类的方法来实现
8.3 里氏替换原则
任何父类出现的地方子类都可以替代
8.4 依赖倒转原则
DIP,要依赖于抽象,不要依赖于具体的实现
8.5 接口隔离原则
ISP,不应该强迫别人依赖他们不需要使用的方法。一个接口不需要提供太多的行为。一个接口应该尽量只提供一个对外的功能,让别人去选择需要实现什么样的行为,而不是把所有的行为都封装到一个接口当中
8.6 合成复用原则
CRP,尽量使用对象组合,而不是继承来达到复用的目的。继承关系是强耦合,组合关系是低耦合
8.7 迪米特法则
LoP,又称最少知识原则,一个对象应当对其他对象尽可能少的了解。