[你必须知道的.NET]第九回:品味类型---值类型与引用类型(中)-规则无边

2024-01-02 01:08

本文主要是介绍[你必须知道的.NET]第九回:品味类型---值类型与引用类型(中)-规则无边,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

接上回[第八回:品味类型---值类型与引用类型(上)-内存有理]的探讨,继续我们关注值类型和引用类型的话题。

本文将介绍以下内容:

  • 类型的基本概念 
  • 值类型深入
  • 引用类型深入
  • 值类型与引用类型的比较及应用

  

1. 引言

上回[第八回:品味类型---值类型与引用类型(上)-内存有理]的发布,受到大家的不少关注,我们从内存的角度了解了值类型和引用类型的所以然,留下的任务当然是如何应用类型的不同特点在系统设计、性能优化等方面发挥其作用。因此,本回是对上回有力的补充,同时应朋友的希望,我们尽力从内存调试的角度来着眼一些设计的分析,这样就有助于对这一主题进行透彻和全面的理解,当然这也是下一回的重点。

从内存角度来讨论值类型和引用类型是有理有据的,  而从规则的角度来了解值类型和引用类型是无边无际的。本文旨在从上文呼应的角度,来把这个主题彻底的融会贯通,无边无迹的应用,还是来自反复无常的实践,因此对应用我只能说以一个角度来阐释观点,但是肯定不可能力求全局。因此,我们从以下几个角度来完成对值类型与引用类型应用领域的讨论。 

2. 通用规则与比较

通用有规则:

  • string类型是个特殊的引用类型,它继承自System.Object肯定是个引用类型,但是在应用表现上又凸现出值类型的特点,那么究竟是什么原因呢?例如有如下的一段执行:

 

 简单的说是由于string的immutable特性,因此每次对string的改变都会在托管堆中产生一个新的string变量,上述string作为参数传递时,实际上执行了s=s操作,在托管堆中会产生一个新的空间,并执行数据拷贝,所以才有了类似于按值传递的结果。但是根据我们的内存分析可知,string在本质上还是一个引用类型,在参数传递时发生的还是按址传递,不过由于其特殊的恒定特性,在函数内部新建了一个string对象并完成初始化,但是函数外部取不到这个变化的结果,因此对外表现的特性就类似于按值传递。至于string类型的特殊性解释,我推荐Artech的大作《深入理解string和如何高效地使用string》。

另外,string类型重载了==操作符,在类型比较是比较的是实际的字符串,而不是引用地址,因此有以下的执行结果:

            string aString = "123";
            
string bString = "123";
            Console.WriteLine((aString 
== bString)); //显示为true,等价于aString.Equals(bString);
            string cString = bString;
            cString 
= "456";
            Console.WriteLine((bString 
== cString)); //显示为false,等价于bString.Equals(cString);
  • 通常可以使用Type.IsValueType来判断一个变量的类型是否为值类型,典型的操作为: 
    public struct MyStructTester
    { }

    
public class isValueType_Test
    {
        
public static void Main()
        {
            MyStructTester aStruct 
= new MyStructTester();
            Type type 
= aStruct.GetType();
            
if (type.IsValueType)
            {
                Console.WriteLine(
"{0} belongs to value type.", aStruct.ToString());
            }
 
        }
    }
  • .NET中以操作符ref和out来标识值类型按引用类型方式传递,其中区别是:ref在参数传递之前必须初始化;而out则在传递前不必初始化,且在传递时必须显式赋值。
  • 值类型与引用类型之间的转换过程称为装箱与拆箱,这值得我们以专门的篇幅来讨论,因此留待后文详细讨论这一主题。
  • sizeof()运算符用于获取值类型的大小,但是不适用于引用类型。
  • 值类型使用new操作符完成初始化,例如:MyStruct aTest = new MyStruct(); 而单纯的定义没有完成初始化动作,此时对成员的引用将不能通过编译,例如: 
MyStruct aTest;
Console.WriteLine(aTest.X); 
  • 引用类型在性能上欠于值类型主要是因为以下几个方面:引用类型变量要分配于托管堆上;内存释放则由GC完成,造成一定的CG堆压力;同时必须完成对其附加成员的内存分配过程;以及对象访问问题。因此,.NET系统不能由纯粹的引用类型来统治,性能和空间更加优越和易于管理的值类型有其一席之地,这样我们就不会因为一个简单的byte类型而进行复杂的内存分配和释放工作。Richter就称值类型为“轻量级”类型,简直恰如其分,处理数据较小的情况时,应该优先考虑值类型。
  • 值类型都继承自System.ValueType,而System.ValueType又继承自System.Object,其主要区别是ValueType重写了Equals方法,实现对值类型按照实例值比较而不是引用地址来比较,具体为:
char a = 'c';
char b = 'c';
Console.WriteLine((a.Equals(b))); 
//会返回true; 
  • 基元类型,是指编译器直接支持的类型,其概念其实是针对具体编程语言而言的,例如C#或者VB.NET,通常对应用.NET Framework定义的内置值类型。这是概念上的界限,不可混淆。例如:int对应于System.Int32,float对应于System.Single。

比较出真知:

  • 值类型继承自ValueType(注意:而System.ValueType又继承自System.Object);而引用类型继承自System.Object。
  • 值类型变量包含其实例数据,每个变量保存了其本身的数据拷贝(副本),因此在默认情况下,值类型的参数传递不会影响参数本身;而引用类型变量保存了其数据的引用地址,因此以引用方式进行参数传递时会影响到参数本身,因为两个变量会引用了内存中的同一块地址。
  • 值类型有两种表示:装箱与拆箱;引用类型只有装箱一种形式。我会在下节以专门的篇幅来深入讨论这个话题。
  • 典型的值类型为:struct,enum以及大量的内置值类型;而能称为类的都可以说是引用类型。 struct和class主要的区别可以参见我的拙作《第四回:后来居上:class和struct》来详细了解,也是对值类型和引用类型在应用方面的有力补充。
  • 值类型的内存不由GC(垃圾回收,Gabage Collection)控制,作用域结束时,值类型会自行释放,减少了托管堆的压力,因此具有性能上的优势。例如,通常struct比class更高效;而引用类型的内存回收,由GC来完成,微软甚至建议用户最好不要自行释放内存。
  • 值类型是密封的(sealed),因此值类型不能作为其他任何类型的基类,但是可以单继承或者多继承接口;而引用类型一般都有继承性。 
  • 值类型不具有多态性;而引用类型有多态性。
  • 值类型变量不可为null值,值类型都会自行初始化为0值;而引用类型变量默认情况下,创建为null值,表示没有指向任何托管堆的引用地址。对值为null的引用类型的任何操作,都会抛出NullReferenceException异常。
  • 值类型有两种状态:装箱和未装箱,运行库提供了所有值类型的已装箱形式;而引用类型通常只有一种形式:装箱。

3. 对症下药-应用场合与注意事项

现在,在内存机制了解和通用规则熟悉的基础上,我们就可以很好的总结出值类型和引用类型在系统设计时,如何作出选择?当然我们的重点是告诉你,如何去选择使用值类型,因为引用类型才是.NET的主体,不必花太多的关照就可以赢得市场。

3.1 值类型的应用场合

  • MSDN中建议以类型的大小作为选择值类型或者引用类型的决定性因素。数据较小的场合,最好考虑以值类型来实现可以改善系统性能;
  • 结构简单,不必多态的情况下,值类型是较好的选择;
  • 类型的性质不表现出行为时,不必以类来实现,那么用以存储数据为主要目的的情况下,值类型是优先的选择;
  • 参数传递时,值类型默认情况下传递的是实例数据,而不是内存地址,因此数据传递情况下的选择,取决于函数内部的实现逻辑。值类型可以有高效的内存支持,并且在不暴露内部结构的情况下返回实例数据的副本,从安全性上可以考虑值类型,但是过多的值传递也会损伤性能的优化,应适当选择;
  • 值类型没有继承性,如果类型的选择没有子类继承的必要,优先考虑值类型;
  • 在可能会引起装箱与拆箱操作的集合或者队列中,值类型不是很好的选择,因为会引起对值类型的装箱操作,导致额外内存的分配,例如在Hashtable。关于这点我将在后续的主题中重点讨论。 

3.2 引用类型的应用场合

  • 可以简单的说,引用类型是.NET世界的全值杀手,我们可以说.NET世界就是由类构成的,类是面向对象的基本概念,也是程序框架的基本要素,因此灵活的数据封装特性使得引用类型成为主流;
  • 引用类型适用于结构复杂,有继承、有多态,突出行为的场合;
  • 参数传递情况也是考虑的必要因素;    

4. 再论类型判等

类型的比较通常有Equals()、ReferenceEquals()和==/!=三种常见的方法,其中核心的方法是Equals。我们知道Equals是System.Object提供的虚方法,用于比较两个对象是否指向相同的引用地址,.NET Framework的很多类型都实现了对Equals方法的重写,例如值类型的“始祖”System.ValueType就重载了Equal方法,以实现对实例数据的判等。因此,类型的判等也要从重写或者重载Equals等不同的情况具体分析,对值类型和引用类型判等,这三个方法各有区别,应多加注意。

4.1 值类型判等

  • Equals,System.ValueType重载了System.Object的Equals方法,用于实现对实例数据的判等。
  • ReferenceEquals,对值类型应用ReferenceEquals将永远返回false。
  • ==,未重载的==的值类型,将比较两个值是否“按位”相等。

4.2 引用类型判等 

  • Equals,主要有两种方法,如下  
public virtual bool Equals(object obj);
public static bool Equals(object objA, object objB);

 一种是虚方法,默认为引用地址比较;而静态方法,如果objA是与objB相同的实例,或者如果两者均为空引用,或者如果objA.Equals(objB)返回true,则为true;否则为false。.NET的大部分类都重写了Equals方法,因此判等的返回值要根据具体的重写情况决定。 

  • ReferenceEquals,静态方法,只能用于引用类型,用于比较两个实例对象是否指向同一引用地址。
  • ==,默认为引用地址比较,通常进行实现了==的重载,未重载==的引用类型将比较两个对象是否引用地址,等同于引用类型的Equals方法。因此,很多的.NET类实现了对==操作符的重载,例如System.String的==操作符就是比较两个字符串是否相同。而==和equals方法的主要区别,在于多态表现上,==是被重载,而Equals是重写。

有必要在自定义的类型中,实现对Equals和==的重写或者重载,以提高性能和针对性分析。 

5. 再论类型转换

类型转换是引起系统异常一个重要的因素之一,因此在有必要在这个主题里做以简单的总结,我们不力求照顾全面,但是追去提纲挈领。常见的类型转换包括:

  • 隐式转换:由低级类型项高级类型的转换过程。主要包括:值类型的隐式转换,主要是数值类型等基本类型的隐式转换;引用类型的隐式转换,主要是派生类向基类的转换;值类型和引用类型的隐士转换,主要指装箱和拆箱转换。
  • 显示转换:也叫强制类型转换。但是转换过程不能保证数据的完整性,可能引起一定的精度损失或者引起不可知的异常发生。转换的格式为, 
(type)(变量、表达式)

例如:int a = (int)(b + 2.02);

  • 值类型与引用类型的装箱与拆箱是.NET中最重要的类型转换,不恰当的转换操作会引起性能的极大损耗,因此我们将以专门的主题来讨论。
  • 以is和as操作符进行类型的安全转换,详见本人拙作《第一回:恩怨情仇:is和as》。
  • System.Convert类定义了完成基本类型转换的便捷实现。
  • 除了string以外的其他类型都有Parse方法,用于将字符串类型转换为对应的基本类型;
  • 使用explicit或者implicit进行用户自定义类型转换,主要给用户提高自定义的类型转换实现方式,以实现更有目的的转换操作,转换格式为,
static 访问修饰操作符 转换修饰操作符 operator 类型(参数列表);

 例如:

public Student
{
    
//
    
    
static public explicite opertator Student(string name, int age)
    {
        
return new Student(name, age);
    }

    
//
}

其中,所有的转换都必须是static的。  

6. 结论

现在,我们从几个角度延伸了上回对值类型和引用类型的分析,正如本文开头所言,对类型的把握还有很多可以挖掘的要点,但是以偏求全的办法我认为还是可取的,尤其是在技术探求的过程中,力求面面俱到的做法并不是好事。以上的几个角度,我认为是对值类型和引用类型把握的必经之路,否则在实际的系统开发中常常会在细小的地方栽跟头,摸不着头脑。

品味类型,我们以应用为要点撬开值类型和引用类型的规矩与方圆。

品味类型,我们将以示例为导航,开动一个层面的深入分析,下回《第十回:品味类型---值类型与引用类型(下)-应用征途》我们再见。

  

参考文献

(USA)Jeffrey Richter, Applied Microsoft .NET Framework Programming

(USA)David Chappell, Understanding .NET

  

温故知新

[开篇有益]

[第一回:恩怨情仇:is和as]

[第二回:对抽象编程:接口和抽象类]

[第三回:历史纠葛:特性和属性]

[第四回:后来居上:class和struct]

[第五回:深入浅出关键字---把new说透]

[第六回:深入浅出关键字---base和this]

[第七回:品味类型---从通用类型系统开始]

[第八回:品味类型---值类型与引用类型(上)-内存有理]

这篇关于[你必须知道的.NET]第九回:品味类型---值类型与引用类型(中)-规则无边的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/560970

相关文章

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

poj 1258 Agri-Net(最小生成树模板代码)

感觉用这题来当模板更适合。 题意就是给你邻接矩阵求最小生成树啦。~ prim代码:效率很高。172k...0ms。 #include<stdio.h>#include<algorithm>using namespace std;const int MaxN = 101;const int INF = 0x3f3f3f3f;int g[MaxN][MaxN];int n

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

2、PF-Net点云补全

2、PF-Net 点云补全 PF-Net论文链接:PF-Net PF-Net (Point Fractal Network for 3D Point Cloud Completion)是一种专门为三维点云补全设计的深度学习模型。点云补全实际上和图片补全是一个逻辑,都是采用GAN模型的思想来进行补全,在图片补全中,将部分像素点删除并且标记,然后卷积特征提取预测、判别器判别,来训练模型,生成的像

自定义类型:结构体(续)

目录 一. 结构体的内存对齐 1.1 为什么存在内存对齐? 1.2 修改默认对齐数 二. 结构体传参 三. 结构体实现位段 一. 结构体的内存对齐 在前面的文章里我们已经讲过一部分的内存对齐的知识,并举出了两个例子,我们再举出两个例子继续说明: struct S3{double a;int b;char c;};int mian(){printf("%zd\n",s

【编程底层思考】垃圾收集机制,GC算法,垃圾收集器类型概述

Java的垃圾收集(Garbage Collection,GC)机制是Java语言的一大特色,它负责自动管理内存的回收,释放不再使用的对象所占用的内存。以下是对Java垃圾收集机制的详细介绍: 一、垃圾收集机制概述: 对象存活判断:垃圾收集器定期检查堆内存中的对象,判断哪些对象是“垃圾”,即不再被任何引用链直接或间接引用的对象。内存回收:将判断为垃圾的对象占用的内存进行回收,以便重新使用。

flume系列之:查看flume系统日志、查看统计flume日志类型、查看flume日志

遍历指定目录下多个文件查找指定内容 服务器系统日志会记录flume相关日志 cat /var/log/messages |grep -i oom 查找系统日志中关于flume的指定日志 import osdef search_string_in_files(directory, search_string):count = 0

两个月冲刺软考——访问位与修改位的题型(淘汰哪一页);内聚的类型;关于码制的知识点;地址映射的相关内容

1.访问位与修改位的题型(淘汰哪一页) 访问位:为1时表示在内存期间被访问过,为0时表示未被访问;修改位:为1时表示该页面自从被装入内存后被修改过,为0时表示未修改过。 置换页面时,最先置换访问位和修改位为00的,其次是01(没被访问但被修改过)的,之后是10(被访问了但没被修改过),最后是11。 2.内聚的类型 功能内聚:完成一个单一功能,各个部分协同工作,缺一不可。 顺序内聚:

Mysql BLOB类型介绍

BLOB类型的字段用于存储二进制数据 在MySQL中,BLOB类型,包括:TinyBlob、Blob、MediumBlob、LongBlob,这几个类型之间的唯一区别是在存储的大小不同。 TinyBlob 最大 255 Blob 最大 65K MediumBlob 最大 16M LongBlob 最大 4G

Oracle type (自定义类型的使用)

oracle - type   type定义: oracle中自定义数据类型 oracle中有基本的数据类型,如number,varchar2,date,numeric,float....但有时候我们需要特殊的格式, 如将name定义为(firstname,lastname)的形式,我们想把这个作为一个表的一列看待,这时候就要我们自己定义一个数据类型 格式 :create or repla