ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

Unity C# 爆破计划(七):类与对象

2021-02-20 19:32:51  阅读:173  来源: 互联网

标签:爆破 属性 get C# Unity Console 父类 public 构造函数


文章目录


七、类与对象

Covers:类、属性、静态类型、继承

我们正式进入面向对象知识的学习。这节的内容很多,以防你赶时间,先上语言对比:

C#C++
成员种类字段属性方法、索引、事件成员变量、成员函数
访问权限5 种(已经介绍过)3 种
成员默认权限privateprivate
类默认权限internal无此概念
多继承不支持简单多继承支持
继承权限固定为 public默认 private 继承
密封类使用 sealed 修饰词使用 final 关键字
默认构造、析构空实现空实现
this 的类型引用指针
调用静态方法CLASSNAME.Method()CLASSNAME::Method()
抽象类是什么特指 abstract class CLASSNAME泛指包含纯虚函数的类
接口类是什么特指 interface ICLASSNAME泛指一类特殊的抽象类
动态多态方式对象多态指针多态

启蒙

我尽量不废话。

  • 概念上,类是一种事物的蓝图,对象是具体的个体,对象是类的实例,类是对象的抽象,比如“狗”是一类生物,“汪汪”和“嘟嘟”都是狗的实例,一个类可以有无数实例;
  • 实现上,类和结构体都是对数据和函数的组装方式,类是引用类型,对象的“真身”保存在托管堆中,在栈上只有它的引用。

我们用类完成对现实世界各种事物的建模,让它们相互作用,产生复杂的程序行为,是为面向对象编程。现实世界的事物极为复杂,为了能够描述它们,程序世界的类也具有足够复杂的特性,如此一来面向对象就成为了构建复杂服务的有效方法论。

  • 组成类的各种零件,包括数据和函数,都是其 成员
  • 类中的成员变量称为 字段
  • 类中的成员函数称为 方法

类经过 实例化 产生对象,在 C# 中,一个对象的生命周期中有这些关键的时间点:

  1. 要完成实例化,需要先从系统获取一块足够容纳对象所有字段的内存资源,这叫 空间分配

  2. 获得了空间,接下来要为实例的所有字段填写内容(不然狗就没有名字没有性别没有年龄),这叫类的 初始化

  3. 要完成初始化,需要调用类的一种特殊方法,叫做 构造函数,一般来说它接受参数,称为 有参构造函数,把这些参数赋给字段,字段就获得了内容;类的字段允许有默认值,这些字段不必须填写内容;如果一个类的所有字段都有默认值,或者它有一个不接受任何参数的构造函数,这个类就有 默认构造函数,一个通过默认构造函数完成初始化的对象是平凡的、不被爱的,有时却是必须的;

    Mediocre, unloved, yet essential at one time or another.

  4. 完成了初始化的对象具有类的所有性质,其方法可以被调用,其字段可以被直接或间接地修改;

  5. 当对象离开其作用域时,就完成了它的使命,此时它的另一特殊方法被自动调用,叫做 析构函数,它不接受参数,负责在对象的生命周期结尾,进行一些善后处理;

    注意:理论上,对象应该在走出作用域时立即被析构,但事实上,因为 CLR 采用垃圾回收机制(GC)来释放内存资源,因此当 GC 认为需要释放内存时,析构函数才会被调用。C# 不保证对象一旦走出作用域就立即被析构,只能保证在程序退出前析构。

    你可以用 GC.Collect 方法令 GC 立即释放一次内存。

  6. 最后,对象占用的内存资源被 CLR 自动回收,对象的生命周期结束。

我们用一个例子来演示这些特性:

using System;

namespace Lifetime
{
    class Dog
    {
        // Fields:
        public string Name = "";
        public int Age = 0;

        // Default constructor:
        public Dog()
        {
            Console.WriteLine("A dog is born mediocre, unloved, all alone in a brutal world.\n");
        }

        // Constructor:
        public Dog(string name, int age)
        {
            Name = name;
            Age = age;
            Console.WriteLine("A loved dog, {0}, aged {1}, is born.\n", Name, Age);
        }

        // Destructor:
        ~Dog()
        {
            if (Name == "")
            {
                Console.WriteLine("That dog stepped clumsily away and disappeared, we haven't seen him since.\n");
            }
            else
            {
                Console.WriteLine("{0}, now {1} years old, closed his eyes peacefully, surrounded by children.\n",
                    Name, Age);
            }
        }

        // Methods:
        public void Bark()
        {
            Console.Beep(2200 - Age * 100, 250);
        }

        public void GrowOld()
        {
            ++Age;
            Console.WriteLine("{0} is {1} years old now.\n", Name == "" ? "That dog" : Name, Age);
            Bark();
        }
    }

    class Program
    {
        static void Lifetime()
        {
            // Let there be two dogs:
            Dog dog = new Dog();
            Dog barky = new Dog("Barky", 3);

            // Watch them grow old over 5 years:
            for (int i = 0; i < 5; ++i)
            {
                dog.GrowOld();
                barky.GrowOld();
            }
        }

        static void Main()
        {
            Lifetime();
            GC.Collect();
            Console.ReadKey();
        }
    }
}

注意其中的语法:

  • 构造函数:ACCESS CLASSNAME(PARAMS)
  • 析构函数:~CLASSNAME()

属性

属性与访问器

有一定经验的程序员都知道,给予用户的权限越大,程序需要考虑的安全问题就越多,对于网络服务而言更是如此。下面的代码直接将字段设为公有,允许用户直接操作,造成了问题:

using System;

namespace Learning
{
    class ElementFinder
    {
        public int[] Array;

        public ElementFinder(int[] array)
        {
            Array = array;
        }

        public int Find(int data)
        {
            for (int i = 0; i < Array.Length; ++i)
            {
                if (Array[i] == data)
                {
                    return i;
                }
            }

            return -1;
        }
    }

    class Program
    {
        static void Main()
        {
            int[] arr = {1, 2, 3, 4, 5};
            var finder = new ElementFinder(arr);
            finder.Array = null;
            Console.WriteLine(finder.Find(3));
        }
    }
}

运行上面的例子会抛出异常。

分析:类 ElementFinder 原本的功能是保存一个整型数组的引用,当用户调用 Find 方法时,就从保存的数组中寻找参数给定的数值,返回该值第一次出现的下标,若找不到则返回 -1;这里,ElementFinder 中保存的数组引用 Array 是公开可见的,用户(我们用 Main 中的代码模拟用户的调用行为)可以随意修改,而当用户将其修改为空引用 null 时,ElementFinder 并不知道这一点,它仍在 Find 方法中对 Array 进行解引用,因此出错。

通过上面的例子我们发现:字段暴露给用户是危险的,最好的办法是限制用户能够进行的操作。C# 为我们提供了 属性 来实现这一点。属性是类的一种具名成员,它不同于字段也不同于方法,本身不占空间,但对用户而言却表现得如同一个字段,可以像操作变量那样访问,当用户这样做时,我们可以定义属性如何响应。属性充当了与用户之间的界面(API)。

属性通过 访问器 与用户交互,访问器是两个小函数,名为 getset,分别定义了该属性出现在等号右侧和左侧时的行为。可以 只编写 get 访问器,这样属性就是只读的

我们用访问器来重写 ElementFinder:

class ElementFinder
{
    int[] _array;

    public int[] Reference
    {
        get { return _array; }
        set
        {
            if (value == null)
            {
                throw new NullReferenceException("ElementFinder.Reference: Array reference must not be null!");
            }
            else
            {
                _array = value;
            }
        }
    }

    public ElementFinder(int[] array)
    {
        if (array == null)
        {
            throw new NullReferenceException("ElementFinder.Constructor: Array reference must not be null!");
        }
    }

    public int Find(int data)
    {
        for (int i = 0; i < _array.Length; ++i)
        {
            if (_array[i] == data)
            {
                return i;
            }
        }

        return -1;
    }
}

我们将 Array 字段变为私有(默认权限),改名为 _array(这种私有变量小写加前缀下划线的命名方法是 C# 的一种常规代码规范),并定义属性 Reference,当它处于等号右侧(读取其值)时,直接返回 _array,而当处于等号左侧(赋值)时,将检查赋给的值是否为 null,若是,则产生一个异常,并给出“哪里出了问题”的描述。

我知道你有很多疑问:

  • 什么是 throw?—— 这个关键字用于直接产生一个异常终止程序的运行:throw new EXCEP(MESSAGE);,EXCEP 是 C# 的内置异常类之一,有很多种(写这句代码时你的 IDE 应该会弹出一个大列表来供你选择),其中 NullReferenceException 表示“空引用异常”;MESSAGE 是任意字符串,你可以随便写,会在这个异常抛出时显示给用户(我们会在后面更系统地学习异常);
  • 折腾了半天依然会抛出异常,重写后的代码有啥优点?—— 你不能奢望你的用户永远不遇到异常,但你可以通过一些异常描述信息帮助他们排错,更可以让 Bug 更早展现出来以便排除;如果这个代码是你自用的,你也可以帮助未来的你尽快找到 Bug 所在。
  • 我不管,不要让这东西抛出异常!—— 当然可以,不 throw 而改成你认为妥当的其他处理即可,比如只在屏幕上打印一条信息……这样真的好吗?

言归正传,属性的定义如同字段一样,需要权限、类型和名称,只是后面加上了一个花括号,内含两个分别由 getset 引导的代码块

  • 如果属性是只读的,只需编写 get 访问器即可;

  • set 访问器中,用户想要为属性赋予的值用关键字 value 表示,这是 C# 的规定。

  • 如果属性是只读的,且 get 访问器只有 1 行,那么可以这样简写:

    public TYPE Property => EXPR;
    

    它将返回表达式 EXPR 的值。

  • 当同时编写 set 和 get 访问器时,如果它们只有 1 行,也可以这样简写:

    ...
    {
        get => EXPR;
        set => FIELD = value;
    }
    

    其中 get 将返回 EXPR,set 将用户写在等号右侧的值赋给字段 FIELD。

自动属性

当属性的 get 和 set 访问器都是简单的(get 直接返回一个字段,set 直接将 value 赋给同一个字段),那么还可以使用 自动属性 来简化编写,而不需要写一个私有字段,再为其配备一个属性:

public TYPE Property { get; set; }

也可以只写 get,但 不能只写 set

我的建议是,仅用自动属性来创建只读字段 —— 如果你定义了一个公共的自动属性,它又有 get 又有 set,那和你直接定义一个公共字段有什么区别呢?

继承(基础知识)

父类与子类

继承是面向对象的重要概念,这个特性允许我们根据一个类来定义另一个类,有利于代码重用,减小工作量。被继承的类称为 父类基类,继承父类的类称为 子类派生类,我们也用 派生 这个词,它等价于继承,称“A 类派生出了 B 类”,或“B 类派生自 A 类”。

子类具有父类的所有成员(但未必有访问权限),同时也可以新增成员,父类对子类中新增的成员一无所知;子类也可以继续派生出其他类,形成 继承链,直到类被密封(显式定义禁止继承)。

举个栗子:生物分类学就是典型的继承链。所有“动物”从“生物”基类派生出来,“脊椎动物”继承自“动物”类,“哺乳动物”继承自“脊椎动物”类,“犬科动物”继承自“哺乳动物”类,“狗”继承自“犬科动物”类,“哈士奇犬”继承自“狗”类。可见,每派生出一个子类,就在父类基础上增加了更详细的定义,但我们一定可以说出“子类对象是(is-a)父类对象”,比如“哈士奇犬是狗”、“狗是犬科动物”、“犬科动物是哺乳动物”等。

不满足“is-a”关系的继承是不成立的,但有时我们不容易发现逻辑错误。比如,狗不能继承自“宠物”类,因为“狗是宠物”这句话不成立(并非所有狗都是宠物);当你执意这样继承时,就否认了两类的一部分差集(“野生的狗”)的存在,窄化了逻辑,降低了程序的扩展性和重用性。精辟的抽象需要严密的构思,谨慎继承、多做预想。

我们写一个例子来了解继承的语法,这次没有狗:

using System;

namespace Inheritance
{
    class Shape
    {
        protected string Type = "";
        public string ShapeType => Type;
    }

    class Rectangle : Shape
    {
        double _height;
        double _width;

        public Rectangle(double h, double w)
        {
            Type = "Rectangle";
            if (h <= 0.0)
            {
                throw new Exception("Rectangle.ctor: Height must be greater than 0");
            }

            if (w <= 0.0)
            {
                throw new Exception("Rectangle.ctor: Width must be greater than 0");
            }

            _height = h;
            _width = w;
        }

        public double Height
        {
            get => _height;
            set
            {
                if (value <= 0.0)
                {
                    Console.WriteLine("Height must be greater than 0, aborted");
                }
                else
                {
                    _height = value;
                }
            }
        }

        public double Width
        {
            get => _width;
            set
            {
                if (value <= 0.0)
                {
                    Console.WriteLine("Width must be greater than 0, aborted");
                }
                else
                {
                    _width = value;
                }
            }
        }

        public double Area => _height * _width;
        public double Perimeter => (_height + _width) * 2.0;
    }

    class Program
    {
        static void Main()
        {
            var rect = new Rectangle(3, 2.5);
            rect.Width = 1.414;
            rect.Height = 0.828;
            Console.WriteLine("{0}:", rect.ShapeType);
            Console.WriteLine("\tArea = {0}", rect.Area);
            Console.WriteLine("\tPerimeter = {0}", rect.Perimeter);
        }
    }
}

这个例子中我们定义了 Shape 基类,并派生出了一个子类 Rectangle,它的构造函数接受宽高数据(必须大于 0);对象初始化后,可以用其属性来获取其面积和周长,也可以通过基类的 ShapeType 方法取得其种类。

控制台输出:

Rectangle:
        Area = 1.170792
        Perimeter = 4.484

注意继承的语法:class CLASSNAME : BASE,用冒号来表示继承

父类的初始化

“先有鸡还是先有蛋”这个问题不好回答,但“先有父亲还是先有儿子”的答案显而易见。

在构造子类对象之前,必须先构造一个其父类的对象,这个过程是编译器自动帮我们完成的,但有时我们希望能够自己控制父类对象的构造,尤其是父类本身只有有参构造函数时。

上面的 Shape 例子中,我们为 Type 赋了默认值 ""(空字符串),因此 Shape 类有默认构造函数。在构造父类对象时,首先将 Type 赋予默认值,紧接着又在构造子类 Rectangle 对象时,重新为 Type 赋了值,这里产生了一个不必要的赋值步骤。我们改写父类的构造函数,使其只能有参构造:

class Shape
{
    protected string Type;

    protected Shape(string type)
    {
        Type = type;
    }

    public string ShapeType => Type;
}

注意:只要一个类具有有参构造函数,它就默认不具有默认(无参)构造函数,除非显式定义;上面的代码中即使给 Type 加上默认值,也无法再无参构造 Shape 对象。

接着改写 Rectangle 类的构造函数,使其调用父类的有参构造:

public Rectangle(double h, double w) : base("Rectangle")
{
    /* Type = "Rectangle"; */
    if (h <= 0.0)
    ...
}

注意调用父类构造函数的语法:CTOR : base(PARAMS),CTOR 是子类构造函数的签名(说白了就是上例中冒号前面的部分),PARAMS 是传递给父类有参构造函数的实际参数。

静态成员

静态的意思是“独立于类的所有实例”、“一成不变”,静态的方法和字段不需要依托于类的实例,甚至不需要有任何实例;非静态方法和字段依托于类的实例,没有实例就不能调用。

换言之,静态成员是整个类所共有的、全局唯一的

你已经看过多次 static 关键字。在定义方法时,如果加上 static 关键字,那么该方法就是静态方法;在定义字段时,加上 static 的字段就是静态字段。我们在调用类的某个方法时,有时点号前面是类名(Console.WriteLine),有时前面是实例名(myint.ToString),前者调用的是静态方法,后者调用的是非静态方法。

举个栗子:“狗”类如果有一个字段“有无尾巴”,那它显然应该是一个静态字段,因为所有狗都有尾巴;假如你修改这个字段为 false,那么在你的程序世界中所有狗就都没有尾巴。

再举个栗子:“狗”类如果定义一个方法“叫”,功能是在控制台打印一个“汪!”,那么这个方法就应该是静态方法,因为所有狗叫的效果都一样;如果你要让每个狗(每个实例)有不同的叫声(方法会产生与个体有关的行为),那么就不能设计为静态方法,同时你还要添加一个字段来保存每只狗怎么叫。

静态类

.NET 2.0 引入静态类的概念,静态类是一种特殊的类,不能被实例化,一般作为一些方法和“全局变量”的容器。我们熟悉的 Console 类就是一个静态类,你不能实例化 Console。

静态类有这些特点:

  1. 仅包含静态成员
  2. 不能实例化
  3. 是密封类(说白了就是不能被继承)

静态类虽然不能实例化,但可以拥有构造函数,静态构造函数前面要加上 static 修饰,它在第一次调用静态类的方法时被自动调用。


T.B.C.

标签:爆破,属性,get,C#,Unity,Console,父类,public,构造函数
来源: https://blog.csdn.net/lfod1997/article/details/113891543

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有