【C# 学习笔记 ③】C#面向对象编程(基础概念、静态成员、继承、多态 => 虚方法、接口、抽象类及它们的区别)

由于在自己的工作和学习过程中,只查看某个大佬的教程或文章无法满足自己的学习需求和解决遇到的问题,所以自己在追赶大佬们步伐的基础上,又自己总结、整理、汇总了一些资料,方便自己理解和后续回顾,同时也希望给大家带来帮助,所以才写下该篇文章。在本文中,所有参考或引用大佬们文章内容的位置,都附上了原文章链接,您可以直接前往查阅观看。在原文章内容的基础上,若无任何补充内容,同时避免直接大段摘抄大佬们的文章,该情况下也只附上了原文章链接供大家学习。本文旨在总结归纳,并希望给大家提供帮助,未用作任何商用用途。文章内容如有错误之处,望各位大佬指出。如果涉及侵权行为,将会第一时间对文章进行删除。


个人博客主页
一个努力学习的程序猿


本文所在专栏: C# 专栏,欢迎大家前往查看更多内容


专栏内目前所有文章:
不定期持续更新中


C# 学习笔记 ③

  • 面向对象和类相关概念表述
  • 继承
  • 多态
    • virtual 虚方法
    • interface 接口
    • abstract 抽象类
    • 接口、抽象类、虚方法对比
  • static 静态成员


面向对象和类相关概念表述

对于 面向对象编程 这个词汇大家肯定不陌生。有其他语言开发经验的话,肯定也会有自己的理解。不理解也没关系,只要能熟练使用下列用法就可以。

那最简单来说,面向对象编程就是我们在写代码的时候,使用对象去编程。而对象在第一篇文章已经有过使用和简述,比如类的实例化,这时就会生成一个对象。这时我们就可以使用类的实例化对象,去对某个事物进行归纳总结,也就是使用它的属性和方法做一些操作和记录。就比如在第一篇文章的类,它就是对 Pet(宠物)的一些描述,回顾一下:

class Pet
{
    public string name = "狗";
    public int age = 1;
    public Pet() {}
	public Pet(string name) 
	{
        this.name = name;
    }
}

在第一篇文章说明引用类型的时候提到过:对象(Object)类型是 C# 通用类型系统中所有数据类型的终极基类,也就是对象类型可以被分配任何其他类型(值类型、引用类型、预定义类型或用户自定义类型)的值。所以把上文对类的说明延申到所有其他类型上,也就有了最终的 “面向对象编程” 的概念,也就是最熟知的:万物皆对象

接下来要说的就是和类相关的内容。


首先对于实例化对象和类之间的关系,在查阅资料过程中找到了一个很形象的图,在这里分享给大家:

【图片转自C#(面向对象)】

【C# 学习笔记 ③】C#面向对象编程(基础概念、静态成员、继承、多态 => 虚方法、接口、抽象类及它们的区别)_第1张图片

这个过程就是:首先肯定有一个现实世界的实体,比如 Pet 宠物。而宠物肯定会有 name 名称、age 年龄等用于描述这个宠物的字段,它也肯定会有 run 跑、walk 走、eat 吃等行为把这些所有的属性(描述字段)和方法(实体行为)放到代码里,就抽象成了所有的这些数据类型,也就放进了概念世界。最后通过逻辑实现就成了使用的类,这时候将它实例化,在代码里就有了一个拥有和现实世界宠物一样信息的对象,也就是现实实体的物理映射

而在这个过程中,显然宠物的种类多种多样,每个宠物拥有的行为可能各不相同,比如有的宠物会飞,有的宠物会游泳。但是它们至少都会一些共同点,比如都有名称、性别、年龄等。那么把这些共同点都总结成一个类,在代码里就被称为基类再根据基类去扩展出它们各自独有的属性和方法,派生出的新类也就是子类。它们之间没有什么限制,即一个基类可以有多个子类(派生类)从基类派生出的子类仍可以继续派生。但是有的时候,某个方法并不会总是一成不变的,比如有的宠物叫声是汪,有的宠物叫声是喵,那你就无法在基类确定具体的叫声,但你知道每个宠物都会叫(假设所有宠物都会叫)。

那么根据以上表述就可以总结出面向对象编程的三大思想

(1)把这些属性和方法存到类里,同时将成员变量设置成 private,让访问这些成员变量只能靠 public 成员函数来处理,这整个动作就是面向对象编程三大思想中的封装。当然,更广义的封装概念,其实就是把这些属性和方法存到类里这个过程。

(2)上文所提到的派生的过程,就是面向对象编程三大思想中的继承

(3)上文最后提到的这种同一个行为具有多个不同表现形式的现象,就是面向对象编程三大思想中的多态

其中的继承和多态将在下文说明。


继承

继承的概念在上文已经提到(没注意的话可以回到前一个标题查看),总结来说就是继承允许我们根据一个类来定义另一个类。通过继承操作,我们就可以在面对高重复性的类的时候,不用完全重新编写新的类,只需要有一个设计好的基类,然后设计一个新的类去继承它即可。这样做之后就会提高代码的复用性,节省开发时间。那么它具体会继承什么,直接看完整代码:

using System;

namespace Test
{
    // 基类(父类)
    class Pet
    {
        protected string name;
        protected int age;
        protected string test = "Pet中";

        public Pet()
        {
            Console.WriteLine("Pet");
        }
        public Pet(string n, int a)
        {
            Console.WriteLine("带入参的Pet");
            name = n;
            age = a;
        }

        public void setName(string n)
        {
            name = n;
        }
        public string getName()
        {
            return name;
        }
        public void setAge(int a)
        {
            age = a;
        }
        public void talk()
        {
            Console.WriteLine("某种叫声");
        }
    }

    // 另一个基类 anotherPet
    // C# 不支持多重继承。但是,您可以使用接口来实现多重继承(接口的具体内容将在下文表述,在这里不详细说明)
    public interface anotherPet
    {
        void anotherTalk(string talk);
    }

    // 派生类(子类)
    // 一个父类可以有多个子类,但是一个子类只能有一个父类
    // 如果要实现多重继承,那么可以用接口来实现(接口的具体内容将在下文表述,在这里不详细说明)
    class Dog: Pet, anotherPet
    {
        // 注意 new 关键字
        new protected string test = "Dog中";

        // 调用子类的构造函数时,会首先调用父类的无参构造函数
        public Dog()
        {
            Console.WriteLine("Dog");
        }
        // 如果子类带参构造函数没有新代码,那么可以直接使用 base 进行基类访问
        public Dog(string n, int a) : base(n, a)
        {
            Console.WriteLine("带入参的Dog");
        }

        public int getAge()
        {
            // 子类可以直接使用父类的成员变量和成员方法
            // Console.WriteLine(getName());
            return age;
        }

        // 子类和父类拥有同名方法,那么就只调用子类的同名方法,而不会调用父类的同名方法
        // 注意 new 关键字
        new public void talk()
        {
            // 如果真的要调用父类的同名方法,那就用 base 的方式调用,进行"基类访问"
            base.talk();
            Console.WriteLine("旺旺");
        }

        public void getTest() 
        {
            // 子类和父类拥有同名变量,情况和同名方法一样。想要使用父类的同名变量,就用 base。
            Console.WriteLine(test);
            Console.WriteLine(base.test);
        }

        public void anotherTalk(string talk) 
        {
            Console.WriteLine(talk);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // 1、调用无参构造函数时,会首先调用父类的无参构造函数
            Dog dog = new Dog();

            // 2、调用有参构造函数时,如果子类有参构造函数没有新内容,那么可以直接使用 base 进行基类访问
            Dog dog2 = new Dog("豆子", 5);

            // 3、子类可以直接调用父类的成员方法和成员变量
            dog.setAge(5);
            Console.WriteLine("age: {0}",  dog.getAge());

            // 4、子类和父类拥有同名方法,那么就只调用子类的同名方法,而不会调用父类的同名方法
            // 如果要调用父类的同名方法,那就用 base 的方式调用,进行基类访问
            dog.talk();

            // 5、子类和父类拥有同名变量,那么就只调用子类的同名变量,而不会调用父类的同名变量
            // 如果要调用父类的同名方法,那就用 base 的方式调用,进行基类访问
            dog.getTest();

            // 6、利用接口实现多重继承
            dog.anotherTalk("interface");
        }
    }
}

输出结果:

Pet
Dog        
带入参的Pet
带入参的Dog
age: 5     
某种叫声
旺旺
Dog中
Pet中
interface

在这个完整代码中,主要分成以下几点来进行说明。


1、首先说明构造函数。在代码中使用了无参构造函数和有参构造函数。当我们使用了继承之后,在使用子类的构造函数的时候,无论是无参构造函数还是有参构造函数,都会先去调用父类对应的构造函数所以在使用的时候,需要留意调用顺序。而需要说明的是,虽然会先去调用父类对应的构造函数,但这并不意味着子类就可以不用写构造函数,子类仍需要写对应的无参、有参构造函数,否则报错。

在这里插入图片描述

而如果子类的构造函数和父类的构造函数内容相同的话,那么为了避免内容重复书写,就可以像上例一样使用 base 来实现基类访问。

public Dog(string n, int a) : base(n, a)
{
   Console.WriteLine("带入参的Dog");
}

这时依然会先调用父类构造函数中的内容,只不过父类的入参就会通过 base 传递过去,从而实现数据初始化。随后执行子类构造函数中的内容。


2、在子类中,我们可以直接调用父类的成员变量和成员方法。也就是说,子类会拥有所有父类中所有的字段、属性和方法,这就是继承的主要特点。

比较特殊的是,如果在子类中,要声明和父类同名的变量或方法,那么要用 new 关键字(当然也有其他方法,其他方法在下文说明),如上例中一样:

// 注意 new 关键字
new protected string test = "Dog中";

// 子类和父类拥有同名方法,那么就只调用子类的同名方法,而不会调用父类的同名方法
new public void talk()
{
   // 如果真的要调用父类的同名方法,那就用 base 的方式调用,进行"基类访问"
   base.talk();
   Console.WriteLine("旺旺");
}

public void getTest() 
{
   // 子类和父类拥有同名变量,情况和同名方法一样。想要使用父类的同名变量,就用 base
   Console.WriteLine(test);
   Console.WriteLine(base.test);
}

如果不用 new 关键字,虽然不报错,且依然会优先调用子类的同名变量或方法,但是 C# 会给出警示:

在这里插入图片描述

而如果真的想在子类的同名方法里,调用父类的同名方法或变量,那么就依然可以使用 base 来进行基类访问。


3、最后关于多重继承,在 C# 中,一个父类可以有多个子类,每个子类还可以有它自己的子类,但是一个子类只能有一个父类如果子类要实现多重继承,也就是一个子类有多个父类,那么可以用接口来实现。目前先简单提一下,接口和类的用法是有差异的。接口的具体内容将在下文表述,在这里不详细说明。

    // 基类(父类)
    class Pet
	{
		// ...
    }

    // 另一个基类 anotherPet
    public interface anotherPet
    {
        void anotherTalk(string talk);
    }

    // 派生类(子类)
    class Dog: Pet, anotherPet
    {
        public void anotherTalk(string talk) 
        {
            Console.WriteLine(talk);
        }
    }

除此以外需要注意的是,多重继承时,基类必须放在任何接口之前,否则报错:

在这里插入图片描述


多态

多态的概念在上文已经提到(没注意的话可以回到第一个标题查看),总结来说就是同一个行为具有多个不同表现形式

相比来说,通过上文对继承的叙述,如果部分成员变量或方法不需要调整,那么就会提高代码复用性。如果所有成员变量和成员方法都不需要调整,那么肯定就不需要继承。而使用继承必然会有新增的成员变量或方法,或者对父类的同名成员方法进行调整。在这里,对父类同名成员方法进行调整的过程,就是多态的概念,即同一个名称,会因为不同的实例而执行不同操作。通过多态,我们就可以提高代码的可理解性,比如有个方法名称为 talk,但是另一个方法也想表达“说”这个概念,只不过会多传参数或者不传参数,那么此时将方法命名为另一个没有被使用过的名称,就会很麻烦。

需要补充的是:之前已经提到过,其实所有类型都继承自 Object,那也就意味着,每个类型之间其实都体现了多态的概念。

具体来说,多态性可以是静态或动态的。先说一下静态多态性。


1、静态多态性(静态绑定)是在编译时发生函数响应,也就是当你在调用一个对象的方法的时候,系统在编译时,就可以根据传递的参数个数、参数类型等去决定要调用这同一个范围内(同一个类中)的哪个同名方法。

换句话说,这同一个范围内(同一个类中),可以使用相同的函数名去定义多个函数,但这些函数的定义必须彼此不同。不同的点可以是参数列表中的参数类型不同,也可以是参数个数不同。这种重新定义同名方法的操作就被称作(函数)重载。也就是说,同一个范围内,一定不能重载只有返回类型不同的函数声明

(需要注意区分的是:在上一节继承的使用中,子类中使用 new 关键字去声明了一个和父类完全相同的同名方法,这并没有和静态多态性的概念发生冲突。首先,子类和父类本身并不属于同一个范围。其次,这种方式其实属于动态多态性。具体内容在下文说明)

示例代码:【代码转自菜鸟教程】

using System;

namespace Test
{
    class Print
    {
        public void print(int i)
        {
            Console.WriteLine("输出整型: {0}", i);
        }

        public void print(double d)
        {
            Console.WriteLine("输出浮点型: {0}" , d);
        }

        public void print(string s)
        {
            Console.WriteLine("输出字符串: {0}", s);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Print p = new Print();
            // 调用 print 来打印整数
            p.print(1);
            // 调用 print 来打印浮点数
            p.print(1.23);
            // 调用 print 来打印字符串
            p.print("CSDN");
        }
    }
}

输出结果:

输出整型: 1
输出浮点型: 1.23
输出字符串: CSDN

除了能够对函数进行重载之外,C# 也支持对运算符的重载。由于笔者平常很少使用运算符重载,所以如果各位有需要可以前往以下文章查阅:运算符重载


2、动态多态性(动态绑定)是在运行时发生函数响应,也就是当你在调用一个对象的方法的时候,经过了调用基类的操作,就比如上节使用的继承。此时系统只有在运行时,才能根据传递的参数个数、参数类型等实际情况去决定要调用哪个同名方法。

而动态多态性的实现方式,在继承中已经有了一个使用方式,再叙述一下:

子类和父类中可以拥有传递的参数个数、参数类型不同,或返回值类型不同的同名方法,和静态多态性一样。但如果子类中要使用和父类完全相同的同名方法(参数个数相同、参数类型相同、返回值类型相同),可以用 new 关键字声明

简例如下:

using System;

namespace Test
{
    class Pet
    {
        public void talk()
        {
            Console.WriteLine("父类,无参");
        }
    }

    class Dog : Pet
    {
        public void talk(string s)
        {
            Console.WriteLine("子类,有参:{0}", s);
        }
        new public void talk()
        {
            Console.WriteLine("子类,无参");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Dog dog = new Dog();
            dog.talk();
            dog.talk("旺旺");
        }
    }
}

输出结果:

子类,无参
子类,有参:旺旺

而这种用 new 关键字进行声明的方式,更像是一种在子类中突然发现父类中已经存在完全相同的同名方法后,为了避免修改父类而被迫使用的方式(因为在项目中,该父类可能已经生成了多个子类,现在修改父类很可能会造成异常)。具体来说,有的时候,父类和子类不会在同一个文件下,方法也会有很多,所以找起来本身就会不方便。而其中有可能部分方法在父类存在是没有意义的,就比如上例的 talk 方法,它必须要到具体的子类中去实现,才会为其赋予具体含义。在这种情况下,我们为了避免更多问题,当然可以不去在父类中声明这个 talk 方法,毕竟在子类中可以调用父类的成员变量和成员方法,这样就可以不去考虑是否父类和子类出现完全相同的方法。但是在项目中,由于开发内容很多,如果父类没有特殊标注或者干脆父类中不去实现那么开发子类时很可能会遗忘要实现一个父类的方法,或者还需要关注是否和父类完全相同,从而考虑使用 new 关键字来声明。所以在面对这种情况时,如果在设计中,就已经确定某些方法只能到具体的子类中实现时,且子类和父类的方法可能会完全相同时,那么此时为了提醒各个子类去关注这个情况就可以使用以下几种方式:接口 Interface、抽象类 abstract、虚方法 virtual。接下来就叙述它们的用法,最后总结它们之间的区别。


virtual 虚方法

先说一下 virtual 虚方法。就像上文说的一样,如果当我们想定义一个方法,它需要在子类(继承类)中具体实现时,就可以使用 virtualoverride 关键字来实现。不过在继承的子类中,并不是一定要实现它。具体看代码:

using System;

namespace Test
{
    class Shape
    {
        protected int width, height;
        public Shape(int a = 0, int b = 0)
        {
            width = a;
            height = b;
        }
        public virtual int area()
        {
            Console.WriteLine("父类area");
            return 0;
        }
    }
    class Rectangle: Shape
    {
        public Rectangle(int a = 0, int b = 0) : base(a, b)
        {

        }
        public override int area ()
        {
            Console.WriteLine("子类Rectangle");
            return (width * height);
        }
    }
    class Triangle: Shape
    {
        public Triangle(int a = 0, int b = 0): base(a, b)
        {
        
        }
        public override int area()
        {
            Console.WriteLine("子类Triangle");
            return (width * height / 2);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Rectangle r = new Rectangle(4, 5);
            Triangle t = new Triangle(4, 5);
            Console.WriteLine("矩形面积:{0}", r.area());
            Console.WriteLine("三角形面积:{0}", t.area());
        }
    }
}

输出结果:

子类Rectangle
矩形面积:20
子类Triangle
三角形面积:10

需要说明的是,父类声明的虚方法,在继承的子类中,并不是一定要实现它。就相当于父类通过 virtual 字段来提示,方便其他开发人员查阅哪些方法可能需要子类去实现,最后到了继承类中按需去实现,且如果要实现就用 override(重写)。


interface 接口

接口的简单使用在继承中已经展示,当时说到使用接口就可以实现所谓的多重继承。它使用关键字 interface。接下来再用一个实例具体说一下:

using System;

namespace Test
{
    class Shape
    {
        protected int width, height;
        public Shape(int a = 0, int b = 0)
        {
            width = a;
            height = b;
        }
    }

    interface IArea
    {
        int area();
    }

    class Rectangle: Shape, IArea
    {
        public Rectangle(int a = 0, int b = 0) : base(a, b)
        {

        }
        public int area()
        {
            return (width * height);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Rectangle r = new Rectangle(4, 5);
            Console.WriteLine("矩形面积:{0}", r.area());
        }
    }
}

接口的声明默认是 public。其次,在定义接口的时候,在这里它的名称为:IArea,原因是通过网上查阅,看起来通常建议接口名称以 I 字母开头,不过起什么名称都无所谓,在这里简单提一下,各位最后根据项目规范去命名即可。

随后,这个接口中目前有一个方法 area(),我们可以按照需求设置参数列表(入参信息)和返回类型。但是一定要注意,只要在接口中声明的方法,那么在继承的子类中,就必须要把接口中所有的方法都进行具体的实现,而且必须参数列表和返回类型相同,否则报错:

在这里插入图片描述
在这里插入图片描述

需要补充的是:在接口中你无法像普通的类一样声明成员变量(字段),否则报错:

在这里插入图片描述


如果要对 area() 进行重载,那么就需要声明,并在子类中实现它。比如:

interface IArea
{
    int area();
    int area(int a, int b);
    int area(int a);
}

需要注意的是,在接口中并没有对这些方法进行具体的实现,最主要的是你也无法对其进行具体的实现,否则会报错:

在这里插入图片描述

我们在接口中不去实现它,这也和使用接口的情景有关。接口的作用通过前文分析可以得到一些结论,它的目的就是为了提醒子类必须去具体实现某个方法,且既然使用接口就代表着在设计时就决定了该方法必须要在子类实现,否则可以使用虚方法。因此在接口中对这些方法进行具体实现是没有意义的,也是不被允许的。既然如此,创建一个接口实例肯定也没有意义,当然也是不被允许的。那么也就没有构造函数

在这里插入图片描述

所以接口最根本的作用就是:给某个事物定义它将要做什么或者说它应该有什么,随后在具体要实现这个事物时,继承这个接口。随后遵循在设计时所定义的内容,去实现它。


除了上述使用外,一个接口也可以继承其他接口。在这种情况下,最后它的实现类就需要实现所有接口的成员。比如:

using System;

namespace Test
{
    class Shape
    {
        protected int width, height;
        public Shape(int a = 0, int b = 0)
        {
            width = a;
            height = b;
        }
    }

    interface ITest
    {
        void test();
    }

    interface IArea: ITest
    {
        int area();
    }

    class Rectangle: Shape, IArea
    {
        public Rectangle(int a = 0, int b = 0) : base(a, b)
        {

        }
        public int area()
        {
            return (width * height);
        }
        public void test()
        {
            Console.WriteLine("测试");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Rectangle r = new Rectangle(4, 5);
            Console.WriteLine("矩形面积:{0}", r.area());
            r.test();
        }
    }
}

在上篇文章中还提到结构体也可以继承接口,和上文说明一样,结构体中需要实现所有接口的成员。

using System;

interface ITest
{
    void test();
}
struct Pet: ITest
{
    public string name;
    public int age;
    public Pet(string name, int age) {
        this.name = name;
        this.age = age;
    }
    public void test() {
        Console.WriteLine("name = {0}", name);
        Console.WriteLine("age = {0}", age);
    }
}

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            Pet pet = new Pet("猫", 2);
            pet.test();
        }
    }
}

abstract 抽象类

抽象类的关键字为 abstract ,它更像是把虚方法和接口结合在了一起,直接上例子:

using System;

namespace Test
{
    abstract class Shape
    {
        protected int width, height;
        abstract public int area();
        public Shape(int a = 0, int b = 0)
        {
            width = a;
            height = b;
        }
    }

    class Rectangle: Shape
    {
        public Rectangle(int a = 0, int b = 0) : base(a, b)
        {

        }
        public override int area()
        {
            return (width * height);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Rectangle r = new Rectangle(4, 5);
            Console.WriteLine("矩形面积:{0}", r.area());
        }
    }
}

首先,说它像虚方法的点是:当在父类中,你想要让继承的子类必须去实现某方法时,在虚方法中用 virtual,而抽象类用 abstract。但需要注意的是,使用 abstract 关键字声明的方法不能是私有的,否则报错:(网上看好像有的人说可以是私有的,在这里可能有误,请各位自行批判性判别

在这里插入图片描述

同时不能用 abstract 关键字声明成员变量(字段),否则报错:

在这里插入图片描述

在这里提到的属性,在前文一直没提到过,它主要就是简化之前对 private 成员变量的 get 和 set。感兴趣的话,为了避免大篇幅引用,可以前往以下文章查阅:

C#中属性的定义及用法


除此以外需要注意的是:抽象方法只能在抽象类中声明。也就是说,如果类包含抽象方法(用 abstract 声明的方法),那么该类也必须被声明是抽象的(用 abstract 声明的类)。

在继承时,和虚方法一样,需要用 override 关键字,当然也可以用 new,否则报错:

在这里插入图片描述


而说它像接口的点是:在父类中,你同样无法具体实现一个用 abstract 声明的方法,否则报错。(网上看好像有可以实现的,但是笔者测试直接声明应该是不行,在这里可能有误,请各位自行批判性判别

在这里插入图片描述

在子类中也必须要实现这些抽象方法抽象类也无法创建实例。感觉像是,把接口的内容移到了父类中。但是需要注意的是,接口可以用作多重继承,但是抽象类不能


接口、抽象类、虚方法对比

在了解了以上所有内容后你就可以发现三者的区别:

如果使用抽象类,那么相比使用虚方法和接口就会更加强硬。因为抽象方法就在父类中,那么所有子类就真的就是必须要去实现它,否则就直接报错了。而虚方法就轻松很多,因为你并不是一定要去实现它。同理的,接口也是一样,如果你不想实现它,那么不继承就可以了。但是这样就需要冒着可能会忘记实现某方法的另外报错问题。所以从目前总结来看,接口更像是一种规范,虚方法更像是一种提示和注释,而抽象类就是绝对的共性。因此,根据某方法的重要性就可以总结出以下内容:

1、如果使用虚方法那么很容易会找不到这条信息,因为父类上也没有其他关键字,所以如果某方法确实无关紧要,那么可以用 virtual 进行提示

2、其次,当你明确建议但又不是完全要在子类中实现时,就可以用 interface 接口写在某个文件的顶部,用作规范,最后按需继承也可以。

3、最后,当你明确某方法必须要实现时,用 abstract 关键字声明,在继承时就可以明显看到写在父类上的关键字,就清楚的知道,其中一定有要实现的抽象方法

那么以上就是所有的基础使用方式和相关概念,如果没有基础的话,很容易混乱,抓不住重点,笔者最开始就会有这样的感觉。现在将最后总结一下它们用法上的差异:

接口 interface 抽象类 abstract 虚方法 virtual
基类采用这三种用法,继承类是否可以多重继承 × ×
基类采用这三种用法,继承类是否必须要具体实现相关内容 ×
基类采用这三种用法后,是否可以创建实例 × ×
基类采用这三种用法后,是否可以使用构造函数 / 析构函数 ×
其他差异点 接口中只能声明方法,属性,事件,索引器 抽象类和虚方法所在的父类,就和普通的类一样,其中可以有抽象方法/虚方法,也可以定义其他成员方法,成员变量等等

最后说明:以上内容笔者也是参考了很多文章得到的一些基础性结论,大体上应该没有错误,但细节方面可能存在错误,请大家还是通过自己再尝试和具体案例,批判性查阅以上内容。如果有差异或错误的,也可以在评论区提出。最主要的是,在笔者查阅的相关文章之间,在部分观点上已经或多或少有些出入,所以笔者也已经避免提到这些模糊概念,且有可能有错误的地方,也做一些简单的标识,供大家注意。


static 静态成员

所谓静态成员,就是无论实例化多少个对象,最后只会有一个该静态成员的副本。定义静态成员需要使用 static 关键字。先来用代码验证这一点,再说明其他内容:

using System;

namespace Test
{
    class Test
    {
        public static int num = 0;
        public static int num2 = 0;
        public string test = "";
        public void count()
        {
            num++;
            num2++;
        }
        public int getNum()
        {
            return num;
        }
        // 静态函数只能访问静态变量
        public static int getNum2()
        {
            // test = "csdn";
            return num2;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Test t = new Test();
            Test t2 = new Test();
            t.count();
            t.count();
            // 类中只有一个该成员的实例
            Console.WriteLine("第一个输出:{0}", t.getNum());
            Console.WriteLine("第二个输出:{0}", t2.getNum());
            // 无法使用实例来访问静态成员,即无需实例化即可访问静态成员
            // 访问静态函数用法:
            Console.WriteLine("第三个输出:{0}", Test.getNum2());
        }
    }
}

输出结果:

第一个输出:2
第二个输出:2
第三个输出:2

首先我们可以看到,当 t 对象对静态成员做修改时,t2 对象的静态成员也会同步做修改,这也就验证了无论实例化多少个对象,最后只会有一个该静态成员的副本。

其次,之所以用 t.getNum 而不是直接调用 t.num 是因为,静态成员是无法通过实例化对象来访问的,否则报错:
在这里插入图片描述

同时,在报错信息中也可以得知,如果我们要想直接调用静态成员,那么就要直接用类名来访问,就像上例的 Test.getNum2 一样。也就是说,我们不用进行对象实例化,就可以调用静态成员。但需要注意的是,当你在类中声明了一个静态函数之后,在其中只能访问静态变量,否则报错;

在这里插入图片描述

这个报错是很好理解的,既然所有实例化对象都使用一个静态成员副本,那么你在静态函数中对非静态成员作操作,那就肯定需要对某一个具体的实例化对象做操作。但是这么多对象,都用这一个副本,在静态函数中显然就无法明确得知该值要赋给谁。


通过上例的使用,不难看出它的使用场景。当你需要定义一些全局的公共方法时,就可以使用静态成员,这样也可以省略实例化对象的操作:

using System;

namespace Test
{
    class Tools
    {
        public static void getTriangleArea(int width, int height)
        {
            Console.WriteLine("三角形面积:{0}", ((width * height) / 2));
        }
        public static void getRectangleArea(int width, int height)
        {
            Console.WriteLine("矩形面积:{0}", width * height);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Tools.getRectangleArea(3, 4);
            Tools.getTriangleArea(3, 4);
        }
    }
}

除此以外,如果所有实例化对象都使用一个静态成员副本,那么使用继承会发生什么:

using System;

namespace Test
{
    class Shape
    {
        public static int width = 0;
        public static int height = 0;
        public static int getArea()
        {
            return (width * height);
        }
    }

    class Triangle: Shape
    {
        new public static int getArea()
        {
            return (width * height * 2);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Triangle.width = 5;
            Triangle.height = 4;
            Console.WriteLine("结果:{0}", Shape.getArea());
            Console.WriteLine("结果:{0}", Triangle.getArea());
            Console.WriteLine("结果:{0}", Shape.getArea());
        }
    }
}

输出结果:

结果:20
结果:40
结果:20

在这里就可以发现,子类依然会拥有父类的成员变量和成员函数,只不过它们是静态的。那同样的,子类也可以不用实例化,就去调用父类的静态成员。重载重写也都是同理,不会对父类有影响。那利用这个特性,如果父类的同名静态成员不满足要求,那就可以用继承对它进行重写,最后再通过不同的类名来访问需要的同名静态成员。

最后一定要提醒的是,静态成员最好不要滥用,当你真的是需要做一些公共用法时再使用。


由于在自己的工作和学习过程中,只查看某个大佬的教程或文章无法满足自己的学习需求和解决遇到的问题,所以自己在追赶大佬们步伐的基础上,又自己总结、整理、汇总了一些资料,方便自己理解和后续回顾,同时也希望给大家带来帮助,所以才写下该篇文章。在本文中,所有参考或引用大佬们文章内容的位置,都附上了原文章链接,您可以直接前往查阅观看。在原文章内容的基础上,若无任何补充内容,同时避免直接大段摘抄大佬们的文章,该情况下也只附上了原文章链接供大家学习。本文旨在总结归纳,并希望给大家提供帮助,未用作任何商用用途。文章内容如有错误之处,望各位大佬指出。如果涉及侵权行为,将会第一时间对文章进行删除。


个人博客主页
一个努力学习的程序猿


本文所在专栏: C# 专栏,欢迎大家前往查看更多内容


专栏内目前所有文章:
不定期持续更新中

你可能感兴趣的:(#,C#,c#)