基础知识

仅供个人学习,来源于

.NET中的所有类型的基类是什么?

在.NET中所有的内建类型都继承自System.Object类型。在C#中,不需要显示地定义类型继承自System.Object,编译器将自动地为类型添加上这个继承申明。

值类型和引用类型的区别是什么?

在.NET中的类型分为值类型和引用类型,它们各有特点,其共同点是都继承自System.Object,但最明显的区分标准却是是否继承自System.ValueType。也就是说所有继承自System.ValueType的类型是值类型,而其他类型都是引用类型。常用的值类型包括:结构、枚举、整数型、浮点型、布尔型等等;而在C#中所有以class关键字定义的类型都是引用类型。

  1. 赋值时区别

这是值类型与引用类型最显著的一个区别:值类型的变量直接将获得一个真实的数据副本,而对引用类型的赋值仅仅是把对象的引用赋给变量,这样就可能导致多个变量引用到一个对象实例上。

  1. 内存分配区别

引用类型的对象将会在堆上分配内存,而值类型的对象则会在堆栈上分配内存。

  1. 继承结构不同

由于所有的值类型都有一个共同的基类System.ValueType,因此值类型具有了一些引用类型所不具有的共同性质,比较重要的一点就是值类型的比较方法:Equals。所有的值类型已经实现了内容的比较(而不再是引用地址的比较),而引用类型没有重写Equals方法还是采用引用比较。

装箱与拆箱的原理是什么?

  1. 装箱

CLR需要做额外的工作把堆栈上的值类型移动到堆上,这个操作就被称为装箱。

  1. 拆箱

装箱操作的反操作,把堆中的对象复制到堆栈中,并且返回其值。

struct与class的区别是什么?

  1. struct(结构)是值类型,而class(类)是引用类型

所有的结构对象都分配在堆栈上,而所有的类对象都分配在堆上。

  1. struct与class相比,不具备继承的特性

struct虽然可以重写定义在System.Object中的虚方法,但不能定义新的虚方法和抽象方法

  1. struct不能有无参数的构造方法

class默认就有无参数的构造方法,struct也不能为成员变量定义初始值。

C#中方法的参数传递有几种方式?

  1. ref关键字

引用传递参数,需要在传递前初始化;(ref 要求参数在传入前被初始化)

  1. out关键字

引用传递参数,需要在返回前初始化;(out 要求参数在方法返回前被初始化)

  1. params关键字

允许方法在定义时不确定参数的数量,这种形式非常类似数组参数,但形式更加简洁易懂。

浅复制与深复制的区别是什么?

  1. 浅复制

复制一个对象的时候,仅仅复制原始对象中所有的非静态类型成员和所有的引用类型成员的引用。(新对象和原对象将共享所有引用类型成员的实际对象。

  1. 深复制

复制一个对象的时候,不仅复制所有非静态类型成员,还要复制所有引用类型成员的实际对象。

.NET中的栈和堆的差异?

每一个.NET应用程序最终都会运行在一个OS(操作系统)进程中,假设这个OS的传统的32位系统,那么每个.NET应用程序理论上都可以拥有一个4GB的虚拟内存。.NET会在这个4GB的虚拟内存块中开辟三块内存作为 堆栈、托管堆 以及 非托管堆

  1. .NET中的堆栈

堆栈用来存储值类型的对象和引用类型对象的引用(地址),其分配的是一块连续的地址,在.NET应用程序中,堆栈上的地址从高位向低位分配内存,.NET只需要保存一个指针指向下一个未分配内存的内存地址即可。
对于所有需要分配的对象,会依次分配到堆栈中,其释放也会严格按照栈的逻辑(FILO,先进后出)依次进行退栈。(这里的“依次”是指按照变量的作用域进行的)。
.NET Core综合 - 图1

  1. .NET中的托管堆

众所周知,.NET中的引用类型对象时分配在托管堆上的,和堆栈一样,托管堆也是进程内存空间中的一块区域。But,托管堆的内存分配却和堆栈有很大区别。受益于.NET内存管理机制,托管堆的分配也是连续的(从低位到高位),但是堆中却存在着暂时不能被分配却已经无用的对象内存块
.NET Core综合 - 图2

  1. .NET中的非托管堆

.NET程序还包含了非托管堆,所有需要分配堆内存的非托管资源将会被分配到非托管堆上。非托管的堆需要程序员用指针手动地分配和释放内存,.NET中的GC和内存管理不适用于非托管堆,其内存块也不会被合并移动,所以非托管堆的内存分配是按块的、不连续的。因此,这也解释了我们为何在使用非托管资源(如:文件流、数据库连接等)需要手动地调用Dispose()方法进行内存释放的原因。

能简要说说.NET的GC运行机制么?

GC是垃圾回收(Garbage Collect)的缩写,它是.NET众多机制中最为重要的一部分,也是对我们的代码书写方式影响最大的机制之一。
.NET中的垃圾回收是指清理托管堆上不会再被使用的对象内存,并且移动仍在被使用的对象使它们紧靠托管堆的一边
通常情况下,我们不需要手动干预垃圾回收的执行,不过CLR仍然提供了一个手动执行垃圾回收的方法:GC.Collect()

了解Dispose和Finalize方法么?

  1. Dispose方法

通常我们会在Dispose方法中实现一些托管对象和非托管对象的释放以及业务逻辑的结束工作等等
我们一般会借助using等语法来帮助Dispose方法被正确调用

  1. Finalize方法

Finalize方法类似于C++中的析构函数(方法),但又和C++的析构函数不同。Finalize在GC执行垃圾回收时被调用。
Finalize方法由于有CLR保证调用,因此比Dispose方法更加安全(这里的安全是相对的,Dispose需要类型使用者的及时调用),但在性能方面Finalize方法却要差很多

.NET GC简单理解

内存分配

  • 计算对象大小。
  • 添加对象指针和同步索引块。
  • 从内存指针处开始,分配对象内存。
  • 问题:内存不能无限制增长。

    垃圾回收

  • 从应用程序实例出发,标记所有的引用对象。

  • 将标记对象移动到低地址端,修正实例引用地址与内存指针。
  • 问题:全内存移动对象,垃圾回收性能不高。

    分代回收

  • 将对象分代(0、1、2),对低代对象进行垃圾回收,幸存对象升级为高代对象。

  • 低代对象垃圾回收也无法满足时高代对象进行垃圾回收,若2代垃圾回收后仍不满足,抛出异常。
  • 问题:托管资源由CLR自动实现垃圾回收,但非托管资源无法处理。

    终结器机制

  • 非托管资源分配内存时,若对象在析构函数中编写资源释放代码,编译器根据析构函数自动生成Object.Finalize()方法,将该对象指针加入到终结列表。

  • 垃圾回收时,若终结列表中存在该对象指针则移除,并将对象实例添加到待终结对象列表,否则触发垃圾回收。
  • CLR启用高优先级后台线程,遍历执行待终结对象列表中的Finalize方法,并从队列中移除对象实例。
  • 问题:非托管资源在0代无法回收,可能存在升代情况,无法及时释放资源。

    IDisposable实现

  • 非托管资源对象继承IDisposable接口,实现Dispose()方法提供资源释放功能。

  • 使用者调用Dispose()方法及时回收非托管资源,并通知CLR不必再回收该非托管资源。
  • 在析构函数中调用Dispose()方法作为保护机制,防止因使用者未调用Dispose()方法,保证非托管资源能够最终释放。

    GC触发时机

  • 0代超过预算时。

  • 显式调用System.GC.Collect()。
  • Windows报告低内存。
  • AppDomain正在被卸载。
  • CLR正常关闭时。

    了解GC中的分代机制么?

    在.NET的GC执行垃圾回收时,并不是每次都扫描托管堆内的所有对象实例,这样做太耗费时间而且没有必要。相反,GC会把所有托管堆内的对象按照其已经不再被使用的可能性分为三类,并且从最有可能不被使用的类别开始扫描,.NET对这样的分类类别有一个称呼:代(Generation)
    GC会把所有的托管堆内的对象分为0代、1代和2代
  1. 第0代

新近分配在堆上的对象,从来没有被垃圾收集过。任何一个新对象,当它第一次被分配在托管堆上时,就是第0代

  1. 第1代

经历过一次垃圾回收后,依然保留在堆上的对象。

  1. 第2代

经历过两次或以上垃圾回收后,依然保留在堆上的对象。如果第2代对象在进行完垃圾回收后空间仍然不够用,则会抛出OutOfMemoryException异常
对于这三代,我们需要知道的是并不是每次垃圾回收都会同时回收3个代的所有对象,越小的代拥有着越多被释放的机会

.NET Core综合 - 图3
根据.NET的垃圾回收机制,0代、1代和2代的初始分配空间分别为256KB、2M和10M

.NET托管堆是否可能出现内存泄漏?

内存泄露是指内存空间上产生了不再被实际使用却又不能被分配的内存空间,其意义很广泛,像内存碎片、不彻底的对象释放等都属于内存泄露现象。内存泄露将导致主机的内存随着程序的运行而逐渐减少,无论其表现形式怎样,它的危害是很大的,因此我们需要努力地避免。

.NET中的类可以多继承吗?

在C#中申明一个类型时,只支持单继承(即继承一个父类),但支持实现多个接口(Java也是如此)

了解.NET中的重写、重载和隐藏吗?

重写、重载和隐藏的机制,是设计高可扩展性的面向对象程序的基础。

  1. 重写(Override)

是指子类用Override关键字重新实现定义在基类中的虚方法,并且在实际运行时根据对象类型来调用相应的方法。

  1. 隐藏(new)

是指子类用关键字重新实现定义在基类中的方法,但在实际运行时只能根据引用来调用相应的方法。

  1. 重载(Overload)

拥有相同名字和返回值的方法却拥有不同的参数列表,它是实现多态的立项方案,在实际开发中也是应用得最为广泛的。常见的重载应用包括:构造方法、ToString()方法等等;

如何声明一个类使其不能被继承?

在C#中可以通过sealed关键字来申明一个不可被继承的类,C#将在编译阶段保证这一机制

如何针对不同类型的异常进行捕捉?

Exception 使得程序方便易懂,但有时这样的捕捉对于业务处理没有任何帮助,对于特殊异常应该采用特殊处理能够更好地引导规划程序流程

了解Conditional特性吗?

通常在编译程序时可以选择Debug版本还是Release版本,编译器将会根据”调试“和”发布“两个不同的出发点去编译程序。
单纯的诊断和断言可能并不能完全满足测试的需求,有时可能会需要大批的代码和方法去支持调试和测试,这个时候就需要用到Conditional特性。Conditional特性用于编写在某个特定版本中运行的方法,通常它编写一些在Debug版本中支持测试的方法。当版本不匹配时,编译器会把Conditional特性的方法内容置为空

  1. // 只希望在DEBUG版本中出现
  2. [Conditional("DEBUG")]
  3. protected void Debug()
  4. {
  5. Console.WriteLine(_birthday.ToString("yyyy-MM-dd"));
  6. Console.WriteLine(_id);
  7. }

① Debug版本:
.NET Core综合 - 图4
② Release版本:
.NET Core综合 - 图5
Conditional机制很简单,在编译的时候编译器会查看编译状态和Conditional特性的参数,如果两者匹配,则正常编译。否则,编译器将简单地移除方法内的所有内容。

如何避免在类型转换时的异常?

在.NET中提供了另外一种语法来进行尝试性的类型转换,那就是关键字 is 和 as 所做的工作。

  1. is 只负责检查类型的兼容性,并返回结果:true 和 false。→ 进行类型判断

    1. public static void Main(string[] args)
    2. {
    3. object o = new object();
    4. // 执行类型兼容性检查
    5. if(o is ISample)
    6. {
    7. // 执行类型转换
    8. ISample sample = (ISample)o;
    9. sample.SampleShow();
    10. }
    11. Console.ReadKey();
    12. }
  2. as 不仅负责检查兼容性还会进行类型转换,并返回结果,如果不兼容则返回 null 。→ 用于类型转换 ```csharp

public static void Main(string[] args) { object o = new object(); // 执行类型兼容性检查 ISample sample = o as ISample; if(sample != null) { sample.SampleShow(); }

  1. Console.ReadKey();

}

  1. 两者的共同之处都在于:**不会抛出异常!**<br />综上比较,as is 在执行效率上会好一些,在实际开发中应该量才而用,在只进行类型判断的应用场景时,应该多使用 is 而不是 as
  2. <a name="syejE"></a>
  3. ### StringBuilder有何作用?
  4. StringBuilder其设计思想源于构造器(Builder)设计模式,致力于解决复杂对象的构造问题。**StringBuilder类型在最终生成String对象之前,将不会产生任何String对象,这很好地解决了字符串操作的性能问题。**
  5. <a name="KIlDX"></a>
  6. ### BASE64编码的作用及C#对其的支持?
  7. BASE64编码的设计致力于混淆那些8位字节的数据流(解决网络传输中的明码问题),在网络传输、邮件等系统中被广泛应用。需要明确的是:BASE64不属于加密机制,但它却是把明码变成了一种很难识别的形式。<br />BASE64的算法如下:<br />BASE64把所有的位分开,并且重新组合成字节,新的字节只包含6位,最后在每个字节前添加两个0,组成了新的字节数组。例如:一个字节数组只包含三个字节(每个字节又有8位比特),对其进行BASE64编码时会将其分配到4个新的字节中(为什么是4个呢?计算3*8/6=4),其中每个字节只填充低6位,最后把高2位置为零。
  8. <a name="J59MO"></a>
  9. ### 了解SecureString类型吗?
  10. **SecureString意为安全的字符串,它被设计用来保存一些机密的字符串,完成传统字符串所不能做到的工作**。<br />为了保证安全性,SecureString是被分配在非托管内存上的(而普通String是被分配在托管内存中的),并且SecureString的对象从分配的一开始就以加密的形式存在,我们所有对于SecureString的操作(无论是增删查改)都是逐字符进行的。<br />逐字符机制:在进行这些操作时,驻留在非托管内存中的字符串就会被解密,然后进行具体操作,最后再进行加密。不可否认的是,在具体操作的过程中有小段时间字符串是处于明码状态的,但逐字符的机制让这段时间维持在非常短的区间内,以保证破解程序很难有机会读取明码的字符串。<br />为了保证资源释放,SecureString实现了标准的Dispose模式(Finalize+Dispose双管齐下,因为上面提到它是被分配到非托管内存中的),保证每个对象在作用域退出后都可以被释放掉。
  11. <a name="PSEwM"></a>
  12. ### 了解字符串驻留池吗?
  13. 字符串具有不可变性,程序中对于同一个字符串的大量修改或者多个引用赋值同一字符串在理论上会产生大量的临时字符串对象,这会极大地降低系统的性能。对于前者,可以使用StringBuilder类型解决,而后者,**.NET则提供了另一种不透明的机制来优化,这就是传说中的字符串驻留池机制**。<br />使用了字符串驻留池机制之后,当CLR启动时,会在内部创建一个容器,该容器内部维持了一个类似于key-value对的数据结构,其中key是字符串的内容,而value则是字符串在托管堆上的引用(也可以理解为指针或地址)。**当一个新的字符串对象需要分配时,CLR首先监测内部容器中是否已经存在该字符串对象,如果已经包含则直接返回已经存在的字符串对象引用;**如果不存在,则新分配一个字符串对象,同时把其添加到内部容器中取。But,这里有一个例外,就是**当程序员用new关键字显示地申明新分配一个字符串对象时,该机制将不会起作用**。
  14. <a name="qkrtk"></a>
  15. ### int[]是值类型还是应用类型?
  16. **.NET中无论是存储值类型对象的数组还是存储引用类型的数组,其本身都是引用类型,其内存也都是分配在堆上的**。
  17. <a name="RXBrX"></a>
  18. ### 你知道数组之间如何转换的吗?
  19. 数组类型的转换需要遵循以下两个原则:<br />(1)包含值类型的数组不能被隐式转换成其他任何类型;<br />(2)两个数组类型能够相互转换的一个前提是两者维数相同;<br />我们可以通过以下代码来看看数组类型转换的机制:
  20. ```csharp
  21. // 编译成功
  22. string[] sz = { "a", "a", "a" };
  23. object[] oz = sz;
  24. // 编译失败,值类型的数组不能被转换
  25. int[] sz2 = { 1, 2, 3 };
  26. object[] oz2 = sz;
  27. // 编译失败,两者维数不同
  28. string[,] sz3 = { { "a", "b" }, { "a", "c" } };
  29. object[] oz3 = sz3;

能说说泛型的基本原理吗?

泛型的语法和概念类似于C++中的template(模板),它方便我们设计更加通用的类型,也避免了容器操作中的装箱和拆箱操作。

泛型的主要约束和次要约束是什么?

当一个泛型参数没有任何约束时,它可以进行的操作和运算是非常有限的,因为不能对实参进行任何类型上的保证,这时候就需要用到泛型约束。泛型的约束分为:主要约束和次要约束,它们都使实参必须满足一定的规范,C#编译器在编译的过程中可以根据约束来检查所有泛型类型的实参并确保其满足约束条件。

  1. 主要约束

一个泛型参数至多拥有一个主要约束,主要约束可以是一个引用类型、class或者struct。

  1. 次要约束

次要约束主要是指实参实现的接口的限定。对于一个泛型,可以有0到无限的次要约束,次要约束规定了实参必须实现所有的次要约束中规定的接口。

能说说流的概念吗?.NET中有哪些流?

流是一种针对字节流的操作,它类似于内存与文件之间的一个管道
常见的流类型包括:
.NET Core综合 - 图6
Stream类型继承自MarshalByRefObject类型,这保证了流类型可以跨越应用程序域进行交互。所有常用的流类型都继承自System.IO.Stream类型,这保证了流类型的同一性,并且屏蔽了底层的一些复杂操作,使用起来非常方便

你知道如何使用压缩流吗?

.NET中提供了对于压缩和解压的支持:GZipStream类型和DeflateStream类型,它们位于System.IO.Compression命名空间下,且都继承于Stream类型(对文件压缩的本质其实是针对字节的操作,也属于一种流的操作),实现了基本一致的功能。

  1. public class Program
  2. {
  3. // 缓存数组的长度
  4. private const int bufferSize = 1024;
  5. public static void Main(string[] args)
  6. {
  7. string test = GetTestString();
  8. byte[] original = Encoding.UTF8.GetBytes(test);
  9. byte[] compressed = null;
  10. byte[] decompressed = null;
  11. Console.WriteLine("数据的原始长度是:{0}", original.LongLength);
  12. // 1.进行压缩
  13. // 1.1 压缩进入内存流
  14. using (MemoryStream target = new MemoryStream())
  15. {
  16. using (GZipStream gzs = new GZipStream(target, CompressionMode.Compress, true))
  17. {
  18. // 1.2 将数据写入压缩流
  19. WriteAllBytes(gzs, original, bufferSize);
  20. }
  21. compressed = target.ToArray();
  22. Console.WriteLine("压缩后的数据长度:{0}", compressed.LongLength);
  23. }
  24. // 2.进行解压缩
  25. // 2.1 将解压后的数据写入内存流
  26. using (MemoryStream source = new MemoryStream(compressed))
  27. {
  28. using (GZipStream gzs = new GZipStream(source, CompressionMode.Decompress, true))
  29. {
  30. // 2.2 从压缩流中读取所有数据
  31. decompressed = ReadAllBytes(gzs, bufferSize);
  32. }
  33. Console.WriteLine("解压后的数据长度:{0}", decompressed.LongLength);
  34. Console.WriteLine("解压前后是否相等:{0}", test.Equals(Encoding.UTF8.GetString(decompressed)));
  35. }
  36. Console.ReadKey();
  37. }
  38. // 01.取得测试数据
  39. static string GetTestString()
  40. {
  41. StringBuilder builder = new StringBuilder();
  42. for (int i = 0; i < 10; i++)
  43. {
  44. builder.Append("我是测试数据\r\n");
  45. builder.Append("我是长江" + (i + 1) + "号\r\n");
  46. }
  47. return builder.ToString();
  48. }
  49. // 02.从一个流总读取所有字节
  50. static Byte[] ReadAllBytes(Stream stream, int bufferlength)
  51. {
  52. Byte[] buffer = new Byte[bufferlength];
  53. List<Byte> result = new List<Byte>();
  54. int read;
  55. while ((read = stream.Read(buffer, 0, bufferlength)) > 0)
  56. {
  57. if (read < bufferlength)
  58. {
  59. Byte[] temp = new Byte[read];
  60. Array.Copy(buffer, temp, read);
  61. result.AddRange(temp);
  62. }
  63. else
  64. {
  65. result.AddRange(buffer);
  66. }
  67. }
  68. return result.ToArray();
  69. }
  70. // 03.把字节写入一个流中
  71. static void WriteAllBytes(Stream stream, Byte[] data, int bufferlength)
  72. {
  73. Byte[] buffer = new Byte[bufferlength];
  74. for (long i = 0; i < data.LongLength; i += bufferlength)
  75. {
  76. int length = bufferlength;
  77. if (i + bufferlength > data.LongLength)
  78. {
  79. length = (int)(data.LongLength - i);
  80. }
  81. Array.Copy(data, i, buffer, 0, length);
  82. stream.Write(buffer, 0, length);
  83. }
  84. }
  85. }

上述代码的运行结果如下图所示:
.NET Core综合 - 图7
需要注意的是:使用 GZipStream 类压缩大于 4 GB 的文件时将会引发异常。
扩展:许多资料表明.NET提供的GZipStream和DeflateStream类型的压缩算法并不出色,也不能调整压缩率,有些第三方的组件例如SharpZipLib实现了更高效的压缩和解压算法,我们可以在nuget中为项目添加该组件。

能说说Serializable特性的作用吗?

在.NET中,通过Serializable特性提供了序列化对象实例的机制,当一个类型被申明为Serializable后,它就能被诸如BinaryFormatter等实现了IFormatter接口的类型进行序列化和反序列化

  1. [Serializable]
  2. public class Person
  3. {
  4. ......
  5. }

但是,在实际开发中我们会遇到对于一些特殊的不希望被序列化的成员,这时我们可以为某些成员添加NonSerialized特性。
注意:当一个基类使用了Serializable特性后,并不意味着其所有子类都能被序列化。事实上,我们必须为每个子类都添加Serializable特性才能保证其能被正确地序列化。

能说说委托的基本原理是啥吗?

委托实现了和函数指针类似的功能,那就是提供了程序回调指定方法的机制

  1. // 定义的一个委托
  2. public delegate void TestDelegate(int i);
  3. public class Program
  4. {
  5. public static void Main(string[] args)
  6. {
  7. // 定义委托实例
  8. TestDelegate td = new TestDelegate(PrintMessage);
  9. // 调用委托方法
  10. td(0);
  11. td.Invoke(1);
  12. Console.ReadKey();
  13. }
  14. public static void PrintMessage(int i)
  15. {
  16. Console.WriteLine("这是第{0}个方法!", i.ToString());
  17. }
  18. }

.NET Core综合 - 图8

委托回调静态方法 和 实例方法有什么区别?

能说说什么是链式委托吗?

链式委托也被称为“多播委托”,其本质是一个由多个委托组成的链表。回顾上面1.2中的类结构,System.MulticastDelegate 类便是为链式委托而设计的。当两个及以上的委托被链接到一个委托链时,调用头部的委托将导致该链上的所有委托方法都被执行。

  1. // 定义的一个委托
  2. public delegate void TestMulticastDelegate();
  3. public class Program
  4. {
  5. public static void Main(string[] args)
  6. {
  7. // 申明委托并绑定第一个方法
  8. TestMulticastDelegate tmd = new TestMulticastDelegate(PrintMessage1);
  9. // 绑定第二个方法
  10. tmd += new TestMulticastDelegate(PrintMessage2);
  11. // 绑定第三个方法
  12. tmd += new TestMulticastDelegate(PrintMessage3);
  13. // 调用委托
  14. tmd();
  15. Console.ReadKey();
  16. }
  17. public static void PrintMessage1()
  18. {
  19. Console.WriteLine("调用第1个PrintMessage方法");
  20. }
  21. public static void PrintMessage2()
  22. {
  23. Console.WriteLine("调用第2个PrintMessage方法");
  24. }
  25. public static void PrintMessage3()
  26. {
  27. Console.WriteLine("调用第3个PrintMessage方法");
  28. }
  29. }

.NET Core综合 - 图9
现在,我们再用一种更简单明了的方法来写:

  1. TestMulticastDelegate tmd = PrintMessage1;
  2. tmd += PrintMessage2;
  3. tmd += PrintMessage3;
  4. tmd();

最后,我们要用一种比较复杂的方法来写,但是却是链式委托的核心所在:

  1. TestMulticastDelegate tmd1 = new TestMulticastDelegate(PrintMessage1);
  2. TestMulticastDelegate tmd2 = new TestMulticastDelegate(PrintMessage2);
  3. TestMulticastDelegate tmd3 = new TestMulticastDelegate(PrintMessage3);
  4. // 核心本质:将三个委托串联起来
  5. TestMulticastDelegate tmd = tmd1 + tmd2 + tmd3;
  6. tmd.Invoke();

我们在实际开发中经常使用第二种方法,但是却不能不了解方法三,它是链式委托的本质所在。

链式委托的执行顺序是如何形成的?

+=的本质又是调用了Delegate.Combine方法,该方法将两个委托链接起来,并且把第一个委托放在第二个委托之前,因此可以将两个委托的相加理解为Deletegate.Combine(Delegate a,Delegate b)的调用。

  1. // 申明委托并绑定第一个方法
  2. TestMulticastDelegate tmd = new TestMulticastDelegate(PrintMessage1);
  3. // 绑定第二个方法
  4. tmd += new TestMulticastDelegate(PrintMessage2);
  5. // 绑定第三个方法
  6. tmd += new TestMulticastDelegate(PrintMessage3);
  7. // 获取所有委托方法
  8. Delegate[] dels = tmd.GetInvocationList();

上述代码调用了定义在 System.MulticastDelegate 中的 GetInvocationList() 方法,用以获得整个链式委托中的所有委托。接下来,我们就可以按照我们所希望的顺序去执行它们。

如何定义有返回值方法的委托链?

委托的方法既可以是无返回值的,也可以是有返回值的,但如果多一个带返回值的方法被添加到委托链中时,我们需要手动地调用委托链上的每个方法,否则只能得到委托链上最后被调用的方法的返回值

  1. // 定义一个委托
  2. public delegate string GetStringDelegate();
  3. class Program
  4. {
  5. static void Main(string[] args)
  6. {
  7. GetStringDelegate myDelegate1 = GetDateTimeString;
  8. myDelegate1 += GetTypeNameString;
  9. myDelegate1 += GetSelfDefinedString;
  10. foreach (var del in myDelegate1.GetInvocationList())
  11. {
  12. Console.WriteLine(del.DynamicInvoke());
  13. }
  14. Console.ReadKey();
  15. }
  16. static string GetDateTimeString()
  17. {
  18. return DateTime.Now.ToString();
  19. }
  20. static string GetTypeNameString()
  21. {
  22. return typeof(Program).ToString();
  23. }
  24. static string GetSelfDefinedString()
  25. {
  26. string result = "我是一个字符串!";
  27. return result;
  28. }
  29. }

.NET Core综合 - 图10

委托都有哪些可以应用的场合?

委托的功能和其名字非常类似,在设计中其思想在于将工作委派给其他特定的类型、组件、方法或程序集。委托的使用者可以理解为工作的分派者,在通常情况下使用者清楚地知道哪些工作需要执行、执行的结果又是什么,但是他不会亲自地去做这些工作,而是恰当地把这些工作分派出去

能说说事件如何使用吗?

事件是一种使对象或类能够提供通知的成员。客户端可以通过提供事件处理程序为相应的事件添加可执行代码。

事件 和 委托 有什么关系?

委托的本质是一个类型,而事件的本质是一个特殊的委托类型的实例
我们定义一个事件时,实际上是定义了一个特定的委托成员实例。
总结:事件是一个特殊的委托实例,提供了两个供订阅事件和取消订阅的方法:add_event 和 remove_event,其本质都是基于委托链来实现。

如何设计一个带有多个事件的类型?

解决方案:当某个类型具有相对较多的事件时,我们可以考虑显示地设计订阅、取消订阅事件的方法,并且把所有的委托链表存储在一个集合之中。这样做就能避免在类型中定义大量的委托成员而导致类型过大。

能说说反射的基本原理吗?

反射是一种动态分析程序集、模块、类型及字段等目标对象的机制,它的实现依托于元数据
元数据:就是描述数据的数据。在CLR中,元数据就是对一个模块定义或引用的所有东西的描述系统。

.NET中如何实现反射?

在.NET中,为我们提供了丰富的可以用来实现反射的类型,这些类型大多数都定义在System.Reflection命名空间之下,例如Assembly、Module等。利用这些类型,我们就可以方便地动态加载程序集、模块、类型、方法和字段等元素。

能说说什么是特性吗?

特性是一种有别于普通命令式编程的编程方式,通常被称为申明式编程方式。所谓申明式编程方式就是指程序员只需要申明某个模块会有怎样的特性,而无需关心如何去实现。
下面的代码就是特性在ASP.NET MVC中的基本使用方式:

  1. [HttpPost]
  2. public ActionResult Add(UserInfo userInfo)
  3. {
  4. if (ModelState.IsValid)
  5. {
  6. // To do fun
  7. }
  8. return RedirectToAction("Index");
  9. }

当一个特性被添加到某个元素上时,该元素就被认为具有了这个特性所代表的功能或性质,例如上述代码中Add方法在添加了HttpPost特性之后,就被认为只有遇到以POST的方式请求该方法时才会被执行。
Note:特性在被编译器编译时,和传统的命令式代码不同,它会被以二进制数据的方式写入模块文件的元数据之中,而在运行时再被解读使用。特性也是经常被反射机制应用的元素,因为它本身是以元数据的形式存放的。

.NET中如何自定义一个特性?

除了直接使用.NET中内建的所有特性之外,我们也可以建立自己的特性来实现业务逻辑。
具体来说,定义一个特性的本质就是定义一个继承自System.Attribute类的类型,这样的类型就被编译器认为是一个特性类型。
① 定义一个继承自System.Attribute的类型MyCustomAttribute

  1. /// <summary>
  2. /// 一个自定义特性MyCustomAttribute
  3. /// </summary>
  4. [AttributeUsage(AttributeTargets.Class)]
  5. public class MyCustomAttribute : Attribute
  6. {
  7. private string className;
  8. public MyCustomAttribute(string className)
  9. {
  10. this.className = className;
  11. }
  12. // 一个只读属性ClassName
  13. public string ClassName
  14. {
  15. get
  16. {
  17. return className;
  18. }
  19. }
  20. }

一个继承自System.Attribute的类型,就是一个自定义特性,并且可以将其添加到适合的元素之上。特性将会被写入到元数据之中,所以特性的使用基本都是基于反射机制。
② 在入口方法中使用MyCustomAttribute

  1. [MyCustom("UseMyCustomAttribute")]
  2. public class UseMyCustomAttribute
  3. {
  4. public static void Main(string[] args)
  5. {
  6. Type t = typeof(UseMyCustomAttribute);
  7. // 通过GetCustomAttributes方法得到自定义特性
  8. object[] attrs = t.GetCustomAttributes(false);
  9. MyCustomAttribute att = attrs[0] as MyCustomAttribute;
  10. Console.WriteLine(att.ClassName);
  11. Console.ReadKey();
  12. }
  13. }

关于自定义特性,有几点需要注意:

  • 虽然没有强制规定,但按照约定最好特性类型的名字都以Attribute结尾;
  • 在C#中为了方便起见,使用特性时都可以省略特性名字后的Attribute,例如上述代码中的[MyCustom(“UseMyCustomAttribute”)]代替了[MyCustomAttribute(“UseMyCustomAttribute”)];
  • 特性类型自身也可以添加其他的特性;

    .NET中特性可以在哪些元素上使用?

    特性可以被用来使用到某个元素之上,这个元素可以是字段,也可以是类型。对于类、结构等元素,特性的使用可以添加在其定义的上方,而对于程序集、模块等元素的特性来说,则需要显式地告诉编译器这些特性的作用目标。
    例如,在C#中,通过目标关键字加冒号来告诉编译器的使用目标:
    1. // 应用在程序集
    2. [assembly: MyCustomAttribute]
    3. // 应用在模块
    4. [module: MyCustomAttribute]
    5. // 应用在类型
    6. [type: MyCustomAttribute]
    我们在设计自定义特性时,往往都具有明确的针对性,例如该特性只针对类型、接口或者程序集,限制特性的使用目标可以有效地传递设计者的意图,并且可以避免不必要的错误使用特性而导致的元数据膨胀。
    AttributeUsage特性就是用来限制特性使用目标元素的,它接受一个AttributeTargets的枚举对象作为输入来告诉AttributeUsage西望望对特性做何种限定。例如上面展示的一个自定义特性,使用了限制范围: ```csharp

[AttributeUsage(AttributeTargets.Class)] public class MyCustomAttribute : Attribute { ….. }

  1. **Note:**一般情况下,自定义特性都会被限制适用范围,我们也应该养成这样的习惯,为自己设计的特性加上AttributeUsage特性,很少会出现使用在所有元素上的特性。即便是可以使用在所有元素上,也应该显式地申明[AttributeUsage(AttributesTargets.All)]来提高代码的可读性。
  2. <a name="Wr9I1"></a>
  3. ### 如何获知一个元素是否申明了某个特性?
  4. 在.NET中提供了很多的方法来查询一个元素是否申明了某个特性,每个方法都有不同的使用场合,但是万变不离其宗,都是基于反射机制来实现的。<br />首先,还是以上面的MyCustomAttribute特性为例,新建一个入口方法类Program
  5. ```csharp
  6. /// <summary>
  7. /// 一个自定义特性MyCustomAttribute
  8. /// </summary>
  9. [AttributeUsage(AttributeTargets.Class)]
  10. public class MyCustomAttribute : Attribute
  11. {
  12. private string className;
  13. public MyCustomAttribute(string className)
  14. {
  15. this.className = className;
  16. }
  17. // 一个只读属性ClassName
  18. public string ClassName
  19. {
  20. get
  21. {
  22. return className;
  23. }
  24. }
  25. }
  26. [MyCustom("Program")]
  27. class Program
  28. {
  29. static void Main(string[] args)
  30. {
  31. Type attributeType = typeof(MyCustomAttribute);
  32. Type thisClass = typeof(Program);
  33. }
  34. }

(1)System.Attribute.IsDefined方法

  1. // 使用IsDefined方法
  2. bool isDefined = Attribute.IsDefined(thisClass, attributeType);
  3. Console.WriteLine("Program类是否申明了MyCustomAttribute特性:{0}", isDefined);

(2)System.Attribute.GetCustomerAttribute方法

  1. // 使用Attribute.GetCustomAttribute方法
  2. Attribute att = Attribute.GetCustomAttribute(thisClass, attributeType);
  3. if (att != null)
  4. {
  5. Console.WriteLine("Program类申明了MyCustomAttribute特性,特性的成员为:{0}", (att as MyCustomAttribute).ClassName);
  6. }

(3)System.Attribute.GetCustomerAttributes方法

  1. // 使用Attribute.GetCustomAttributes方法
  2. Attribute[] atts = Attribute.GetCustomAttributes(thisClass, attributeType);
  3. if (atts.Length > 0)
  4. {
  5. Console.WriteLine("Program类申明了MyCustomAttribute特性,特性名称为:{0}", ((MyCustomAttribute)atts[0]).ClassName);
  6. }

(4)System.Reflection.CustomAttributeData类型

  1. // 使用CustomAttributeData.GetCustomAttributes方法
  2. IList<CustomAttributeData> attList = CustomAttributeData.GetCustomAttributes(thisClass);
  3. if (attList.Count > 0)
  4. {
  5. Console.WriteLine("Program类申明了MyCustomAttribute特性");
  6. // 注意:这里可以对特性进行分析,但无法得到其实例
  7. CustomAttributeData attData = attList[0];
  8. Console.WriteLine("该特性的名字是:{0}", attData.Constructor.DeclaringType.Name);
  9. Console.WriteLine("该特性的构造方法有{0}个参数", attData.ConstructorArguments.Count);
  10. }

一个元素是否可以重复声明同一个特性?

当一个特性申明了AttributeUsage特性并且显式地将AllowMultiple属性设置为true时,该特性就可以在同一元素上多次申明,否则的话编译器将报错
通常情况下,重复申明同一特性往往会传入不同的参数。
例如下面一段代码,类型Program多次申明了MyCustomAttribute特性

  1. [MyCustom("Class1")]
  2. [MyCustom("Class2")]
  3. [MyCustom("Class3")]
  4. public class Program
  5. {
  6. public static void Main(string[] args)
  7. {
  8. }
  9. }
  10. /// <summary>
  11. /// 一个自定义特性MyCustomAttribute
  12. /// </summary>
  13. [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
  14. public class MyCustomAttribute : Attribute
  15. {
  16. private string className;
  17. public MyCustomAttribute(string className)
  18. {
  19. this.className = className;
  20. }
  21. // 一个只读属性ClassName
  22. public string ClassName
  23. {
  24. get
  25. {
  26. return className;
  27. }
  28. }
  29. }

MQ

MQ的适用场景

(1)异步处理
更快地返回结果,减少客户等待时间,提升总体性能
(2)流量控制
优点在于 能够根据下游的处理能力自动调节流量,达到“削峰填谷”的作用
缺点在于 增加了系统调用链环节,总体响应时间也会延长,同时也增加了系统的复杂度
另一种限流方式:令牌桶控制流量
令牌桶的基本原理:单位时间内只发放固定数量的令牌到令牌桶中,规定服务在处理请求之前必须先从令牌桶中拿出一个令牌,如果令牌桶中没有令牌,则拒绝请求。这样就可以保证在单位时间内,能处理的请求不会发放令牌的数量,起到流量控制的作用。
.NET Core综合 - 图11
(3)服务解耦
可以实现各个系统应用之间的解耦,达到下游系统的增加或变化,上游服务不需要更改的效果。
引入MQ同样会带来一些问题

  • 引入MQ会带来延迟问题(需要考虑业务容忍度)
  • 增加了系统的复杂度(多了一个中间件,也多了运维成本)
  • 可能会产生数据的不一致问题(无法实现强一致性)

    如何选择MQ?

    作为一款合格的MQ产品,必须具备几个特性:

  • 消息的可靠传递:确保不丢失消息;

  • Cluster:支持集群,确保不会因为某个节点宕机导致服务不可用,当然也不能丢消息;
  • 性能:具备足够好的性能,能够满足大多数场景的性能要求;

目前市面上主流的可供选择的MQ产品:

  • RabbitMQ
    • 优点:最流行的MQ之一,支持灵活的路由配置(Exchange)、支持众多的客户端编程语言
    • 问题:
      • 对消息堆积的支持并不太好,当大量消息积压时,会导致RabbitMQ性能急剧下降
      • 性能不够好,虽然每秒能够处理几万到十几万消息,但对部分性能要求很高的场景不太够用
      • Erlang非常小众,学习成本高,不太适合主流开发人员做扩展和二次开发
  • RocketMQ
    • 优点:不错的性能、稳定性 和 可靠性,还在持续的成长,此外还有非常活跃的中文社区,易于做扩展和二次开发
      • 性能:每秒大概能处理几十万条消息,高出RabbitMQ一个数量级
    • 问题:国际上的流行程度 和 与周边生态系统的集成和兼容程度要略逊一筹
  • Kafka
    • 优点:与周边生态系统的兼容性是最好的,尤其在大数据和流计算领域。大量地使用批量和异步设计思想,做到了超高的性能。
      • 性能:每秒大概能处理几十万条消息,与RocketMQ没有量级上的差异
    • 问题:同步收发消息的响应延迟比较高,不太适合在线业务场景。因为它是攒一波再一起处理的设计,对于每秒钟消息数量没有那么多的时候,时延会比较高。

      Kafka

      .NET Core操作Kafka

      云原生

      .NET+云原生

      微服务

      .NET+微服务

      单元测试

      .NET+单元测试

      设计模式

      参考