Zephus 发表于 2022-3-16 09:38

unityC#脚本入门(二)

初学笔记,有错误欢迎大佬指出

一、继承

什么是继承

unity支持的脚本语言有一个特征叫做继承,继承是面对对象编程的基础之一,当一个类继承自另一个类时,他会获得被继承类的特征,在继承的语境下,被继承类被称为父类或基类,而继承类被称为子类或派生类。继承结果是父类中存在的项,也就出现在子类中,因此方法和变量可以在子类中使用,就像在父类中一样,无需再次创建,因为已经存在于父类中。



处理继承时,需要注意三个访问修饰符,Public,Private和Protected,

[*]Public公开的父类的特征将存在于子类中并且可供访问,
[*]Private私有的特征将存在于子类中但不可访问,
[*]Protected就类似两者的混合,在父类或者子类中存在且可以访问,但是在类外则与私有特征一样。
目前为止我们使用的大多数类可能都是继承的,例如:




[*]作为组件应用于游戏的所有脚本都是MonoBehaviour,这代表他们继承于MonoBehaviour类,默认情况下在Unity中创建的脚本遵循这种格式 冒号后是被继承的类,我们平常使用的GameObject,transform,start方法和update等方法都是继承于MonoBehaviour类,所以我们可以访问这些特征。
继承分层


[*]继承结构是分层的,例如我们有一个父类名为Animal,它将包含所有必须的定义和属性,从它我们可以派生出两个子类vertebrate和invertebrate,而这两个子类又可以成为其他类的父类,那么从vertebrate派生的子类就可以拥有Animal提供的信息和vertebrate里额外增加的信息,正如这个示例,面对对象编程中的继承被成为IS-A关系(IS-A就是∈的意思)。
继承中的构造函数


[*]在子类继承的项中,构造函数是个例外,因为他们对类是唯一的不会共享,但是在子类中调用构造函数时,其父类的构造函数会立即被调用。
[*]由于类可能有多个不同的构造函数,因此我们可能想要控制调用哪个基类的构造函数,为此可以使用base,通过在子类构造函数的参数列表后加一个冒号,可以在基类构造函数的参数列表中显式调用基类的具体构造函数,如不显式调用则会隐式调用默认构造函数。
[*]除了调用基类的构造函数,base关键字还可以用来访问基类的其他成员,它非常适合访问基类版本的任何内容,因为它不同于派生的版本,覆盖函数时通常会有这样的需要。
//这是基类,
//也称为父类。
public class Fruit
{
    public string color;
    //这是 Fruit 类的第一个构造函数,
    //不会被任何派生类继承。
    public Fruit()
    {
      color = "orange";
      Debug.Log("1st Fruit Constructor Called");
    }

    //这是 Fruit 类的第二个构造函数,
    //不会被任何派生类继承。
    public Fruit(string newColor)
    {
      color = newColor;
      Debug.Log("2nd Fruit Constructor Called");
    }

    public void Chop()
    {
      Debug.Log("The " + color + " fruit has been chopped.");      
    }
    public void SayHello()
    {
      Debug.Log("Hello");
    }
}
//这是派生类,
//也称为子类。
public class Apple : Fruit
{
    //这是 Apple 类的第一个构造函数。
    //它立即调用父构造函数,甚至在它运行之前调用。
    public Apple()
    {
      //注意 Apple 如何访问公共变量 color,
      //该变量是父 Fruit 类的一部分。
      color = "red";
      Debug.Log("1st Apple Constructor Called");
    }
    //这是 Apple 类的第二个构造函数。
    //它使用“base”关键字指定
    //要调用哪个父构造函数。
    public Apple(string newColor) : base(newColor)
    {
      //请注意,该构造函数不会设置 color,
      //因为基类构造函数会设置作为参数传递的 color。
      Debug.Log("2nd Apple Constructor Called");
    }
}
public class test02 : MonoBehaviour
{
    void Start ()
    {
      //让我们用默认构造函数来说明继承。
      //为子类实例化对象时,系统会默认为父类实例化对象,
      //(默认调用的是空构造函数)调用父类的属性和方法
      //所以这里一共会调用2次基类的,1次派生类的空构造函数,
      Fruit myFruit = new Fruit();
      Apple myApple = new Apple();
      //调用 Fruit 类的方法。
      //这里在基函数中Color还是等于orange
      myFruit.SayHello();
      myFruit.Chop();
      //调用 Apple 类的方法。
      //注意 Apple 类如何访问Fruit 类的所有公共方法。
      //在派生函数中Color还是等于orange
      myApple.SayHello();
      myApple.Chop();

      //现在,让我们用读取字符串的
      //构造函数来说明继承。
      myFruit = new Fruit("yellow");
      myApple = new Apple("green");

      //调用 Fruit 类的方法。
      myFruit.SayHello();
      myFruit.Chop();

      //调用 Apple 类的方法。
      //注意 Apple 类如何访问Fruit 类的所有公共方法。
      myApple.SayHello();
      myApple.Chop();
    }
}二、多态(PolyMorphism)

个人理解:通过继承实现的不同对象调用相同的方法,表现出不同的行为,称之为多态。
继承层次结构的包含情况

在继承层次结构中,任何子类都可以成为父类,这表示在需要基类的时候,可以用派生类来替代它,现在假设一个游戏使用继承层次结构,其中的Orc和Goblin派生自Enemy,而Enemy又派生自Humanoid,如果我们想要创建一个集合令其包含场景中的所有Enemy对象,不必创建两个集合各自包含两个派生类,可以创建一个集合让它包含所有Enemy对象,那么Orc和Goblin都是此集合的元素。多态也适用于函数参数等,例如:



这个函数包含Collider参数,但是游戏对象没有Collider组件,但是它们可能有Box Collider,Sphere Collider,Mesh Collider或类似组件,当我们调用这个函数时,我们不知道会使用什么类型的Collider,事实上每个对象的特定Collider都会传入函数,因为这些所有不同的Collider都是继承于Collider父类,因此他们都会发挥作用。
转型

多态的另一种用法涉及构造函数和对象引用,我们可以声明基类类型的对象,然后调用一个派生类的构造函数,这是因为变量引用需要的是基类的类型,子类的构造函数会创建衍生类型的项,这个过程被称为向上转型,当对象向上转型时,他只能被视为其父类的一个对象,因此只能使用父类中可用的变量和方法,并且使用时会把他们视作位于父类对象中(虚拟函数是个例外,它将调用最新覆盖版本),为了再把这个子类视作子类,我们需要向下转型子类变量,使它恢复为子类型。
示例:

public class FruitSalad : MonoBehaviour
{
    void Start ()
    {
            //在这个例子中Apple是Fruit是的子类。
      //请注意,这里的变量“myFruit”的类型是Fruit
      //但是被分配了对 Apple 的引用。
      //这是由于多态而起作用的。由于 Apple 是 Fruit的子类,
      //因此这样是可行的。虽然 Apple 引用存储
      //在 Fruit 变量中,但只能像 Fruit 一样使用
      Fruit myFruit = new Apple();//向上转型
      myFruit.SayHello();

      //这称为向下转换。Fruit 类型的变量“myFruit”
      //实际上包含对 Apple 的引用。因此,
      //可以安全地将它转换回 Apple 变量。这使得
      //它可以像 Apple 一样使用,而在以前只能像 Fruit一样使用。
      Apple myApple = (Apple)myFruit; //向下转型
      myApple.SayHello();
    }
}
三、成员隐藏

通过继承,父类的成员在子类中自动可用或继承到子类中,在子类中重新声明父类成员的过程就是成员隐藏。
隐藏成员使用关键字new的方式略有不同,为了隐藏基类的成员,应该在成员的类型前面使用new声明子类成员,一般来说以这种方式声明的成员使用不会有影响,但是当子类向上转型为父类并且成员被使用的时候,它将被看作来自父类的成员,尽管实例为子类。



四、覆盖

覆盖是更改子类中的父类方法,覆盖之后我们在子类中调用方法时,将调用最新版本的方法。
使用继承层次结构时,我们经常想要于基类略微不同的函数版本,只需要在子类中重新创建方法就可以,这里需要用到两个关键字,virtual和override,他们位置位于方法的返回类型之前。

[*]virtual:父类中的方法需要定义为virtual,声明为virtual的方法可以被带有override的子类覆盖。
[*]override:子类中的需要定义为override。
覆盖的另一个用法

如果我们想要在子类中为方法添加功能同时又不失去基类中方法的原始功能,那么可以使用base关键字来同时调用方法的父版本。
需要注意的是:

[*]虽然翻译为覆盖但并不会更改掉基类中的方法,可以理解为只是复制了一份。
[*]如果我们需要在多层的继承中使用基类的原始功能,那么需要在每一层的覆盖中都添加base。
例如:
public class Fruit
{

    //这个方法是虚方法,因此可以在子类中将它覆盖
    public virtual void Chop ()
    {
      Debug.Log("The fruit has been chopped.");      
    }

}
public class Apple : Fruit
{
    //这个方法是覆盖方法,因此
    //可以覆盖父类中的任何虚方法。
    public override void Chop ()
    {
      base.Chop();
      Debug.Log("The apple has been chopped.");      
    }
}


public class FruitSalad : MonoBehaviour
{   
    void Start ()
    {
      Apple myApple = new Apple();

      //请注意,Apple 版本的方法
      //将覆盖 Fruit 版本。另外请注意,
      //由于 Apple 版本使用“base”关键字
      //来调用 Fruit 版本,因此两者都被调用。
      myApple.SayHello();

      //“覆盖”在多态情况下也很有用。
      //由于 Fruit 类的方法是“虚”的,
      //而 Apple 类的方法是“覆盖”的,因此
      //当我们将 Apple 向上转换为 Fruit 时,
      //将使用 Apple 版本的方法。
      Fruit myFruit = new Apple();
      myFruit.SayHello();
    }
}

五、接口

接口的格式


[*]接口可以看作关于功能的协定,实现接口的任何类,必须公开声明接口拥有的所有方法,事件,索引器和属性,否则就会报错。
[*]作为交换,通过多态其他类可以将实现类视作接口。需要注意接口不是类。不能有自己的实例。
[*]接口通常是在类外部声明,在声明接口时,通常每个接口使用一个脚本(不强求)。
eg:
//这是只有一个必需方法的基本接口。
//格式为 访问修饰符interface关键字   接口名
public interface IKillable
{
    void Kill();
}
//这是一个与泛型结合的接口,
//其中T是将由实现类提供的数据类型的占位符。
public interface IDamageable<T>
{
    void Damage(T damageTaken);
}接口的优势之一

是允许跨多个类定义通用功能,因此可以根据类实现的接口安全的对类的用途作出假设,要实现接口,只需要在类具有的任何继承只会加一个逗号,后跟接口名,如果没有继承关系,则不需要逗号,如果接口有泛型类型,则名称应后跟尖括号。并在里面输入类型。
eg:
public class Avatar : MonoBehaviour, IKillable, IDamageable<float>
{
    //IKillable 接口的必需方法
    public void Kill()
    {
      //执行一些有趣操作
    }
    //IDamageable 接口的必需方法
    public void Damage(float damageTaken)
    {
      //执行一些有趣操作
    }
}与继承相比

接口的优势就是可以同时实现多个接口,但是却不能同时继承多个基类,并且接口可以跨多个互不相关的类定义通用功能。
六、扩展方法


[*]通过拓展方法,可以向类型添加功能而不必创建Drive Type或更改原视类型,它非常适用于需要向类添加功能,但是又不能编辑类的情况。
[*]例如unity中提供的Transform类,我们不能访问源代码,假如我们想要使用函数轻松重置Transform的位置,旋转和缩放,那么这个函数最好放在Transform类中。但我们无法直接向这个类添加,并且把这个函数加入派生类也没有意义,所以我们为其创建扩展。
[*]扩展方法必须放在非泛型静态类中,常见做法是专门创建一个类来包含它们。扩展方法的用法与实例方法类似,它们也声明为静态方法。要使函数成为扩展方法而非静态方法,需要在参数中使用this关键字。
eg:
//创建一个包含所有扩展方法的类
//是很常见的做法。此类必须是静态类。
public static class ExtensionMethods
{
    //扩展方法即使像普通方法一样使用,也必须声明为静态。
    //请注意,第一个参数具有“this”关键字,后跟一个 Transform加上变量。
    //此变量表示扩展方法会成为那个类的一部分。
    //虽然这里的声明有参数,但是调用函数时不需要参数
    public static void ResetTransformation(this Transform trans)
    {
      trans.position = Vector3.zero;
      trans.localRotation = Quaternion.identity;
      trans.localScale = new Vector3(1, 1, 1);
    }
}
        //创建好之后,我们就可以把它视为所扩展的类的成员。
    void Start () {
      //请注意,即使方法声明中有一个参数
      //也不会将任何参数传递给此扩展方法。
      //调用此方法的Transform 对象会自动作为第一个参数传入。
      transform.ResetTransformation();
    }
页: [1]
查看完整版本: unityC#脚本入门(二)