类型:
基本概念:
值类型:通常分配在线程的堆栈上,并且不包含任何指向实例数据的指针,因为变量本身就包含了其实例数据。MSDN中的定义为值类型直接包含它们的数据。
引用类型:实例分配在托管堆上,变量保存了视力数据的内存引用,MSDN中的定义为引用类型存储对值的内存地址的引用,位于堆上。
嵌套结构:
指在值类型中嵌套定义了引用类型,或者在引用类型变量中嵌套定义了值类型。
引用类型嵌套值类型:类的私有字段为值类型,那么它作为引用类型实例的一部分,也分配在托管堆上。
public class NestedValueinRef
{
//aInt做为引用类型的一部分将分配在托管堆上
private int aInt;
public NestedValueinRef()
{
//aChar则分配在该段代码的线程上
char achar =' a ';
}
}
值类型嵌套引用类型:该引用类型将作为值类型的成员变量,堆栈上将保存该成员的引用,而成员的实际数据还是保存在托管堆中。
public struct NestedRefinValue
{
public MyClass myClass;
public NestedRefinValue(MyClass mc)
{
myClass.x=1;
myClass.y=2;
myClass=mc;
}
}
结论:值类型实例总是分配在它声明的地方,声明为局部变量时其被分配在堆栈上,声明为引用类型成员时其被分配到托管堆上;引用类型实例则总是分配在托管堆上。
若执行下段代码:
AType[] myType=new AType[10];
试问:如果AType是值类型,则分配多少内存;若为引用类型,又分配多少内存?
分析:数组本身是引用类型,内存分配于托管堆,而myType为指向托管堆的引用。不同的是值类型和引用类型数组在托管堆的布局是有区别的:如果AType为值类型,则myType将保持指向托管堆中的一块大小为4*10byte的内存地址,并且将所有的元素赋值为0;若AType为引用类型,则数组由10个引用组成,并且所有的元素被设置为null值。
通用规则与比较
通用规则:
string类型是个特殊的引用类型,因此每次对string的改变都会在托管堆中产生一个新的string变量。string类型重载了==操作符,比较的是实际的字符串,而不是引用地址。
public static void Main()
{
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 static void Main()
{
MyStructTester aStruct = new MyStructTester(); //实例化值类型
System.Type type = aStruct.GetType(); //实例化System类型,并将值类型赋给type
if (type.IsValueType)
{
Console.WriteLine("{0} belongs to value type.", aStruct.ToString());
}
}
.NET中以操作符ref和out来标识值类型按引用方式传递。ref在参数传递之前必须初始化;out在传递之前不必初始化,且在传递时必须显式赋值。
值类型与引用类型之间的转换过程称为装箱和拆箱操作。
sizeof()运算符用于获取值类型的大小,但是不适用于引用类型。
值类型都继承自System.ValueType,而System.ValueType又继承自System.Object。其主要区别是ValueType重写了Equals方法,实现对值类型按照实例值比较而不是引用地址来比较。
char a = 'c';
char b = 'c';
Console.WriteLine((a.Equals(b))); //输出true;
比较
值类型继承自ValueType;而引用类型继承自System.object。
值类型变量包含其实例数据,每个变量保存了其本身的数据拷贝,因此值类型的参数传递不会影响参数本身;引用类型变量保存了其数据的引用地址,;因此以按值方式进行参数传递时会影响到参数本身,因为两个变量会引
用内存中的同一块地址。
典型的值类型:struct,enum以及大量的内置值类型;而能成为类的都可以说是引用类型。
值类型是密封的,因此值类型不能作为其他任何类型的基类,但可以单继承或者多继承接口;引用类型一般都有继承性。
值类型不具有多态性;引用类型有多态性。
值类型变量不可谓null值,值类型都会自行初始化为0;引用类型变量默认情况下,创建为null值,标识没有指向任何托管堆的引用地址。对值为null的引用类型的任何操作,都会抛出NullReferenceException异常。
值类型有两种状态:装箱和未装箱,运行库提供了所有值类型的已装箱形式;引用类型通常只有一种形式:装箱。
值类型的应用场合:
数据较小的场合,最好考虑以值类型来实现系统性能的改善。
结构简单,不必在多态的情况下,值类型是较好的选择。
以存储数据为主要目的的情况下,值类型是优先的选择。
类型的选择没有子类继承的必要,优先考虑值类型。
引用类型的应用场合:
引用类型适用于结构复杂、有继承、有多态、突出行为的场合。
类型的比较通常使用:Equals(),ReferenceEquals(),==,!=这四种方法,其中Equals()为核心方法。
值类型判等:
Equals,System.ValueType重载了System.Object的Equals方法,用于实现对实例数据的判等。 //比较两个对象是否指向相同的引用地址。
ReferenceEquals,对值类型应用将永远返回false。
==,未重载的==值类型,将比较两个值是否“按位”相等。
引用类型判等:
Equals有两种方法:
public virtual bool Equals(object obj); 虚方法,引用地址比较。
public static bool Equals(object objA,object objB); 静态方法,如果objA是与objB相同的实例,或者两者均为空引用,或者如果objA.Equals(objB)返回为true,则为true,否则为false。
ReferenceEquals:静态方法,只能用于引用类型,用于比较两个实例对象是否指向同一引用地址。
==:默认为引用地址比较。和Equals方法的区别是,在多态表现上,==是被重载,而Equals是覆写。
演示:
Console.WriteLine("类型判等---");
//值类型总是返回false,经过两次装箱的myStruct不可能指向同一地址,返回false
Console.WriteLine(ReferenceEquals(myStruct, myStruct));
//同一引用类型对象,将指向同样的内存地址,返回true
Console.WriteLine(ReferenceEquals(myClass, myClass));
//RefenceEquals认为null等于null,因此返回true
Console.WriteLine(ReferenceEquals(null, null));
//重载的值类型判等方法,成员大小不同
Console.WriteLine(myStruct.Equals(myStruct2));
//重载的引用类型判等方法,指向引用相同
Console.WriteLine(myClass.Equals(myClass2));
类型转换
隐式转换:由低级类型向高级类型的转换过程。包括:值类型的隐式转换,主要是数值类型等基本类型的隐式转换;引用类型的隐式转换,主要是派生类向基类的转换;值类型和引用类型的隐式转换,主要指装箱和拆箱转换。
显式转换:也叫强制类型转换。但是转换过程不能保证数据的完整性,可能引起一定得精度损失或者引起不可知的异常发生。转换的格式为:(type)(变量、表达式);
例如: int a = (int) (b+2.02);
使用explicit或者inplicit进行用户自定义类型转换:主要给用户提供自定义的类型转换实现方式,以实现更有目的的转换操作,格式为:
static 访问修饰操作符 转换修饰操作符 operator 类型(参数列表);
例如:public static explicit operator VipCustom(Custom user)
参数的基础
参数按照调用方式可以分为:形参和实参。形参就是被调用方法的参数,实参就是调用方法的参数。
public static void Main()
{
string myString = "This is your argument.";
//myString是实参
ShowString(myString);
}
//astr是形参
private static void ShowString(string astr)
{
Console.WriteLine(astr);
}
参数的基本语法:
1.形参和实参必须类型、个数与顺序对应匹配;
2.参数列表可以为空,这种函数称为无参函数;
3.Main(string[]args),Main函数的参数可以为空,可以为string数组,其作用是接受命令行参数,例如在命令行下运行程序时,args提供了输入命令行参数的入口。
传递的基础:
泛型类型参数:可以是静态的,例如MyGeneric
;也可以使动态的,例如MyGeneric
中的T可以是任何类型的变量,在运行期动态替换为相应的类型参数。泛型类型参数一般以T开头来命名。
可变数目参数:使用params修饰符可以方便地提供对于参数个数不明确的情况的实现。
params用于提示编译器实现对参数进行数组封装,将可变数目的控制由编译器来完成,形如:
static void ShowAgeSum(string team,params int[] ages){……}
params修饰的参数必须为一维数组,可以是任何类型,因此,如果需要接受任何类型的参数时,只要设置数组类型为object即可。params必须在参数列表的最后一个,并且只能使用一次。
传递的分类讨论:
1.值类型参数的按值传递:值类型实例传递的是该值类型实例的一个拷贝,被调用方法操作的是属于其本身的实例拷贝,因此不影响原来调用方法中的参数值。
2.引用类型参数的按值传递:值 为对象引用,因此传递的是引用地址拷贝,会改变原来对象的引用指向。
3.引用类型参数的按引用传递:传递的是参数的地址。如果参数为值类型,则传递的是该值类型的地址;如果参数为引用类型,则传递的是对象引用的地址。
4.string类型的特殊性:string本身为引用类型,当它按值传送时表现出特殊的情况。
public static void RunSnippet()
{
string str = "Old String";
ChangeStr(str);
Console.WriteLine(str); //输出Old String
}
static void ChangeStr(string aStr)
{
aStr = "Changing String";
Console.WriteLine(aStr);//输出Changing String
}
原理:执行string str = "Old String"后,在堆栈中产生str指向Old String;执行ChangeStr(str)后,在堆栈中产生astr,并将str的地址指针拷贝给astr使其指向Old String;执行aStr = "Changing String"后将astr的指向修改为新的Changing String
5.值类型和引用类型,按引用传递必须以ref或者out关键字来修饰,规则是:方法定义和方法调用必须同时显式的使用ref或者out;允许通过out或者ref参数来重载方法。
不论参数本身是值类型还是引用类型,按引用传递时,传递的是参数的地址,也就是实例的指针。
public static void RunSnippet()
{
int i = 100;
string str = "One";
ChangeByValue(ref i);
ChangeByRef(ref str);
Console.WriteLine(i);//200
Console.WriteLine(str);//One more
}
static void ChangeByValue(ref int iVlaue)
{
iVlaue = 200;
Console.WriteLine(iVlaue);//200
}
static void ChangeByRef(ref string sValue)
{
sValue = "One more.";
Console.WriteLine(sValue);//One more
}
6.不允许通过区分ref和out来实现方法重载
static void ShowInfo(string str)
{
Console.WriteLine(str);
}
static void ShowInfo(ref string str)
{
Console.WriteLine(str);
}
//ShowInfo不能定义仅在 ref 和 out 上有差别的重载方法
static void ShowInfo(out string str)
{
str = "Hello, anytao.";
Console.WriteLine(str);
}
不同点:ref的参数必须是一个实际的对象,而不能指向NULL;而使用out的参数可以接受指向null的对象,然后在嗲用方法内部必须完成对象的实体化。
装箱与拆箱
宏观:从类型转换角度来说,装箱与拆箱是值类型与引用类型之间的桥梁,实现二者间的自由转换:装箱就是值类型数据转化为无类型的引用对象,通常这种转换主要指转换为System.Object类型或者该值类型实现的任何接口引用类型;拆箱就是引用类型转换为值类型,通常伴随着从堆中赋值对象实例的操作。
微观:装箱操作,在托管堆中分配新创建对象的内存,并将值类型的字段拷贝到该内存中,然后返回新对象的地址;拆箱操作,获取已装箱对象中来自值类型部分字段的地址。
雷区:装箱和拆箱并非完全对称的互逆操作,拆箱在执行上并不包含字段的拷贝过程。而引用类型就无所谓装箱拆箱,其本身就在“箱子”里。
只有被装过箱的对象才能被拆箱,而并非所有的引用类型。将并非装箱而来的引用类型强制转换为值类型,将抛出InvalidCastException异常。
规则:类型一致,拆箱必须保证执行后的结果是原来未装箱时的类型,否则将抛出InvalidCastException异常。
装箱与拆箱主要是针对值类型而言的,引用类型总是以装箱形式存在的。
装箱和拆箱分为显式转换和隐式转换两种情况,在实际的编码中应该警惕隐式转换带来的性能与异常问题。