能否回答下面问题呢?
new一个class对象和new一个struct或者enum有什么不同?
new在.NET中有几个用途,除了创建对象实例,还能做什么?
new运算符,可以重载吗?
范型中,new有什么作用?
new一个继承下来的方法和override一个继承方法有何区别?
int i和int i = new int()有什么不同?
一、基本概念
一般说来,new关键字在.NET中用于以下几个场合,这是MSDN的典型解释:
1、作为运算符, 用于创建对象和调用构造函数。
2、作为修饰符,用于向基类成员隐藏继承成员。
作为修饰符,基本的规则可以总结为:
实现派生类中隐藏方法,则基类方法必须定义为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();
}
}
}
3、作为约束,用于在泛型声明中约束可能用作类型参数的参数的类型。
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);
}
}
4、使用new实现多态。
(0)浪子 在 《今天你多态了吗?》 提出两个这样的 问题 :
A) “使用基类继承多态,有一点特别需要注意的就是:基类(抽象或者非抽象)中需要获得多态效果的成员必须有 abstract 或 virtual 修饰。”使用 new 来重写的成员不能形成多态吗?
B) “多态就是使得你能够用一种统一的方式来处理一组各具个性却同属一族的不同个体的机制。” new 重写后的成员是否符合了这个范畴?
(1)你通常怎样用多态?
假设我有一个类,里面有一个 PrintStatus 方法,用于打印实例的当前状态,我希望该类的派生类都带有一个 PrintStatus 方法,并且这些方法都用于打印其实例的当前状态。那么我会这样表达我的愿望:
// Code #01
class Base
{
public virtual void PrintStatus()
{
Console.WriteLine( " public virtual void PrintStatus() in Base " );
}
}
于是我可以写一个这样的方法:
// Code #02
public void DisplayStatusOf(Base[] bs)
{
foreach (Base b in bs)
{
b.PrintStatus();
}
}
bs 中可能包含着不同的 Base 的派生类,但我们却可以忽略这些“个性”而使用一种统一的方式来处理某事。在 .NET 2.0 中,XmlReader 的 Create 有这样一个版本:
public static XmlReader Create(Stream input);
你可以向 Create 传递任何可用的“流”,例如来自文件的“流”(FileStream)、来自内存的“流”(MemoryStream)或来自网络的“流”(NetworkStream)等。虽然每一中“流”的工作细节都不同,但我们却使用一种统一的方式来处理这些“流”。
(2)假如有人不遵守承诺...
DisplayStatusOf 隐含着这样一个假设:bs 中如果存在派生类的实例,那么该派生类应该重写 PrintStatus,当然必须加上 override 关键字:
// Code #03
class Derived1 : Base
{
public override void PrintStatus()
{
Console.WriteLine( " public override void PrintStatus() in Derived1 " );
}
}
你可以把这看作一种承诺、约定,直到有人沉不住气...
// Code #04
class Derived2 : Base
{
public new void PrintStatus()
{
Console.WriteLine( " public new void PrintStatus() in Derived2 " );
}
}
假设我们有这样一个数组:
// Code #05
Base[] bs = new Base[]
{
new Base(),
new Derived1(),
new Derived2()
} ;
把它传递给 DisplayStatusOf,则输出是:
// Output #01
// public virtual void PrintStatus() in Base
// public override void PrintStatus() in Derived1
// public virtual void PrintStatus() in Base
从输出结果中很容易看出 Derived2 并没有按照我们期望的去做。但你无需惊讶,这是由于 Derived2 的设计者没有“遵守约定”的缘故。
(3)new:封印咒术
new 似乎给人一种这样的感觉,它的使用者喜欢打破别人的约定,然而,如果使用恰当,new 可以弥补基类设计者的“短见”。
从 Output #01 中我们可以看到,new 只是把 Base.PrintStatus 封印起来而不是消灭掉,你可以解除封印然后进行访问。对于 Derived2 的使用者,解封的方法是把 Derived2 的实例转换成 Base 类型:
// Code #06
Base d2 = new Derived2();
d2.PrintStatus();
// Output #02
// public virtual void PrintStatus() in Base
而在 Derived2 内部,你可以透过 base 来访问:
// Code #07
base .PrintStatus();
这种方法是针对实例成员的,如果被封印的成员是静态成员的话,就要透过类名来访问了。
(4)假如 Base.PrintStatus 是某个接口的隐式实现...
假如 Base 实现了一个 IFace 接口:
// Code #08
interface IFace
{
void PrintStatus();
}
class Base : IFace
{
public virtual void PrintStatus()
{
Console.WriteLine( " public virtual void PrintStatus() in Base " );
}
}
我们只需要让 Derived2 重新实现 IFace:
// Code #09
class Derived2 : Base, IFace
{
public new void PrintStatus()
{
Console.WriteLine( " public new void PrintStatus() in Derived2 " );
}
}
Derived1 保持不变。则把:
// Code #10
IFace[] fs = new IFace[]
{
new Base(),
new Derived1(),
new Derived2(),
}
传递给:
// Code #11
public void DisplayStatusOf(IFace[] fs)
{
foreach (IFace f in fs)
{
f.PrintStatus();
}
}
的输出结果是:
// Output #03
// public virtual void PrintStatus() in Base
// public override void PrintStatus() in Derived1
// public new void PrintStatus() in Derived2
从输出结果中,我们可以看到,虽然 Derived2.PrintStatus 应用了 new,但却依然参与动态绑定,这是由于 new 只能割断 Derived2.PrintStatus 和 Base.PrintStatus 的联系,而不能割断它与 IFace.PrintStatus 的联系。我在 Derived2 的定义中重新指定实现 IFace,这将使得编译器认为 Derived2.PrintStatus 是 IFace.PrintStatus 的隐式实现,于是,在动态绑定时 Derived2.PrintStatus 就被包括进来了。
(5)谁的问题?
必须指出,如果 Base(Code #01)和 Derived2(Code #04)同时存在的话,它们俩其中一个存在着设计上的问题。为什么这样说呢?Base 的设计者在 PrintStatus 上应用 virtual 说明了他希望派生类能透过重写这一方法来参与动态绑定,即多态性;而 Derived2 的设计者在 PrintStatus 上应用 new 则说明了他希望割断 Derived2.PrintStatus 和 Base.PrintStatus 之间的联系,这将使得 Derived2.PrintStatus 无法参与到 Base 的设计者所期望的动态绑定中。如果在 Base.PrintStatus 上应用 virtual(即对多态性的期望)是合理的话,那么 Derived2.PrintStatus 应该换用另外一个名字了;如果在 Derived2.PrintStatus 上应用 new(即否决参与动态绑定)是合理的,那么 Base.PrintStatus 应该考虑是否去掉 virtual 了,否则就会出现一些奇怪的行为,例如 Output #01 的第三行输出。
假如继承体系中多态性行为的期望是合理的话,那么更实际的做法应该是把 Base 定义成这样:
// Code #12
abstract class Base
{
public abstract void PrintStatus();
}
而原来 Base 中的实现应该下移到一个派生类中:
// Code #13
class Derived3 : Base
{
public override void PrintStatus()
{
Console.WriteLine( " public override void PrintStatus() in Derived3 [originally implemented in Base] " );
}
}
这样,Derived2.PrintStatus 将使得编译无法完成,从而迫使其设计者要么更改方法的名字,要么换用 override 修饰。这种强制使得 Derived2 的设计者不得不重新考虑其设计的合理性。
假如继承体系中多态性行为的期望不总是合理呢?例如 Stream 有这样一个方法:
public abstract long Seek( long offset, SeekOrigin origin);
现在假设我有一个方法在处理输入流时需要用到 Stream.Seek:
// Code #14
public void Resume(Stream input, long offset)
{
//
input.Seek(offset, SeekOrigin.Begin);
//
}
当我们向 Resume 传递一个 NetworkStream 的实例,Resume 将会抛出一个 NotSupportedException,因为 NetworkStream 不支持 Seek。那么这是否说明 Stream 的设计有问题呢?
设想 Resume 是一个下载工具进行断点续传的方法,然而,并不是所有的服务器都支持断点续传的,于是,你需要首先判断输入流是否支持 Seek 操作,再决定如何处理输入流:
// Code #15
public void Resume(Stream input, long offset)
{
if (input.CanSeek)
{
//
input.Seek(offset, SeekOrigin.Begin);
//
}
else
{
//
}
}
如果 CanSeek 为 false,那就只好从头来过了。
实际上,我们并不能保证任何 Stream 的派生类都能够支持某个(些)操作,我们甚至不能保证来自同一个派生类的所有实例都支持某个(些)操作。你可以设想有这样一个 PriorityStream,它能够根据当前登录账号的权限来决定是否提供写操作,这使得拥有足够权限的人才能修改数据。或许 Stream 的设计者已经预料到这类情况的发生,所以 CanRead、CanSeek 和 CanWrite 就被加入到 Stream 里了。
值得注意的是,Code #07 的 Derived2 可能是一个很糟糕的设计,也可能是一个很实用的设计。在本文,它是一个很糟糕的设计,如果你足够细心,你会察觉到 Derived2 的设计者希望 Derived2.PrintStatus 绕过 Base.PrintStatus 而直接和 IFace.PrintStauts 进行关联,表面上这没什么不妥,但实质上 Base.PrintStatus 和 IFace.PrintStauts 在约定上是同质的,这意味着如果与 IFace.PrintStauts 进行关联就等于承认自己和 Base.PrintStatus 是同质的,这样的话,为什么不直接在 Derived2 里重写 PrintStatus 呢?
二、深入浅出
作为修饰符和约束的情况,不是很难理解的话题,正如我们看到本文开篇提出的问题,也大多集中在new作为运算符的情况,因此我们研究的重点就是揭开new作为运算符的前世今生。
new运算符用于返回一个引用,指向系统分配的托管堆的内存地址。因此,在此我们以Reflector工具,来了解以下new操作符执行的背后,隐藏着什么玄机。
(1)new一个class时,new完成了以下两个方面的内容:一是调用newobj命令来为实例在托管堆中分配内存;二是调用构造函数来实现对象初始化。
(2)new一个struct时,new运算符用于调用其带构造函数,完成实例的初始化。
(3)new一个int时,new运算符用于初始化其值为0。
(4)另外必须清楚,值类型和引用类型在分配内存时是不同的,值类型分配于线程的堆栈(stack)上,并变量本身就保存其实值,因此也不受GC的控制,;而引用类型变量,包含了指向托管堆的引用,内存分配于托管堆(managed heap)上,内存收集由GC完成。
另外还有以下规则要多加注意:
(1)new运算符不可重载。
(2)new分配内存失败,将引发OutOfMemoryException异常。
对于基本类型来说,使用new操作符来进行初始化的好处是,某些构造函数可以完成更优越的初始化操作,而避免了不高明的选择,例如:
string str = new string('*', 100);
string str = new string(new char[] {'a', 'b', 'c'});
而不是
string str = "***************************************";