(转载)深入浅出关键字---把new说透
[你必须知道的.NET] 第五回:深入浅出关键字---把new说透
Author: 王涛
Date:2007-4-28
©2007 Anytao.com ,原创作品,转贴请注明作者和出处。
本文将介绍以下内容:
- 面向对象基本概念
- new 关键字深入浅出
- 对象创建的内存管理
1. 引言
园子里好像没有或者很少把new 关键字拿出来说的,那我就占个先机吧,呵呵。那么,我们到底有必要将一个关键字拿出来长篇大论吗?看来是个问题。回答的关键是:你真的理解了new 吗?如果是,那请不要浪费时间,如果不是,那请继续本文的循序之旅。
下面几个 问题可以大概的考察你对new 的掌握,开篇之前,希望大家做个检验,如果通过了,直接关掉本页即可。如果没有通过,希望本文的阐述能帮你找出答案。
- new 一个class 对象和new 一个struct 或者enum 有什么不同?
- new 在.NET 中有几个用途,除了创建对象实例,还能做什么?
- new 运算符,可以重载吗?
- 范型中,new 有什么作用?
- new 一个继承下来的方法和override 一个继承方法有何区别?
- int i 和int i = new int() 有什么不同?
2. 基本概念
一般说来,new 关键字在.NET 中用于以下几个场合,这是MSDN 的典型解释:
- 作为运算符, 用于创建对象和调用构造函数。
本文的重点内容,本文在下一节来重点考虑。
- 作为修饰符,用于向基类成员隐藏继承成员。
作为修饰符,基本的规则可以总结为:实现派生类中隐藏方法,则基类方法必须定义为virtual ;new 作为修饰符,实现隐藏基类成员时,不可和override 共存,原因是这两者语义相斥:new 用于实现创建一个新成员,同时隐藏基类的同名成员;而override 用于实现对基类成员的扩展。
另外,如果在子类中隐藏了基类的数据成员,那么对基类原数据成员的访问,可以通过base 修饰符来完成。
例如:
new 作为修饰符
- using System;
-
- namespace Anytao.net.My_Must_net
- {
- class Number
- {
- public static int i = 123;
-
- public void ShowInfo()
- {
- Console.WriteLine("base class---");
- }
-
- public virtual void ShowNumber()
- {
- Console.WriteLine(i.ToString());
- }
- }
-
- class IntNumber : Number
- {
- new public static int i = 456;
-
- public new virtual void ShowInfo()
- {
- Console.WriteLine("Derived class---");
- }
-
- public override void ShowNumber()
- {
- Console.WriteLine("Base number is {0}", Number.i.ToString());
- Console.WriteLine("New number is {0}", i.ToString());
- }
- }
-
- class Tester
- {
- public static void Main(string[] args)
- {
- Number num = new Number();
- num.ShowNumber();
- IntNumber intNum = new IntNumber();
- intNum.ShowNumber();
-
- Number number = new IntNumber();
- // 究竟调用了谁?
- number.ShowInfo();
- // 究竟调用了谁?
- number.ShowNumber();
- }
- }
- }
- 作为约束,用于在泛型声明中约束可能用作类型参数的参数的类型。
MSDN 中的定义是:new 约束指定泛型类声明中的任何类型参数都必须有公共的无参数构造函数。当泛型类创建类型的新实例时,将此约束应用于类型参数。
注意:new 作为约束和其他约束共存时,必须在最后指定。
其定义方式为:
- class Genericer<T> where T : new()
- {
- public T GetItem()
- {
- return new T();
- }
- }
实现方式为:
- class MyCls
- {
- private string _name;
-
- public string Name
- {
- get { return _name; }
- set { _name = value; }
- }
-
- public MyCls()
- {
- _name = "Emma";
- }
- }
-
- class MyGenericTester
- {
- public static void Main(string[] args)
- {
- Genericer<MyCls> MyGen = new Genericer<MyCls>();
- Console.WriteLine(MyGen.GetItem().Name);
- }
- }
- 使用new 实现多态。 这不是我熟悉的话题,详细的内容可以参见 《多态与 new [C#] 》,这里有较详细的论述。
3.深入浅出
作为修饰符和约束的情况,不是很难理解的话题,正如我们看到本文开篇提出的问题,也大多集中在new 作为运算符的情况,因此我们研究的重点就是揭开new 作为运算符的前世今生。
Jeffrey Richter 在其著作中,极力推荐读者使用ILDASM 工具查看IL 语言细节,从而提高对.NET 的深入探究,在我认为这真是一条不错的建议,也给了自己很多提高的空间挖掘。因此,以下是本人的一点建议,我将在后续的系列中,关于学习方法论的讨论中深入探讨,这里只是顺便小议,希望有益于大家。
1 不断的学习代码;
2 经常看看IL 语言的运行细节,对于提供.NET 的认识非常有效。
文归正题,new 运算符用于返回一个引用,指向系统分配的托管堆的内存地址。因此,在此我们以Reflector 工具,来了解以下new 操作符执行的背后,隐藏着什么玄机。
首先我们实现一段最简单的代码,然后分析其元数据的实现细节,来探求new 在创建对象时到做了什么?
new 作为运算符
- using System;
-
- namespace Anytao.net.My_Must_net
- {
- class MyClass
- {
- private int _id;
-
- public MyClass(int id)
- {
- _id = id;
- }
- }
-
- struct MyStruct
- {
- private string _name;
-
- public MyStruct(string name)
- {
- _name = name;
- }
- }
-
- class NewReflecting
- {
- public static void Main(string[] args)
- {
- int i;
- int j = new int();
- MyClass mClass = new MyClass(123);
- MyStruct mStruct = new MyStruct("My Struct");
- }
- }
- }
使用Reflector 工具反编译产生的IL 代码如下为:
IL 元数据分析
- .method public hidebysig static void Main(string[] args) cil managed
- {
- .entrypoint
- .maxstack 2
- .locals init (
- [0] int32 num,
- [1] int32 num2,
- [2] class Anytao.net.My_Must_net._05_new.MyClass class2,
- [3] valuetype Anytao.net.My_Must_net._05_new.MyStruct struct2)
- L_0000: nop
-
- // 初始化j 为0
- L_0001: ldc.i4.0
- L_0002: stloc.1
-
- // 使用newobj 指令创建新的对象,并调用构造函数以0x76 (123 的16 进制)初始化
- L_0003: ldc.i4.s 0x7b
- L_0005: newobj instance void Anytao.net.My_Must_net._05_new.MyClass::.ctor(int32)
- L_000a: stloc.2
- // 加载“My Struct”
- L_000b: ldloca.s struct2
- L_000d: ldstr "My Struct"
- // 调用构造函数执行初始化
- L_0012: call instance void Anytao.net.My_Must_net._05_new.MyStruct::.ctor(string)
- L_0017: nop
- L_0018: ret
- }
从而可以得出以下结论:
- new 一个class 时,new 完成了以下两个方面的内容:一是调用newobj 命令来为实例在托管堆中分配内存;二是调用构造函数来实现对象初始化。
- new 一个struct 时,new 运算符用于调用其带构造函数,完成实例的初始化。
- new 一个int 时,new 运算符用于初始化其值为0 。
- 另外必须清楚,值类型和引用类型在分配内存时是不同的,值类型分配于线程的堆栈(stack )上,并变量本身就保存其实值,因此也不受GC 的控制,;而引用类型变量,包含了指向托管堆的引用,内存分配于托管堆(managed heap )上,内存收集由GC 完成。
- 另外还有以下规则要多加注意:
- new 运算符不可重载。
- new 分配内存失败,将引发OutOfMemoryException 异常。
对于基本类型来说,使用new 操作符来进行初始化的好处是,某些构造函数可以完成更优越的初始化操作,而避免了不高明的选择,例如:
- string str = new string('*', 100);
-
- string str = new string(new char[] {'a', 'b', 'c'});
而不是
- string str = "***************************************";
4. 结论
我能说的就这么多了,至于透了没透,作者的能量也就这么多了。希望园子的大牛们常来扔块砖头,对我也是一种莫大的促进。但是作为基本的原理和应用,我想对大部分的需求是满足了。希望这种力求深入浅出的介绍,能给你分享new 关键字和其本质的来龙去脉能有所帮助。
言归正传,开篇的几个题目,不知读者是否有了各自的答案,我们不妨畅所欲言,做更深入的讨论,以便揭开其真实的面纱。
参考文献
(USA )Stanley B.Lippman, C# Primer
(USA )David Chappell Understanding .NET