前言

这玩意最烦了,而且还是c#入门期最长的(当然是指那种定义/理论的字数最长)也是最核心一篇了,讲完其实个人觉得对于入门而言就差不多了(其他就是杂七杂八的玩意了)

但不管怎么说,如果说看到这里,这也就代表C#入门最关键(至少对我而言)的东西了——类型,继承和接口,如果说愿意耐心看完的话,你会学到很多的(当然,这也是说给我听的)。

如果说你连类型都不知道,那就好比造车,造车你连怎么造都不懂,那会封装有何用,或者说你没学会javase,你就去学SpringFramework是一样的道理,至此,如果说你学会了,那么看了这篇会有所收获的(在这里我就不说封装怎么做了,毕竟学过面向对象都知道怎么做)。

同样,继承和接口一类也是为了类型服务的,或者说一切的根基就是类型,但如果说你只会了类型,那只能说明你能实现方法了,但是实现了之后呢,只会形成一坨石山吗?你真的愿意看着那百万行石山不头疼吗?因此,作者我打算把这仨并在一起讲了

由于讲太多影响读者阅读,作者就不打算写特别特别细了

类型

定义

好吧,还是得要定义,我还是简单写下吧。这里的类型其实就是封装的大前提,如果说你会类型了,你才会搞封装,在这里我直接说可能没啥信任度,我就干脆搬微软定义了(原链接:类、结构和记录 - C# | Microsoft Learn):

在 C# 中,类型(类、结构或记录)的定义类似于指定类型可以执行的作的蓝图。

如上所述,他就是一个蓝图(玩过工厂模拟应该知道蓝图是什么玩意),主要就分三种:结构记录

ps:这里的类型并非内置类型,内置类型就是所谓的变量,详细请见:C#的基本结构,变量|真理の小屋

按照人话讲类就是大蓝图、结构就是小蓝图、记录就是特殊的蓝图

下面就是类

定义

类就是大蓝图,一般来说,如果不用优化,一般来说,学面向对象相关的都可能只会学到这玩意。

好吧,就这么多,详细点我就搬百度百科的内容了(原链接:类(编程术语)_百度百科):

类(Class)是面向对象程序设计(OOP,Object-Oriented Programming)实现信息封装的基础。类是一种用户定义的引用数据类型,也称类类型。每个类包含数据说明和一组操作数据或传递消息的函数。类的实例称为对象。

额,先讲这么多,接下来直接看结构了:

结构

定义

好吧,java没有这货,这里就简单解释下。

这里的结构应该来说叫结构体,你只用理解是类的“轻量化”版本就行。

如果要深入了解,我这里又引用微软的话了(原链接:结构类型 - C# reference | Microsoft Learn):

结构类型(或 struct type)是一种可封装数据和相关功能的值类型。

结构类型具有值语义。 也就是说,结构类型的变量包含类型的实例。 默认情况下,在分配中,通过将参数传递给方法并返回方法结果来复制变量值。 对于结构类型变量,将复制该类型的实例。 有关更多信息,请参阅值类型。

通常,可以使用结构类型来设计以数据为中心的较小类型,这些类型只有很少的行为或没有行为。 例如,.NET 使用结构类型来表示数字(整数和实数)、布尔值、Unicode 字符以及时间实例。 如果侧重于类型的行为,请考虑定义一个类。 类类型具有引用语义。 也就是说,类类型的变量包含的是对类型的实例的引用,而不是实例本身。

由于结构类型具有值语义,因此建议定义不可变的结构类型。

区别

好吧,我还是得要在这里简单说下跟类的区别吧:

这俩货最主要的区别就是:结构(结构体)就是值类型,类就是引用类型。

换言之,我举个简单的例子:引用类型就是一张同样账户的银行账户,这里所有人都能用(同加减),而结构就是钱包,给了多少花了多少跟别人没有关系。

好吧,接下来就是C#为了省代码而出现的记录:

记录

定义

在我看来,这个玩意就是特殊的类/结构,这个就是一个超绝省代码(当然这只是其中一个方面)的妙妙小玩意(这个玩意是引用/值类型,得看加不加struct了),没了

额,好吧,以防不懂,接着掏出微软文档(原链接:记录 - C# reference | Microsoft Learn):

可使用 record 修饰符定义一个引用类型,用来提供用于封装数据的内置功能。 record class 语法用作等价符号以标识引用类型,record struct 则用于定义一种具有类似功能的 值类型。

在记录上声明主构造函数时,编译器会为主构造函数参数生成公共属性。 记录的主构造函数参数称为位置参数。 编译器创建镜像主构造函数或位置参数的位置属性。 编译器不会在没有 record 修饰符的类型上合成主构造函数参数的属性。


这里就不好举例了,麻烦,直接放代码了。

好吧,还是得要个示例,不然你们看的云里雾里的:

示例

这里直接在这里汇总吧

就实现一个爱丽丝和她的传奇年龄(什)

总之,看三个不同的实现吧:

在类这方面,其实上一期就用过了。如果说要修改类里的对象的值,是需要用到get方法和set方法的,在Csharp里,这货省了,详细请参见代码:

class Program
{
    public class Human //类
    {
        public string Name //属性
        {
            get; set;
        }
        public int Age //属性
        {
            get; set;
        }
        public void Take() //方法
        {
            Console.WriteLine($"Name:{Name},Age:{Age}");
        }
    }
    static void Main(string[] args)
    {
        Human human = new Human
        {
            Name = "Alice",
            Age = 30
        }; //对象初始化器语法
        Human human1 = human; //引用赋值,指向同一对象
        human1.Age = 31;
        human.Take();
        human1.Take();
    }
}
以防看不懂,这里来个折叠

这里面的属性(成员的一种)是可以直接用get和set函数省略的,所以就成了这样。

那么就有人要问了,属性到底是什么玩意呢,好吧,我得简单解释下:

属性在编程中是指类或对象的特征或状态。

对象初始化器好像也是C#独有的,他就是对象初始化器是一种在创建对象时直接为其属性赋值的简便方法,C#为了省代码连这都可以省吗?有点意思

这里是结果:

好吧,我也没办法在这方面讲太细,主要也没必要讲那么细,点到为止,那接下来就是结构了。

结构

class Program
{
    struct Human //结构
    {
        public string Name { get; set; }
        public int Age { get; set; }
        
        public void Take()
        {
            Console.WriteLine($"Name:{Name},Age:{Age}");
        }
    }
    static void Main(string[] args)
    {
        Human human = new Human
        {
            Name = "Alice",
            Age = 30
        };
        Human human1 = human; //值类型赋值,复制一份
        human1.Age = 31; //修改human1的Age,不影响human
        human.Take();
        human1.Take();
    }
}

这里是结果:

如果你愿意翻回类的示例,你可以发现,类里的Age都是31(只要你不是用特殊的方法比如ref),而结构里的Age一个是30,一个则是31。

讲的差不多了,接下来就到同样还是C#的记录,欸我去C#怎么这么坏

记录

在此之前,声明下,其实说实话这个接口有点脱离记录原本的设计意图了,不过为了方便演示所有特性就合并在一起了,希望读者能够理解

class Program
{
    record Human(String Gift, int Year) //Gift和Year主要体现其省代码的特性
    {
        
        public string Name { get; init; } //不可变属性
        public int Age { get; init; } //不可变属性
        public void Take()
        {
            Console.WriteLine($"Name: {Name}, Age: {Age}");
        }
        public void Birthday()
        {
            Console.WriteLine($"{Name}'s {Year} years birthday is a {Gift}");
        }
    }
    static void Main(string[] args)
    {
        Human human = new Human("Egg",18) { Name= "Alice", Age = 30 }; //创建不可变对象
        //human.Age = 32; //出现CS8852错误,无法修改不可变属性
        Human human1 = human with { Age = 31 }; //强制修改不可变属性
        human.Take();
        human.Birthday();
        human1.Take();
    }
}

这里是结果:

如你所见,这玩意是真的省

总结

类型好麻烦,写完了,这里简单列表格做个汇总

结构

记录

类型

引用

默认引用,但可以是值

赋值行为

复制引用

复制值

默认引用,但可以赋值

可变

默认是

默认是

默认非

是否可继承

默认是,若值类型则否

类完事,写继承和接口了

继承

定义

用人话讲,继承就是儿子(子类)继承父亲(父类)的遗产。

至于为啥要搞这个,因为继承本身就是一个减少重复的东西,不然重复造轮子就形成了一种超级石山(是真石山)

ps:学算法其实也一样的,不过那个我不会细讲

至少作者我学过java,所以我完全可以说到这里就直接到示例了。但实际上,C#和java还是有一定的区别的(至少我认为是这样的)。

总之,这玩意挺麻烦的,至此我打算分开讲这四种:一般的继承、抽象、密封类和多态。

哦对,顺带一提,C#又不是Cpp(C++)那些,是不能多重继承(单指类)的(儿子继承多个父亲的遗产子类继承多个父类)

一般的继承

定义

这个么,就是一个(或多个)儿子(子类)继承一个父亲(父类),这就是一般的继承,抽象都是基于这个玩意的(当然这个之后会详细讲这是什么玩意)

顺带,在这个继承里,我会顺带讲下Override(重写)的用法,不然没啥继承的意义在里面

用法

由于接下来示例不好直接掏代码了,我就简单解释下怎么用了

这个玩意么,其实比一般的java继承(一般)麻烦多了,他的Override(重写)是得要在方法体里加上virtual关键字比如说这样(这里仅放出部分代码作为示例,不能完整使用),如果说不加,那也没办法重写:

namespace Fourth
{
    internal class Animal
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public virtual void Speak() //virtual为允许子类重写该方法
        {
            Console.WriteLine($"Name:{Name},Age:{Age},Animal");
        }
    }
}

java么,只要对那个方法来个@Override就完事了(这种玩意甚至都不需要)

按理来讲,会顺带讲到Protected那些乱七八糟的权限的,但其实我觉得没啥太大卵用(至少目前是这样),就不讲了

差不多了,开始讲那个抽象了

抽象

定义

抽象,这个玩意就是你是这个家的儿子(子类),你就必须完成父亲(父类)那所谓的“家训”(方法)

弄出这个玩意的主旨其实就是为了方便合作打代码的

我还是丢个微软官方的文档吧(原链接:abstract 关键字 - C# reference | Microsoft Learn

abstract 修饰符指示被修改内容的实现已丢失或不完整。 abstract 修饰符可用于类、方法、属性、索引和事件。 在类声明中使用 abstract 修饰符来指示某个类仅用作其他类的基类,而不用于自行进行实例化。 标记为抽象的成员必须由派生自抽象类的非抽象类来实现。

抽象类可以包含抽象成员(这些成员没有实现,必须在派生类中重写)和完全实现的成员(如常规方法、属性和构造函数)。 这允许抽象类提供通用功能,同时仍要求派生类实现特定的抽象成员。

密封

定义

密封这玩意其实挺另类的,他就是所谓的“断子绝孙”一样,一旦搞了这个密封,就代表你不能把这个搞成父类了

我感觉这里更多的意义就在于他的字面意思“密封”,剩下没必要讲

用法

好吧,由于不可能在示例里提到(毕竟这更多就是告诉你不要用这个类继承),我在这里简单说一下,你只要在类(class)/重写方法(override)前加上sealed就行,就像这样(这里不放完整代码了):

namespace Fourth
{
    internal sealed class Dog : Animal //密封类
    {
        public sealed override void Speak(int Year) //密封方法
        {
            Console.WriteLine($"Name:{Name},Age:{Year},Dog");
        }
    }
}

多态

定义

好吧,我承认这里少了

说实话,这里我不好解释这货是什么,因为这个概念其实也是代码工程化的基础,我讲错容易出事,而且对我而言也没必要(这里更多是面向读者的),我干脆直接搬微软的文档,以便读者理解(原链接:多形性 - C# | Microsoft Learn

多态性常被视为自封装和继承之后,面向对象的编程的第三个支柱。 Polymorphism(多态性)是一个希腊词,指“多种形态”,多态性具有两个截然不同的方面:

  • 在运行时,派生类的对象可以在方法参数、集合或数组等位置被视为基类的对象。 在出现此多形性时,该对象的声明类型不再与运行时类型相同。

  • 基类可以定义和实现 虚拟方法,派生类可以 重写 它们,这意味着它们提供自己的定义和实现。 在运行时,客户端代码调用该方法,CLR 查找对象的运行时类型,并调用虚方法的重写方法。 你可以在源代码中调用基类的方法,执行该方法的派生类版本。

虚方法允许你以统一方式处理多组相关的对象。 例如,假定你有一个绘图应用程序,允许用户在绘图图面上创建各种形状。 你在编译时不知道用户将创建哪些特定类型的形状。 但应用程序必须跟踪创建的所有类型的形状,并且必须更新这些形状以响应用户鼠标操作。 你可以使用多态性通过两个基本步骤解决这一问题:

  1. 创建一个类层次结构,其中每个特定形状类均派生自一个公共基类。

  2. 使用虚方法通过对基类方法的单个调用来调用任何派生类上的相应方法。

示例

好吧,这里简单用图解释下接下来要搞什么玩意,这样(当然,这不标准,不过用来简单演示就够用了):

总之要搞这样的结构

一般的继承

Amimal类(父类):

namespace Fourth
{
    internal class Animal
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public virtual void Speak() //实现主方法
        {
            Console.WriteLine($"Name:{Name},Age:{Age},Cat");
        }
    }
}

Cat类(子类):

namespace Fourth
{
    internal class Cat : Animal
    {
        public override void Speak()
        {
            Console.WriteLine($"Name:{Name},Age:{Age},Cat");
        }
    }
}

Dog类(子类):

namespace Fourth
{
    internal class Dog : Animal
    {
        public override void Speak() //重写方法
        {
            Console.WriteLine($"Name:{Name},Age:{Age},Dog");
        }
        public void Bark() //子类Dog特有的方法
        {
            Console.WriteLine("Woof! Woof!");
        }
    }
}

Program类(主类):

using Fourth;
class Program
{
    
    static void Main(string[] args)
    {
        Animal animal = new Animal //创建父类Animal的对象
        {
            Name = "Animal",
            Age = 1
        };
        Cat cat = new Cat //创建子类Cat的对象
        {
            Name = "Kitty",
            Age = 2
        };
        Dog dog = new Dog //创建子类Dog的对象
        {
            Name = "Doggy",
            Age = 3
        };
        animal.Speak();//调用方法
        cat.Speak();
        dog.Speak();
        dog.Bark(); //调用子类Dog特有的方法
    }
}

这里是结果:

抽象类

怎么说呢,为了体现抽象类的具体用法,会使用部分不同的用法,请见谅

Amimal类(父类):

namespace Fourth
{
    internal abstract class Animal
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public abstract void Speak(int Year); //抽象方法,没有方法体,必须在子类中实现

        public void Eat() //普通方法,有方法体,可以直接使用
        {
            Console.WriteLine($"{Name} {Age} is eating.");
        }
    }
}

Cat类(子类):

namespace Fourth
{
    internal class Cat : Animal
    {
        public override void Speak(int Year) //实现抽象方法
        {
            Console.WriteLine($"Name:{Name},Age:{Year},Cat");
        }
    }
}

Dog类(子类):

namespace Fourth
{
    internal class Dog : Animal
    {
        public override void Speak(int Year) //实现抽象方法
        {
            Console.WriteLine($"Name:{Name},Age:{Year},Dog");
        }
        public void Bark() //子类Dog特有的方法
        {
            Console.WriteLine("Woof! Woof!");
        }
    }
}

Program类(主类):

using Fourth;
class Program
{
    
    static void Main(string[] args)
    {
        //Animal animal = new Animal //这里无法创建抽象类Animal的对象
        //{
        //    Name = "Animal",
        //    Age = 1
        //};
        Cat cat = new Cat //创建子类Cat的对象
        {
            Name = "Kitty",
            Age = 2
        };
        Dog dog = new Dog //创建子类Dog的对象
        {
            Name = "Doggy",
            Age = 3
        };
        cat.Speak(1);//调用抽象方法
        dog.Speak(2);
        cat.Eat(); //调用普通方法
        dog.Eat();
        dog.Bark(); //调用子类Dog特有的方法
    }
}

这里是结果:

至此,可算是讲完继承了

接口

定义

接口,在C#里这个玩意更多像是一种代码规范或者说“契约”的方式

我说这一句话可能没说太明白,直接上文档吧(原链接:接口关键字 - C# reference | Microsoft Learn):

接口定义协定。 实现该协定的任何 class、record 或 struct 必须提供接口中定义的成员的实现。

接口可以定义成员 的默认实现 。 它还可以定义 static 成员,以便为通用功能提供单个实现。

从 C# 11 开始,接口可以定义 static abstract 或 static virtual 成员来声明实现类型必须提供声明的成员。 通常,static virtual 方法声明实现必须定义一组重载运算符。

在以下示例中,类 ImplementationClass 必须实现一个不含参数但返回 SampleMethod 的名为 void 的方法。

用法

正因为是“契约”,所以就可以随便继承多个父类,当然“契约”(子)就就没必要特意重复“契约”(父)的所有方法了(不然根本没有意义)

这里再顺带,如果接口的方法名一样,则会出现“去重”现象,就需要显示接口(好吧,这货很麻烦),之后代码注释里会详细讲

当然了接口方法是public的,不需要显式声明的

示例

为了方便演示,我直接上代码了:

ps:当然因为要演示所有接口相关特性,所以会比较怪,一般来说不会搞那么麻烦的

Interface类(接口类):

namespace Fifth
{
    interface FamilyMember //FamilyMember接口
    {
        void FamilyName();

        void Role();

        void Take(); //Take方法,用于显示接口

    }
    interface Parent //Parent接口
    {
        void Familys();
    }
    interface Child //Child接口
    {
        void Childs();
    }
    interface FamilyDetails : Parent, Child //继承Parent, Child这俩多个接口
    {
        void Details();

        void Take(); //Take方法,用于显示接口
    }
}

Family类(实现类):

namespace Fifth
{
    internal class Family : FamilyMember, FamilyDetails //继承FamilyMember和FamilyDetails接口
    {
        public string Name { get; set; }
        public void FamilyName() //实现FamilyName接口的方法
        {
            Console.WriteLine($"The family name is {Name}.");
        }
        public void Role() //实现FamilyName接口的方法
        {
            Console.WriteLine("The role is Parent.");
        }
        public void Familys() //实现FamilyDetails接口的方法(继承自Parent接口)
        {
            Console.WriteLine("This is the Fifth family.");
        }
        public void Childs() //实现FamilyDetails接口的方法(继承自Child接口)
        {
            Console.WriteLine("This family has two children.");
        }
        public void Details() //实现FamilyDetails独有的接口的方法
        {
            Console.WriteLine("Family Details: Name - " + Name + ", Role - Parent, Children - 2");
        }
        /*public void Take() //实现FamilyMember和FamilyDetails接口中的Take方法
        {
            Console.WriteLine("This is the Take method from Family class.");
        }
        好吧,这里故意注释掉Take方法的实现,就是用来展示接口方法冲突的情况
         */
        void FamilyDetails.Take() //实现FamilyDetails接口中的Take方法
        {
            Console.WriteLine("FamilyDetails.");
        }
        void FamilyMember.Take() //实现FamilyMember接口中的Take方法
        {
            Console.WriteLine("FamilyMember.");
        }
        /*注意:由于Family类同时实现了FamilyMember和FamilyDetails接口,
         * 而这两个接口中都有一个名为Take的方法,所以使用了显示接口,
         * 当然,显示接口是不能被public修饰符修饰的
         */
    }
}

Program类(主类):

using Fifth;
class Program
{
    static void Main(string[] args)
    {
        Family myFamily = new Family { Name="Smith"}; //创建Family类的实例并设置Name属性
        //调用各个接口的方法
        myFamily.FamilyName();
        myFamily.Role();
        myFamily.Familys();
        myFamily.Childs();
        myFamily.Details();
        Console.WriteLine();
        //myFamily.Take(); 这一行会报错,因为Take方法在Family类中没有public实现
        //通过接口引用调用Take方法
        ((FamilyDetails)myFamily).Take();
        ((FamilyMember)myFamily).Take();
    }
}

结果:

后记

至此,基本内容已经完成,很显然,有点偏离我原本的笔记式博客,在这里我对各位抱歉

不过再怎么说,学完这些你们都会有所收获吧,如果说有哪里写high了漏了(错了)请务必在评论区/QQ/B站(这里虽然提到B站了但不推荐)告诉我,我会做出修改的

这个是屑