c#中的值类型和引用类型

什么是值类型,什么是引用类型.先从一个例子来说.加入你有一张不错的报纸,然后你想把这张报纸给你朋友看,你可以将这张报纸复印一份,然后交给你朋友,这样你朋友就有了一份这张报纸的副本,接下来你在这张报纸上面做的任何操作都不会影响到你的朋友,比如你在报纸上面记笔记之类的.而你朋友的报纸并不会收到改变,这种行为就是值类型的行为.
在假设你看到了一个不错的网站,你想分享给你朋友,你要做到就是将这个网站的地址给你朋友就好了,这种行为就是引用类型的行为.如果该网站做了一些改变,比如添加了新的内容或者删除了一些内容,这样你和你朋友都能看的这个网站所改变的东西.
MSDN的这张图说明了CTS中各个类型是如何相关的。注意,类型的实例可以只是值类型或自描述类型,即使这些类型有子类别也是如此。
IC104408

值类型:

C#的所有值类型均隐式派生自System.ValueType:

  • 结构体:struct(直接派生于System.ValueType);
    • 数值类型:
      • 整 型:sbyte(System.SByte的别名),short(System.Int16),int(System.Int32),long (System.Int64),byte(System.Byte),ushort(System.UInt16),uint (System.UInt32),ulong(System.UInt64),char(System.Char);
      • 浮点型:float(System.Single),double(System.Double);
      • 用于财务计算的高精度decimal型:decimal(System.Decimal)。
    • bool型:bool(System.Boolean的别名);
    • 用户定义的结构体(派生于System.ValueType)。
  • 枚举:enum(派生于System.Enum);
  • 可空类型(派生于System.Nullable<T>泛型结构体,T?实际上是System.Nullable<T>的别名)。

每种值类型均有一个隐式的默认构造函数来初始化该类型的默认值。例如:

int i = new int();
//等价于:
Int32 i = new Int32();
//等价于:
int i = 0;
//等价于:
Int32 i = 0;

使用new运算符时,将调用特定类型的默认构造函数并对变量赋以默认值。
值得注意的是,System.ValueType直接派生于System.Object。即System.ValueType本身是一个类类型,而 不是值类型。其关键在于ValueType重写了Equals()方法,从而对值类型按照实例的值来比较,而不是引用地址来比较。

 

引用类型

C#有以下一些引用类型:

  • 数组(派生于System.Array)
  • 用户用定义的以下类型:
    • 类:class(派生于System.Object);
    • 接口:interface(接口不是一个“东西”,所以不存在派生于何处的问题。Anders在《C# Programming Language》中说,接口只是表示一种约定[contract]);
    • 委托:delegate(派生于System.Delegate)。
  • object(System.Object的别名);
  • 字符串:string(System.String的别名)。

可以看出:

  • 引用类型与值类型相同的是,结构体也可以实现接口;
  • 引用类型可以派生出新的类型,而值类型不能;
  • 引用类型可以包含null值,值类型不能(可空类型功能允许将 null 赋给值类型);
  • 引用类型变量的赋值只复制对对象的引用,而不复制对象本身。而将一个值类型变量赋给另一个值类型变量时,将复制包含的值。

对于最后一条,经常混淆的是string。我曾经在一本书的一个早期版本上看到String变量比string变量效率高;我还经常听说String是引用类型,string是值类型,等等。例如:

string s1 = "Hello, ";
string s2 = "world!";
string s3 = s1 + s2;//s3 is "Hello, world!"

这确实看起来像一个值类型的赋值。再如:

string s1 = "a";
string s2 = s1;
s1 = "b";//s2 is still "a"

改变s1的值对s2没有影响。这更使string看起来像值类型。实际上,这是运算符重载的结果,当s1被改变时,.NET在托管堆上为s1重新分配了内存。这样的目的,是为了将做为引用类型的string实现为通常语义下的字符串。

误区

引用类型在堆上,值类型在栈上

这句话的第一部分是正确的,引用类型实例总是在堆上创建的.但是第二句话就不对了.变量的值是在它声明的位置存储的.也就是说如果一个类中有一个int类型的变量,那么在这个类的所有对象中,该变量的值总是和其他变量存储在一起的,也就是在堆上.只用局部变量和方法参数在栈上.

对象在c#中默认是通过引用传递的

如果以引用的方式传递一个变量.那么调用的方法通过修改其参数值来更改调用者的变量值,引用类型变量的值是引用,而不是对象本身.不需要引用来传递参数本身,就可以修改参数引用的那个对象的内容.如下面的方法更改了涉及的对象StringBuilder的内容,但调用者的表达式引用的仍然是之前的那个对象.

void AppendHello(StringBuilder builder)
{
     builder.Append("hello");
}

在调用这个方法时,参数值(对某StringBuilder的一个引用)是以值传递的,如果在方法内部更改builder的值–如执行builder=null; 语句,调用者看不见这个改变(这样更改的是builder的一个副本,调用者当然看不到的),刚好和错误的认识相反.

装箱和拆箱

装箱指把一个值类型放入一个未具名类型的引用类型中,比如:

int valueType = 0;
object referenceType = i;//boxing

拆箱则是从前面的装箱对象中取出值类型:

object referenceType;
int valueType = (int)referenceType;//unboxing

装箱和拆箱是比较耗费性能的,还会引入一些诡异的bug,我们应当避免装箱和拆箱。

装箱和拆箱最大的问题是会自动发生。比如:

Console.WriteLine(“A few numbers: {0}, {1}.”, 25, 32);

其中,Console.WriteLine()接收的参数类型是(string,object,object)。因此,实际上会执行以下操作:

int i = 25;
obeject o = i;//boxing

然后把o传给WriteLine()方法。在WriteLine()方法的内部,为了调用i上的ToString()方法,又会执行:

int i = (int)o;//unboxing
string output = i,ToString();

所以正确的做法应该是:

Console.WriteLine(“A few numbers: {0}, {1}.”, 25.ToString(), 32.ToString());
应该尽量减少装箱和拆箱的操作

转载自:http://www.cnblogs.com/mrcooldog/archive/2008/03/03/1088769.html

发表评论

电子邮件地址不会被公开。 必填项已用*标注