CodeSnippet.Cn
代码片段
Csharp
架构设计
.NetCore
西班牙语
kubernetes
MySql
Redis
Algorithm
Ubuntu
Linux
Other
.NetMvc
VisualStudio
Git
pm
Python
WPF
java
Plug-In
分布式
CSS
微服务架构
JavaScript
DataStructure
Shared
《深入理解C#第三版》摘录
0
Csharp
小笨蛋
发布于:2022年01月18日
更新于:2022年01月21日
111
#custom-toc-container
> 最近一直在看《深入理解C#第三版》虽然是很老的一本书,但还是能在其中了解一些之前不清楚或比较模糊的知识点,随读随录。 ### 走出误区 经常都会听到各式各样的错误说法。我可以确定,这些错误的信息会长时间地流传下去。许多人会不自觉地接受它们,根本意识不到自己的认识其实已经出现了偏差。本节将处理一些典型错误,解释真实的情况到底是什么。 #### 误区1:“结构是轻量级的类” 这个误解存在着多种形式。有人认为值类型不能或不应有方法或其他有意义的行为——它们应作为简单的数据转移类型来使用,只应该有public字段或简单的属性。对于这种说法,一个非常典型的反例就是DateTime类型:它作为值类型来提供是很有道理的,因为它非常适合作为和数字或字符相似的一个基本单位来使用。另外,它也理应被赋予对它的值执行计算的能力。换个角度来看这个问题,是数据转移类型一般都是引用类型。总之,具体应该如何决定,应取决于需要的是值类型的语义,还是引用类型的语义,而不是取决于这个类型简单与否。 还有一些人认为值类型之所以显得比引用类型“轻”,是因为性能。事实是在某些情况下,值类型很“能干”——它们不需要垃圾回收,(除非被装箱)不会因类型标识而产生开销,也不需要解引用。但在其他方面,引用类型显得更“能干”——在传递参数、赋值、将值返回和执行类似的操作时,只需复制4或8字节(要看运行的是32位还是64位CLR),而不是复制全部数据。假定ArrayList是一个所谓“纯的”值类型,那么将一个ArrayList表达式传给一个方法时,就得复制它的所有数据!几乎在所有情况下,性能问题都不是根据这种判断来决定的。瓶颈从来都不是想当然的,在你根据性能进行设计之前,需要衡量不同的选择。 值得注意的是,将这两者相结合也不能解决问题:类型(不管是类还是结构)拥有多少方法并不重要,每个实例所占用的内存不会受到影响。(代码本身会消耗内存,但这只会发生一次,而不是每个实例都发生。) ####误区2:“引用类型保存在堆上,值类型保存在栈上” 这个误区主要应归咎于转述这句话的人根本没有动脑筋。第一部分是正确的——引用类型的实例总是在堆上创建的。但第二部分就有问题了。前面讲过,变量的值是在它声明的位置存储的。所以,假定一个类中有一个int类型的实例变量,那么在这个类的任何对象中,该变量的值总是和对象中的其他数据在一起,也就是在堆上。只有局部变量(方法内部声明的变量)和方法参数在栈上。 ####误区3:“对象在C#中默认是通过引用传递的” 这或许是传播得最广的一个误区了。同样,说这句话的人一般(但并不总是)知道C#实际的行为是什么,但不知道“引用传递”(pass by reference)的真正意思是什么。可惜,那些真正知道引用传递是什么意思的人,在听到这句话时就会被完全搞糊涂。 “引用传递”的正式定义相当复杂,要涉及左值(l-values)和类似的计算机科学术语。但最重要的一点是,假如以引用传递的方式来传送一个变量,那么调用的方法可以通过更改其参数值,来改变调用者的变量值。现在请记住,引用类型变量的值是引用,而不是对象本身。不需要按引用来传递参数本身,就可以更改该参数引用的那个对象的内容。例如,下面的方法更改了相关对象StringBuilder的内容,但调用者的表达式引用的仍然是之前的那个对象: ![图片alt](/uploads/images/20220118/125652-0937bfb9978445948d87378a6eb62ba1.png ''代码片段:Www.CodeSnippet.Cn'') 调用这个方法时,参数值(对StringBuilder的一个引用)是以值传递(pass by value)的方式传递的。如果想在方法内部更改builder变量的值——如执行builder = null;语句,调用者看不见这个改变,刚好跟错误认识相反。 有趣的是,在这种错误说法中,不仅“引用传递”的说法有误,而且“对象传递”的说法也存在问题。无论是引用传递还是值传递,永远不会传递对象本身。涉及一个引用类型时,要么以“引用传递”的方式传递变量,要么以“传值”的方式传递参数值(引用)。最起码,这回答了“当null作为一个传值参数的值来使用时会发生什么”的问题。假如传递的是对象,这时就会出问题,因为没有一个对象可供传递!相反,null引用会采用和其他引用一样的“值传递”的方式传递。 ### 3.4 高级泛型 #### 3.4.1 静态字段和静态构造函数 就像实例字段从属于一个实例一样,静态字段从属于声明它们的类型。如果在SomeClass中声明了静态字段x,不管创建SomeClass的多少个实例,也不管从SomeClass派生出多少个类型,都只有一个SomeClass.x字段。那么它与泛型的关系是怎样的呢? 答案是:每个封闭类型都有它自己的静态字段集。代码清单3-6中也可以看到这一点,我们将默认的T1和T2的相等比较器存储在静态字段里。下面我们通过另一个示例来看看更详细的内容。代码清单3-8创建了一个含有静态字段的泛型类型。我们为不同的封闭类型设置字段的值,然后打印这些值,证明它们是各自独立的。 ##### 代码清单3-8 证明不同的封闭类型具有不同的静态字段 ![](/uploads/images/20220118/135807-a3d50b7f1a8f4cef95ed873b85df565e.png) 我们将每个字段的值都设为一个不同的值,并打印封闭类型使用的类型实参的名称和每个字段的值。代码清单3-8的输出如下: ![](/uploads/images/20220118/135841-191a31bc5cf649eea3a4c785c18f0b56.png) 所以,基本的规则是:“每个封闭类型有一个静态字段。”同样的规则也适用于静态初始化程序(static initializer)和静态构造函数(static constructor)。然而,一个泛型类型可能嵌套在另一个泛型类型中,而且一个类型可能有多个泛型参数。虽然听起来很复杂,但它的工作方式与你想象的差不多。代码清单3-9展示了一个例子,这一次是用静态构造函数来演示有多少类型。 ##### 代码清单3-9 嵌套泛型类型的静态构造函数 ![](/uploads/images/20220118/140248-40cf2f2ca5e74a69b4e2d20b31684a9f.png) 第一次调用DummyMethod()时,不管使用的是什么类型,都会导致Inner类型的初始化,此时静态构造函数打印一些诊断信息。每个不同的类型实参列表都被看做一个不同的封闭类型,所以代码清单3-9的输出如下: ![](/uploads/images/20220118/140835-fabf8cf70db546099d457ac225840e84.png) 和非泛型类型一样,任何封闭类型的静态构造函数只执行一次。所以,代码清单3-9的最后一行不会产生第6行输出。Outer
.Inner
的静态构造函数之前已经执行过了,第2行输出就是它产生的。 为了进一步打消你的疑虑,假如在Outer内有一个非泛型的PlainInner类,那么每个封闭的Outer类型中仍然只有一个Outer
.PlainInner类型。所以Outer
.PlainInner将独立于Outer
.PlainInner,就像前面看到的那样,各自拥有单独的静态字段集。 现在我们知道了一个不同的类型是由什么构成的,接着,应该思考一下这对生成的本地代码数量的影响。并没你想象得那样糟 ### 3.4.2 JIT编译器如何处理泛型 对于所有不同的封闭类型,JIT的职责就是将泛型类型的IL转换成本地代码,使其能真正运行起来。从某些方面来说,我们并不需要知道具体的转换过程是怎样的——只需留意内存和CPU时间即可。如果JIT为每个封闭类型都单独生成本地代码,就像这些类型相互之间没有任何联系一样,我们将不会感觉出太大差异的。但是,JIT的作者十分聪明,非常有必要看看他们做了什么。 首先看一个简单的、只有一个类型参数的情况。为方便讨论,我们使用List
作为例子。JIT为每个以值类型作为类型实参的封闭类型都创建不同的代码。然而,所有使用引用类型(string、Stream、StringBuilder等)作为类型实参的封闭类型都共享相同的本地代码。之所以能这样做,是由于所有引用都具有相同的大小(32位CLR上是4字节,64位CLR上是8字节。但是,在任何一个特定的CLR中,所有引用都具有相同的大小)。无论实际引用的是什么,引用(构成的)数组的大小是不会发生变化的。栈上一个引用所需的空间始终是相同的。无论使用的类型是什么,都可以使用相同的寄存器优化措施,即使是List
也不例外。 如3.4.1节所述,每个类型还可以有它自己的静态字段,但可执行代码本身是可以重用的。当然,JIT采用的仍然是“懒人”原则——除非需要,否则不会为List
生成代码。而一旦生成代码,代码就会缓存起来,以备将来再次使用List
。 理论上,至少对一些值类型来说,代码是可以共享的。但JIT必须十分谨慎,不仅要考虑到大小,还要考虑到垃圾回收的问题——JIT必须能快速识别一个struct值中的引用是否是活着的。然而,假如值类型具有相同的大小,而且就GC看来具有相同的“内存需求量”,那么是应该能够共享代码的。但到本书完稿时为止,这仍然是一个优先级十分低的需求,所以一直没有实现,而且将来极有可能会一直这样。 虽然一般只有喜欢搞学术研究的人才会对这一级别的细节问题感兴趣,但我要指出的一点是,由于要进行JIT编译的代码增多了,所以确实会对性能造成轻微影响。不过,泛型本身在性能上的优势是相当巨大的,这同样是由于现在有机会将不同的类型通过JIT编译成不同的代码。下面以一个List
为例。在.NET 1.1中,为了将单独的字节添加到一个ArrayList中,需要对每个字节进行装箱,并存储对每个已装箱值的引用。使用List
则无此问题——List
用一个T[]类型的成员数组替代了ArrayList中的object[],而且那个数组具有恰当的类型,会占用恰当(大小)的空间。所以,在List
中,是直接用一个byte[]来存储数组元素。(在许多方面,这使得List
在行为上就像是一个MemoryStream。) 图3-3展示了一个ArrayList和一个List
,它们分别包含6个相同的值。数组本身拥有不止6个元素,从而允许扩充。List
和ArrayList都有一个缓冲区,在必要时会创建一个更大的缓冲区。 ![](/uploads/images/20220121/121757-c2b5c7e310e04b7b83d6eccdf5b0a40a.png) 图3-3 演示在存储值类型时,为什么List
占用的空间比ArrayList少得多 两者在效能上的差异令人难以置信。先来看看ArrayList,假定使用的是一个32位CLR。每个已装箱的字节都要产生8字节的对象开销,另加4字节(本来是1字节,但要向上取整到一个字的边界)用于数据本身。除此之外,引用本身也要消耗4字节。所以,每个有效数据都要花费至少16字节。除此之外,缓冲区中还要为引用准备一些额外的未使用的空间。 相比而言,List
中的每个字节都占用元素数组中一个字节的空间。缓冲区仍有“浪费”的空间,可用于新增项,但最起码,每个未使用的元素只会浪费一个字节。 我们不仅节省了空间,而且加快了执行速度。现在不需要花时间进行装箱,不需要因为对字节进行拆箱而检查类型,也不需要对不再引用的已装箱值进行垃圾回收。 然而,不需要深入到CLR一级,就能很明显地发现一些正在发生的事情。C#一直致力于通过语法上的快捷方式来简化编程。
这里⇓感觉得写点什么,要不显得有点空,但还没想好写什么...
返回顶部
About
京ICP备13038605号
© 代码片段 2024