面向过程编程
是一种以过程为中心的编程思想,分析出解决问题所需要的步骤,然后用函数把步骤一步一步实现,使用的时候一个一个依次调用
面向对象编程
面向对象是一种对现实世界理解和抽象的编程方法,把相关的数据和方法组织为一个整体来看待,从更高的层次来进行程序开发,更贴近事物的自然运行模式特点
特点
提高代码复用率
提高开发效率
提高程序可拓展性
清晰的逻辑关系
关键字class
三大特性:封装、继承、多态
七大原则:开闭原则 、 依赖倒转原则 、 里氏替换原则 、 单一职责原则、接口隔离原则 、 合成复用原则 、 迪米特法则
用程序语言形容对象
类和对象
类的定义:一般是声明在namespace中,是以关键字 class 开始,后跟类的名称。类的主体,包含在一对花括号内。下面是类定义的一般形式:
<access specifier> class class_name
{
// member variables //成员变量
<access specifier> <data type> variable1;
<access specifier> <data type> variable2;
...
<access specifier> <data type> variableN;
// member methods //成员方法
<access specifier> <return type> method1(parameter_list)
{
// method body
}
<access specifier> <return type> method2(parameter_list)
{
// method body
}
...
<access specifier> <return type> methodN(parameter_list)
{
// method body
}
}
请注意:
- 访问标识符 指定了对类及其成员的访问规则。如果没有指定,则使用默认的访问标识符。类的默认访问标识符是 internal,成员的默认访问标识符是 private。
- 帕斯卡命名法,即每个单词的首字母大写。同一个语句块中不能重名。
- 数据类型 指定了变量的类型,返回类型 指定了返回的方法返回的数据类型。
- 如果要访问类的成员,你要使用点(.)运算符。
- 点运算符链接了对象的名称和成员的名称。
对象的定义:通过类创建出来的,相当于声明一个指定类的变量,创建的过程称为实例化对象,类对象都是引用类型
实例化对象的方法:
class Person
{
}
Person p1; //已经在栈上分配空间,但地址指向空,即没有分配内存空间(一般指没有在堆上分配空间为没有分配内存空间)
Person p2 = null; //已经在栈上分配空间,但地址指向空,即没有分配内存空间(一般指没有在堆上分配空间为没有分配内存空间)
Person p3 = new Person(); //分配空间,栈指向堆(引用类型)
成员变量和访问修饰符
成员变量
1.声明在类语句块中
2.用来描述对象的特征
3.可以使任意变量类型
4.数量不做限制
5.是否赋值根据需求来定
enum E_SexType
{
Man,
Woman
}
struct Position{}
class Pet{}
class Person
{
public string name = "张三";//任意变量类型
int age;
E_SexType sex;//枚举
//如果要在类中声明一个和自己相同类型的成员变量时不能对他进行实例化
Person gridFriend;//类
Person[] boyFriend;//类
Position pos;//结构体
Pet pet;//类
}
//成员变量的使用 , 使用.来访问 可以访问的 成员变量和方法
//值类型默认值都为0 , bool类型为false , 引用类型为null
Person p = new Person();
p.name = "名字";
访问修饰符
所有类型和类型成员都具有可访问性级别。 该级别可以控制是否可以从你的程序集或其他程序集中的其他代码中使用它们。 程序集是通过在单个编译中编译一个或多个 .cs 文件而创建的 .dll 或 .exe。 可以使用以下访问修饰符在进行声明时指定类型或成员的可访问性:
public:同一程序集中的任何其他代码或引用该程序集的其他程序集都可以访问该类型或成员。 某一类型的公共成员的可访问性水平由该类型本身的可访问性级别控制。
private:只有同一 class
或 struct
中的代码可以访问该类型或成员。
protected:只有同一 class
或者从该 class
派生的 class
中的代码可以访问该类型或成员。
internal:同一程序集中的任何代码都可以访问该类型或成员,但其他程序集中的代码不可以。 换句话说,internal
类型或成员可以从属于同一编译的代码中访问。
protected internal:该类型或成员可由对其进行声明的程序集或另一程序集中的派生 class
中的任何代码访问。
private protected:该类型或成员可以通过从 class
派生的类型访问,这些类型在其包含程序集中进行声明。
调用方的位置 | public |
protected internal |
protected |
internal |
private protected |
private |
---|---|---|---|---|---|---|
在类内 | ✔️️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
派生类(相同程序集) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
非派生类(相同程序集) | ✔️ | ✔️ | ❌ | ✔️ | ❌ | ❌ |
派生类(不同程序集) | ✔️ | ✔️ | ✔️ | ❌ | ❌ | ❌ |
非派生类(不同程序集) | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ |
成员方法
成员方法 ( 函数 ) 用来表现对象行为
1 . 申明在类语句块中
2 . 是用来描述对象的行为的
3 . 规则和函数申明规则相同
4 . 受到访问修饰符规则影响
5 . 返回值参数不做限制
6 . 方法数量不做限制
注意 成员方法不用加static关键字,必须实例化出对象,再通过对象来使用,相当于该对象执行了某个行为
构造函数和析构函数
1.构造函数
构造函数特点
1.在实例化时会调用的用于初始化的函数
2.如果不写会默认存在一个无参构造函数
构造函数的写法
1 . 没有返回值
2 . 函数名和类名必须相同
3 . 没有特殊需求时一般都 是public 的
class Person
{
string name;
int age;
public Person()
{
name = "张三";
age = 18;
}
public Person(string name,int _age) //重载
{
this.name = name;//this函数里面表示对象自己
this.age = _age;
}
}
注意: 如果不自己实现无参构造函数,而实现了有参构造函数,会失去默认的无参构造
构造函数的特殊写法
class Person
{
string name;
int age;
public Person()this("张三")//在无参构造函数时也可以添加常量进入含参构造函数
{
age = 18;
}
public Person(string _name)
{
this.name = _name;
}
public Person(string _name,int _age) this(name)//this即该类的构造函数 先this再执行这个构造函数
{
this.age = _age;
}
}
2.析构函数
当引用类型的堆内存被回收时 , 会调用该函数,对于需要手动管理内存的语言 ( 比如 c + + ) ,需要在析构函数中做一些内存回收处理。
但是 c # 中存在自动垃圾回收机制 GC , 所以我们几乎不会怎么使用析构函数 。 除非你想在某一个对象被垃圾回收时 , 做一些特殊处理。
注意:在Unity开发中析构函数几乎不会使用,所以该知识点只做了解即可。
class Person
{
string name;
int age;
~Person()// ~+类名即为析构函数,当引用类型的堆内存 被真正回收时 会调用该函数
{
name = "张三";
age = 18;
}
}
3.垃圾回收机制
垃圾回收 , 英文简写GC (Garbage collector)
垃圾回收的过程是在遍历堆(Heap)上动态分配的所有对象
通过识别它们是否被引用来确定哪些对象是垃圾 , 哪些对象仍要被使用
所渭的垃圾就是没有被任何变量 , 对象引用的内容,垃圾就需要被回收释放
垃圾回收有很多种算法 , 比如
引用计数 (Reference counting)
标记清除 ( Mark sweep)
标记整理 ( Mark compact)
复制集合 ( copy collection)
注意 :
GC 只负责堆 ( Heap ) 内存的垃圾回收
引用类型都是存在堆 ( Heap ) 中的 , 所以它的分配和释放都通过垃圾回收机制来管理
栈 ( stack ) 上的内存是由系统自动管理的
值类型在栈 ( stack ) 中分配内存的 , 他们有自己的申明周期 , 不用对他们进行管理 , 会自动分配和释放
c # 中内存回收机制的大概原理
0 代内存 1 代内存 2 代内存
代的概念 :
代是垃圾回收机制使用的一种算法 ( 分代算法 )
新分配的对象都会被配置在第0代内存中
每次分配都可能会进行垃圾回收以释放内存 ( 0代内存满时 )
在一次内存回收过程开始时 , 垃圾回收器会认为堆中全是垃圾 , 会进行以下两步
1 . 标记对象从根 ( 静态字段 、 方法参数 ) 开始检查引用对象 , 标记后为可达对象 , 未标记为不可达对象
不可达对象就认为是垃圾
2 · 搬迁对象压缩堆 ( 挂起执行托管代码线程 ) 释放未标记的对象搬迁可达对象修改引用地址
大对象总被认为是第二代内存目的是减少性能损耗 ,提高性能,不会对大对象进行搬迁压缩 85000字节 (83kb) 以上的对象为大对象
GC.collect()//主动去回收
一般情况不会调用,在Loading条的时候使用。
成员属性
基本概念
1.用于保护成员变量
2.为成员属性的获取和赋值添加逻辑处理
3.解决 3P 的局限性
public一内外访问
private— 内部访问
protected— 内部和子类访问
属性可以让成员变量在外部,只能获取不能修改或者只能修改不能获取
示例
class Person
{
private int age;
public string Age //
{
get
{
//加密处理
return age+5;
}
set
{
//加密处理
if(value<0)
{
value = 0;
Console.WriteLine("年龄不能小于0");
}
age = value-5;
}
}
}
class main
{
Person p1 = new Person;
Person p2 = new Person;
p1.Age = 10; // 实际上为10
p2.Age = -10;// 实际上为-10
}
get和set可以加访问修饰符
注意
1 . 默认不加会使用属性申明时的访问权限2 . 加的访问修饰符要低于属性的访问权限
3 . 不能让 get 和 set 的访问权限 都低于 属性的权限
3 . get 和 set 可以只有一个,此时不需要添加修饰符
class Person
{
private int age;
public string Age //
{
get //默认public
{
//加密处理
return age+5;
}
private set //就只能在类里面设置该成员变量
{
//加密处理
if(value<0)
{
value = 0;
Console.WriteLine("年龄不能小于0");
}
age = value-5;
}
}
}
自动属性
作用 : 外部能得不能改的特征,如果类中有一个特征是只希望外部能得不能改的又没什么特殊处理
那么可以直接使用自动属性
class Person
{
public int Height
{
get;
private set;
}
}
虽然成员属性可以替代成员变量,但会造成性能的消耗(代码变多了),只有需要的时候才引用。
索引器
概念
让对象可以像数组一样通过索引访问其中元素,使程序看起来更加直观,容易编写
语法
//访问修饰符 返回值 this[参数类型 参数名,参数类型,参数名]
//{
// get
// {
// 可以写逻辑
// return 返回值;
// };
// set
// {
// 可以写逻辑
// };
//}
public class Person
{
private string name;
private int age;
private Person[] friends;
public Person this[int index]
{
get
{
if(index == 0 || index > friends.Length -1)
{
return null;
}
else
{
return friends[index];
}
}
set
{
if(friens == null)
{
friends = new People[]{value};
}
else if(index > friends.Length -1)
{
Person[friends.Length+1] newfriends = new Person;//新对象组长度加一
for(int i ;i<friends.Length;i++)//将旧的搬运过去
{
newfriends[i] = friends[i];
}
newfriends[friends.Length+1] = value;//添加新朋友
friends = newfriends;//转移引用到原来的friends
}else
{
friends[index] = value;//更改已有朋友
}
}
}
}
函数和属性的综合体,注意是[]中括号
class main
{
Person p = new Person;
p[0] = new Person;//创建朋友0,让对象可以像数组一样通过索引访问其中元素
}
public class Person
{
private string name;
private int age;
private Person[] friends;
private int[,] array;
public int this[int i,int j]//索引器重载 this相当于变量名
{
get
{
return array[i,j];
}
set
{
array[i,j] = value;
}
}
public Person this[int index]
{
//省略
}
}
索引器主要作用可以让我们以中括号的形式,自定义类中的元素,规则自己定,访问时和数组一样,比较适用于 在类中有数组变量时使用,可以方便的访问和进行逻辑处理。
静态成员
概念
静态关键字 static,用 stat 让修饰的成员变量 、 方法 、 属性等称为静态成员
直接使用类名点出来使用
示例
public class test
{
private static float pi = 3.1415926f;
}
特点
我们要使用的对象 , 变量 , 函数都是要在内存中分配内存空间的,之所以要实例化对象 , 目的就是分配内存空间 , 在程序中产生一个抽象的对象。
程序开始运行时就会分配内存空间 。 所以我们就能直接使用 。
静态成员和程序是一体的,只要使用了它 , 直到程序结束时内存空间才会被释放,所以一个静态成员就会有自己唯一的一个 “ 内存小房间 ”,这让静态成员就有了唯一性。
在任何地方使用都是用的小房间里的内容 , 改变了它也是改变小房间里的内容 。
注意:静态函数中不能使用非静态成员,成员变量只能将对象实例化出来后才能点出来使用不能直接使用非静态成语,否则会报错。
非静态函数可以使用静态成员
静态变量常用唯一的变量,方便别人获取的对象声明,静态方法常用来唯一的方法声明。
const常量可以看做特殊的static
相同点
他们都可以通过类名点出使用
不同点
1 . const 必须初始化 , 不能修改,static 没有这个规则
2 . const 只能修饰变量 、 static可以修饰很多
3 . const 一定是写在访问修饰符后面的 , static没有这个要求
静态类和静态构造函数
静态类
用 static 修饰的类
特点
1 . 只能包含静态成员
2 . 不能被实例化
作用
1 . 将常用的静态成员写在静态类中方便使用
2 . 静态类不能被实例化 , 更能体现工具类的唯删国
静态构造函数
在构造函数上加上static修饰
特点
1 . 静态类和普通类都可以有
2 . 不能使用访问修饰符
3 . 不能有参数
4 . 只会自动调用一次
作用
在静态构造函数中初始化静态变量
class Test
{
static Test()
{
Console.WriteLine("静态构造函数")
}
public Test()
{
Console.WriteLine("普通构造函数")
}
}
class Program
{
Static void main()
{
Test t = new Test();
Test t2 = new Test();
}
}
/*输出
静态构造函数
普通构造函数
普通构造函数
*/
拓展方法
为现有 非静态 变量类型添加新方法
作用
1 . 提升程序拓展性
2 . 不需要再对象中重新写方法
3 . 不需要继承来添加方法
4 . 为别人封装的类型写额外的方法
特点
1 . 一定是写在静态类中
2 . 一定是个静态函数
3 . 第一个参数为拓展目标
4 . 第一个参数用 this 修饰
//访问修饰符 static 返回值 函数名(this 拓展类名 参数名,参数类型 参数名,......)
//成员方法 是需要 实例化对象后 才能使用的
//value 代表 使用该方法的 实例化对象
static class tool
{
public static void SpeakValue(this int value)//here 第一个参数是规则,规定了为谁拓展的方法
{
Console.WriteLine("为Int拓展的方法"+value);
}
}
class Program
{
static void Main(string[] Args)
{
Console.WriteLine("拓展方法");
int i = 2;
i.SpeakValue();
}
}
//输出
//拓展方法
//为Int拓展的方法2
注意:如果拓展的方法和原有的方法重名且参数相同,那么调用原有的方法
运算符重载
概念
让自定义类和结构体,能够使用运算符,使用关键字operator
特点
1 . 一定是一个公共的静态方法
2 . 返回值写在operator前
3 . 逻辑处理自定义
作用
1 . 让自定义类和结构体对象可以进行运算
注意
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;
}
}
class Program
{
static void Main(String[] Args)
{
Point p1 = new Point();
Point p2 = new Point();
p1.x = 3;
p1.y = 4;
p2.x = 5;
p2.y = 6;
Point p3 = p1 + p2;
Point p4 = p2 + 3;//正确,重载参数中必须有一个为返回值的类型。
Point p5 = 3 + p4;//错误,重载和参数顺序有关
}
}
可重载的运算符:算数运算符(都可以),逻辑运算符(只有非!),位运算符(都可以),条件运算符(都可以,但是必须成对实现)
不可重载的运算符:逻辑与&&,逻辑或||,索引符[],强势运算符(),特殊运算符(点. 三目运算符?:赋值符号=)
内部类和分部类
内部类
概念
在一个类中,再声明一个类
特点
在使用时要用包裹者点出自己
作用
亲密关系的表现
注意
访问修饰符作用很大
calss Person
{
public int age;
public string name;
public Body body;
public class Body
{
Arm leftArm;
Arm rightArm;
class Arm;
}
}
class Program
{
static void Main(string[] Args)
{
Person p = new Person();
Person p.Body b = new Person.Body();
Person p.Body.Arm a = new Person.Body.Arm();//错误,访问权限不足
}
}
分部类
概念
把一个类分成几部分,关键字 partial
作用
分布描述一个类
增加程序拓展性
注意
分布类可以写在多脚本文件中
分布类的访问修饰符要一致
分布类中不能有重复成员
partial class Student
{
public name;
public num;
}
partial class Student
{
public sex;
public void Speak();
}
分部方法
概念
将方法的声明和实现分离,用分部关键字去声明的方法
特点
1 . 不能加访问修饰符默认私有
2 . 只能在分部类中申明
3 . 返回值只能是void
4 . 可以有参数,但不能用out关键字
partial class Student
{
public name;
public num;
partial void Speak();
}
partial class Student
{
public sex;
partial void Speak()
{
//实现逻辑
}
}
概念
若类A继承类B,那么类A继承类B的所有成员,拥有类B的所有特征以及行为
被继承的类称为 父类,基类,超类
继承的类称为 子类,派生类
子类可以有自己的特征和行为
特点
单根性 子类只能有一个父类
传递性 子类可以间接继承父类的父类
语法
//class 类名:被继承的类
class Teacher
{
public string name;
public int number;
public void SpeakNumber();
}
class TeachingTeacher : Teacher
{
public string subject;
public void SpeakSubject();
}
class Program
{
static void Main(string[] Args)
{
TeachingTeacher t = new TeachingTeacher();
t.name = "张三";
}
}
访问修饰符的影响
//public private protect internal
子类和父类的同名成员
csharp允许子类父类出现相同的成员,但不推荐使用
class Teacher
{
public string name;
}
class TeachingTeacher : Teacher
{
public string name;
}
class Program
{
static void Main(string[] Args)
{
TeachingTeacher t = new TeachingTeacher();
t.name = "张三";//默认为TeachingTeacher类中的name
}
}
里式替换原则
概念
任何父类出现的地方,子类都可以替代
也就是 父类容器装子类对象,因为子类对象包含了父类的所有内容
方便了对象的存储和管理
实现
class GameObject{}
class Player:GameObject{}
class Monster:GameObject{}
class Program
{
static void Main(string[] Args)
{
GameObject player = new Player();//用父类容器装子类对象
GameObject monster = new Monster();//用父类容器装子类对象
GameObject[] object = new Gameobject[] {new Player(),new Monster()};
}
}
is和as
//is 一个对象是否指定类对象
//返回值 bool
//as 将一个对象转换为指定类对象
//返回值 指定类对象 失败返回Null
//语法
//类对象 is 类名 该语句块有bool返回值true或false
//类对象 as 类名 返回值对象或Null
if (player is Player)
{
Player p = player as Player;//p = player
Player p1 = monster as Player;//p1==null
}
继承中的构造函数
概念
当申明一个子类对象时,先执行父类的构造函数,再执行子类的构造函数
注意:父类无参构造很重要,子类可以通过base关键字代表父类调用父类构造
class Teacher
{
public Teacher()
{
Console.WriteLine("父类构造函数");
}
}
class TeachingTeacher : Teacher
{
public TeachingTeacher()
{
Console.WriteLine("子类构造函数");
}
}
class Program
{
static void Main(string[] Args)
{
TeachingTeacher t = new TeachingTeacher();
}
}
//输出
//父类构造函数
//子类构造函数
class Teacher
{
public Teacher(int i)
{
Console.WriteLine("父类构造函数");
}
}
class TeachingTeacher : Teacher //报错,因为父类的无参构造函数被有参构造函数顶掉了,详细查看类的构造函数
{
//父类无参构造很重要
}
通过base关键字调用指定父类构造
class Teacher
{
public Teacher(int i)
{
Console.WriteLine("父类构造函数");
}
}
class TeachingTeacher : Teacher
{
public TeachingTeacher(int i):base(i)
{
//不管是默认无参构造还是通过base进行有参构造
Console.WriteLine("一个参数构造函数");
//父类构造必须执行
}
public TeachingTeacher(int i ,string str)this(i)
{
//父类构造必须执行
Console.WriteLine("两个参数构造函数");
}
}
//输出
//父类构造函数
//一个参数构造函数
//两个参数构造函数
先执行父类构造函数,在执行子类构造函数
万物之父和装箱拆箱
万物之父object
object是所有类型的基类,他是一个引用类型的类
作用
可以利用里氏替换原则,用object容器装所有对象,可以用来表示不确定类型
Father f = new son;
if(f is son)
{
(f as son).speak();
}
//object之引用类型
object o = new son();
if(o is son)
{
(o as son).speak();
}
//object之值类型
object o2 = 1;//int类
object o3 = 1f;//flout类
//强转
int fl = (float)o2;
//object之string
object o4 = "123456";
string str1 = o4.ToString();
string str2 = o4 as string;
//object之数组
object o5 = new int[10];
int[] arr = o5 as int[];//或者int[] arr = (int[])o5;
装箱拆箱
用object存值类型(装箱)
再把object转为值类型(拆箱)
装箱
把值类型用引用类型存储
栈内存会迁移到堆内存中
拆箱
把引用类型的值类型取出来
堆内存会迁移到栈内存中
好处
不确定类型时可以方便参数的存储和传递
坏处
存在内存迁移 , 增加性能消耗
class Program
{
static void Main(string[] Args)
{
TestFunc(1,2,"nihao",new son());
}
static void TestFunc(params object[] array)
{
}
}
密封类
概念
使用sealed关键字,让类无法再被继承
sealed class father
{}
class son:father
{}//报错 无法被继承
在面向对象程序的设计中 , 密封类的主要作用就是不允许最底层子类被继承
可以保证程序的规范性 、 安全性
同样行为的不同表现
多态Vob(virtual 虚函数 override 重写 base 父类)
让继承同一父类的子类们,在执行相同方法时有不同表现
class GameObject
{
public virtual void Atk1()
{
Console.WriteLine("GameObjectAtk1");
}
public void Atk2()
{
Console.WriteLine("GameObjectAtk2");
}
}
class Player:GameObject
{
public override void Akt1()
{
base.Atk1;//可以用base来执行父类中的方法
Console.WriteLine("Player")
}
}
class Monster:GameObject
{
public new void Akt2()
{
Console.WriteLine("Monster")
}
}
class Program
{
static void Main(string[] Args)
{
GameObject p = new Player();
GameObject m = new Monster();
m.atk(); //执行了父类输出GameObjectAtk2 不是期望的输出
(m as Monster).atk; //执行了子类输出Monster
p.atk(); //使用virtual和override:输出GameObjectAtk1 Player 是期望的输出
}
}
让一个对象有唯一性为的特征
抽象类和抽象方法aob
抽象类
被抽象关键字abstract修饰的类,恰当的使用让基类更加安全
特点
1 . 不能被实例化的类
2 . 可以包含抽象方法
3 . 继承抽象类必须重写抽象方法
abstract class Thing
{
public string name;
}
class Water:Thing
{
}
class Program
{
static void Main(string[] Args)
{
Thing w = new Water();//可以里式替换原则
Thing t = new Thing();//错误,抽象类不能实例化
}
}
抽象方法
被抽象关键字abstract修饰的方法
特点
1 . 只能在抽象类中声明
2 . 没有方法体
3 . 不是私有的
4 . 继承后必须实现 用override重写
abstract class Fruit
{
public string name;
public abstract void Bad();
}
class Banana
{
public override void Bad()
{
int baddata;
}
}
虚方法和抽象方法的区别
abstract class Fruit { public string name; public abstract void Bad();//抽象方法只有方法体 且只能写在抽象类中 public virtual void Size() { //虚方法可以选择是否写逻辑 } } class Banana { public override void Bad()//必须实现抽象方法 { int badData; } //可以不实现虚方法 都可以用override重写 }
接口
关键字interface 接口是 行为的抽象规范 ,是一种自定义的类型
接口声明的规范
1 . 只包含方法 、 属性 、 索引器 、 事件
2 . 成员不能被实现
3 . 成员可以不用与访问修饰符 , 不能是私有的
4 . 接口不能继承类 , 但是可以继承另一个接口
接口的使用规范
1 . 类可以继承多个接口
2 . 类继承接口后 , 必须实现接口中所有成员
特点
1 . 它和类的申明类似
2 . 接口是用来继承的
3 . 接口不能被实例化 , 但是可以作为容器存储对象
接口的声明
类可以继承一个类和多个接口,继承了接口后必须实现其中的内容并且必须是 public 的
//interface 接口名
//{
//}
//接口命名规范 I+帕斯卡命名法
inrerface IFly
{
//方法
void Fly();//成员不能被实现 不能为私有 默认public
//成员属性
string Name
{
get;//成员不能被实现
set;//成员不能被实现
}
//索引
int this[int index]
{
get;//成员不能被实现
set;//成员不能被实现
}
//事件
event Action doSomething;//成员不能被实现
}
class Animal
{
}
class Person:Animal,IFly//一个类可以继承一个类和多个接口
{
//方法
public virtual void Fly() //可以加virtual关键字在子类继承重写
{
}
//成员属性
public string Name
{
get;
set;
}
//索引
public int this[int index]
{
get
{
return 0;
}
set
{
}
}
//事件
public event Action doSomething;
}
class Program
{
static void Main(string[] Args)
{
IFly f = new Person();//可以里式替换原则
IFly fly = new IFly();//错误,接口不能实例化
}
}
通过相同的行为,可以储存具有同样行为的类
比如 会飞的动物,会坐飞机的人,会飞的飞机
接口继承接口时,不需要实现,类继承接口需要实现所有内容
interface IWalk
{
void Walk();
}
interface IFly
{
void Fly();
}
interface IMove:IWalk,IFly
{
}
class Test()
{
public void Walk()
{
}
public void Fly()
{
}
}
class Program
{
static void Main(string[] Args)
{
IWalk t1 = new Test();//可以里式替换原则
IFly t2 = new Test();//可以里式替换原则
IMove t3 = new Test();//可以里式替换原则
}
}
当继承的两个接口存在同名方法时,显式实现接口 不能写访问修饰符
interface IAtk
{
void Atk()
}
interface ISuperAtk
{
void Atk()
}
class Player:IAtk,ISuperAtk
{
//用接口名.行为名来显式实现接口
void IAtk.Atk()
{}
void ISuperAtk.Atk()
{}
}
class Program
{
static void Main(string[] Args)
{
Player p = new Player;//需要转换成对应接口去使用
(p as IAtk).Atk();
(p as ISuperAtk).Atk();
}
}
继承类:是对象间的继承 , 包括特征行为等等
继承接口:是行为间的继承,继承接口的行为规范 ,按照规范去实现内容
密封方法
用密封关键字 sealed 修饰的重写函数
作用 : 让虚方法或者抽象方法之后不能再被重写
特点 : 和 override— 起出现
abstract class Animal
{
public string name;
public abstract void Eat();
public virtual Speak()
{
Console.WriteLine("哼哼");
}
}
class Person:Animal
{
public override void Eat()
{
Console.WriteLine("吧唧");
}
public sealed override void Speak() //密封了该重写的方法
{
}
}
class SuperPerson:Person
{
public override void Eat()
{
base.Eat();
}
public override void Speak() //报错该重写的方法密封了
{
base.Speak();
}
}
命名空间是用来组织和重用代码的
作用
就像是一个工具包 , 类就像是一件一件的工具 , 都是申明在命名空间中的
命名空间的使用
//namespace 命名空间名
//{
// 类名
//}
namespace MyNameSpace
{
class Test1
{}
}
namespace MyNameSpace
{
class Test2:Test1 //可以分成两块,两个文件来写,同属于一个命名空间
{}
}
在不同名命名空间中,需要使用using
去引用或者指明出处
using MyNameSpace;
using System;
namespace Test
{
class Program
{
public static void Main(string[] args)
{
Test1 t = new Test2();//使用using
MyNameSpace.Test1 t = new MyNameSpace.Test2();//不使用using的话必须指明出处
}
}
}
不同命名空间中允许有同名类
namespace MyNameSpace1
{
class Test1
{}
}
namespace MyNameSpace2
{
class Test1
{}
}
如果要使用不同命名空间的同名类,则必须指明出处
using System;
namespace Test
{
class Program
{
public static void Main(string[] args)
{
MyNameSpace1.Test1 t = new MyNameSpace2.Test1();//不同命名空间的同名类必须指明出处
}
}
}
命名空间可以包裹命名空间
namespace MyGame
{
namespace UI
{
class Image;
}
namespace Scene
{
class Image;
}
namespace Battle
{}
}
using System;
namespace Test
{
class Program
{
public static void Main(string[] args)
{
Image i = new Image();
}
}
}
命名空间中的类默认为internal
public 公共类
internal 该程序集
abstract 抽象类
sealed 密封类
partial 分部类
object中的静态方法
Equals
判断两个对象是否相等
最终的判断权 , 交给左侧对象的Equals 方法 ,
不管值类型引用类型都会按照左侧对象Equals 方法的规则来进行比较
namespace Test
{
class Test1
{}
class Program
{
public static void Main(string[] args)
{
Console.WriteLine(object.Equals(1,1));//True
Test1 t1 = new Test1();
Test1 t2 = new Test1();
Console.WriteLine(object.Equals(t1,t2));//False 地址不同
t2 = t1;
Console.WriteLine(object.Equals(t1,t2));//True
}
}
}
ReferenceEquals
比较两个对象是否是相同的引用,主要是用来比较引用类型的对象,值类型对象返回值始终是 false 。
namespace Test
{
class Test1
{}
class Program
{
public static void Main(string[] args)
{
Console.WriteLine(ReferenceEquals(1,1));//False object是基类不写也行
Test1 t1 = new Test1();
Test1 t2 = new Test1();
Console.WriteLine(ReferenceEquals(t1,t2));//False 地址不同
t2 = t1;
Console.WriteLine(ReferenceEquals(t1,t2));//True 地址相同
}
}
}
object中的成员方法
普通方法 GetType
该方法在反射相关知识点中是非常重要的方法 , 之后我们会具体的讲解这里返回的 T y p e 类型 。
该方法的主要作用就是获取对象运行时的类型 Type ,
通过 Type 结合反射相关知识点可以做很多关于对象的操作 。
namespace Test
{
class Test1
{}
class Program
{
public static void Main(string[] args)
{
Test1 t = new Test1();
Type type =t.GerType();
}
}
}
普通方法MemberwiseClone
该方法用于获取对象的浅拷贝对象 , 口语化的意思就是会返回一个新的对象 ,
但是新对象中的引用变量会和老对象中一致 。
namespace Test
{
class Test1
{
public int i =1;
public Test2 t= new Test2();
public Test1 Clone()
{
return MemberwiseClone() as Test1;//MemberwiseClone()是protect的,因此要用public包裹起来
}
}
class Test2
{
public int i = 2;
}
class Program
{
public static void Main(string[] args)
{
Test1 t1 = new Test1();
Test2 t2 = t1.Clone();
Console.WriteLine(t1.i); //1
Console.WriteLine(t1.t.i); //2
Console.WriteLine(t2.i); //1
Console.WriteLine(t2.t.i); //2
//改变Test1和Test2中i的值
t2.i = 20;
t2.t.i = 21;
Console.WriteLine(t1.i); //1
Console.WriteLine(t1.t.i); //21 新对象中的引用变量会和老对象中一致
Console.WriteLine(t2.i); //20
Console.WriteLine(t2.t.i); //21
}
}
}
object中的虚方法
虚方法 Equals
默认实现还是比较两者是否为同一个引用 , 即相当于 ReferenceEquals
但是微软在所有值类型的基类 system.valueType 中重写了该方法 , 用来比较值相等 。
我们也可以重写该方法 , 定义自己的比较相等的规则
单独写出String字符串比较常用
单独写出Stringbuilder比较常用
存储上:结构体和类最大的区别是在存储空间上的 , 因为结构体是值 , 类是引用 ,
因此他们的存储位置一个在栈上 , 一个在堆上 ,
使用上:结构体和类在使用上很类似 , 结构体甚至可以用面向对象的思想来形容一类对象 。
结构体具备着面向对象思想中封装的特性 , 但是它不具备继承和多态的特性 , 因此大大减少了它的使用频率 。
由于结构体不具备继承的特性 , 所以它不能够使用protect访问修饰符 。
tip:
类和结构有以下几个基本的不同点:
结构体是值类型,类是引用类型。
结构体存在栈上,类存在堆中,栈中保存的只是引用。
结构体成员不能使用protect访问修饰符,而类可以。
结构体成员变量声明无法赋予初值,而类可以。
结构不能声明无参的构造函数,而类可以。
结构体声明了有参构造函数后,默认的无参构造函数不会被顶掉。
结构体不能声明析构函数,而类可以。
结构不支持继承,而类可以。
结构体的构造函数中,必须为结构体所有字段赋值,类的构造函数无此限制。
结构体不能被静态static修饰(不存在静态结构体),而类可以。
结构体不能再自己内部声明和自己一样的结构体变量,而类可以。
结构体可以继承接口,因为接口是行为的抽象。
如何选择类和结构体
想要用继承和多态时 , 直接淘汰结构体 , 比如玩家 、 怪物等
对象时数据集合时 , 优先考虑结构体 , 比如位置 、 坐标等等
从值类型和引用类型赋值时的区别上去考虑 , 比如经常被赋值传递的对象 , 并且改变赋值对象 , 原对象不想跟着变化时 , 就用结构体 。 比如坐标 、 向量 、 旋转等等
相同点
都可以被继承
都不能直接实例化
都可以包含方法申明
子类必须实现未实现的方法
都遵循里氏替换原则
区别
抽象类中可以有构造函数 ; 接口中不能
抽象类只能被单一继承 ; 接口可以被继承多个
抽象类中可以有成员变量 ; 接口中不能
抽象类中可以申明成员方法 , 虚方法 , 抽象方法 , 静态方法 ; 接口中只能申明没有实现的抽象方法
抽象类方法可以使用访问修饰符 ; 接口中建议不写 , 默认 publ 让
如何选择
表示对象的用抽象类 , 表示行为拓展的用接口
不同对象拥有的共同行为 , 我们往往可以使用接口来实现
举个例子 :
动物是一类对象 , 我们自然会选择抽象类 ; 而飞翔是一个行为 , 我们自然会选择接口 。
学习过程中参考了以下内容,诚挚感谢知识的分享者!
微软 | Microft docs(C# 编程指南)
菜鸟教程 | C# 教程
唐老狮 | C#四部曲
Suzkfly | C#随机数