键盘敲烂,月薪过万作业不做,等于没学
当前系列: 编程语言 修改讲义


声明在一个类中的所有“内容”,都被称之为类成员。除了已经学习过的字段和方法,还有:


构造函数

从语法的角度,当运行new Student()生成对象的时候,实际上是调用了Student类中的构造函数(constructor) .....

等等,这玩意儿在哪里呢?我在Student类里看不到啊!

Good question!带着脑子听课,才是正确的姿势。Good question!带着脑子听课,才是正确的姿势。

如果一个类没有显式的声明任何构造函数,默认就自带一个无参的无内容的构造函数。所以你看不到,但是飞哥可以把它写出来给你一眼:

class Student
{
    public Student()  //显式(explicit)的声明
    {
      Console.WriteLine("源栈欢迎你");
    }

可以看出,构造函数是在类中:

  1. 和类名相同的
  2. 像方法一样可以带参数
  3. 但没有返回的
  4. 用于创建类的实例的……

@想一想@:为什么构造函数没有返回?

如果方法没有返回值,还要给个void,构造函数是咋回事?

其实构造函数是有返回值的,它必然返回当前类的一个实例(对象)!这是不可更改的铁律,所以,在语法设计的时候就干脆省略之,既节省了代码输入,又可以和方法进行区分,赞一个!^_^

参数

构造函数默认无参,但也可以有参数。有可以有内容,我们加上试试:

public Student(string sname)  //有参构造函数
{
    Console.WriteLine("源栈欢迎你," + sname);
}

构造函数有参数了,调用的时候就得给参数:

Student atai = new Student("阿泰");

当前对象this

给构造函数传参干嘛呢?

假设我们类中有一个字段name:

private string name;

注意它的private修饰符,@想一想@:它都private了,怎么给它赋值呢?

利用构造函数赋值就是一种常用的方式。构造函数通常用于给字段赋值,就像这样:

public Student(string sname)
{
    name = sname; 
}

理解: name是当前对象的name

public void greet()
{
    Console.WriteLine("hello, my name is" + name);
}

更多的时候我们不会额外的修改构造函数的参数名,以避免和字段名重复:

public Student(string name)
{
    this.name = name; //添加this区分字段name和参数name
}

这里因为构造函数的参数name和字段name重名了,所以我们需要在字段name前面显式的加上一个this,告诉编译器:这是当前对象的name.

在类中使用实例成员,实际上都隐式的自带了一个this,除非这种出现混淆的情景,一般都可以省略。

注意: this是只读(readonly)的、不能赋值的。不要混淆this.name—和this

@想一想@:为什么不直接在字段声B时直接值呢?

ES6中的构造函数:(详见

class Student{
    constructor(name){
        this.name = name;
    }
}

let atai = new Student('atai');


属性(Property)

@想一想@:之前private的字段,比如age,如何在外部获取呢?

可以通过方法实现:

public int getAge()
{
    return _age;        //返回字段_age的值
}

甚至我们还可以通过方法给private的字段赋值:

public void setAge(int age)
{
    this.age = age;        //最终还是利用字段存储
}

这种专门封装字段的方法被称之为属性(或者getter和setter)。

因为封装字段如此常用,以致于形成了一种惯例:类的字段都应该是私有的,必须进行封装。

甚至我们会把属性当成是一种数据(本质上是一种方法),说:

  • 方法是对象的行为/动作
  • 属性是对象的状态/数据

单纯的封装看起来确实有些多余(所以C#等语言引入了一些更简洁的“语法糖”),但还可以在方法体中实现任何我们想要实现的逻辑。比如,对字段的读写进行更复杂的控制:

  • 学生的age不能大于100,不能小于0
    public void setAge(int age)
    {
        //对传入的age值进行验证
        if (age < 0 || age > 100)
        {
            Console.WriteLine("给age的赋值超过了");
            return;
        }
        this.age = age;        //最终还是利用字段存储
    }
  • 拿到的学生的姓名自动加上后缀:(源栈)
    public string getName()
    {
        return name + "(源栈)";
    }

属性是对字段的封装,请再一次仔细体会封装

  • 外部无法直接访问
  • 屏蔽内部实现细节

你也可以

  • 只有getter没有setter,这样属性就只能读不能写;
  • 只有setter没有getter,这样属性就只能写不能读;


析构函数

和构造函数在对象的创建时被调用相反,析构函数在对象被销毁时调用:

        ~Student()   //Java中是finalize()
        {
            Console.WriteLine("对象被垃圾回收");
        }

和默认的无参构造函数一样,析构函数也默认自带,不需要显式声明。

同时,在99.9999999%的情况下,我们都不需要声明它或者在它的内部添加任何逻辑,此处仅为演示使用。

垃圾回收

析构函数在.NET运行时在垃圾回收(garbage collection)时自动调用。

所谓垃圾回收,更易懂的说法是:不再使用的内存回收。所谓回收,就是清除内存上存放的数据,以便再次被使用。

通过前面的学习我们已经知道:变量是有作用域的,变量会在离开作用域(出栈)时被清空,其所占用的内存空间自然就被回收;但是变量所指向的(堆里的)对象呢?

@想一想@:能否在清空一个变量的时候,就销毁它引用/指向的对象?

如果没有自动的垃圾回收机制,就需要开发人员手工的书写代码清除——这无疑是一个非常无聊而且容易被遗忘的事情!一旦遗忘,就很容易造成程序只是不断的新建对象占用内存而没有释放,这就是曾经让开发人员非常头痛的、大名鼎鼎的内存泄漏(memory leak)

所以,C#、Java、甚至Javascript这些现代编程语言,都提供了自动的垃圾回收功能。简单的说,以C#为例,.NET运行时会在程序运行时自动的检查堆中的对象,是否还在被使用(甚至判断它以后是否还有可能被使用),对已经确定没有被使用,也不太可能再被使用的对象就自动的清除掉。

垃圾清理的算法和运行过程,都是自动的,强烈建议人为干预。

强制垃圾回收

但为了演示,我们使用了GC强制进行垃圾回收

static void gcShow()  //就为了新建一个Student对象
{  new Student("曾俊清");
} 

Main()函数中调用:

gcShow();         //方法调用完成,方法中的对象不会再被引用
GC.Collect();     //强制垃圾回收,否则因为可用内存足够大,不一定会被回收


作业

  1. 之前User/Problem/HelpMoney类的字段封装成属性,其中:
    1. user.Password在类的外部只能改不能读
    2. 如果user.Name为“admin”,输入时修改为“系统管理员”
    3. problem.Reward不能为负数


学习笔记
源栈学历
键盘敲烂,月薪过万作业不做,等于没学

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 编程语言 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码