【C#学习笔记】C#特性的继承,封装,多态

在这里插入图片描述

文章目录

  • 封装
    • 访问修饰符
    • 静态类和静态方法
    • 静态构造函数
  • 继承
    • 继承原则
    • sealed修饰符
    • 里氏替换原则
    • 继承中的构造函数
  • 多态
    • 接口
      • 接口的实例化
    • 抽象类和抽象方法
      • 抽象类和接口的异同
    • 虚方法
    • 同名方法
      • new覆盖的父类方法
      • 继承的同名方法
    • 运行时的多态性
    • 编译时的多态性


照理继承封装多态应该是学习笔记中最先学习的。但是本系列不是面向新手的,基础的继承封装多态的概念应当是要被掌握的。而本文需要讲述C#中的一些继承封装多态的特性。

部分摘自C#学习笔记(二)— 封装、继承、多态

封装

封装,就是将类的属性和方法封闭起来,外部成员不可直接调用,只能通过预留的接口访问。

访问修饰符

在之前引用对象介绍class的时候,我们已经介绍过:
访问修饰符主要分为四种:

  • public:同一程序集中的任何其他代码或引用该程序集的其他程序集都可以访问该类型或成员。(public属性可被继承)
  • private:只有同一 class 或 struct 中的代码可以访问该类型或成员。(private属性不可被继承)
  • protected:只有同一 class 或者从该 class 派生的 class 中的代码可以访问该类型或成员。(只有该类和其子类可访问)
  • internal:同一程序集中的任何代码都可以访问该类型或成员,但其他程序集中的代码不可以。 换句话说,internal 类型或成员可以从属于同一编译的代码中访问。

我们可以用访问修饰符来定义类的访问类型
111
默认当类未定义访问修饰符时,访问级别是internal。而其内的成员的未定义访问修饰符时默认未private。

class Manager // 在命名空间中定义,默认为internal,可被同一程序集的另一个代码文件访问
{class Parent // 在类中定义的成员类,默认为private,不可被同一程序集的另一个代码文件访问{}
}

(注意,在命名空间中,类的访问修饰符只能为internal或public,因为其他访问修饰符在命名空间中访问不了,也就没有定义的必要了)

静态类和静态方法

使用static修饰符定义的类被称为静态类。静态类基本上与非静态类相同,但存在一个差异:静态类无法实例化。 换句话说,无法使用 new 运算符创建类类型的变量。 由于不存在任何实例变量,因此可以使用类名本身访问静态类的成员。

例如,如果你具有一个静态类,该类名为 UtilityClass,并且具有一个名为 MethodA 的公共静态方法,如下面的示例所示:

static class UtilityClass
{public static int a = 1;public static void MethodA() { }
}
static void main(){UtilityClass.MethodA();
}

使用静态类,我们不用实例化对象,而是直接调用类中的public方法。其内部成员只能是静态的。(而实际上其实普通类也可以通过类名访问静态成员)
无法继承,无法实例化,静态类的主要目的是提供一个容器来组织和管理相关的静态成员。

静态构造函数

当我们第一次访问静态类的时候,会调用一次静态构造函数。而第二次之后不会,因此起到了一个初始化的功能。

static class UtilityClass
{public static int a = 1;public static void MethodA() { }static UtilityClass(){Debug.Log("我是静态类的静态构造函数");}
}
class NormalClass
{public static void MethodA() { }static NormalClass(){Debug.Log("我是普通类的静态构造函数");}public NormalClass(){Debug.Log("我是普通类的实例构造函数");}
}
void Start()
{UtilityClass.MethodA(); // 我是静态类的静态构造函数NormalClass.MethodA(); // 我是普通类的静态构造函数UtilityClass.MethodA(); // 无NormalClass.MethodA(); // 无//UtilityClass u = new UtilityClass(); 错误,静态类无法实例化NormalClass n = new NormalClass(); // 我是普通类的实例构造函数
}

而如果单独实例化一次普通类:

void Start()
{NormalClass n = new NormalClass(); // n.MethodA(); 错误,实例化对象无法访问静态方法// 先后输出两行:// 我是普通类的静态构造函数// 我是普通类的实例构造函数
}

继承

继承就是在已存在的类基础上,建立一个新类,保留原有类的属性和方法,同时又可以增加新的内容。
 已存在的类称为基类或父类,继承的类称为派生类或子类。

继承原则

在C#中只有class,struct,interface可以继承,其中class可以继承唯一的父类和其他任意数量的接口类。struct和interface只能继承接口类。

在这里插入图片描述
每一层继承可以添加一些可被继承的方法给下一层。也有一些成员不可被继承。其关系请看上面写的访问修饰符。

当访问类中的成员时,就是逐级向上访问的,如果子类和父类有同名成员优先访问子类成员。

sealed修饰符

使用sealed修饰符,是为了让该类不能再被继承

sealed class Father{
}class Son:Father{  //错误,不可被继承
}

密封类的目的是不希望一些最底层的类被继承,加强面向对象的规范性,结构性和安全性。

里氏替换原则

任何基类可以出现的地方,派生类一定可以出现。从继承的角度来看,子类是在父类的基础上扩充的,自然比父类要全面。另一方面,要遵循里氏替换原则,子类中定义的一些方法也需要父类可用。

看个例子:

//基类
public class Mouse
{public void Dig(){Debug.Log("打洞");}
}
//派生类
public class MouseSon : Mouse
{public void Dig(){Debug.Log("不会打洞");}
}
static void main()
{Mouse m1 = new Mouse();m1.Dig(); // 打洞MouseSon m2 = new MouseSon();m2.Dig(); // 不会打洞,与父类方法重名时优先访问该类中的方法
}

老鼠的儿子不会打洞,就违反了里氏替换原则。这种情况下父类能用的时候子类是不能用的,子类无法替换父类。

通常里氏替换原则有两种基本要求:

  1. 可以用子类对象代替父类对象
  2. 父类容器包含子类对象时与1实现相同
//基类
public class Mouse
{public void Dig(){Debug.Log("打洞");}
}
//派生类,变异老鼠儿子,可以飞
public class MouseSon : Mouse
{public void Fly(){Debug.Log("飞");}
}
static void main()
{Mouse m1 = new Mouse();m1.Dig(); // 打洞MouseSon m2 = new MouseSon();m2.Dig(); // 打洞m2.Fly(); // 飞Mouse m3 = new MouseSon();m3.Dig(); // 打洞// m3.Fly(); 老鼠爸爸不能飞
}

继承中的构造函数

当子类继承父类之后,如果要在子类定义构造方法,要么父类中不定义任何构造方法,要么父类定义一个无参数的构造方法:

class Parent
{// 父类无构造方法或定义下列无参数构造方法//public Parent()//{//Debug.Log("我是Parent");//}
}
class Son : Parent
{public Son(){Debug.Log("我是Son");}
}

但是如果父类中定义了带参数构造方法且没有定义无参数构造方法,则子类中定义构造方法(无论是否带参数)都报错。除非使用base关键字定义传回对应参数:

class Parent
{public Parent(int i){Debug.Log("我是Parent");}
}
class Son : Parent
{public Son(int i) : base(i){Debug.Log("我是Son");}
}

总的来说,子类的构造方法和父类的构造方法参数数量必须是对应的。如果父类构造方法至少接受一个参数,那么相应的子类构造方法需要接受参数并调用父类构造方法。


多态

接口

定义接口时我们需要用interface关键字定义,标准的接口命名格式需要在开头加上大写的I代表interface。 接口可以包含方法、属性、索引器、事件。不能包含任何其他的成员,例如:常量、字段、域、构造函数、析构函数、静态成员。

interface IHuman
{// 接口中不允许任何显式实现void Name();void Age();public int Make { get; set; }public string this[int index]{get;set;}
}

当继承接口时,也需要实现接口的所有方法,一个接口同样可以继承其他的接口,当一个接口继承多个其他接口后,这个接口被称为派生接口,其他接口被称为基接口。一个继承了派生接口的类,不仅需要实现派生接口中的所有方法,也需要实现基接口中的所有方法:

interface IBaseInterface
{void BaseMethod();
}
interface IEquatable : IBaseInterface
{bool Equals();
}
public class TestManager : IEquatable
{bool IEquatable.Equals(){throw new NotImplementedException();}void IBaseInterface.BaseMethod(){throw new NotImplementedException();}
}

接口的定义一般用于通用的行为,当多个类都需要实现这些行为,并且每个类实现该行为的方法都需要重写时,接口是十分必要的。

接口的实例化

在学习过程中,我本以为接口是无法实例化的,但事实上网络上很多博客的说法是错误的,C#中的接口是可以实例化的!

接口不能被直接实例化。它的成员通过实现该接口的任何类或者结构来实现。(MSDN)

public class SampleClass : IControl, ISurface
{void IControl.Paint(){System.Console.WriteLine("IControl.Paint");}void ISurface.Paint(){System.Console.WriteLine("ISurface.Paint");}
}SampleClass sample = new SampleClass();
IControl control = sample;
ISurface surface = sample;// The following lines all call the same method.
//sample.Paint(); // Compiler error.
control.Paint();  // Calls IControl.Paint on SampleClass.
surface.Paint();  // Calls ISurface.Paint on SampleClass.// Output:
// IControl.Paint
// ISurface.Paint

如上所示,当一个类继承了某接口,我们可以将这个类的实例隐式转换为改接口的实例。这个接口实例包含了object基类的四大方法和自身接口的方法,如果我们调用接口方法会发现接口实例的方法指向的是类重写的方法。这种方法的好处在于当一个类继承的接口拥有同名方法时,我们可以通过实例化接口来区分同名方法的调用。
当然也可以对未实现该接口的类显式转换为该接口,当然这并没有什么用,运行时是会报错的。


抽象类和抽象方法

抽象类和抽象方法的定义关键字是abstract,抽象方法必须要在抽象类中定义,并且抽象方法必须是public的。但是抽象类和接口一样不能被实例化,需要实现抽象类中所有的方法,重写抽象方法的关键词是override,抽象类也无法使用sealed进行修饰,因为它就是用于继承的目的才会被创建。

abstract class Parent
{protected int age = 1; public Parent() { }abstract public void callName();abstract public void callAge();
}
class Child : Parent
{public Child():base(){}public override void callName(){throw new NotImplementedException();}public override void callAge(){Debug.Log(age);}
}

抽象类毕竟也是类,比较特殊的是,虽然抽象类不可实例化,但它可以拥有一些实例化特性,例如属性或者构造方法等是可以不用abstract修饰的。子类也可以像继承了正常类一般使用。
如果一个抽象类继承了接口,那么它也需要实现接口中的方法,但是抽象类可以以抽象方法的形式来实现它:

    abstract class Parent:IHuman{protected int age = 1;public Parent() { }abstract public void callName();abstract public void callAge();public void Name(){throw new NotImplementedException();}abstract public void Age();}

抽象类可以实现一些具体的方法,也可以拥有具体的属性,除了可实现抽象方法外,其他的和普通的类也没有什么区别。

抽象类和接口的异同

相同点:抽象类和接口都需要继承来实现,都不可以被直接实例化,继承了它们的子类都需要实现其中的抽象(接口)方法,继承了接口的还需要实现其中的属性和索引器等
不同点:抽象类不可实例化,接口可以通过继承类的类型转换来间接实例化,抽象类可以有具体方法,接口只能定义函数头
网络上许多博客都把接口当做一个特殊的类,但我看来,接口就是接口,类就是类,这两个是完全不同的东西。甚至有些博客说什么“接口与抽象类的区别还有接口不能继承其他类,抽象类可以。”接口本来就不是类,只能继承接口,怎么继承类?

虚方法

使用virtual关键字来修改方法法、属性、索引器或事件声明,并使它们可以在派生类中被重写。 (静态属性不可使用virtual修饰符)

    class Animal{public virtual void Eat(){Debug.Log("吃素");}}class Lion:Animal{public override void Eat(){Debug.Log("吃肉");}}class Sheep:Animal{}

虚方法与使用抽象方法的区别在于:虚方法可以在任何类中使用,无论是普通类还是抽象类。而使用虚方法,我们可以灵活地决定是否需要在父类定义虚方法,而子类是否需要重写这个虚方法,这些都是自行决定的,不会有强制要求。正如同上述的例子,当需要重写时就用override重写虚方法,不需要就直接继承即可。此外,virtual关键字必须声明函数主体,这也意味着它无法用在接口或者抽象方法中。

        Lion lion = new Lion();lion.Eat(); // 吃肉Sheep sheep = new Sheep();sheep.Eat(); // 吃素Animal Cow = new Animal();Cow.Eat(); // 吃素Animal Tiger = new Lion();Tiger.Eat(); // 吃肉

同名方法

在子类中,可以使用一些同名的方法,主要是两种情形下:

  1. 覆盖父类方法时
  2. 继承的类和接口存在相同的方法名定义时

new覆盖的父类方法

举个例子:

    class Animal{public void Eat(){Debug.Log("吃素");}}class Lion:Animal{public new void Eat() // 是否使用new关键字都会覆盖,使用new来指示这是我们有意覆盖父类方法{Debug.Log("吃肉");}}class Sheep:Animal{}

根据上面继承的结构图,运行时会从子类到父类逐级查找方法,而子类new的同名方法覆盖了父类的方法,则会直接执行子类的方法。如果子类无定义则使用父类的方法:

        Lion lion = new Lion();lion.Eat(); // 吃肉Sheep sheep = new Sheep();sheep.Eat(); // 吃素Animal Cow = new Animal();Cow.Eat(); // 吃素Animal Tiger = new Lion();Tiger.Eat(); // 吃素

继承的同名方法

现在有一个如下的接口:

interface IEat
{void Eat();
}

现在我们有一个sheep类,它同时继承了AnimalIEat,定义如下:

    class Wolf : Animal, IEat{public void Eat(){Debug.Log("吃肉");}}void Start(){Wolf wolf = new Wolf();wolf.Eat(); // 吃肉IEat eat = wolf;eat.Eat(); // 吃肉}

我们发现上述情况下Eat()方法被同时重写了,Wolf类中的Eat方法覆盖了Animal类中的Eat方法,而同时也重写了接口的Eat方法。当出现同名方法时说明会同时覆写这些方法。而如果我们指定准确的方法名的话:

    class Wolf : Animal, IEat{public void Eat(){Debug.Log("吃肉");}void IEat.Eat(){base.Eat();}}void Start(){Wolf wolf = new Wolf();wolf.Eat(); // 吃肉IEat eat = wolf;eat.Eat(); // 吃素}

通过指明接口名可以准确定义不同方法。


运行时的多态性

细心的读者可能发现了:当我们用override重写虚方法时:

        Animal Tiger = new Lion();Tiger.Eat(); // 吃肉

而当我们覆盖原来的方法时:

        Animal Tiger = new Lion();Tiger.Eat(); // 吃素

在运行时,除了逐级向上查找要执行的方法(属性)之外,还会检查它是否是重写了virtual关键字,使用父类容器装子类对象时,如果虚方法已经被子类重写了,那么则会执行重写的方法。对于虚方法的执行只会执行第第一个查找到的overrider方法,talk is cheap,看看下面的例子:

    class Animal{public virtual void Eat(){Debug.Log("吃素");}}class Lion:Animal{public override void Eat(){Debug.Log("吃肉");}}class Tiger : Lion{public new void Eat(){Debug.Log("吃兔子");}}void Start(){Animal tiger = new Tiger();tiger.Eat(); // 吃肉}

上述例子中Eat()执行的是吃肉而非吃兔子,如果逐级上查的话第一个查找到的应当输出吃兔子。但是由于基类AniamlEat()是一个虚方法,所以执行的是逐级查找到的第一个被override重写的Eat()方法,所以是输出吃肉。

    class Tiger : Lion{public override void Eat() // 如果使用override再次重写{Debug.Log("吃兔子");}}void Start(){Animal tiger = new Tiger();tiger.Eat(); // 吃兔子Lion tiger = new Tiger();tiger.Eat(); // 吃兔子}

如果再次使用override重写,才会显示吃兔子,有意思的是尽管Lion类中的方法并不是virtual,我们还是重写了基类Animal的虚方法。

    class Lion:Animal{public sealed override void Eat(){Debug.Log("吃肉");}}class Tiger : Lion{public override void Eat() // 重写报错,Eat方法已经被密封了{Debug.Log("吃兔子");}}

如果我们不希望Tiger类在继承Lion后还重写Eat()方法,我们应当用sealed关键字来密封这个基类方法,保证它不会在继承Lion了之后再重写


编译时的多态性

在程序中,我们也需要用到一些同名但是不同参数的方法,这是为了方便使用同一个方法在不同情况下进行处理。我们称为重载(overload),例如:

    void Start(){HumanEat(吃点啥呢 ?);}string HumanEat(Animal meat) { return ""; }int HumanEat(Animal meat, Vegetable fruit) { return 1; }void HumanEat(Vegetable vegetable) { }

同样是人,同样是吃东西,但是不同的人吃的东西会不一样,素食主义者只吃素,大部分人荤素都吃,肉食主义者只吃肉。甚至有的时候我们需要函数的返回值也要做出区分。当我们需要调用HumanEat方法,又需要具体区分,重载是最好的选择,这样同一个函数就可以有灵活的调用,而不是把全部情况放在一个函数里,每多出一个情况,就重写原来函数,或是定义n多个方法,搞得光记个函数名都头大。


总结:本节的概念有点多,但每一个知识点都有其存在的意义,例如如果我们想要一个可以不用实例化,直接从命名空间调用的工具类,就应该使用静态类。使用静态类的静态构造函数保证初始化时的唯一调用。当使用不同的父类考虑继承的模式。当使用多态时考虑什么情况下应当使用什么样的多态?如果我们想要一个只有结构定义而没有具体实现,且不被实例化的基类就需要抽象类,当我们需要灵活的可继承的功能重写就需要接口。当我们需要多种情况下同种方法的实现就需要重载。如果希望多态性的体现,虚方法会优于覆盖方法。所有的面向对象特性都应当在实际使用时慎重考虑,设计时细心思考,根据经验来进行程序的设计。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/97893.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于 Vercel TiDB Serverless 的 chatbot

作者: shiyuhang0 原文来源: https://tidb.net/blog/7b5fcdc9 # 前言 TiDB Serverless 去年就有和 Vercel 的集成了,同时还有一个 bookstore template 方便大家体验。但个人感觉 bookstore 不够炫酷,借 2023 TiDB hackthon 的…

c#设计模式-结构型模式 之 代理模式

前言 由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接 引用目标对象,代理对象作为访问对象和目标对象之间的中介。在学习代理模式的时候,可以去了解一下Aop切面编程AOP切面编程_aop编程…

使用预制体画刷在游戏场景中快速布置预制体、粒子特效等

有时候在使用tilemap的时候,会希望在场景中添加更复杂的对象。 在2d-extras中,加入了预制件笔刷(Prefab Brush),可以将游戏物体预制体作为瓦片,来方便的在游戏场景中快速的绘制。可以自动适应游戏物体的位置…

Python Django 模型概述与应用

今天来为大家介绍 Django 框架的模型部分,模型是真实数据的简单明确的描述,它包含了储存的数据所必要的字段和行为,Django 遵循 DRY Principle 。它的目标是你只需要定义数据模型,然后其它的杂七杂八代码你都不用关心,…

总结 TCP 协议的相关特性

TCP协议段格式: 如图, 端口号: 是其中一个重要的部分,知道端口号才能确认数据交给哪个应用程序(端口号属于传输层的概念). 4位首部长度:4bit表示的范围是0->15,在此处,单位是"4字节",因此,将这里的数值 * 4,才是真正的报头长度,即TCP 报头最大长度,60…

基于Python的高校毕业生离校系统SpringBoot+Vue【源码+lw】

💕💕作者:计算机源码社 💕💕个人简介:本人七年开发经验,擅长Java、微信小程序、Python、Android、大数据等,大家有这一块的问题可以一起交流! 💕&#x1f495…

Spring Security OAuth2.0认证授权

(单体项目的认证,微服务项目的认证授权) 1.基本概念 1.1 什么是认证 进入移动互联网时代,大家每天都在刷手机,常用的软件有微信、支付宝、头条等,下边拿微信来举例子说明认证相关的基本概念,在…

【bug】Unity无法创建项目

bug UnityHub无法创建项目 UnityHub无法创建项目 出现的问题:在创建新项目时弹出来一个 无法创建项目 尝试的方法: 刷新许可证 ❌没用退出账号重新登陆 ❌没用重启电脑 ❌没用 最后发现是什么问题呢? 2021.3.3这个版本我之前在资源管理器中…

C++------利用C++实现二叉搜索树【数据结构】

文章目录 二叉搜索树概念二叉搜索树的操作查找插入删除 二叉搜索树的应用 二叉搜索树 概念 什么是二叉搜索树,二叉搜索树就是指左孩子永远比根小右孩子永远比根大。这个规则适用于所有的子树。 上面的就是一棵二叉搜索树,我们还可以发现这棵树走一个中…

大语言模型与语义搜索;钉钉个人版启动内测,提供多项AI服务

🦉 AI新闻 🚀 钉钉个人版启动内测,提供多项AI服务 摘要:钉钉个人版正式开始内测,面向小团队、个人用户、高校大学生等人群。该版本具有AI为核心的功能,包括文生文AI、文生图AI和角色化对话等。用户可通过…

LeetCode 2236. 判断根结点是否等于子结点之和

【LetMeFly】2236.判断根结点是否等于子结点之和 力扣题目链接:https://leetcode.cn/problems/root-equals-sum-of-children/ 给你一个 二叉树 的根结点 root,该二叉树由恰好 3 个结点组成:根结点、左子结点和右子结点。 如果根结点值等于…

学习网络编程No.3【socket理论实战】

引言: 北京时间:2023/8/12/15:32,自前天晚上更新完文章,看了一下鹅厂新出的《扫毒3》摆烂至现在,不知道是长大了,还是近年港片就那样,给我的感觉不是很好,也可能是国内市场对港片不…

通过微软Azure调用GPT的接口API-兼容平替OpenAI官方的注意事项

众所周知,我们是访问不通OpenAI官方服务的,但是我们可以自己通过代理或者使用第三方代理访问接口 现在新出台的规定禁止使用境外的AI大模型接口对境内客户使用,所以我们需要使用国内的大模型接口 国内的效果真的很差,现在如果想使…

计算机视觉掩模区域与二值图像

掩模区域 在图像处理中,我们经常需要对图像中的某些特定区域进行操作,例如对某个区域进行滤波、变换、裁剪或者其他处理。为了实现这些操作,我们需要明确指定这些区域,这就是掩模区域的作用。 掩模区域通常由一个二值图像表示&…

Android Alarm闹钟API使用心得

前言 有什么办法可以在不打开App的时候,也能够触发一些操作呢?比如说发送通知,解决这个需求的办法有很多种选择,比如说官方推荐的WorkManager API,可以在后台执行一次性、耗时、定时的任务,但WorkManager是…

【开源项目】Stream-Query的入门使用和原理分析

前言 无意间发现了一个有趣的项目,Stream-Query。了解了一下其基本的功能,可以帮助开发者省去Mapper的编写。在开发中,我们会编写entity和mapper来完成业务代码,但是Stream-Query可以省去mapper,只写entity。 快速入…

Beats:安装及配置 Metricbeat (一)- 8.x

在我之前的文章: Beats:Beats 入门教程 (一)Beats:Beats 入门教程 (二) 我详细描述了如何在 Elastic Stack 7.x 安装及配置 Beats。在那里的安装,它通常不带有安全及 Elasticsearc…

OCR相关模块——版面分析技术、表格文本识别

OCR相关模块——版面分析技术、表格文本识别 版面分析技术表格识别技术 版面分析技术 版面分析模型:飞桨用到了yolov2检测模型,对文档图片中的文本、表格、图片、标题与列表区域进行检测。当前主流是用分割做。 表格识别技术 参考博文

Matplotlib数据可视化(一)

目录 1.Matplotlib简介 2.Matplotlib绘图基础 2.1 创建画布与子图 2.2 添加画布属性 2.3 绘图的保存与显示 1.Matplotlib简介 Matplotlib是一个用于绘制数据可视化图表的Python库。它提供了广泛的功能和灵活性,可以创建各种类型的图表,包括折线图、…

MAC QT开发攻略

文章目录 基础步骤安装QT、QTCreator安装CMakeNinja 安装Clion编译器在QTCreator中新建项目更改CMake生成器 导入Clion CMake生成文件 基础步骤 安装QT、QTCreator 安装CMake 由于clion需要使用cmake构建 Ninja Ninja下载 安装Clion编译器 Clion 2023.1.3 破解版安装教程…