跳过正文
  1. 文章/

C# 学习加固笔记(四):接口、属性与引用

Lickba
作者
Lickba
热爱游戏,也喜欢记录创作过程与踩过的坑。
C# 学习加固 - 这篇文章属于一个选集。
§ 4: 本文

这是「C# 学习加固」系列的第 4 篇,共 5 篇;可通过文末的系列导航继续阅读。

31. 万物基类Object
#

如果class不设置基类,那么他的基类就是object,同时object下有几个虚函数可以重载,这里重点先关注ToString()

因为万物基类都是object,所以任何类型的对象都可以隐式转换为object

ToString()
#

我们用Debug.Log函数时是可以不填写字符串的,那么函数其实会自动调用对象的ToString函数

32. 命名空间
#

分文件编写
#

我们的类、结构体、枚举等可以放一个文件里面写,也可以分开,理论上没有强制要求,但是要考虑有后续的维护。大多情况下一个类型一个文件

命名空间
#

我们会对事物的数据和行为进行封装,这个过程产生类,但是有没有一种可能,我们将一系列目标较为相同的类也打包在一起呢?

这就是命名空间,比如我们去下载的一些插件,一般也是有专门的命名空间

是许多编程语言使用的一种代码组织的形式,通过命名空间来分类,区别不同的代码功能,避免不同的代码片段(通常由不同的人协同工作或调用已有的代码片段)同时使用时由于不同代码间变量名相同而造成冲突

namespace Animal // 包裹的内容归属这个命名空间
{
    public abstract class AnimalObject
    {
        public string name;
        public override string ToString()
        {
            return $"我是动物:{name}";
        }
    }

}
namespace Animal // 包裹的内容归属这个命名空间
{
    public class Dog : AnimalObject
    {

    }
}
using Animal; // 在这个文件引入命名空间
using UnityEngine;

public class Hello : MonoBehaviour
{
    void Start()
    {
        AnimalObject animal = new Dog();
    }

}

33. 接口
#

大家来想象一种需求

  1. 人、鹦鹉、机器人三种类型,他们的共同特点是都会说话
  2. 分类上,我们不可能将他们分为一类,因为除了会说话,没有任何其他的共同特点
  3. 我们有一个公用函数,让会说话的对象进行说话

上面这种需求其实不相关的类型但是有共同特点,应该如何解决?

接口
#

C#中的继承是单继承,也就是一个class只能继承一个基类,但是无论是否有继承某个class,都可以进行多个接口的继承

接口:抽象0个或多个函数,定义的函数一定是抽象函数,一定需要派生类进行实现

一个对象,首先是它自身的类型,其次可以是它的基类类型,也可以是它的接口类型,但是转换类型后只能使用这个类型的成员

using UnityEngine;

public class Hello : MonoBehaviour
{
    void Start()
    {
        Robot robot = new Robot();
        Person person = new Person();
        Parrot parrot = new Parrot();
        Talk(robot);    // 隐式转换
        Talk(person);   // 隐式转换
        Talk(parrot);   // 隐式转换

        ITalk talker = robot; // 隐式转换
        Robot robot2 = (Robot)talker; // 显式转换,
    }
    // 让某个对象进行说话
    public void Talk(ITalk talker)
    {
        talker.Talk();
    }

}
public interface ITalk
{
   void Talk(); // 实际上是抽象函数,不需要定义访问修饰符,因为没意义,因为如果不公开就不可能被访问到了
}
using UnityEngine;

public class Robot : ITalk
{
    public void Talk() // 实现接口不需要写override,因为接口方法本身就是抽象的,不需要再声明为抽象
    {
        Debug.Log("机器人说话了!");
    }
}

34. 属性
#

属性就是将成员变量和成员函数结合

using UnityEngine;

public class Hello : MonoBehaviour
{
    void Start()
    {
        Hero hero = new Hero();
        hero.HP = 100;
        hero.MP = 50;
        hero.HP -= 1;
        hero.Hurt();
        hero.HP -= 12;

        hero.MP -= 35;      // 修改属性意味着 访问 属性的 set 方法
        Debug.Log(hero.MP); // 获取属性意味着 访问 属性的 get 方法
        hero.MP -= 35;
        Debug.Log(hero.MP);

        hero.FlySpeed = 100; // 访问 FlySpeed 的 set 方法
        Debug.Log(hero.FlySpeed); // 访问 FlySpeed 的 get 方法
    }


}

public class Hero : Unit
{
    public int HP;
    private int mp;

    // 成员属性
    // 属性在外部和变量是类似的
    // 属性相当于提供了两个函数,分别是 get 和 set,但是也可以只保留其中一个
    // 属性的类型决定了get 和 set 的返回类型
    public int MP
    {
        get 
        {
            Debug.Log("有人在获取MP,我怀疑他要检查能不能放技能");
            return mp; 
        }
        set 
        {
            Debug.Log("有人在设置MP,我怀疑他要放技能");
            if(value < 0)
            {
                Debug.LogError("MP不能小于0");
                mp = 0;
                return; // 直接返回,避免后续代码执行
            }
            if (value < mp)
            {
                Debug.Log("MP减少了!MP:"+value);
            }
            else if(value > mp)
            {
                Debug.Log("MP增加了!MP:"+value);
            }
            else
            {
                Debug.Log("MP没有变化!");
            }

            mp = value; 
        }
    }

    public void Hurt() 
    {
        Debug.Log("我受伤了!" +HP);
    }

    private int lv;

    public override int LV
    {
        get
        {
            return lv + 1;
        }

    }

    private int moveSpeed;
    public override int MoveSpeed 
    {
        get => 23;
        set => moveSpeed = value; 
    }
}

public abstract class Unit
{
    public virtual int LV { get { return 0; }}
    public abstract int MoveSpeed { get; set; }

    // 简写,相当于提供了默认的成员变量
    // 并且自动在get的时返回、set的时赋值,但是无法直接访问这个成员变量,相当于只能通过属性访问
    public int FlySpeed
    {
        get;            
        set;
        // private set;    // 保护起来,给get或set单独设置访问修饰符
    }
}

35. 引用与Null
#

引用
#

现实中,我们通过人的身份证号来“唯一的”确认一个人的身份,但是我们同样也会用姓名、昵称、代号等方式来表明某个人。

所以所谓引用就是一个别称

大家可以思考一个问题,C#中可以通过函数传递数据,那么到底传递的是什么?这里我们先只考虑class的情况(说明其他类型有所不同,我们之后会探讨其他类型)。这里其实就相当于起了一个别名,我们通过别名获取以及操作的依然是原先的那个对象。

即使不通过函数,C#中我们也可以主动给对象起别名,无论是创建对象时的名称还是另起的别名,这些都是引用

Null空引用
#

null意味着空对象,也就是没有数据的对象,任何基于class定义的类型都可以用“object = null”的方式来表明这个对象为“空”、“无效”

只定义对象,但是没有new或者其他引用方式,默认也是null

using UnityEngine;

public class Hello : MonoBehaviour
{
    void Start()
    {
        Hero hero = new Hero("张三");
        TestFunction(hero);

        Debug.Log(hero.name); // Start中的hero其实就是TestFunction中的unit

        Hero hero3 = hero;
        Debug.Log(hero3);

        // 如果修改hero3,等于修改hero,因为他们指向同一个对象
        hero3.name += "_修改一下";
        Debug.Log(hero.name); // 输出: 张三_TestFunction_修改一下
        Debug.Log(hero3.name); // 输出: 张三_TestFunction_修改一下
        hero = null; //让hero无效
        Debug.Log(hero3.name);  // 如果一个引用 = null,其他引用仍然有效
        Debug.Log(hero.name);   // 如果对象是null,是不能调用任何成员的,否则会报错,要避免

        Hero hero4; // 只有变量,没有实际引用的默认就是null
        // Debug.Log(hero4.name); // 这会报错,因为hero4没有被初始化

        // 目前的测试只对class类型有效,int、float等值类型不会影响原对象
    }

    void TestFunction(Hero unit)
    {
        unit.name += "_TestFunction";
        unit = null; // 这只是让unit指向null,不影响原对象
    }

}

public class Hero
{
    public string name;

    public Hero(string name)
    {
        this.name = name;
    }
}

36. 引用的比较
#

class类型的相等(==)或不等(!=)对比,实际上就是比较引用是否相同(引用所指向的对象是否相同)

同时对象对比时null也可以参加,因为对象可能为null,也可以和null做对比

object.RefrenceEquals
#

我们也可以用object.RefrenceEquals 这个函数来比较两个引用指向的对象是否相等,我们也可以采用“==”的方式来比较引用

C#中允许重载运算符,如果一定要看它们的引用是否相等,要用object.ReferenceEquals

重写Equals函数对比
#

object中有个虚函数,Equals(),可以用来自定义对比结果,注意此时并不是对比引用是否相同,而是“我们认为是否相同”

只是逻辑相等,和引用无关

using UnityEngine;

public class Hello : MonoBehaviour
{
    void Start()
    {
        int num1 = 3;
        int num2 = 3;
        Debug.Log(num1 == num2); // True,值类型只比较数值是否相等

        Hero hero1 = new Hero("张三");
        Hero hero2 = new Hero("张三");
        // 引用的比较
        // 
        Debug.Log(hero1 == hero2); // False,虽然他们的成员相等,但是不是一个对象
        if (hero1 != null) // 如果英雄存在(是否不为null),则可以XXX
        {
            
        }

        // object.ReferenceEquals方法比较两个对象是否是同一个对象
        Debug.Log(object.ReferenceEquals(hero1, hero2)); // False
        Debug.Log(object.ReferenceEquals(null, null));   // True
        Debug.Log(object.ReferenceEquals(hero1, null));  // False,hero1不是null

        // 类.Equals()实际上就是objectade函数,静态函数(后续学习)
        Debug.Log(Hero.Equals(hero1, hero2));
        Debug.Log(Hero.Equals(null, null));
        Debug.Log(Hero.Equals(hero1, null));

        // 对象.Equals()
        Hero hero3 = hero1;
        Hero hero4 = new Hero("李四");
        Hero hero5 = new Hero("张三"); // 不同对象但是逻辑上同名
        Debug.Log(hero1.Equals(hero3));
        Debug.Log(hero1.Equals(hero4));
        Debug.Log(hero1.Equals(hero5)); // True
    }
}

public class Hero
{
    public string name;

    public Hero(string name)
    {
        this.name = name;
    }

    // 我们就强行要求,同名且同类型就是相等的(当然它们实际上还是两个对象,但是我们希望得到相等的结果)
    public override bool Equals(object obj)
    {
        Hero temp = obj as Hero;
        // 如果这个obj不是Hero类型,会转换失败,temp将为null
        // 既然类型都不相同,当然不相等
        if (temp == null)
        {
            return false;
        }
        else //else说明转换成功了,obj是 Hero 对象
        {
            return this.name == temp.name; // 如果名字相同,则认为是相等的
        }
    }
}

37. 值类型与引用类型
#

值类型
#

int a = 3;

int b = a;

在b=a时,会重新开辟内存空间并完整的复制一次a的数据,相当于两者无关,只是在复制的时两者数据相等而已

主要的值类型:枚举、结构体、int、float、bool等(string不是值类型,它是特殊的引用类型)

比较时(相等/不等)一般是判断两者的值是否相同

结论:值类型在赋值时都是拷贝值,和原先的变量无关、

引用类型
#

Dog dahuang = new Dog();

Dog dog = dahuang;

在dog = dahuang时,dog会引用dahuang的内存地址,相当于两者是一个对象,只是名称不一样

主要的引用类型:class、interface

比较时(相等/不等)一般是判断两者是否引用同一个对象

结论:引用类型在赋值时都是拷贝引用,和原先的变量是同一个对象

为什么需要这种区别?
#

值类型都是轻量级数据,用完就放弃(销毁、回收),在C#中一般过了的作用域就回收掉。但是会有传递的需求,因为是轻量级数据,复制成本也不高,所以采用这种方案。

想对的,引用类型一般是重量级数据,那么它的复制成本就高。不适合采用值类型的方案。但是这也不意味着引用类型就不在销毁,当一个对象再也没有人引用时会被标记为“无用”状态等待系统自动的回收,但是这个回收是有成本

如果一个class中有一个成员变量是int、float这类值类型,会怎么样?此时值类型的作用域就在这个类产生的某个对象内,所以这个对象没有被回收(销毁)也就不会回收,但是即使是class中的值类型,传递时依然采用的是完全拷贝的形式

class中的值类型:修改对象内的成员都要用对象名.成员名称去做到

using UnityEngine;

public class Hello : MonoBehaviour
{
    void Start()
    {
        int a = 3;
        int b = a;

        b = 4; // 此时a依然=3
        Test(b); // Test(int num = 4) 函数调用的过程也是复制

        // hero1和hero2其实是一个对象的引用
        Hero hero1 = new Hero("张三",4);
        Hero hero2 = hero1;

        hero1 = null;
        hero2 = null; // 此时张三这个英雄相当于无名,无名就意味着无用,等待销毁

        TestHero(hero1);
        SetHeroLv(hero1.friendHero);
        Test(hero1.lv); // 相当于Test函数中的num= hero1.lv,但是两者依然无关联
        // 引用类型的变量在除了它的有限范围(作用域)时候会失效,但是不意味着他引用的对象也失效
    }

    void SetHeroLv(Hero hero)
    {
        hero.lv = 10086;
        // class中的值类型:修改对象内的成员都要用对象名.成员名称去做到
        // 引用类型,因为指向引用的对象,所以修改对象内的成员变量,直接修改即可
    }
    void Test(int num)
    {
        int num2 = -1;
        for (int i = 0; i < 100; i++)
        {
            num2 = i;
            if (i==88)
            {
                SaveNum(i); // 复制给SaveNum函数中的num
            }
        }
    }

    private int num;
    void SaveNum(int num)
    {
        this.num = num; // this关键字指向当前类的成员变量
    }
    void TestHero(Hero hero)
    {
        Debug.Log(hero.name);
    }
}
public class Hero
{
    public string name;
    public int lv;
    public Hero friendHero;

    public Hero(string name, int lv)
    {
        this.name = name;
        this.lv = lv;
    }
}
C# 学习加固 - 这篇文章属于一个选集。
§ 4: 本文