C#学习笔记9:类继承、类的函数重写、类方法的多态、子类构造方法

-呈阶梯状分布- 21 次阅读 2905 字 发布于 2026-08-12 最后更新于 2026-08-12 C#学习笔记


类继承

类如何继承自一个类

书籍分为小说、文学、历史、科普等等,他们是不同的类,却都属于书籍这一类别,同样来说,C#中的多个类也可以像上面一样归属与一个类。此时,书籍被称为父类(或基类),而书籍下的更多类型被称为父类(或派生类)。
那么我们应该怎么写出这样的逻辑呢?看下面代码:

using System;

namespace Csharp
{
    class Book  // 书籍类(父类)
    {
        public string name = "书籍名称";
        public int id = 31;  // 书籍编号
        public void Read()
        {
            Console.WriteLine("书籍被阅读");
        }
    }
    class Novel : Book  // 小说类(子类)
    {
        
    }
    class Program
    {
        static void Main(string[] args)
        {
            
        }
    }
}

可以看到,我们一般定义类的方式是class 类名{},但此处类Novel的定义方式却是class Novel : Book{},此时Novel就是类名,而Book就是Novel(当前类)所继承的父类。
此时,我们就说Novel继承了Book,或者Novel继承自Book,Novel就是Book的子类(派生类),Book就是Novel的父类(基类)。

类继承的作用

继承关系确定后,子类将获得父类的以下内容:成员变量,属性,成员方法,构造方法(这个在后面说)。你可以尝试输入以下代码:

using System;

namespace Csharp
{
    class Book  // 书籍类(父类)
    {
        public string name = "书籍名称";
        public int id = 31;  // 书籍编号
        public void Read()
        {
            Console.WriteLine("书籍被阅读");
        }
    }
    class Novel : Book  // 小说类(子类)
    {
        
    }
    class Program
    {
        static void Main(string[] args)
        {
            Novel novel = new Novel();
            novel.Read();
            Console.WriteLine(novel.name);
            Console.WriteLine(novel.id);
        }
    }
}

输出内容如下:

书籍被阅读
书籍名称
31

可以看出,Novel类确实是继承了Book的成员变量和成员方法,属性你也可以自己尝试一下,你一定会得到相同的结果。

但是,此处有一个关键点:我把成员变量和成员方法都设置为了public,这是为什么?

类继承的作用域:protected(保护的)

如果你的成员变量和成员方法都是private作用域下的,那么就只能在类内部访问。不仅仅是外部访问不到,子类希望继承时也是访问不到的,此时我们就可以设置public作用域来解决。

但是,我们有时既希望外部访问不到,又希望子类能继承到,该怎么办呢?我们就可以通过protect作用域解决。将成员变量和成员方法设置为该作用域后,只能在本类和本类的子类中访问到,而不允许被从外部访问。

类的函数重写

我们根据上面的protected修改成员变量的作用域后,又增加了一个成员方法:

using System;

namespace Csharp
{
    class Book  // 书籍类(父类)
    {
        protected string name = "书籍名称";
        protected int id = 31;  // 书籍编号
        public void Read()  // 阅读
        {
            Console.WriteLine("书籍被阅读");
        }
        public void PrintInfo()  // 输出书籍信息
        {
            Console.WriteLine("书籍:{0}(编号{1})", name, id);
        }
    }
    class Novel : Book  // 小说类(子类)
    {
        
    }
    class Program
    {
        static void Main(string[] args)
        {

        }
    }
}

此时,子类Novel可以随意使用父类的Read()和PrintInfo()两个方法。但是,我们希望对Novel使用Read()的时候,不再输出"书籍被阅读",而是输出"小说被阅读";对Novel使用PrintInfo()时,改为输出"小说:{0}(编号{1})"。此时应该怎么做呢?
我们可以使用类的函数重写,直接在子类中用子类的同名方法覆盖父类方法。接下来,我将介绍两种重写方式:虚函数(用Read演示)和隐藏(用PrintInfo演示)。

虚函数virtual-override

虚函数,就是可以被重写的函数。我们在Book的Read方法前打上virtual,用于标识这是可以被子类重写的虚函数:

using System;

namespace Csharp
{
    class Book  // 书籍类(父类)
    {
        protected string name = "书籍名称";
        protected int id = 31;  // 书籍编号
        public virtual void Read()  // 阅读
        {
            Console.WriteLine("书籍被阅读");
        }
        public void PrintInfo()  // 输出书籍信息
        {
            Console.WriteLine("书籍:{0}(编号{1})", name, id);
        }
    }
    class Novel : Book  // 小说类(子类)
    {
        
    }
    class Program
    {
        static void Main(string[] args)
        {

        }
    }
}

此时,我们就可以在Novel类下重新写上与Read同名的方法,以此重写父类的Read方法,在子类中重写虚函数时应当打上override(重写)标识。

using System;

namespace Csharp
{
    class Book  // 书籍类(父类)
    {
        protected string name = "书籍名称";
        protected int id = 31;  // 书籍编号
        public virtual void Read()  // 阅读
        {
            Console.WriteLine("书籍被阅读");
        }
        public void PrintInfo()  // 输出书籍信息
        {
            Console.WriteLine("书籍:{0}(编号{1})", name, id);
        }
    }
    class Novel : Book  // 小说类(子类)
    {
        public override void Read()
        {
            Console.WriteLine("小说被阅读");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Novel novel = new Novel();
            novel.Read();
        }
    }
}

输出结果如下:

小说被阅读

可以看到,我们确实在子类中重写了Read()方法。

隐藏父类方法new

我们还有一种方式,可以在父类无法写上virtual(如调用不可修改的其他人的代码时)时,也在子类中重写父类方法。只需要在子类的方法前打上new标识,就可以让父类的同名方法被隐藏,只能调用子类中重写后的方法。

using System;

namespace Csharp
{
    class Book  // 书籍类(父类)
    {
        protected string name = "书籍名称";
        protected int id = 31;  // 书籍编号
        public virtual void Read()  // 阅读
        {
            Console.WriteLine("书籍被阅读");
        }
        public void PrintInfo()  // 输出书籍信息
        {
            Console.WriteLine("书籍:{0}(编号{1})", name, id);
        }
    }
    class Novel : Book  // 小说类(子类)
    {
        public override void Read()
        {
            Console.WriteLine("小说被阅读");
        }
        public new void PrintInfo()
        {
            Console.WriteLine("小说:{0}(编号{1})", name, id);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Novel novel = new Novel();
            novel.PrintInfo();
        }
    }
}

输出结果如下:

小说:书籍名称(编号31)

可以看到,隐藏父类方法的方式也能帮我们解决重写的问题,那么,虚方法和隐藏父类方法这两种方式有什么区别呢?

类方法的多态

我们前面实例化Novel类型对象novel时,用的都是下面的代码:

Novel novel = new Novel();

你可以尝试用下面的代码实例化一个novel对象:

Book novel = new Novel();

此时,novel对象的类型为Book,但里面存储的却是Book的子类Novel,这实际上是被允许的,并且这种写法能够实现面向对象中一个非常重要的概念——多态。先不说多态是什么,我们先写下下面代码:

using System;

namespace Csharp
{
    class Book  // 书籍类(父类)
    {
        protected string name = "书籍名称";
        protected int id = 31;  // 书籍编号
        public virtual void Read()  // 阅读
        {
            Console.WriteLine("书籍被阅读");
        }
        public void PrintInfo()  // 输出书籍信息
        {
            Console.WriteLine("书籍:{0}(编号{1})", name, id);
        }
    }
    class Novel : Book  // 小说类(子类)
    {
        public override void Read()
        {
            Console.WriteLine("小说被阅读");
        }
        public new void PrintInfo()
        {
            Console.WriteLine("小说:{0}(编号{1})", name, id);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Book novel = new Novel();
            novel.Read();
            novel.PrintInfo();
        }
    }
}

输出结果如下:

小说被阅读
书籍:书籍名称(编号31)

可以看到,使用了虚方法virtual-override的Read()方法使用的是子类Novel的方法,但使用隐藏父类方法new的PrintInfo()方法使用的是父类Book的方法,从这里,我们就能看出这两种重写方式的区别。

在这里,我们清楚地知道novel对象的背后是一个Novel类型的值,但有的时候,我们并不清楚对象会被赋予什么值。例如,小说和漫画,同属于书籍,但不是同一类,我们就可以写下面的代码:

using System;

namespace Csharp
{
    class Book  // 书籍类(父类)
    {
        protected string name = "书籍名称";
        protected int id = 31;  // 书籍编号
        public virtual void Read()  // 阅读
        {
            Console.WriteLine("书籍被阅读");
        }
        public void PrintInfo()  // 输出书籍信息
        {
            Console.WriteLine("书籍:{0}(编号{1})", name, id);
        }
    }
    class Novel : Book  // 小说类(子类)
    {
        public override void Read()
        {
            Console.WriteLine("小说被阅读");
        }
        public new void PrintInfo()
        {
            Console.WriteLine("小说:{0}(编号{1})", name, id);
        }
    }
    class Comic : Book  // 漫画类(子类)
    {
        public override void Read()
        {
            Console.WriteLine("漫画被阅读");
        }
        public new void PrintInfo()
        {
            Console.WriteLine("漫画:{0}(编号{1})", name, id);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Console.Write("请输入小说/漫画:");
            string userInput = Console.ReadLine();

            Book theBook;

            if (userInput == "小说")
            {
                theBook = new Novel();
            }
            else if (userInput == "漫画")
            {
                theBook = new Comic();
            }
            else
            {
                theBook = new Book();
            }

            theBook.Read();
        }
    }
}

此时,如果Book类型的theBook被赋值为new Novel(),那么theBook.Read()就是子类Novel的Read()方法,被赋值为new Comic()时同理。
通过类型为父类但赋值为子类的定义方式,我们就能实现同一段代码在对象值不同时执行不同行为,这就是类方法的多态。
正因虚方法能实现多态,而隐藏父类方法不行,所以我们在大多数情况下会使用虚方法的方式来重写父类方法,而new只有在父类方法无法打上virtual时才会少量谨慎地使用。

子类构造方法

我们可以在父类没有构造方法的情况下,为子类创建构造方法,此时和一般构造方法没有任何区别:

using System;

namespace Csharp
{
    class Book  // 书籍类(父类)
    {
        protected string name = "书籍名称";
        protected int id = 31;  // 书籍编号
    }
    class Novel : Book  // 小说类(子类)
    {
        public Novel(string name, int id)
        {
            this.name = name;
            this.id = id;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            
        }
    }
}

但是,当父类有了构造方法,那么子类必须也有构造方法,并且要在其中包含父类构造方法的参数,用于构造父类。

using System;

namespace Csharp
{
    class Book  // 书籍类(父类)
    {
        protected string name = "书籍名称";
        protected int id = 31;  // 书籍编号
        public Book(string name, int id)
        {
            this.name = name;
            this.id = id;
        }
        public void PrintInfo()
        {
            Console.WriteLine("书籍名称:{0}(编号{1})", name, id);
        }
    }
    class Novel : Book  // 小说类(子类)
    {
        public Novel(string name, int id) : base(name, id)
        {
            
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Novel novel = new Novel("书名", 999);
            novel.PrintInfo();
        }
    }
}

可以看到,子类Novel的构造方法参数和父类构造方法一致,且后面加上了:base(name, id),这里的base指的就是父类构造方法,通过这种方式将子类构造方法中获得的参数传递给父类构造方法。
虽然子类构造方法必须包含父类构造方法的全部参数并传递,但子类构造方法也可以有自己的独有参数,也就是说,子类构造方法的参数必须比父类多或和父类相同,且必须包含父类构造方法全部参数(名称可以不一致,但必须按照相同的参数类型和数量传递),如下面这个例子:

using System;

namespace Csharp
{
    class Book  // 书籍类(父类)
    {
        protected string name = "书籍名称";
        protected int id = 31;  // 书籍编号
        public Book(string name, int id)
        {
            this.name = name;
            this.id = id;
        }
        public void PrintInfo()
        {
            Console.WriteLine("书籍名称:{0}(编号{1})", name, id);
        }
    }
    class Novel : Book  // 小说类(子类)
    {
        public string author;
        public Novel(string author, string name, int id) : base(name, id)
        {
            this.author = author;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Novel novel = new Novel("LadderStep", "书名", 999);
            novel.PrintInfo();
            Console.WriteLine(novel.author);
        }
    }
}

可以看到,Novel构造方法在包含Book构造方法全部参数的前提下,又新增加了一个参数author(作者),输出结果如下:

书籍名称:书名(编号999)
LadderStep

综上所述,你可以在父类没有构造方法时像是直接写构造方法一样写子类构造方法,但是如果父类有构造方法,子类构造方法必须包含父类构造方法的全部参数并传递给父类构造方法,且可以在此基础上增加其他参数。

OK~面向对象第二篇结束,面向对象会有些绕,还是要多看多写才能加深理解。
先写到这,下一篇讲抽象、密封、接口。