四、匿名类型
var关键字,它用于表示隐式类型化的变量。var与new关键字一起使用时,可以创建匿名类型。匿名类型只是继承自Object且没有名称的类。该类的定义从初始化器中推断,类似于隐式类型化的变量。
五、结构
结构是值类型,而不是引用类型。它们存储在栈中或存储为内联(inline)(如果它们是存储在堆中的另一个对象的一部分),其生存周期的限制与简单的数据类性一样。
*.结构不支持继承。
*.对于结构,构造函数的工作有一些区别。尤其是在编译器总是提供了一个无参数的默认构造函数,它是不允许替换的。
*.使用结构,可以指定字段如何在内存中布局。因为结构实际上是把数据项组合在一起,有时大多数或者全部字段都声明为public。严格来说,这与编写.Net代码的规则相反------根据Mircrosoft,字段(除了const字段之外)应总是私有的,并由公有属性封装。但是,对于简单的结构,许多开发人员都认为共有字段是可接受的编程方式。
1.结构是值类
虽然结构是值类型,但是在语法上常常可以把它们当作类来处理。
注意,因为结构是值类型,所以new运算符与类和其他引用类型的工作方式不同。new运算符并不分配堆中的内存,而是指调用相应的构造函数,根据传送给它的参数,初始化所有的字段。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace _003 { public class MyClass { const int a = 123456; readonly int b; int c = 0; private MyClass() { b = 123456; c = 654321; } public static MyClass CreateMyClass() { MyClass my = new MyClass(); return my; } public void Printf() { Console.WriteLine(a + " " + b + " " + c); } } struct MyStruct { public int a; public int b; public MyStruct(int _a, int _b) { a = 100; b = 101; } } class Program { static void Main(string[] args) { //const int a = 100; //readonly int b; MyClass mc = MyClass.CreateMyClass(); mc.Printf(); MyStruct ms = new MyStruct(1, 2); Console.WriteLine(ms.a + " " + ms.b); Console.ReadKey(); } } }
结构遵循其它数据类型都遵循的规则:在使用前所有的元素都必须初始化。在结构上调用new运算符,或者给所有的字段分别赋值。结构就完全初始化了。当然,如果结构定义为类的成员字段,在初始化包含的对象时,该结构会自动初始化为0。
结构是会影响性能的值类型,但根据使用的结构方式。这种影响可能是正面的,也可能时负面的。正面的影响是为结构分配内存时,速度非常快,因为它们将内联或者保存在栈中。在结构超出了作用域被删除时,速度也很快,不需要等待垃圾回收。负面影响是,只要把结构作为参数来传递或者把一个结构赋予另一个结构(如A = B, 其中A 和 B 是结构),结构的所有内容就被复制,而对于类,则只复制引用。这样就会有性能损失,根据结构的大小,性能损失也不同。注意,结构主要用于小的数据结构。但当把结构作为参数传递给方法时,应把它作为ref参数传递,以避免性能损失------此时只传递了结构在内存中的地址,这样传递速度就与在类中的传递速度一样快了。但如果这样做,就必须注意被调用的方法可以改变结构的值。
2.结构和继承
结构不是为继承而设计的。这意味着:它不能从一个结构中继承。唯一的例外是对应的结构(和C#中的其它类型一样)最终派生于System.Object。因此,结构也可以访问System.Object的方法。在结构中,甚至可以重写System.Object中的方法------如重写ToString()方法。结构的继承链是:每个结构派生自System.ValueType类,System.ValueType类又派生自System.Object。ValueType并没有给Object添加任何新成员,但提供了一些更适合结构的实现方式。注意:不能为结构提供其它基类:每个结构都派生自ValueType。
3.结构的构造函数
为结构定义构造函数的方式与为类定义构造函数的方式相同,但不允许定义无参数的构造函数。这看起来似乎没有意义,但其原因隐藏在.NET运行库的实现方式中。在一些罕见的情况中,.NET运行库不能调用用户提供的自定义无参构造函数,因此Microsoft干脆采用一种非常简单的方式:禁止在C#的结构内使用无参数的构造函数。
默认构造函数把数值的字段初始化为0,把引用类型字段初始化为null,且总是隐式地给出,即使提供了其他带参数的构造函数,也是如此。提供字段的初始值也不能绕过默认构造函数。
6.弱引用
在应用程序代码内实例化一个类或结构时,只要有代码引用它,就会形成强引用。例如,如果有一个类MyClass(),并创建了一个变量 myClassVariable来引用该类的对象,那么只要myClassVariable在作用域内,就存在对MyClass对象的强引用,如下:
MyClass myClassVariable = new MyClass();
这意味着垃圾回收器不会清理MyClass对象使用的内存。一般而言这是好事,因为可能需要访问MyClass对象,但是如果MyClass对象很大,并且不经常访问呢?此时可以创建对象的弱引用。
弱引用允许创建和使用对象,但是垃圾回收器运行时,就会回收对象并释放内存。由于存在潜在的Bug和性能问题,一般不会这么做,但是在特定情况在使用弱引用时很合理的。
弱引用是使用WeakReference类创建的。因为对象可能在任意时刻被回收,所以在引用该对象前必须确认它存在。以前面的MathTest类为例,这次使用WeakReference类创建对它的弱引用:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace _004 { class MyClass { private int a; private int b; public int A { get { return a; } set { a = value; } } public int B { get { return b; } set { b = value; } } public MyClass() { a = 100; b = 101; } public void Printf() { Console.WriteLine(A + " " + B); } } class Program { static void Main(string[] args) { MyClass mc = new MyClass(); mc.Printf(); WeakReference myWR = new WeakReference(new MyClass()); MyClass mc2; if (myWR.IsAlive) { mc2 = myWR.Target as MyClass; mc.A = 1993; mc.Printf(); } Console.ReadKey(); } } }
创建myWR时,会向其结构函数传递一个新的MyClass对象。MyClass对象成为了WeakReference对象的目标。想要使用MyClass对象时,就需要检查myWR对象以确保其未被回收。IsAlive属性就用于这个目的。如果IsAlive为true,就从目标属性得到MyClass对象的引用。注意,因为Target属性返回的是Object类型,所以必须将其强制转化为MyClass类型。然后,调用垃圾回收器(CG.Collect()),并尝试再次获得MyClass对象。这一次,IsAlive属性返回false,如果确实想要用MyClass对象,就必须实例化一个新的MyClass对象。
7.部分类
partial关键字允许把类、结构、方法或借口放在多个文件中。一般情况下,一个类全部驻留在单个文件中,但有时,多个开发人员需要访问同一个类,或者某种类型的代码生成器生成了一个类的某部分,所以把类放在多个文件中是有益的。
partial关键字的用法是:把partial放在class、struct或interface关键字的前面。在下面的例子中,TheBigClass类驻留在两个不同的源文件BigClassPart1.cs 和 BigClassPart2.cs中。
编译包含这两个源文件的项目时,会创建一个TheBigClass类,它有两个方法 MethodOne 和 MethodTwo()。
如果声明类时使用了下面的关键字,这些关键字就必须应用于同一个类的所有部分:
*public
*private
*protected
*internal
*abstract
*sealed
*new
*一般约束
在嵌套的类型中,只要partial关键字位于class关键字的前面,就可以嵌套部分类。在把部分类编译到类型中时,属性、XML注释、接口、泛型类型的参数属性和成员合并。
8.静态类
如果类只包含静态方法和属性,该类就是静态类。静态类在功能上与使用私有静态构造函数创建的类相同。不能创建静态类的实例。使用static关键字,编译器可以检查用户是否不经意间给该类添加了实例成员。如果是,就生成一个编译错误。这可以确保不创建静态类的实例。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace _004 { static class MyClass { static int a = 123; public static void Printf() { Console.WriteLine("静态类输出{0}", a); } } class Program { static void Main(string[] args) { MyClass.Printf(); Console.ReadKey(); } } }
9.Object类
前面提到过,所有的.NET类都派生自System.Object。实际上,如果在定义类时没有指定基类,编译器就会自动假定这个类派生自Obejct。本章举的例子没用使用继承,所以前面介绍的每个类都是派生自System.Object。对于结构,这个派生是间接的:就够总是派生自System.ValueType,System.ValueType又派生自System.Object。
其实际意义在于,除了自己定义的方法和属性等外,还可以访问为Obejct定义的许多公有的和受保护的成员方法。这些方法可用于自己定义的所有其它类中。
1.System.Object()方法
下面简要总结每个方法的作用。
*ToString()方法:是获取对象的字符串表示的一种便捷方式。当只需要快速获取对象的内容,以进行调试时,就可以使用这个方法。在数据的格式化方面,它几乎没有提供选择:例如,在原则上日期可以表示为许多不同的格式,但DateTime.ToString()没有在这方面提供任何选择。如果需要更复杂的字符串表示,例如,考虑用户的格式化首选项或区域性(区域),就应实现IFormattable接口。
*GetHashCode()方法:如果对象放在名为映射(也称为散列表或字典)的数据结构中,就可以使用这个方法。处理这些结构的类使用该方法确定把对象放在结构的什么地方。如果希望把类用作字典的一个键,就需要重写GetHashCode()方法。实现该方法重载的方式有一些相当严格的限制。
*Equals()(两个版本)和ReferenceEquals方法:注意有3个用于比较对象相等性的不同方法,这说明.NET Framework 在比较相等性方面有相当复杂的模式。这3个方法和比较运算符“==”在使用方式上有微妙的区别。而且,在重写带一个参数的虚Equals方法时也有一些限制,因为System.Collections名称空间中的一些基类要调用该方法,并希望它以特定的方式执行。
*Finalize()方法:它最接近C++风格的析构函数,在引用对象作为垃圾被回收以清理资源时调用它。Object中实现的Finalize()方法实际上什么也没有作,因而被垃圾回收器忽略。如果对象拥有对未托管资源的引用,则在该对象被删除时,就需要删除这些引用,此时一般要重写Finalize()。垃圾收集器不能直接删除这些对未托管资源的引用,因为它只负责托管的资源。,于是它只能依赖用户提供的Finalize()。
*GetType()方法:这个方法返回从System.Type派生的类的一个实例。这个对象可以提供对象成员所属类的更多信息,包括基本类型、方法、属性等。System.Type还提供了.NET的反射技术的入口点。
*MemberwiseClone()方法:它只复制对象,并返回对副本的一个引用(对于值类型,就是一个装箱的作用)。注意,得到的副本是一个浅表赋值,即它复制了类中的所有值类型。如果类包含内联的引用,就只复制引用,而不复制引用的对象。这个方法是受保护的,所以不能用于复制外部的对象。该方法不是虚方法,所以不能重写它的实现代码。
2.ToString方法
ToString()方法,它是快速获取对象的字符串表示的最便捷方式。
Object.ToString()声明为虚方法,在这些例子中,实现该方法的代码都是为C#预定义数据类型重写过的代码,以返回这些类型的正确字符串表示。Colors枚举是一个预定义的数据类性,它实际上实现为一个派生自System.Enum的结构,而System.Enum有一个相当智能的ToString()重写方法,它处理用户定义的所有枚举。
如果不在自己定义的类中重写ToString(),该类将只继承System.Object的实现方式------它显示类的名称。如果希望ToString()返回一个字符串,其中包含类中对象的值信息,就需要重写它。
该例子仅说明了C#的语法特性。C#已经有表示货币量的预定义的类型decimal。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace _004 { public class CStudentsInfo { private string m_Name; private int m_Age; private string m_Gender; public string NAME { get { return m_Name; } set { m_Name = value; } } public int AGE { get { return m_Age; } set { m_Age = value; } } public string GENDER { get { return m_Gender; } set { m_Gender = value; } } public CStudentsInfo(string _name, string _gender, int _age) { m_Name = _name; m_Age = _age; m_Gender = _gender; } public override string ToString() { string str = string.Empty; StringBuilder sb = new StringBuilder(); sb.Append("我的名字叫:"); sb.Append(m_Name); sb.Append(" "); sb.Append("我今年:"); sb.Append(m_Age.ToString()); sb.Append("岁了! "); sb.Append("我是:"); sb.Append(m_Gender); return sb.ToString(); } } class Program { static void Main(string[] args) { CStudentsInfo studentInfo = new CStudentsInfo("夏洛特! ", "男的! ", 5); string str = studentInfo.ToString(); Console.WriteLine(str); Console.ReadKey(); } } }
10.扩展方法