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

如何选择抽象类和接口

  • 表示对象的用抽象类,表示行为拓展的用接口
  • 不同对象拥有的共同行为,我们往往可以使用接口来实现
  • 举个例子:动物是一类对象,我们自然会选择抽象类;而飞翔是一个行为,我们自然会选择接口。
posted @ 2021-09-29 09:30  tavee  阅读(129)  评论(0编辑  收藏  举报
****************************************** 页脚Html代码 ******************************************