1、值类型,引用类型,拆,装箱,常用的引用类型,值类型。
栈:一种先进后出(后进先出)的存储数据的结构体
堆:一块连续的,自由的存储空间。
值类型:变量直接保存其数据。
引用类型:变量保存其数据的引用(地址),而不是具体的数据。
C#的值类型包括:结构体(数值类型,bool型,用户定义的结构体),枚举,可空类型。
C#的引用类型包括:数组,用户定义的类、接口、委托,object,字符串。
数组的元素,不管是引用类型还是值类型,都存储在托管堆上。
引用类型在栈中存储一个引用,其实际的存储位置位于托管堆。为了方便,本文简称引用类型部署在托管推上。
值类型总是分配在它声明的地方:作为字段时,跟随其所属的变量(实例)存储;作为局部变量时,存储在栈上。
值类型在内存管理方面具有更好的效率,并且不支持多态,适合用作存储数据的载体;引用类型支持多态,适合用于定义应用程序的行为。
应该尽可能地将值类型实现为具有常量性和原子性的类型。
应该尽可能地确保0为值类型的有效状态。
应该尽可能地减少装箱和拆箱。
装箱:把值类型转换成引用类型的过程。把一个值类型赋值给一个引用类型
拆箱:把引用类型转换成值类型的过程。把一个引用类型赋值给一个值类型(需要用装箱的数据类型转换)
object obj;
short i = 123;
obj = i;//装箱操作
int j = (short )obj;//拆箱操作,需要用装箱的数据类型转换
Console.WriteLine(obj);
隐式转换和强制转换。
隐式转换:第一种,把小的赋值给大的。第二种,把派生类(子类)赋值给基类,也会发生隐式转换。第三种、把某一类的对象赋值给实现该类的接口时候发生隐式转换。
强制转换:大的赋值给小的时候强制转换。拆箱的时候发生强制转换。
装箱和拆箱的内部原理
装箱 是值类型到 object 类型或到此值类型所实现的任何接口类型的隐式转换。对值类型装箱会在堆中分配一个对象实例,并将该值复制到新的对象中。
拆箱(取消装箱)是从 object 类型到值类型或从接口类型到实现该接口的值类型的显式转换。取消装箱操作包括:
检查对象实例,确保它是给定值类型的一个装箱值。(拆箱后没有转成原类型,编译时不会出错,但运行会出错,所以一定要确保这一点。用GetType().ToString()判断时一定要使用类型全称,如:System.String 而不要用String。)
将该值从实例复制到值类型变量中。
值类型举例:
int i = 0; 执行,i入栈。
ChangeAge(i);方法入栈,执行方法i = 10;
当i = 10执行完后出栈。此时方法执行完,也出栈。
栈中留下i=0;
所以输出结果为0
内存图解如下:
namespace ConsoleApplication1
{
class Program
{
static void ChangeAge(int i)
{
i = 10;
}
static void Main(string[] args)
{
int i = 0;
ChangeAge(i);
Console.WriteLine(i);//输出结果为0
}
}
}
C#支持两个预定义的引用类型
名称
CTS类型
说明
object
System.Object
根类型,CTS中的其他类型都是从它派生而来的(包括值类型)
string
System.String
Unicode字符
1.object类型
许多编程语言和类结构都提供了根类型,层次结构中的其他对象都从它派生而来。C#和.NET也不例外。在C#中,object类型就是最终的父类型,所有内在和用户定义的类型都从它派生而来。这是C#的一个重要特性,它把C#与VB和C++区分开来,但其行为与Java中的非常类似。所有的类型都隐含地最终派生于System.Object类,这样,object类型就可以用于两个目的:
可以使用object引用绑定任何特定子类型的对象。例如,使用object类型把堆栈中的一个值对象装箱,再移动到堆中。对象引用也可以用于反射,此时,必须有代码来处理未知的特定类型对象。这类似于C++中的void指针或VB中的Variant数据类型。
object类型执行许多基本的一般用途的方法,包括Equals()、GetHashCode()、GetType()和ToString()。用户定义的类可能需要使用一种面向对象技术--重写,提供其中一些方法的替代执行方法。例如,重写ToString()时,要给类提供一个方法,该方法可以提供类本身的字符串表示。如果类中没有提供这些方法的实现,编译器就会在对象中选择这些实现,它们在类中的执行不一定正确。
2.string类型
有C和C++字符串不过是一个字符数组,因此客户机程序员需要做很多工作,才能把一个字符串复制到另一个字符串上,或者连接两个字符串。实际上,对于一般的C++程序员来说,执行包装了这些操作细节的字符串类是一个非常头痛的耗时过程。VB可以使用string类型,Java中的String类在许多方面都类似于C#字符串。
C#有string关键字,翻译为.NET类时,它就是System.String。
输出结果:
s1 is a String
s2 is a String
s1 is now another string
s2 is new a String
改变s1的值对s2没有影响,这与我们期待的引用类型正好相反。当用值 a String 初始化s1时,就在堆上分配了一个新的String对象。在初始化s2时,引用也指向这个对象。所以s2的值也是a string,但是当改变s1的值时,并不会替换原来的值,堆上为新分配一个对象。S2变量仍指向原来的对象,所以它的值没有改变
string是一个引用类型,String对象保留在堆中,而不是堆栈中。因此当把一个字符串变量赋予另一个字符串时,会得到对内存中同一个字符串的两个引用,但是String与引用类型在常见的操作上有区别。字符串时不可以改变的,修改其中一个字符串,就会创建一个全新的String对象,另一个字符串不发生变化。例:
class Program
{
static void Main(string[] args)
{
string s1 = "a String";
string s2 = s1;
Console.WriteLine("s1 is " + s1);
Console.WriteLine("s2 is "+s2);
s1 = "another string";
Console.WriteLine("s1 is now "+s1);
Console.WriteLine("s2 is new "+s2);
}
}
开始bb=alist,此时bb为null,bb指向alist的内存地址,bb=new List<int>(),bb指向内存的地址改变了,不再和alist是同一个地址,
所以bb无论怎么改alist依然为空。
关于引用类型需要注意的地方。举例:
一、class Program
{
static void Main(string[] args)
{
List<int> alist = null;
List<int> bb = alist;
bb = new List<int>();
bb.Add(1);
if (alist == null)
{
Console.WriteLine("a list is null");
}
else
{
Console.WriteLine("a list is not null");//结果a list is null
}
}
}
二、class Program
{
public class Student
{
public Dictionary<int, string> dicValue;
public void AddDic(Dictionary<int, string> dicValue)
{
if (dicValue == null)
{
dicValue = new Dictionary<int, string>();
输出结果:发现程序一直为null;
分析原因如下:
在AddDic方法中重新new了一次导致内存地址的指向发生了改变disValue已经和原来的内存地址指向不同了。解决方法:
第一种方法:声明dicValue的时候将他初始化。
第二种方法:在AddDic中的dicValue参数前加上out或ref,重新将地址指给原理的dicValue。
}
for (int i = 0; i < 10; i++)
{
dicValue.Add(i, "ss" + i);
}
}
}
static void Main(string[] args)
{
Student s = new Student();
s.dicValue = null;
s.AddDic(s.dicValue);
Console.WriteLine(s.dicValue.Count);
}
} [C#]
一个关于null赋值的问题
class Program
{
static void Main(string[] args)
{
Children children = new Children();
SetInstanceNull(children);
if (children == null)
{
Console.WriteLine("children is null");
}
else
{
Console.WriteLine(children.inter);
}
}
static void SetInstanceNull(Children childrenParam)
{
childrenParam.inter = 10;
childrenParam = null;
}
}
class Children
{
public int inter = 0;
}
程序输出结果:10
问题解析:我们知道方法参数如果是引用类型的话,则方法调用时,将把实例对象的地址传递给方法参数,这样在被调用方法中就可以通过实例对象的地址来操作实例对象的数据。故在SetInstanceNull方法中我们能将children实例中inter成员的值改更为10。然而childrenParam = null语句却没有使children为null,而仅仅是把childrenParam值为null。有人说children和chilrenParam是两个不同的变量,所以才有这样的结果。的确,这种原因的产生是因为他们是两个不同的变量导致的,但为什么不同呢?如果我们用object.ReferenceEquals方法去验证两个变量的相等性的话,我们会发现结果是相等的。那这个相等一定表示这两个变量相同吗?答案是否定的。在C#里面,当初始化一个类的时候,系统将使所有的引用引用类型参数引用为空,当遇到实例化一个类的时候,例如:new Children(),系统会在堆上分配一个内存空间存放Children实例,并将该地址返回给引用参数children。这种其实就是指针了。这样引用参数children与刚才实例化的Children实例就建立了一一映射关系。当调用方法SetInstanceNull时,系统将children参数的引用复制给childrenParam参数。这样在SetInstanceNull方法里面就可以操作刚才实例化的Children实例。所以Children实例中的inter成员能够被更改。childrenParam = null中语句只影响到childrenParam而没有影响到children给了我们一点提示,那就是将引用类型参数赋值为null其实是切断参数与实例之间的联系,当没有任何参数与该实例有联系的时候,该实例就会被垃圾回收器给回收。