值类型是一种轻量级的C++/CLI类机制,非常适合于小型的数据结构,且从语义的角度来看,与数值(Value)类似。
与之相比,引用类型的实例--包括那些声明在堆栈上的,是由垃圾回收器管理的,而值类型的实例却不是。一般来说,一个值类较好的实现应只有一些数据成员,而不需要继承性,这样,在函数传递及返回值、或是赋值操作时,不会带来巨大的数据开销。
值类初印像 请看例1中的Point类,可以通过替换ref为value,来把一个引用类变为值类;与引用类(ref)相似,值类(value)也是一个包含了空格的关键字。与大家想像的一样,值类(value)与值结构(value struct)之间唯一的区别就是,前者默认的可访问性为private,而后者则为public。
例1:
using namespace System; public value class Point { int x; int y; public: //定义属性X与 Y的读写实例 property int X { int get() { return x; } void set(int val) { x = val; } } property int Y { int get() { return y; } void set(int val) { y = val; } } //定义实例构造函数 Point(int xor, int yor) { X = xor; Y = yor; } void Move(int xor, int yor) { X = xor; Y = yor; } virtual bool Equals(Object^ obj) override { if (obj == nullptr) { return false; } if (GetType() == obj->GetType()) { Point^ p = static_cast<Point^>(obj); return (X == p->X) && (Y == p->Y); } return false; } static bool operator==(Point p1, Point p2) { return (p1.X == p2.X) && (p1.Y == p2.Y); } // static bool operator==(Point% p1, Point% p2) // { // return (p1.X == p2.X) && (p1.Y == p2.Y); // }
// static bool operator==(Point& p1, Point& p2) // { // return (p1.X == p2.X) && (p1.Y == p2.Y); // } virtual int GetHashCode() override { return X ^ (Y << 1); } virtual String^ ToString() override { return String::Concat("(", X, ",", Y, ")"); } }; |
值类自动继承自System::ValueType,而System::ValueType则继承自System::Object,但是,这却不能显式地声明。值类隐式表明了为"sealed",也就是说,它不能被作为一个基类,另外,为其类成员指定一个protected是没有任何意义,并且也是不允许的。如果想显式声明一个值类(或引用类),可像如下所示:
value class X sealed {/*...*/}; |
请注意,此处没有默认的构造函数。对一个值类来说,CLI本身把类实例中所有字段的位都设置为零,所以,不能提供自己的默认构造函数;然而,零、false、nullptr对其他类型来说,也许并不是合适的默认值,因此,对某些特定类型来说,就要用引用类型来取代值类型了。(遵从C++/CLI的实现会将false与nullptr表示为位全部为零。)
值类的另一个限制是它们带有一个默认的拷贝构造函数和一个赋值操作符,两者都会进行逐位复制,并不可被重载。
如果要实现Point类中的Equals函数,相比引用类中的而言要简单一些。请记住,我们正在重载定义System::Object中的这个版本,而其接受一个Object^,因为这种类型的参数很可能有一个nullptr值,在此,先可以省去检查是否为自身比较这一步,而对引用类的Equals实现来说,这一步是必需的,因为可有多个句柄引用同一对象。但是话说回来,在目前的这个值类中,没有两个值的实例可表示同一个实例,两个相同的值实例,只代表两个Point有相同的坐标,但修改其中一者的x坐标,不会影响到另一者的相同值。
当一个Point的实例传递到Equals时,作为值类型(其最终也都继承自System::Object)而言,装箱就发生了--也就是说,在垃圾回收堆上分配了一个Object的实例,而其包含了传递进来Point的一份副本。因为是创建了一个新的对象,所以只有一个句柄,也不会有相同的其他Point。
之前接受Point句柄的 == 操作符函数,现在已经精简到一行,并且由接受句柄改为接受Point值,且用于选择成员的指向操作符 -> 也被替换为点操作符。因为给定的值类型为sealed,所以与值类型参数Point唯一匹配的则为同类型的值了。同样地,既无需检查nullptr来确认是否为自身比较,也无需检查传递进来的对象是否类型完全一致。
而之前用于追踪引用的 == 操作符函数基本上无需太多改动,但删除了检测同一类型这一部分。然而,这两个== 操作符函数,最好只保留一个,以免在point1 == point2调用时引发歧义。(在声明函数参数时,也可使用标准C++引用符&,而不是%,因为两者可在本地类型与值类型之间互换。但由于这种类型的实例不存在于垃圾回收堆中,所以在垃圾回收期间不会改变它们的位置,因此也不需要对它们的位置进行追踪。)
例2使用了值类中的大多数成员,最主要的是它包含了静态Point类的实例,而这在引用类中是不可能完成的。事实上,不只是不能有一个引用类的静态实例,甚至也不能有一个此类型的静态句柄。
例2:
using namespace System;
Point p1; static Point p2(3,4);
int main() { static Point p3(4,7);
Console::WriteLine("p2 is {0}", p2); Point% p4 = p3;
Point p5 = p2; p5 = p2;
Console::WriteLine("p1 == p2 is {0}", p1 == p2); Console::WriteLine("p1.Equals(p2) is {0}", p1.Equals(p2)); } p2 is (3,4) p1 == p2 is False p1.Equals(p2) is False |
在第一次调用Console::WriteLine时,用传值的方式传递进一个Point,但是,这个函数却指望着接受一个对象引用,在此,Point值被自动装箱,并把装箱后的对象引用传递给函数。
在定义中可看到,p5是由默认的拷贝构造函数初始化,而接下来的一行代码,默认的赋值操作符把p2逐位复制给p5。