C#:面向对象:其他类成员:构造函数/属性/索引器/析构函数

更多
2020年10月22日 08点15分 作者:叶飞 修改

讲了底层实现,我们继续讲语法。

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

构造函数(constructor)

构造函数是在类中,和类名相同的、像方法一样可以带参数,但没有返回的,用于创建类的实例的……飞哥,等等,你等等,你说的这玩意儿在哪里呢?我在Student类里看不到啊!

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

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



 internal class Student
    {
        //无参构造函数
        public /*没有返回*/ Student(/*可以有参数*/)   //和类同名
        {
            //像方法一样,也可以有内容
        }
    }

我们挨着讲,首先,想一想,为什么构造函数没有返回?如果方法没有返回值,还要给个void,构造函数是咋回事?其实构造函数是有返回值的,它必然返回当前类的一个实例(对象)!这是不可更改的铁律,所以,C#在语法设计的时候就干脆省略之,既节省了代码输入,有可以和方法进行区分,赞一个!^_^

接下来,既然可以有参数,有可以有内容,我们加上试试:

class Student
    {
        public Student(string name)  //有参构造函数
        {
            Console.WriteLine($"你好!{name}同学,源栈欢迎你……");
        }
    }

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


      Student ywq = new Student("于维谦");



演示:

同时你会发现再也不能使用new Student()调用无参的构造函数了。这是因为:一旦声明了任何构造函数,之前默认自带的无参构造函数消失。

如果你还想保留无参构造函数,需要在类中显式声明。即一个类中可以有多个参数不同的构造函数,如下所示:

    internal class Student
    {
        public Student() {}
        public Student(string name){}
        public Student(string name, int age){}
    }

调用的时候,按方法重载规则进行匹配。


演示:

给构造函数传参干嘛呢?它通常用于给字段赋值,就像这样:

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

这里要先说一下:同一个类中的所有同类(静态/实例)成员之间都可以互相访问,静态和实例之间访问后文会讲。

另外因为构造函数的参数name和字段name重名了,所以我们需要在字段name前面显式的加上一个this.,告诉编译器:这是当前对象的name。在类中使用实例成员,实际上都隐式的自带了一个this.,除非这种出现混淆的情景,一般都可以省略。

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

另外,还有一种很常用的做法,也可以避免这种混淆:统一给private字段添加一个下划线(_),这也是接下来我们教材将使用的命名规范。

this还可以用于构造函数的复制,我们先看代码:

public Student(string name)
{
    this.name = name;      //代码行 1
}

public Student(string name, int age)
{
    this.name = name;      //代码行 2
    this.age = age;
}


代码行1和代码行2有些重复,是不是?所以我们可以这样写:
public Student(string name)   // 构造函数 1
{
    this.name = name;
}

public Student(string name, int age)
    : this(name)   // 使用this()调用构造函数 1
{
    this.age = age;
}

这样,当运行Student(name, age)之前,会首先运行Student(name),这在构造函数内部的逻辑复杂,代码量大的时候尤其有用。

演示:

注意字段name的private修饰符,同学们有没有想过,它都private了,怎么给它赋值呢?哈哈,利用构造函数赋值就是一种常用的方式。

再想一想,为什么不直接在字段声明时直接赋值呢?


有时候我们会觉得访问修饰符还是有些粗糙,实际开发中,我们会有这样的需求:只是让字段不能被修改,但是可以被读取。这就需要通过这两个修饰符来实现了:

只读(readonly)和常量(const)

他们都是只能读不能改的:

internal readonly string at = "源栈";
internal const string BELONG = "源栈";   //建议const名称全大写

internal readonly string name;
//internal const string BELONG;    //报错

internal readonly Teacher fg = new Teacher();
//internal const Teacher xy = new Teacher();  //报错

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

但是,注意他们的语法区别:

  • const一旦声明,就要赋值;readonly可用延后到constructor
  • const只能是int/bool/string等基本类型;readonly可以是其他类型(如Teacher)
  • const默认static,由类名直接调用;readonly默认是实例成员,由对象调用
           Console.WriteLine(Student.BELONG);
            Console.WriteLine(new Student().name);

此外,const还可用于方法体内修饰变量,readonly不行。

语法背后,const是在编译时完成赋值,而readonly是在运行时赋值。

演示:

internal class Teacher
{
    public Teacher()
    {
        int[] arr = new int[5];
        arr[10] = 10;    //运行时错误
    }
}

//配合:
internal readonly Teacher fg = new Teacher();
//编译通过,运行时报错

如果我们需要对字段的读写进行更复杂的控制,比如:

  • 学生的age不能大于100,不能小于0
  • 学生的姓名自动加上(源栈)后缀

怎么办?这就需要:


属性(Property)

属性本质上是一种方法。比如,上面的需求首先可以通过方法实现:


private int _age;
internal void setAge(int age)
{
    //对传入的age值进行验证
    if (age < 0 || age > 100)
    {
        Console.WriteLine("给age的赋值超过了");
        return;
    }
    _age = age;        //最终还是利用字段存储
}
internal int getAge()
{
    return _age;        //返回字段_age的值
}

我们可以看到,最终存储数据的还是字段_age,但因为它是私有的,所以外部无法直接获取,只能通过setAge()和getAge()方法进行赋值和取值。而我们就在方法体中实现任何我们想要实现的逻辑。

所以,我们又说属性(这里还只有方法)是对字段的封装。

因为封装字段如此常用,以致于形成了一种惯例:类的字段都应该是私有的,必须进行封装。在其他面向对象的语言(比如Java)中,就是直接使用如上文所示的方法进行封装(但被专门称之为getter和setter)。但C#一开始就专门定义了属性,如下所示:

private int _age;      //仍然需要字段_age
public int Age         //通常和字段同名,但首字母大写
{
    get { return _age; }    //当需要获取属性值的时候,返回字段_age的值
    set { _age = value; }   //当给属性赋值的时候,将值存放到_age中
}
然后,就可以像使用字段一样使用属性(虽然属性本质上是方法):
Student zjq = new Student();
zjq.Age = 25;                   //赋值,运行属性中的set{}
Console.WriteLine(zjq.Age);     //取值,运行属性中的get{}

所以我们经常说,方法是对象的行为(动作),属性是对象的状态(数据)。

要在属性中实现上述age赋值时的过滤要求,只需要在Age属性的set{}中添加代码即可:

  set
            {
                if (value < 0 || value > 100)      //用value代表付给属性的值
                {
                    Console.WriteLine("给age的赋值超过了合理范围");
                    return;                        //结束赋值过程
                }
                _age = value;
            }


如果要在属性中为取值添加逻辑,只需要在get中添加代码:
private string _name;
public string Name
{
    get
    {
        return _name + "(源栈)";
    }
    set { _name = value; }
}

(请再一次仔细体会封装):封装就是屏蔽内部实现:在set中实现

当然,你也可以同时在set和get中添加逻辑。

你也可以

  • 只有get没有set,这样属性就只能读不能写;
  • 只有set没有get,这样属性就只能写不能读;

演示:

很多时候,我们其实不需要添加什么额外的逻辑。这种情况,C#为我们很贴心的设计了一个“语法糖(仅在语法层面上的细微改善)”:自动属性,如下所示:

        //注意不再需要_scroe字段
        public int Score { get; set; }


这样的写法,C#编译器会为我们自动的声明一个字段来存放属性的值。我们还可以:
public int Score { get; }       //让Score只读
//public int Score { set; }     //但不能让Score只写
//让Score外部可读,内部可写
public int Score { get; private set; }

区别:private set和完全没有set

但是,自动属性中,我们不能在set或get中添加使用任何逻辑。一旦添加逻辑,我们就像之前那样,配合字段使用属性。

如果get只有一行的话,我们还可以使用表达式体:

 private int _score;
 public int Score
{
  get => _score;           //等同于: get { return _score; }
}     
我们还可以在实例化类的同时,给属性赋值(语法上公开字段也可以,但字段不建议暴露):
Student zjq = new Student
{
    Name = "曾俊清",
    Age = 23,    //多个属性之间用逗号隔开
};

这种形式更进一步,就变成了

匿名类

即没有类名,也不需要声明,可以直接使用的类。比如:

var zjq = new  /*注意:没有类名了*/
{
    Name = "曾俊清",
    Age = 23,
};
Console.WriteLine(zjq.Name);

注意其语法特点:

  • 其变量类型只能用var,否则你也没法写
  • 所有属性,只能是只读的(readonly),就是说不能接下来再写个:zjq.Name="小曾";
  • 只是匿名而已,其实还是一个“类”,编译时会给名字(<AnonymousType>)
  • 拥有相同(名称+类型+次序)属性的匿名类会被当做同一个类
var wx = new
{
    Name = "王新",
    Age = 25,
};   //再new一个匿名类给变量wx
wx = zjq;  //wx和zjq之间可以互相赋值

和属性类似的,还有

索引器

索引器通常用于封装具有多个元素的数组,在集合(后期会学)中使用得较多。它类似于属性,都是一种往对象里存值取值的方式。但

其声明语法如下所示:

//也需要一个字段来实质上保存数据
private string[] _courses = { "SQL", "C#", "JavaScript" };
//和属性一样可以指定访问修饰符,需要指定返回类型
public string this[int index]
//不同:this关键字和[]运算符
//index还可以是其他类型,比如string
{
    //get和set的使用和属性几乎一样
    get { return _courses[index]; }
    //一样可以只读只写
    set { _courses[index] = value; }
}
  • 和属性相比,它没有名称,使用this
  • 而且多了一个[],里面指明索引器参数(有时又被成为“下标”)。参数必须要有类型和名字,可以有多个,多个参数见用逗号分开(和方法参数一样)

调用时:

Student wx = new Student();
Console.WriteLine(wx[1]);
  • 使用的运算符是中括号([])而不是点(.)
  • 必须有(至少一个)参数


最后的最后,我们来看一看对象的又一成员:

析构函数

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

        ~Student()
        {
            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不能为负数
  2. 调用这些类的有参/无参构造函数,生成这些类的对象,调用他们的方法
  3. 一起帮的求助可以有多个(最多10个)关键字,请为其设置索引器,以便于我们通过其整数下标进行读写。
  4. 设计一种方式,保证:
    1. 每一个Problem对象一定有Body赋值
    2. 每一个User对象一定有Name和Password赋值


源栈培训 C# 语法 基础 对象
赞: 0 踩: 0

打赏
已收到打赏的 帮帮币

你的 打赏 非常重要!
为了保证文章的质量,每一篇文章的发布,都已经消耗了作者 1 枚 帮帮币
没有“帮帮币”,作者无法发布新的文章。

全系列阅读
评论 / 0

后台开发


ADO和EF

如何通过C#进行数据库的读取,包含ADO.NET和Entity Framework相关知识……

其他:WebForm和WebApi

其他ASP.NET框架,如WebForm、WebApi……

RazorPages(Core)

微软推荐的、最新的、基于Razor页面和.NET core的新一代Web项目开发技术,包括Razor Tag Helper、Model绑定和Validation、Session/Cookie、内置依赖注入等……

MVC(Framework)

过去两年间最流行的、基于.NET Framework和MVC模式的ASP.NET MVC框架,主要用于讲解安全、性能、架构和各种实战功能演示……

C#语法

从入门的变量赋值、分支循环、到面向对象,以及更先进的语言特性,如:泛型、Lambda、Linq、异步方法等…………

Java语法

面向过程的变量赋值、分支循环和函数封装;面向对象的封装、继承和多态;以及更高阶的常用类库(集合/IO/多线程……)、lambda等

Java Web开发

SpringMVC

分层架构和综合实战

全部
关键字



帮助

反馈