为何开始
人已是大三之年,虽是身在985,心里却没有半分985的底气,自从大二分流以来,自己几乎是没再系统的学过什么,除了几位知识还算渊博的老师教了较为详细的数据库、数据结构的知识之外,其他老师大抵都是迷迷糊糊的念些大家都知道的大条话,然后让我们自己去作罢了,而回顾自己所做的,大抵几乎都是拿着Vue和SpringBoot所写的几个破烂网页,以及不少的文档,哈,文档,我倒是觉得这才是他们最喜欢的东西。本来这些,都是我心的小声话,直到这一学期有一门.net的课程,我本以为这又会是一门写写项目水水文档的课程,但当我发现老师开始讲授C#中每一个语法特性时甚至开始让我们阅读反编译的IL语言的程序时,我瞬间又有了当年大一苦苦学习C++和C然后刨根问底的感觉,但回想起C++和C我脑子里到底还剩下多少呢?想到这里,我不由得出了一身冷汗,我最基础的编程能力,我最基础的用代码解决问题的能力似乎正在消失,我开始逐渐沦为框架的奴隶,至于底层的原理,似乎早就已经不存在了。
想到此处,悲从中来,但如同每个意识到自己过错的人,人不能就此就认命了,我在此写下我技术的忏悔录,从一门门我学过的东西开始,我将记录下我所还记得的,以及曾经记得的一切,如有错误,还请大家指正。
正文
C#中命名空间与类的初步理解
首先先是一段向最为基础的C#代码
using System;
namespace ProgramSpace
{public sealed class Program{public static void Main(){Console.WriteLine("Hi,C#");}}
}
可以说作为一段代码,它简直像个小透明一般,天真无邪,一共五行语句,它只是乖乖的输出一个"Hi,C#",大家学习一门新语言往往不屑于看它,然而它真的无邪吗?我先在这里留下五个问题,若是大家人人都能说的上来,那我只能怪我自己水平低的离谱了。
using System是什么意思,VS中这一行被标注为多余的,但用CSC命令编译没有这一行会报错,为什么?
namespace ProgramSpace它限定了一个怎样的空间,它和类是一个怎样的对应关系?这个代码文件叫什么,这个文件的名称必须和它一样吗?
在public sealed class Program中,sealed是什么意思,它有什么作用,这个文件的名称需要和类一样吗,这个类如果不是public类型这个程序还能正常执行吗?
在public static void Main()中,public是必须的吗,static是必须的吗,void能变成其他类型吗?
在语句Console.WriteLine("Hi,C#")中,Console.WriteLine这个功能是在哪里实现的?
这些问题并不普通,应为大家都不会去在意,现在我们一点一点来回答问题并说明命名空间与类的知识。
首先第一个问题,using System的含义为在程序中包含System命名空间,并使用其中的类,在进行这样的声明后,在一个命名空间外使用该命名空间内的类与变量时就可以不再前缀命名空间的名字,如第五句中的Console.WriteLine本应写为System.Console.WriteLine,现在只需写为Console.WriteLine。除此之外,因为VS编译器往往会自动引入System命名空间,所以在VS编程环境下,此句往往可以被省略,但要注意的是,在没有VS支持下的环境里,此种做法有可能并不被允许,例如直接使用CSC命令行编译。
对于第二个问题,命名空间实际上限定了一个只有不同名类的空间,毕竟其设计的目的就是令名称相同的类互不冲突,值得注意的是,命名空间是可以嵌套的,也就是说你可以写出以下形式的代码:
namespace namespace1
{namespace namespace2{//......}
}
至于命名空间与类的关系,一个命名空间中可以包含多个类,而且命名空间与类并没有名称的限制,同样的,命名空间与文件也没有名称的限制,一个文件也可以包含多个命名空间,总的来说C#的命名与数量包含关系是相对宽松的,没有像java一样强制要求文件名与类名相同且一个文件中只能有一个类。
除此之外,还有个很有趣的问题,在我们的示例代码中,如果你将命名空间的声明删除,也就是说直接将类暴露于文件中,你会发现此文件依旧可以编译运行,这时我们不禁想问,这个类去了哪里,我们又该如何在命名空间中使用它,求助于chatGPT我们得到了以下的回答
// Q: C#中没有写在命名空间中的类将被存放在哪个命名空间中?
// A: 在C#中,如果没有显式地将类放在命名空间中,则该类将被放置在默认命名空间中。
// 默认命名空间是指在当前文件或项目中没有指定命名空间的代码。
// 如果整个项目中没有指定默认命名空间,则该类将被放置在全局命名空间中。
// 在使用类时,可以通过在代码中添加命名空间或使用完全限定名来引用这些类。
// 在C#中,全局命名空间没有名称,因此无法通过名称直接调用它。
// 相反,全局命名空间中的类型可以通过其完全限定名称来调用。
// 完全限定名称是指包括命名空间、类名和可能的嵌套类型在内的完整路径
// 例如,如果您有一个名为MyClass的类,它位于全局命名空间中,则可以使用以下方式调用它:
MyClass myObject = new MyClass();
// 这里并没有指定任何命名空间,因为MyClass在全局命名空间中。
//如果您有一个命名空间MyNamespace其中包含一个名为MyClass的类,则需要指定完整命名空间路径:
MyNamespace.MyClass myObject = new MyNamespace.MyClass();
通过反汇编我们也可以发现这一点,
也就是说我们没有写入命名空间的类将成为一种全局变量一般的存在,我们可以直接采用名称来使用这个类,这听起来是很不错的,简直太方便了,但当你开始发现你开始找不到自己到底在哪里写了一个同名的类的时候,你就不会觉得这么好了,所以,还是最好写在命名空间中吧。
对于第三个问题,在 C# 中,sealed 是一个关键字,用于表示一个类、方法或属性是最终的,不能被继承或重写,通常,在以下情况下我们将使用sealed关键字:
类已经达到了其预期的设计目的,不需要被其他类继承和扩展。
为了避免在继承层次结构中的方法或属性被修改,从而破坏代码的稳定性和可靠性。
在当前情况下,显然属于第一种,在此处的类中我们完成了Main函数,这个类显然已经完成了它的使命,不过值得注意的是并不是每次和到处都是这样,需要注意的是,sealed 关键字应该谨慎使用,因为它可能会限制代码的扩展性和灵活性,在设计类和方法时,应该权衡是否需要使用 sealed 关键字,至于其他的限定,我们将在之后涉及到。
对于public大家必然是都不陌生了,但在此还是要注意的是,包含Main函数的类必须为public型,否则编译器将无法访问其中Main函数以启动程序,话是这么说的,但实际上此处加不加public对编译结果并不影响,但为了良好的编程习惯,最好还是写上吧。
对于第四个问题,我们要记住的是,约定!约定!还tm是约定!public static void Main是编译器与我们的约定,这样的一个函数将成为我们程序执行的入口,其中的三个限定只要更改任何一个编译器都会翻脸不认人,吗?实际上编译器的脾气好得很,在这一串修饰中,public并不必须,甚至private也可以,对于void其实改为int型也可以,这点主要看我们的需求,在这之中只有static是妥妥不能改变的,但还是那句话,为了规范。
对于第五个问题,可以回去看看第一个问题的解释。
不得说的一个工具
大家可能也注意到了,我上面使用了反汇编技术来查看程序的结构,以上我使用的程序是VS自带的一款.net平台反汇编工具ildasm,它可以将.NET程序集中的IL代码转换为易于阅读和理解的汇编语言代码,简单来说它可以看到程序的内部结构还可以看到各部分的IL语言实现,至于IL是什么我们后面再说,它的使用方法并不复杂,如下所示:
在开始菜单打开VS文件夹(不局限于版本),选择Developer PowerShell
在命令行中输入"ildasm 程序集名"即可使用
它将成为你日后深入了解.NET的好帮手。
使用命令行生成一个C#程序集
是不是以为我会开始回忆关于字段、方法、属性、事件的内容了,这些我们先放放,主要是我太害怕自己会忘了命令行这个不大不小的点,以及后面还有一个小把戏。
在平时的开发中,我们几乎很少用到命令行来编译.cs文件,因为VS为我们提供了很方便的编译手段,我们能够较为轻松快捷的获取结果,但有时候我们需要使用命令行界面(CLI)而不是集成开发环境(IDE)来编译我们的代码或者我们需要以一种自动化的方式批量编译多个C#源代码文件又或是我们的代码量是在少得可怜,我们需要一种更为轻量化的编译方式。总之,使用csc命令行编译器可以提供更高的自定义和灵活性,同时也需要更多的手动工作和命令行操作。
在正式使用CSC命令之前,我们需要注意一点,我们需要将csc.exe的位置加入环境变量之后我们才可以使用,具体方法为在环境变量中加入csc项,内容为:
"C:/WINDOWS/Microsoft.NET/Framework/v4.0.30319/"
此处要注意的是,在Framework文件夹下有多个版本,大家可以自由选择,但还是要注意的是,低版本的csc.exe可能不支持某些语法,比如System.Console.WriteLine($"MaxValue: {n}")中内插符$的用法。
在加入完csc后别忘了在Path中加入"%csc%;"。
在完成这些准备后,我们就可以开始愉快的在cmd窗口使用csc命令来编译.cs文件了
我是很想在这里详细的记录一下csc的各种命令的,但很遗憾,我也是刚刚接触,会的可能不比任何一个人多,在以后的时间里,我会逐渐丰富这部分的内容,不过这都是后话了。
对于文件Program.cs,我们可以使用以下的命令来进行编译:
csc /out:Program.exe /t:exe /r:mscorlib.dll Program.cs
其中,"/out"控制输出文件的名称,"/t"控制输出程序的类型,"/r"用于控制引用的外部程序集。
说真的,看到这么一串还是很让人头疼的,但好在很多情况下我们不用写这么多,在默认情况下,输出程序名会与我们的源文件名相同,但我们又要注意一些问题了,那就是多个源文件的情况,哎,你是知道的,这些细节往往是无用的,但总要有人把他们写下来,如果多个源代码文件中存在一个包含 main 方法的源文件,则编译器会使用该文件的名称作为默认的输出程序名。如果没有任何一个源文件包含 main 方法,则编译器将生成一个库文件(.dll),而不是可执行文件(.exe),同时编译器将使用第一个源文件的名称作为编译结果的名称。总之,我们可以说,一般编译单个源文件且对名称没有要求时,我们可以省略"/out",同样与之相对应,在有多个源文件时,我们最好指定一个名称,以防我们获取到一个不可预测的回旋。
除了这个,"/t"在一般情况下也是可以省略的,因为C#编译器在缺省情况下会自动选择编译结果的类型,如以上所提到的.exe或者.dll。"/r"在很多情况下也是可以省略的,因为mscorlib.dll中包含了太多基础的内容,所以编译器一般都会直接加入它,而无需手动引用。
所以综上所述,我们的命令行可以缩写为csc Program.cs,真不错啊。
很好,我们现在已经明白了最基础的csc命令行的用法,下面我们稍稍操作一下,玩个小把戏。
我们现在有两块代码:
// 写在r.cs中的
public class csharpr
{public void rfunc(){System.Console.WriteLine("Hello r");}
}
// 写在f.cs中的
public class csharpf
{public void ffunc(){System.Console.WriteLine("Hello f");}
}
首先我们先使用命令行:csc /t:module r.cs
在此之后我们将获得一个文件:r.netmodule,在此说明一下,.netmodule是.net平台中的中间代码文件,由IL语言组成,关于IL的具体事项我们将在下面一部分说明(抱歉啊,字段、方法、属性、事件的内容又要延后了)
在此之后,我们使用第一个模块和第二个模块生成一个程序集:
csc /out:two.dll /t:library /addmodule:r.netmodule f.c s 在此之后我们将获得一个.dll文件:two.dll
我们可以来测试一下这个文件,首先我们写一段下面的代码(这三段代码为了省事都没有使用命名空间)
// 写在文件Client.cs里
public sealed class Program
{public static void Main(){System.Console.WriteLine("Hi,.NET");csharpf o = new csharpf();o.ffunc();}
}
然后使用命令:csc Client.cs /r:two.dll
运行生成的文件,不出意外,我们得到了以下的结果:
Hi,.NET
Hello f
我猜你会说,啊,不就这吗,谁没见过,你别急,我们再玩的花一点。
打开我们的VS,新建一个CLR C++ 控制台项目,就是底下这个:
写下以下的代码:
#include "pch.h"
#include "stdafx.h"using namespace System;int main(array<System::String ^> ^args)
{Console::WriteLine(L"Hello World");csharpf^ o = gcnew csharpf();o->ffunc();return 0;
}
再新建一个头文件 "stdafx.h"写入以下内容
之后再将我们的.dll文件放入项目的Debug文件夹内
#using "../Debug/two.dll"
using namespace System;
运行,你看见了什么宝贝儿?我猜是:
Hello World
Hello f
有没有一种出现错乱的感觉?为什么C#语言写成的.dll文件能在C++的项目中运行?这是为什么?说真的我也很难一时半会儿说清,这些问题都留到下一部分吧,有关与IL语言以及.net平台。
IL与.NET顺带C#
字段、方法、属性、事件的内容我们放到下一部分(这次真的不会再拖了呜)。我们在前面说了很多关于C#和.NET以及IL的事,我想它们之间的关系我们还没有说清楚,若是各位早已对其中的关系了如指掌,那么上面的这个小把戏肯定没法给你带来震撼,说不定你早已不耐烦的来到了这里,然后继续不耐烦的翻到了下面,但我是要把我脑子中的东西写下来,你是知道的,明天我就忘得一干二净了。
言归正传,要说明它们之间的关系,我们应当先从.NET平台开始说起,正如我们所言,.NET不是一门语言也不是一个编译器,它是一个平台,一个框架,或者再俗一点,一个新秩序国王。在这位国王的统治下,多种语言都可以运行在.NET平台上,而这些语言写成的程序并不会像传统C/C++语言写成的程序一样直接运行在电脑的操作系统中,而是运行在.NET的虚拟机中,类似于java的虚拟机,这也就代表着.NET框架下的程序是可以像java程序一样,跨平台运作,并不会受到操作系统的限制。回到我们所说的多种语言,C#就是.NET支持的多种语言的一种,而且是主流,除了C#你还可以使用很多其他的语言进行开发,具体有那些,建议还是去官网看看吧。
至于IL,我们要回到.NET是个"国王"这一点上来,若是一个国王治下的大臣们(各种语言)没法互相沟通,他怎样称得上是国王?另外,这些程序需要运行在.NET虚拟机上,有虚拟机就要有对应的操作指令,就如同操作系统的汇编指令,于是,IL呱呱坠地。
在.NET框架下,每一个程序都将先编译成中间语言IL程序,在执行时,再由即时编译器编译为本地平台代码来实现操作。这样做有两个好处,第一点是各种语言有了互通的能力,第二点是一个程序的执行可以不依赖于某个操作系统,怎么样,IL是不是还不错。
除了以上我们说到的好处,IL还有相当棒的一点,当我们搞不懂一个程序的运作机理时,我们可以反编译它,通过IL看清它的底细,具体的实践后面我们也会提到。
说到这里了,以上的小把戏是不是就不难搞懂了,估计大家也意识到了,因为在上面的小把戏中,我们使用的C++与正常的C++代码是大有不同的,因为这是基于.NET平台的C++项目,其中是做了一些适应.NET平台的改动的,至于这种C++,等我研究透了我或许会写一下吧。言归正传,因为两种语言都是基于.NET所以我们的.dll文件无疑是由IL语言构成,这也就让它可以被用IL执行的C++程序所引用,IL成为了沟通两种语言的桥梁。
IL虽好,但看起来有点小费事,毕竟它是汇编语言的形态,在这里留存一下部分IL指令:
真要命啊,真不少。
字段、属性、方法、类、事件
很好,我们终于说到这段了,在开始这一切之前我们首先要有一个意识,那就是C#中万物皆为对象,也就是说一切全都建立在类上,这与java十分类似(说真的我真怀疑是不是更应该是java与C#类似),也就是说你可以写出以下的代码:
using System;
namespace ProgramSpace
{public class Program{public static void Main(){Console.WriteLine(7.toString());}}
}
没错,在C#中就算是常量也是类的一种,7也是一个对象。
字段
先搞懂一件事,什么是字段?字段是在类或结构中直接声明的任意类型的变量。字段是其包含类型的成员。这话可真够抽象的,还是让我们来看个例子。
using System;
namespace ProgramSpace
{public class MyClass{private static readonly int ? num = 0;}
}
没错,这段代码中的num即为一个字段,前面的一大串皆为修饰,而重点也就在这一大串修饰上,来吧让我们看看有什么花样。
访问修饰符
首先是最开始private,对于这个修饰,我想我们是都不陌生的,但同类的修饰符都有多少呢?2个?3个?其实有五个,真是让人没想到。
public:同一程序集中的任何其他代码或引用该程序集的其他程序集都可以访问该类型或成员。 某一类型的公共成员的可访问性水平由该类型本身的可访问性级别控制。
private:只有同一 class 或 struct 中的代码可以访问该类型或成员。
protected:只有同一 class 或者从该 class 派生的 class 中的代码可以访问该类型或成员。
internal:同一程序集中的任何代码都可以访问该类型或成员,但其他程序集中的代码不可以。 换句话说,internal 类型或成员可以从属于同一编译的代码中访问。
protected internal:该类型或成员可由对其进行声明的程序集或另一程序集中的派生 class 中的任何代码访问。
private protected:该类型或成员可以通过从 class 派生的类型访问,这些类型在其包含程序集中进行声明。
这些规则真是复杂呀,要是想挨个说得明明白白,估计我得写到下星期了,好在有两件事情让我们得以宽心。第一件事是我们一般使用的较多的就是前四种类型,他们的含义还是相对直接明了的,另一件事是我们有可爱的微软官方的C#说明:访问修饰符 - C# 编程指南 | Microsoft Learn,不得不说有人帮我们做了太多繁琐的事情,哦对,还有一句话,最好不要将类中的字段声明为public,如果要访问或者修改,最好使用函数。
static
对于static修饰符大家肯定不陌生了,不再赘述,但有些有趣的性质,以及一些不为人知的小细节依旧值得我们去研究一下,依旧是可爱的官方说明:static 修饰符 - C# 参考 | Microsoft Learn
const
const在哪?很遗憾它没有出现在我们的例子里,因为它并不可以和static共存,毕竟一个静态的常数,着实还是有些无聊了,而且还有一点,它也不能和readonly共存,一个只读的常量,也是够无聊的,但这里我们不禁发出疑问,为什么有了const之后我们还需要readonly,这真是太奇怪了,明明这就是一个东西,至于为什么,我们去readonly那里解释。const 关键字 - C# 参考 | Microsoft Learn
readonly
对于刚进入C#中的人来说,readonly着实是一个新鲜的事物,面对一个新的修饰符,我们首先应当搞懂它的作用,那么它的作用是:在字段声明中,readonly 指示只能在声明期间或在同一个类的构造函数中向字段赋值, 可以在字段声明和构造函数中多次分配和重新分配只读字段。哇哦,这话可真抽象,不过说白了就是可以在声明和构造函数里才可以赋值,这给予了它高于const的灵活性,在构造函数之后,可以说它在功能上等价于const。
那么我们究竟为什么要构建这样一个定义呢,因为readonly可以在字段声明和任何构造函数中多次分配 readonly 字段。 因此,根据所使用的构造函数,readonly 字段可以具有不同的值,这就是它的优势。
但还有一点需要注意一下,在编译时const会被当做常量处理,在反编译中你会看到的是这个字段被直接写为一个常量,但readonly不会,它在编译中不会以常数出现,因为它有可能因构造函数而变化,这是极小的一个差别,但说真的,谁知道这点差别在某一天会不会引发一起程序的惨案。
还是照例附上网址:readonly 关键字 - C# 参考 | Microsoft Learn
可空类型
可爱的int还是放到后面再说吧,我实在是忍不住说这个?了,说真的我第一次看到这种写法时不禁眉头一皱,心想这是什么四不像的玩意,不过后来明白之后,我也依旧没觉得这个写法多好看。
先不谈好不好看这一说,还是先说有什么用吧,我们前面提到过,在C#中万物皆为对象,我们声明的num也不例外,而既然是对象,那就有可能是个空对象,有可能是个null(不知当不当说更像指针为null),所以我们的num有可能遭遇以下的情况:
num = null;
作为num的创造者,我们自然应当决定这种做法是否可行,这时候可空类型就上场了,如下所示
// 使num可以取null
private static readonly ? num = 0;
// 使num不可取null
private static readonly num = 0;
其实可变类型还有种更加帅气的写法
private static readonly Nullable<int> num = 0;
但实在是太麻烦了,还是用?吧。
随着可空类型与不可空类型的出现,一个问题也出现了,如下所示:
// i是一个不可空类型的字段
int i = 0;
// m是外部不知道谁声明的字段不知道是不是可空也不知道是不是null
i = a;
如果此时a为null则程序会出现错误,怎么办?那就优化:
// i是一个不可空类型的字段
int i = 0;
// m是外部不知道谁声明的字段不知道是不是可空也不知道是不是null
//加入判断
i = (null == a) ? 0 : a;
或者
// i是一个不可空类型的字段
int i = 0;
// m是外部不知道谁声明的字段不知道是不是可空也不知道是不是null
//加入判断
if(null != a)i = a;
elsei = 0;
这样写多少是有点麻烦了,要知道这种判断在程序中可能要出现不知多少次,不过还不错的一点是C#为我们的判断加入了一种简单的写法:
// i是一个不可空类型的字段
int i = 0;
// m是外部不知道谁声明的字段不知道是不是可空也不知道是不是null
//加入判断
i = a ?? 0;
这种写法等价于上方的两种写法,还算简单吧。
另外再说一个有趣的现象或者说妙用,对于bool型来说如果使用可空类型,bool型除了true和false将出现第三个取值null,这就相当于对于一个选项来说,除了是或者否还有了不确定这个选项,哇哦,多少有点三进制的感觉了,这个特性到底能做什么,还是只能靠实践来说话了。
类型
这真没什么好说的,来张图吧
唯二值得注意的是,这里的char是两个字节的,而且有了byte这个类型。
属性
属性很难说是一种新的单元,它更像是字段的升级版,虽说它并不提供不可替代的功能,但微软官方依旧公开表示它是C#中的一等公民,由此可见,它自有它的优点和重要性,不必多说,我们来看一下它的用法。
我们首先来看一个最简单的属性
public class MyClass
{// 声明了一个属性public string name{get { return name; }set { name = value; }}
}
相对于字段,这个声明可真是复杂太多了,不着急,让我们从头看看。
首先是get,这实际上是我们name的访问时访问器,当我们如字段般访问name时,我们实则调用了get函数,我们获取的内容实际为get返回的内容,在这个例子中我们返回的就是name自身。我猜这时你会说哇偶,好无聊啊,这有什么用啊,好吧,让我们来个精彩点的。
public class MyClass
{// 声明了一个属性public string name{get { return "我不到啊"; }set { name = value; }}
}
你猜会怎么样?没错,在后面你无论怎么给name赋值,你想要读取或者使用name的值时,你获得的永远是"我不到啊",利用这个特性,你完全可以让整个团队都乱成一锅粥,咳咳,当然,C#提供这个功能不是让我们来谋害同事的,我们更常用这样用:
public class MyClass
{private string firstName;private string lastName;public string name{// 利用get计算出完整名称get { return firstName + " " + lastName; }}
}
public class MyClass
{private string firstName;private string lastName;// 真正会被用的全名private string fullname;// 用于处理修改fullname的数据public string name{// 利用get计算出完整名称get { if(fullname is null)fullname = firstName + " " + lastNamereturn fullname; }}
}
//但说真的这样会不会写个函数更方便
这时你可能会问,还搁呢儿get,我set呢?别急,set是一个可选项,当我们不包含set时,这个属性对外表现为只读,它不可被直接赋值。
我们再说说set,set本身的特性和get很像,只不过是给属性赋值时会触发,我们同样可以在里面玩点小花样,不过要注意的是,在set中,来自外部的值被表示为value
public class MyClass
{private string fullName;// 声明了一个属性public string name{get { return name; }set { name = value; fullName = value; }}
}
//这个例子没什么实际意义说真的
最后,让我们回到最初的那个例子,我们在里面写了真不少括号,那有没有简单一些的写法呢?当然是有的。
在我们的get及set只有一条语句时,我们可以简写为:
public class MyClass
{// 声明了一个属性public string name{get => return name;set => name = value;}
}
还能不能再简单点?当然可以,由于get时返回本身,set时设置本身是绝大部分时的情况,所以在这种情况下我们可以写为:
public class MyClass
{// 声明了一个属性public string name { get; set; }
}
真不错是吧,但它的作用也有待商榷,说真的这和一个普通字段到底有多大的区别?
另外关于修饰符,它基本上是与字段用同一套修饰符规则,当然,还是附上官网,里面还有很多小玩意
C# 中的属性 | Microsoft Learn
方法
方法这个名词无疑还是有点模糊了,至少是对于我而言,其实他有个更加朴实的名称——函数,其实在上面的文章里我已经多次混用,希望没留下什么误会。
可以说主流语言中方法的定义方式和使用方法都很相似,这里不再赘述,还是重点说一下其中的修饰符,先上一段代码:
using System;namespace ProgramSpace
{// 抽象类public abstract class BaseClass{// 使用virtual形成虚函数,此处是为了演示,实际不推荐在抽象类中使用public virtual void Virtualfunc(){Console.WriteLine("In BaseClass vfunc");}// 使用abstract形成抽象函数,抽象函数不可在抽象类中实现public abstract void AbstractFunc();// 此处使用override重写了系统提供的ToString函数,并且使用sealed阻止重写public sealed override string ToString(){Console.WriteLine("In BaseClass sealed");return "BaseClass ToString";}}// 继承上面的抽象类internal class DerivedClass : BaseClass{// 重写上面的虚函数public override void Virtualfunc(){Console.WriteLine("In DerivedClass vfunc");}// 实现继承的抽象函数public override void AbstractFunc(){Console.WriteLine("In DerivedClass vfuncAbstract");}//Error,重写被sealed限制的函数将会报错//public sealed override string ToString()//{// Console.WriteLine("My God");// return "Error";//}}// 测试public class Test{public static void Main(){Console.WriteLine("Hi");DerivedClass o2 = new DerivedClass();BaseClass o3 = o2;o3.Virtualfunc();o3.AbstractFunc();o3.ToString();o2.Virtualfunc();o2.AbstractFunc();o2.ToString();}}
}
// 以下是输出结果
// Hi
// In DerivedClass vfunc
// In DerivedClass vfuncAbstract
// In BaseClass sealed
// In DerivedClass vfunc
// In DerivedClass vfuncAbstract
// In BaseClass sealed
修饰访问符与类型
方法的修饰访问符与类型的规则几乎与字段的相同,可以参考上面
virtual
virtual修饰符用于生成虚函数,我十分确定这是C#从C++那里带来的产物,具体为什么我们还是看一下它的作用。
虚函数的作用很简单,如代码所示,我们用一个子类给父类赋值,理论上来讲,父类只会保存子类中从父类继承的那一部分,所以理论上来讲第二条输出应当是原装的"In BaseClass vfunc",但结果却是"In DerivedClass vfunc",即子类中所重写的内容。所以我们可以说,虚函数的作用就是让被子类赋值的父类的虚函数来使用子类重写的虚函数,哇哦,有点抽象说真的,或许你会说这有什么用啊,我们还是回到C++去看一眼。
C++中没有C#中的自动内存管理,基本上类的使用要手动申请内存,然后用指针指向指向一个类的空间,使用一个类时也要利用指针,当我们想要用一个父类的指针来使用多个子类的空间时,例如使用图形指针同时操控三角形和正方形,我们只需通过图形指针调用计算面积这个虚函数,三角形和正方形就会计算出自己的面积(前提是它们重写了虚函数),这也就实现了图形这个类的多态性:不管什么图形,我只需图形指针就可以方便的进行各种操作,同一种操作的结果会因为图形本身的性质而改变。
来到C#中虽然没有了指针,但道理依旧如此,说起来还是太单薄了,来段代码看看:
// 这里节选了一部分代码,并不完整,具体看后面链接内
public static void Main()
{//Circle Sphere Cylinder都是Shape的子类而且重写了Area虚函数double r = 3.0, h = 5.0;Shape c = new Circle(r);Shape s = new Sphere(r);Shape l = new Cylinder(r, h);Console.WriteLine("Area of Circle = {0:F2}", c.Area());Console.WriteLine("Area of Sphere = {0:F2}", s.Area());Console.WriteLine("Area of Cylinder = {0:F2}", l.Area());
}
官方的参考:virtual - C# 参考 | Microsoft Learn
abstract
abstract的作用是生成抽象函数,抽象函数的作用就是强制子类去实现父类定义的的抽象函数。还是以图形这个类举例,当我完成了图形类,我知道在使用时图形类派生的子类肯定需要有计算面积的功能,但我无法提前直到子类面积的计算方式,而且我可能根本不会与写子类的人交流,为了维护软件质量,我将使用abstract来进行修饰,这样一来,别人继承了我的类,他将必须重写面积这个函数,否则编译器将报错。
这时有人会问了,virtual功能不是差不多吗,都是让子类去重写,这时就是要注意,所有被子类继承的没有被限制为不可重写的函数子类都可以重写,但virtual不具有强制性,而且两者重点不同,还有一点就是,virtual与abstract不能同时使用。
最后再说一点,abstract函数只能出现在abstract类中。
官方的参考:abstract - C# 参考 | Microsoft Learn
sealed
我们在上文提到,所有被子类继承的没有被限制为不可重写的函数子类都可以重写,那这个限制为不可重写的修饰符是什么呢,答案就是sealed,当我们使用sealed去修饰一个函数时,那么它将成为不可重写的状态,一般来讲,这种情况出现在我们认为一个类中的某个功能已经达到目标要求,无需再子类中进行修改。虽说sealed可以保障后方代码的一致性,但它的这种限制也会带来很大的不方便,使用时一定要三思。
同时我们还要注意,sealed和virtual、abstract是不可以共用的,他们的含义很明显相互冲突。
官方的参考:sealed 修饰符 - C# 参考 | Microsoft Learn
override
在重写virtual和abstract和override函数时,我们需要使用override进行标注,override也只能重写这些函数,在一般使用时一般没有什么太多的变数,具体见官网:
override 修饰符 - C# 参考 | Microsoft Learn
static
static同样可以用来修饰函数,但不准和abstract、virtual、override一起用。
在函数中使用static有以下的作用
静态函数可以在不创建类的实例的情况下被直接调用,因为它们属于类而不属于类的实例。这使得静态函数更加方便和灵活,可以被用来执行与类相关的任务,而不需要先创建类的实例。
静态函数只能访问静态成员,不能访问实例成员。这是因为静态函数没有实例对象来访问实例成员。如果静态函数需要访问实例成员,那么它必须首先创建类的实例,然后通过实例来访问实例成员。
静态函数可以用来创建单例模式,即只有一个实例的类。这是因为静态函数只能在类加载时执行一次,因此可以保证单例模式中只创建一个实例。
类
修饰符
对于类的修饰符,修饰访问符同方法和字段,abstract同方法,sealed同方法,static类并不常见,但要注意,一旦一个类被声明为static,其中的所有成员必须为static
构造与析构函数
对于这两个函数大家一定是都不陌生了,构造函数负责在对象生成时进行设置,而析构函数负责在对象被销毁时做出一些操作。但在这里不得不说的一点是,在C#中析构函数的作用已经大大减小,析构函数也是来源于C++,在C++中类中往往存在着手动申请的内存,由于不能指望每次人都记得去释放内存,所以干脆写在virtual内,这样可以确保内存不会丢失,但在C#中,内存已经几乎不存在手动申请,因此析构函数的作用确实下降了。
索引器
说真的这真是个怪东西,还是让我们通过代码看看它:
public class SomeType
{private int[] list = new int[3];// 索引器public int this[int index]{get{int[] thisArray = { 11, 22, 33 };return thisArray[index];}set{list = value;}}// 重载索引器public int this[string content]{get{string[] thisArray = { "a", "b", "c" };// 返回内容在数组中的位置return Array.IndexOf(thisArray, content);} }// Mainpublic static void Main(){SomeType someType = new SomeType(); Console.WriteLine("{0} {1}", someType[0], someType[1]); Console.WriteLine(someType["a"]); }
}
看得出来的是索引器内部的get与set的规则与属性的相同,而且我们可以定义这个索引器数组的类型,甚至还可以重载索引,但它使用的方法着实是奇怪,竟然是直接在对象上使用[],至于它到底能带来多少方便,我对此持保留态度,当然还是附上官方参考:使用索引器 - C# 编程指南 | Microsoft Learn
事件
先不说别的,先来段代码看看:
public class SomeType
{public readonly Int32 SomeReadOnlyFiled = 2000;// 定义事件public event EventHandler ? Changed;// 一号事件static void Mone(Object ? sender, EventArgs e){if (null != sender){SomeType someType = (SomeType)sender;Console.WriteLine("Mone: {0}", someType.SomeReadOnlyFiled);}}// 二号事件void Mtwo(Object ? sender, EventArgs e){if (null != sender){SomeType someType = (SomeType)sender;Console.WriteLine("Mtwo: {0}", someType.SomeReadOnlyFiled);}}// 触发器void Trigger(){Changed?.Invoke(this, null);}// Mainpublic static void Main(){SomeType someType = new SomeType();// 订阅事件someType.Changed += SomeType.Mone;someType.Changed += someType.Mtwo;// 退订事件someType.Changed -= someType.Mtwo;// 触发事件someType.Trigger();}
}
说真的这个事件的模型还是非常简单的,这个模型其实可以加入很多委托的元素以成为一堆令人害怕的东西,不过我们这里先不考虑复杂的情况了,先说说最基础的吧,至于复杂的情况我们还是放在下一章的委托里讲吧。
为了让事件有存储的空间,我们首先要定义一个事件,在此之后就是自定义事件,如我们的一号和二号事件,在我们使用时我们将使用+=来订阅一个事件,用-=来退订一个事件,在最终我们需要触发时,我们可以使用Invoke函数,不过我猜大家肯定也注意到了,我们用的是Changed?.Invoke,这里的?的作用就是确保在没有事件时,即便我们使用触发也不会发生任何事,真不错的设计。
尾声
这篇文章真是写了不少东西呀,断断续续写了三天,以前总觉得别人的什么万字长文有多厉害,自己写一下才知道这不过也就那么回事。我的文章写的很散,具体内容的正确性也有待商榷,这只是我记忆的记录,我着实害怕自己忘得什么也不剩了,就这样吧,下一篇估计也是下周了,下一篇将是委托的专场,哎,这些东西还真是让人有点头疼,总想知道所有事,可总是远着呢,行吧,晚安了。