C#学习笔记二: C#类型详解

前言

这次分享的主要内容有五个, 分别是值类型和引用类型, 装箱与拆箱,常量与变量,运算符重载,static字段和static构造函数. 后期的分享会针对于C#2.0 3.0 4.0 等新特性进行. 再会有三篇博客  这个系列的就会结束了. 也算是自己对园子中@Learning Hard出版的<<C#学习笔记>>的一个总结了. 博客内容基本上都是白天抽空在公司写好的了, 但是由于公司内部网络不能登录博客园所以只能够夜晚拿回来修改,  写的不好或者不对的地方也请各位大神指出. 在下感激不尽了.

1,值类型和引用类型
1.1 值类型与引用类型简介
C#值类型数据直接在他自身分配到的内存中存储数据,而C#引用类型只是包含指向存储数据位置的指针。

C#值类型,我们可以把他归纳成三类:
  第一类: 基础数据类型(string类型除外):包括整型、浮点型、十进制型、布尔型。  
                      整型包括:sbyte、byte、char、short、ushort、int、uint、long、ulong这九种类型;
                      浮点型就包括 float 和 double 两种类型;
                        十进制型就是 decimal ;
                      布尔型就是 bool 型了。
  第二类:结构类型:就是struct型
  第三类:枚举类型:就是enum型
 C#引用类型有五种:class、interface、delegate、object、string、Array。
上面说的是怎么区分哪些C#值类型和C#引用类型,而使用上也是有区别的。所有值类型的数据都无法为null的(这里可空类型是可以为空的),声明后必须赋以初值;引用类型才允许 为null。

1.2 值类型与引用类型的区别
值类型与引用类型的区别是面试中经常经常问到的问题,完美的回答当然不能只是简单地重复两者的概念,因为面试官更希望你答出他们之间深层的区别--不同的内存分布
值类型通常被分配到县城的堆栈上,而引用类型则被分配到托管堆上。不同的分配位置导致了不用的管理机制,值类型的管理由操作系统负责,而引用类型的管理则由垃圾回收器(GC)负责。

 1 class Program
 2 {
 3      static void Main()
 4      {
 5           //valueType是值类型
 6           int valueType = 3;
 7           //refType是引用类型
 8           string regType = "abc";
 9      }
10 }

图1:

值类型和引用类型的区别在实际数据的存储位置:值类型的变量和实际数据都存储在堆栈中;
而引用类型则只有变量存储在堆栈中,变量存储实际数据的地址,实际数据存储在与地址相
对应的托管堆中。

1.3引用类型中嵌套定义值类型
如果类的字段类型是值类型,它将作为引用类型实例的一部分,被分配到托管堆中。但那些作为局部变量
(例如下列代码中的c变量)的值类型,则仍然会分配到线程堆栈中。

 1 //引用类型嵌套值类型的情况
 2 public class NestedValueTypeInRef
 3 {
 4      //valueType作为引用类型的一部分被分配到托管堆上
 5      private int valueType = 3;
 6
 7      public void method()
 8      {
 9           //c被分配到线程堆栈上
10           char c = ‘c‘;
11      }
12 }
13
14 class Program
15 {
16      static void Main(string[] args)
17      {
18         NestedRefTypeInValue refType = new NestedRefTypeInValue();
19      }
20 }

以上代码的内存分配情况如图2所示:

1.4 值类型中嵌套定义引用类型

 1 public class TestClass
 2 {
 3      public int x;
 4      public int y;
 5 }
 6
 7 //值类型嵌套定义引用类型的情况
 8 public struct NestedRefTypeValue
 9 {
10      //结构体字段,注意,结构体字段不能被初始化
11      private TestClass classinValueType;
12
13      //结构体中的构造函数,注意,结构体中不能定义无参的构造函数
14      public NestedRefTypeInValue(TestClass t)
15      {
16           calssinValueType.x = 3;
17           calssinValueType.y = 5;
18           calssinValueType = t;
19      }
20 }
21
22 class Program
23 {
24      static void Main(string[] args)
25      {
26           //值类型变量
27           NestedRefTypeInValue valueType = new NestedRefTypeInValue(new TestClass());
28      }
29 }

以上代码的内存分配情况如图3所示:

从以上分析可以得出结论:值类型实例中会被分配到它声明的地方,声明的是局部变量时,将被分配到栈上。而声明为引用类型时,则被分配到托管堆上。
而引用类型实例中是被分配到托管堆上。

上面只是分析了值类型与引用类型的内存分布方面的区别, 除此之外,二者还存在其他几个方面的区别,现总结如下:
1。值类型继承自ValueType, ValueType又继承自System.Object; 而引用类型则直接继承于System.Object。
2。值类型的内存不受GC控制,作用域结束时,值类型会被操作系统自行释放,从而减少托管堆的压力;而引用类型的内存管理则由GC来完成。所以与引用类相比,只类型在性能上更具有优势。
3。若只类型的密封的(sealed), 你将不能把只类型作为其他任何类型的基类;而引用类型则一般具有继承性,这里指的是类和接口。
4。值类型不能为null值(非空类型占不讨论),它会被默认初始化为数值0; 而引用类型在默认情况下会被初始化为null值,表示不指向托管堆中的任何地址。对值null的引用类型的任何操作,都会引发空指针异常。
5。由于值类型变量包含其实际数据,因此在默认情况下,只类型之间的参数传递不会印象变量本身; 而引用类型变量保存的是数据的引用地址,它们作为参数被传递时,参数会发生改变,从而影响应用类型变量的值。

2,两大类型间的转换--装箱与拆箱
类型转换主要分为以下几种方式:
1, 隐式类型转换:由低级别类型向高级别类型的转换过程。例如:派生类可以隐式的转换为它的父类,装箱过程就输入这种隐式类型转换。
2, 显示类型转换:也叫做强制类型转换,但是这种转换可能会导致精度损失或者出现运行时异常。
3, 通过is和as运算符进行安全的类型转换。
4, 通过.Net 类库中的Convert类来完成类型转换。

下面主要介绍只类型与引用类型间的一种转换:装箱和拆箱
装箱:值类型转换为引用类型的过程
拆箱:引用类型转换为值类型的过程
装箱过程中,系统会在托管堆中生成一份堆栈中值类型对象的副本。而拆箱则是从托管堆中将引用类型所指向的已装箱数据复制回值类型对象的过程。

下面通过示例从内存的角度对两个过程进行分入分析:

 1 class Program
 2  {
 3       static void Main()
 4       {
 5            int i = 3;
 6            //装箱操作
 7            object o = i;
 8            //拆箱操作
 9            int y = (int)o;
10       }
11  }

以上代码分别执行了一次装箱和拆箱操作。
装箱操作可以具体分为以下3个步骤:
(1)内存分配: 在托管堆中分配好内存空间以存放复制的实际数据 
(2)完成实际数据复制:将值类型实例的实际数据复制到新分配的内存中
(3)地址返回: 将托管堆中的对象地址返回给引用类型变量。

装箱过程就是通过这3步完成的,如图4所示。

在IL代码中,装箱过程是由box指令来实现的,上一段代码所对应的IL 代码如下所示:

在这段IL代码中,除了有box指令外,我们还看到了一个unbox指令,正如其字面意思所提示的一样,该指令就是完成拆箱操作的IL指令。
拆箱过程也可以具体分为3个步骤:
(1)检查实例:首先检查要进行拆箱操作的引用类型变量是否为null,如果为null则抛出空指针异常,如果不为null则继续减产变量是否合拆箱后的类型是同一类型,若不是则会抛出InvalidCastExce异常
(2)地址返回:返回已装箱变量的实际数据部分地址
(3)数据复制: 将托管堆中的实际数据复制到栈中

总结:对于拆箱与装箱的理解之所以是如此重要,主要是因为装箱和拆箱操作对性能有很大的影响。 如果程序代码中存在过多的装箱和拆箱操作,由于两个过程
都需要进行数据复制,该操作会消耗大量额外运行时间;并且装箱和拆箱必然会产生多余的对象,这进一步加重了GC的负担,导致程序的性能降低。此外,还会引起一些隐藏的bug。

所以我们在写代码时,应尽量避免装箱拆箱操作,最好使用泛型来编程。当然泛型的好处不止于此,泛型还可以增加程序的可读性,使程序更容易被复用等等,至于泛型以后再做详细介绍.

更多内容请参考:http://www.cnblogs.com/ludbul/p/4466522.html 《C#中如何正确的操作字符串?》

3,常量与变量
这里主要讲一下静态常量const和动态常量readonly

1)const修饰的常量在声明的时候必须初始化;readonly修饰的常量则可以延迟到构造函数初始化 
2)const修饰的常量在编译期间就被解析,即常量值被替换成初始化的值;readonly修饰的常量则延迟到运行的时候
此外const常量既可以声明在类中也可以在函数体内,但是static readonly常量只能声明在类中。

 1 using System;
 2 class P
 3 {
 4     static readonly int A=B*10;
 5     static readonly int B=10;
 6     public static void Main(string[] args)
 7     {
 8         Console.WriteLine("A is {0},B is {1} ",A,B);
 9     }
10 }

Result:A is 0, B is 10;

 1 using System;
 2 class P
 3 {
 4     const int A=B*10;
 5     const int B=10;
 6     public static void Main(string[] args)
 7     {
 8         Console.WriteLine("A is {0},B is {1} ",A,B);
 9     }
10 }

Result:A is 100, B is 10;

解析:
那么为什么是这样的呢?其实在上面说了,const是静态常量,所以在编译的时候就将A与B的值确定下来了(即B变量时10,而A=B*10=10*10=100),那么Main函数中的输出当然是A is 100,B is 10啦。而static readonly则是动态常量,变量的值在编译期间不予以解析,所以开始都是默认值,像A与B都是int类型,故都是0。而在程序执行到A=B*10;所以A=0*10=0,程序接着执行到B=10这句时候,才会真正的B的初值10赋给B。

4,运算符重载
运算符重载只能用于类或结构中,通过类或结构中声明一个名为operator x的方法,即可完成一个运算符的重载。

先来看几行简单的代码:

1 static void Main(string[] args)
2 {
3     int x = 5;
4     int y = 6;
5     int sum = x + y;
6     Console.WriteLine(sum);
7     Console.ReadLine();
8 }

一个int sum=x+y; 加法运算。

稍微封装一下:

 1 static void Main(string[] args)
 2 {
 3     int x = 5;
 4     int y = 6;
 5     int sum = Add(x, y);
 6     Console.WriteLine(sum);
 7 }
 8
 9 static int Add(int x, int y)
10 {
11  return x + y;
12 }

如果现在有一个类,需要得知两个类某个属性的和,我们可能会这样:

 1 public class Person
 2 {
 3     public string Name { get; set; }
 4     public int Age { get; set; }
 5     public Person(string name, int age)
 6     {
 7         this.Name = name;
 8         this.Age = age;
 9     }
10 }
11
12 class Program
13 {
14     static void Main(string[] args)
15     {
16         Person p1 = new Person("aehyok", 25);
17         Person p2 = new Person("Leo", 24);
18         int sum = Add(p1.Age, p2.Age);
19         Console.WriteLine(sum);
20     }
21
22      static int Add(int x, int y)
23      {
24         return x + y;
25      }
26 }

我们再来改动一下:

 1 class Program
 2 {
 3     static void Main(string[] args)
 4     {
 5         Person p1 = new Person("aehyok", 25);
 6         Person p2 = new Person("Leo", 24);
 7         int sum = p1 + p2;
 8         Console.WriteLine(sum);
 9     }
10 }
11
12 public class Person
13 {
14     public string Name { get; set; }
15     public int Age { get; set; }
16     public Person(string name, int age)
17     {
18         this.Name = name;
19         this.Age = age;
20     }
21
22      public static int operator +(Person p1,Person p2)
23      {
24         return p1.Age+p2.Age;
25      }
26 }

5。static字段和static构造函数
主要来说明执行的顺序:
  1、编译器在编译的时候,先分析所需要的静态字段,如果这些静态字段所在的类有静态的构造函数,那么就会忽略字段的初始化;如果没有静态的构造函数,那么就会对静态字段进行初始化。
  2、如果存在多个静态类,那么初始化的静态成员的顺序会根据引用的顺序,先引用到的先进行初始化,但如果类的静态成员的初始化依赖于其他类的静态成员,则会先初始化被依赖的静态成员。
  3、而带有静态构造函数的类的静态字段,只有在引用到的时候才进行初始化。

 1 public class A
 2 {
 3      public static int X = B.Y+1;
 4      static A() { }
 5 }
 6
 7 public class B
 8 {
 9     public static int Y=A.X+1;
10 }
11
12 class Program
13 {
14     static void Main(string[] args)
15     {
16         Console.WriteLine("A.X={0},B.Y={1}",A.X,B.Y);
17         Console.ReadLine();
18     }
19 }

执行结果是:A.X = 1, B.Y = 2;

结果如何呢?再来看第二个小例子:

 1 public class A
 2 {
 3     public static int X = B.Y+1;
 4 }
 5
 6 public class B
 7 {
 8     public static int Y=A.X+1;
 9     static B() { }
10 }
11
12 class Program
13 {
14     static void Main(string[] args)
15     {
16         Console.WriteLine("A.X={0},B.Y={1}",A.X,B.Y);
17         Console.ReadLine();
18     }
19 }

执行结果是:A.X = 2, B.Y = 1;
是否和你想的结果一致呢?其实分析这两种情况 只要记住第一条概念就好:如果这些静态字段所在的类有静态的构造函数,那么就会忽略字段的初始化;如果没有静态的构造函数,那么就会对静态字段进行初始化。

看第一段代码片断:
A:
public static int X = B.Y+1;
static A() { }

B:
public static int Y=A.X+1;

A.X B.X ==> 先调用A.X, A中int X = B.Y + 1; 所以会接着调用B.Y, 因为B中无静态的构造函数,所以就会对静态字段进行初始化。 int Y = 0; 故 X = 1; B.Y = 2;

看第二段代码片断:
A:
public static int X = B.Y+1;

B:
public static int Y=A.X+1;
static B() { }

A.X B.X ==> 先调用A.X, A中int X = B.Y + 1; 所以会接着调用B.Y, 因为B中有静态的构造函数,所以就会忽略字段的初始化。 int Y = 1; 故 X = 2; B.Y = 2;
大家如果有兴趣的话也可以设置断点查看下代码是如何运行的。

时间: 2024-10-13 22:49:51

C#学习笔记二: C#类型详解的相关文章

【JavaWeb】学习笔记二 WEB-INF目录详解

1.WEB-INF是Java的WEB应用安全目录.所谓安全就是客户端无法访问,只有服务端可以访问的目录 2.web.xml是项目部署文件 3.classes文件夹用以放置*.class文件 4.lib文件夹用于存放需要的jar包 5.web.xml可以配置默认的欢迎页 <welcome-file-list> <welcome-file>/test.jsp</welcome-file> </welcome-file-list> 当web.xml修改后,建议重启

hadoop 学习笔记:mapreduce框架详解

hadoop 学习笔记:mapreduce框架详解 开始聊mapreduce,mapreduce是hadoop的计算框架,我 学hadoop是从hive开始入手,再到hdfs,当我学习hdfs时候,就感觉到hdfs和mapreduce关系的紧密.这个可能是我做技术研究的 思路有关,我开始学习某一套技术总是想着这套技术到底能干什么,只有当我真正理解了这套技术解决了什么问题时候,我后续的学习就能逐步的加快,而学习 hdfs时候我就发现,要理解hadoop框架的意义,hdfs和mapreduce是密不

Swift学习笔记(二)参数类型

关于参数类型,在以前的编程过程中,很多时间都忽视了形参与实参的区别.通过这两天的学习,算是捡回了漏掉的知识. 在swift中,参数有形参和实参之分,形参即只能在函数内部调用的参数,默认是不能修改的,如果想要修改就需要在参数前添加var声明. 但这样的声明过后,仍旧不会改变实参的值,这样就要用到inout了,传递给inout的参数类型必须是var类型的,不能是let类型或者字面类型,(字面类型是在swift中常提的一个术语,个人认为就是赋值语句,也不能修改)而且在传递过程中,要用传值符号"&

[学习笔记]struts.xml配置详解(所有基本配置,包括通配符)

1.<include> 利用include标签,可以将一个struts.xml配置文件分割成多个配置文件,然后在struts.xml中使用<include>标签引入其他配置文件.比如一个网上购物程序,可以把用户配置.商品配置.订单配置分别放在3个配置文件user.xml.goods.xml和order.xml中,然后在struts.xml中将这3个配置文件引入: struts.xml: [html] view plaincopy <?xml version="1.0

【Java学习笔记之三十三】详解Java中try,catch,finally的用法及分析

这一篇我们将会介绍java中try,catch,finally的用法 以下先给出try,catch用法: try { //需要被检测的异常代码 } catch(Exception e) { //异常处理,即处理异常代码 } 代码区如果有错误,就会返回所写异常的处理. 首先要清楚,如果没有try的话,出现异常会导致程序崩溃.而try则可以保证程序的正常运行下去,比如说: try { int i = 1/0; } catch(Exception e) { ........ } 一个计算的话,如果除数

MyBatis学习笔记2--配置环境详解

1.MyBatis-config.xml详解 一个完整的配置文件如下所示 <configuration> <!-- <properties resource="jdbc.properties"/> --> <properties> <property name="jdbc.driverClassName" value="com.mysql.jdbc.Driver"/> <prope

Linux学习笔记——用户及权限详解

用户及权限详解    用户.组.权限 安全上下文(secure context): 权限: r   w   x  文件: r:可读,可以使用类似cat等命令查看文件内容: w:可写,可以编辑或删除此文件: X:可执行,exacutable,可以命令提示符下当作命令提交给内核运行:  目录: r:可以对此目录执行ls以列出内部的所有文件: w:可以在此目录创建文件: x:可以使用cd切换进此目录,也可以使用ls -l查看内部文件的详细信息: rwx: r--:只读 r-x:读和执行 ---:无权限

CSS学习笔记(9)--详解CSS中:nth-child的用法

详解CSS中:nth-child的用法 前端的哥们想必都接触过css中一个神奇的玩意,可以轻松选取你想要的标签并给与修改添加样式,是不是很给力,它就是":nth-child". 下面我将用几个典型的实例来给大家讲解:nth-child的实际用途: Tips:还用低版本的IE浏览器的哥们请绕过! :nth-child(2)选取第几个标签,"2可以是你想要的数字" .demo01 li:nth-child(2){background:#090} :nth-child(n

Linux学习笔记—— 用户管理命令详解

 用户管理命令详解  用户管理: useradd,userdel,usermod,passwd,chsh,chfn,finger,id,chage 添加用户: useradd [options] USERNAME    -u (UID)   手动指定UID -g (GID)  (基本组) -G, ...     (附加组) 可以有多个,彼此之间用,号隔开 -c "COMMENT"         注释信息     -d /path/to/somedirectory  指定家目录 -s