C# 语言规范_版本5.0 (第11章 结构)
1. 结构
结构与类的相似之处在于,它们都表示可以包含数据成员和函数成员的数据结构。但是,与类不同,结构是一种值类型,并且不需要堆分配。结构类型的变量直接包含了该结构的数据,而类类型的变量所包含的只是对相应数据的一个引用(被引用的数据称为“对象”)。
结构对于具有值语义的小型数据结构尤为有用。复数、坐标系中的点或字典中的“键-值”对都是结构的典型示例。这些数据结构的关键之处在于:它们只有少量数据成员,它们不要求使用继承或引用标识,而且它们适合使用值语义(赋值时直接复制值而不是复制它的引用)方便地实现。
如第 4.1.4 节中所描述,C# 提供的简单类型,如 int、double 和 bool,实际上全都是结构类型。正如这些预定义类型是结构一样,也可以使用结构和运算符重载在 C# 语言中实现新的“基元”类型。在本章结尾(第 11.4 节)给出了这种类型的两个示例。
1.1 结构声明
struct-declaration 是一种用于声明新结构的 type-declaration(第 9.6 节):
struct-declaration:
attributesopt
struct-modifiersopt partialopt struct identifier type-parameter-listopt
struct-interfacesopt type-parameter-constraints-clausesopt struct-body
;opt
struct-declaration 的组成结构如下:开头是一组可选 attributes(第 17 章),然后依次是一组可选 struct-modifiers(第 11.1.1 节)、可选 partial 修饰符、关键字 struct 和命名结构的 identifier、可选 type-parameter-list 规范(第 10.1.3 节)、可选 struct-interfaces 规范(第 11.1.2 节)、可选 type-parameters-constraints-clauses 规范(第 10.1.5 节)、struct-body(第 11.1.4 节),最后是一个分号(可选)。
1.1.1 结构修饰符
struct-declaration 可以根据需要包含一个结构修饰符序列:
struct-modifiers:
struct-modifier
struct-modifiers struct-modifier
struct-modifier:
new
public
protected
internal
private
同一修饰符在结构声明中出现多次是编译时错误。
结构声明的修饰符与类声明(第 10.1 节)的修饰符具有相同的意义。
1.1.2 分部修饰符
partial 修饰符指示该 struct-declaration 是分部类型声明。封闭命名空间或类型声明中同名的多个分部结构声明,按照第 10.2 节中指定的规则组合形成一个结构声明。
1.1.3 结构接口
结构声明中可以含有一个 struct-interfaces 规范,这种情况下称该结构直接实现给定的接口类型。
struct-interfaces:
:
interface-type-list
第 13.4 节对接口实现进行了进一步讨论。
1.1.4 结构体
结构的 struct-body 用于定义该结构所包含的成员。
struct-body:
{
struct-member-declarationsopt }
1.2 结构成员
结构的成员由两部分组成:由结构的 struct-member-declaration 引入的成员,以及从类型 System.ValueType 继承的成员。
struct-member-declarations:
struct-member-declaration
struct-member-declarations
struct-member-declaration
struct-member-declaration:
constant-declaration
field-declaration
method-declaration
property-declaration
event-declaration
indexer-declaration
operator-declaration
constructor-declaration
static-constructor-declaration
type-declaration
除了在第 11.3 节中指出的区别外,在从第 10.3 节到第 10.14 节中关于类成员的说明也适用于结构成员。
1.3 类和结构的区别
结构在以下几个重要方面和类是不同的:
- 结构是值类型(第 11.3.1 节)。
- 所有结构类型均从类 System.ValueType(第 11.3.2 节)隐式继承。
- 对结构类型变量进行赋值意味着将创建所赋的值的一个副本(第 11.3.3 节)。
- 结构的默认值的计算如下:将所有值类型字段设置为它们的默认值,并将所有引用类型字段设置为 null,这样就产生了该结构的默认值(第 11.3.4 节)。
- 使用装箱和取消装箱操作在结构类型和 object 之间进行转换(第 11.3.5 节)。
- 对于结构,this 的意义不同(第 7.6.7 节)。
- 结构的实例字段声明中不允许包含变量初始值设定项(第 11.3.7 节)。
- 在结构中不能声明无形参的实例构造函数(第 11.3.8 节)。
- 在结构中不允许声明析构函数(第 11.3.9 节)。
1.3.1 值语义
结构是值类型(第 4.1 节)且被称为具有值语义。另一方面,类是引用类型(第 4.2 节)且被称为具有引用语义。
结构类型的变量直接包含了该结构的数据,而类类型的变量所包含的只是对相应数据的一个引用(被引用的数据称为“对象”)。当结构 B 包含类型为 A 的实例字段,且 A 是结构类型时,A 依赖于 B 或从 B 构造的类型将产生编译时错误。如果结构 X 包含结构 Y 类型的实例字段,则 X 直接依赖于 a struct Y。从上述定义可以推出:一个结构所依赖的结构的完整集合就是此直接依赖于 (directly depends on) 关系的传递闭包。例如
struct Node
{
int data;
Node next; // error, Node directly depends on itself
}
是错误的,因为 Node 包含自身类型的实例字段。请再看一个示例
struct A { B b; }
struct B { C c; }
struct C { A a; }
是错误的,因为类型 A、 B 和 C 都彼此相互依赖。
对于类,两个变量可能引用同一对象,因此对一个变量进行的操作可能影响另一个变量所引用的对象。对于结构,每个变量都有它们自己的数据副本(除 ref 和 out 形参变量外),因此对一个变量的操作不可能影响其他变量。另外,由于结构不是引用类型,因此结构类型的值不可能为 null。
给定下列声明
struct Point
{
public int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
代码段
Point a = new
Point(10, 10);
Point b = a;
a.x = 100;
System.Console.WriteLine(b.x);
输出值 10。将 a 赋值给 b 时将创建该值的副本,因此,b 不会受到为 a.x 赋值的影响。假如 Point 被改为声明为类,则输出将为 100,因为 a 和 b 引用同一对象。
1.3.2 继承
所有结构类型均从类 System.ValueType 隐式继承,后者又从类 object 继承。一个结构声明可以指定实现的接口列表,但是不能指定基类。
结构类型永远不会是抽象的,并且始终是隐式密封的。因此在结构声明中不允许使用 abstract 和 sealed 修饰符。
由于对结构不支持继承,所以结构成员的声明可访问性不能是 protected 或 protected internal。
结构中的函数成员不能是 abstract 或 virtual,而 override 修饰符只适用于重写从 System.ValueType 继承的方法。
1.3.3 赋值
对结构类型变量进行赋值意味着将创建所赋的值的一个副本。这不同于对类类型变量的赋值,后者所复制的是引用,而不是复制由该引用所标识的对象。
与赋值类似,将结构作为值形参传递或者作为函数成员的结果返回时,也创建了该结构的一个副本。但是,结构仍可通过 ref 或 out 形参以引用方式传递给函数成员。
当结构的属性或索引器是赋值的目标时,与属性或索引器访问关联的实例表达式必须为变量类别。如果该实例表达式归类为值类别,则发生编译时错误。第 7.17.1 节对此进行了进一步详细的描述。
1.3.4 默认值
如第 5.2 节中所述,有几种变量在创建时自动初始化为它们的默认值。对于类类型和其他引用类型的变量,默认值为 null。但是,由于结构是不能为 null 的值类型,结构的默认值是通过将所有值类型字段设置为它们的默认值,并将所有引用类型字段设置为 null 而产生的值。
引用上面声明的 Point 结构,下面的示例
Point[] a = new Point[100];
将数组中的每个 Point 初始化为通过将 x 和 y 字段设置为零而产生的值。
结构的默认值对应于该结构的默认构造函数所返回的值(第 4.1.2 节)。与类不同,结构不允许声明无形参实例构造函数。相反,每个结构隐式地具有一个无形参实例构造函数,该构造函数始终返回相同的值,即通过将所有的值类型字段设置为它们的默认值,并将所有引用类型字段设置为 null 而得到的值。
设计一个结构时,要设法确保它的默认初始化状是有效的状态。在下面的示例中
using System;
struct
KeyValuePair
{
string key;
string value;
public KeyValuePair(string key, string value) {
if (key == null || value == null)
throw new ArgumentException();
this.key = key;
this.value = value;
}
}
用户定义的实例构造函数不允许出现 null 值(除非在显式调用时)。但 KeyValuePair 变量可能会被初始化为它的默认值,这样,key 和 value 字段就都为 null,所以,设计该结构时,必须正确处理好此问题。
1.3.5 装箱和拆箱
一个类类型的值可以转换为 object 类型或由该类实现的接口类型,这只需在编译时把对应的引用当作另一个类型处理即可。与此类似,一个 object 类型的值或者接口类型的值也可以被转换回类类型而不必更改相应的引用。当然,在这种情况下,需要进行运行时类型检查。
由于结构不是引用类型,上述操作对结构类型是以不同的方式实现的。当结构类型的值被转换为 object 类型或由该结构实现的接口类型时,就会执行一次装箱操作。反之,当 object 类型的值或接口类型的值被转换回结构类型时,会执行一次取消装箱操作。与对类类型进行的相同操作相比,主要区别在于:装箱操作会把相关的结构值复制为已被装箱的实例,而取消装箱则会从已被装箱的实例中复制出一个结构值。因此,在装箱或取消装箱操作后,对已取消装箱的结构进行的更改不会影响已装箱的结构。
当结构类型重写从 System.Object 继承的虚方法(如 Equals、GetHashCode 或 ToString)时,通过该结构类型的实例进行的虚方法调用不会导致装箱。即使将该结构用作类型形参,并且通过类型形参类型的实例进行调用,情况也是如此。例如:
using System;
struct Counter
{
int value;
public
override string ToString() {
value++;
return value.ToString();
}
}
class Program
{
static void Test<T>() where T:
new() {
T x = new T();
Console.WriteLine(x.ToString());
Console.WriteLine(x.ToString());
Console.WriteLine(x.ToString());
}
static
void Main() {
Test<Counter>();
}
}
该程序的输出为:
1
2
3
虽然让 ToString 具有副作用是一种不好的做法,但是该示例证明了 x.ToString() 的三个调用没有发生装箱。
类似地,在受约束的类型形参上访问成员时,从来不会隐式地进行装箱。例如,假设接口 ICounter 包含可用于修改值的方法 Increment。如果将 ICounter 用作约束,则将通过对调用了 Increment 的变量(而不是装箱副本)的引用来调用 Increment 方法的实现。
using System;
interface ICounter
{
void Increment();
}
struct Counter: ICounter
{
int value;
public
override string ToString() {
return value.ToString();
}
void
ICounter.Increment() {
value++;
}
}
class Program
{
static void Test<T>() where T:
ICounter, new() {
T x = new T();
Console.WriteLine(x);
x.Increment(); // Modify x
Console.WriteLine(x);
((ICounter)x).Increment(); //
Modify boxed copy of x
Console.WriteLine(x);
}
static
void Main() {
Test<Counter>();
}
}
对 Increment 的第一个调用将修改变量 x 中的值。这与对 Increment 的第二个调用不等效,第二个调用修改 x 的装箱副本中的值。因此,该程序的输出为:
0
1
1
有关装箱和取消装箱的详细信息,请参见第 4.3 节。
1.3.6 this 的含义
在类的实例构造函数和实例函数成员中,this 为值类别。因此,虽然 this 可以用于引用该函数成员调用所涉及的实例,但是不可能在类的函数成员中对 this 本身赋值。
在结构的实例构造函数内,this 相当于一个结构类型的 out 参数,而在结构的实例函数成员内,this 相当于一个结构类型的 ref 参数。在这两种情况下,this 本身相当于一个变量,因而有可能对该函数成员调用所涉及的整个结构进行修改(如对 this 赋值,或者将 this 作为 ref 或 out 形参传递)。
1.3.7 字段初始值设定项
如第 11.3.4 节中所述,结构的默认值就是将所有值类型字段设置为它们的默认值并将所有引用类型字段设置为 null 而产生的值。由于这个原因,结构不允许它的实例字段声明中含有变量初始值设定项。此限制只适用于实例字段。在结构的静态字段声明中可以含有变量初始值设定项。
下面的示例
struct Point
{
public int x = 1; // Error, initializer not permitted
public int y = 1; // Error, initializer not permitted
}
出现错误,因为实例字段声明中含有变量初始值设定项。
1.3.8 构造函数
与类不同,结构不允许声明无形参实例构造函数。相反,每个结构隐式地具有一个无形参实例构造函数,该构造函数始终返回相同的值,即通过将所有的值类型字段设置为其默认值,并将所有引用类型字段设置为 null 而得到的值(第 4.1.2 节)。结构可以声明具有形参的实例构造函数。例如
struct Point
{
int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
已知以上声明,语句
Point p1 = new Point();
Point p2 = new Point(0, 0);
都将创建一个 Point,其 x 和 y 将初始化为零。
一个结构的实例构造函数不能含有 base(...) 形式的构造函数初始值设定项。
如果该结构实例构造函数没有指定构造函数初始值设定项,则 this 变量就相当于一个结构类型的 out 形参,并且,与 out 形参类似,this 必须在该构造函数返回的每个位置上明确赋值(第 5.3 节)。如果该结构实例构造函数指定了构造函数初始值设定项,则 this 变量就相当于结构类型的 ref 形参,并且,与 ref 形参类似,this 被视为在进入构造函数体时已被明确赋值。请研究下面的实例构造函数实现:
struct Point
{
int x, y;
public int X {
set { x = value; }
}
public int Y {
set { y = value; }
}
public Point(int x, int y) {
X = x; // error, this is not yet definitely assigned
Y = y; // error, this is not yet definitely assigned
}
}
在被构造的结构的所有字段已明确赋值以前,不能调用任何实例成员函数(包括 X 和 Y 属性的 set 访问器)。但是请注意,如果 Point 是类而不是结构,则允许上述的实例构造函数实现。
1.3.9 析构函数
在结构类型中不允许声明析构函数。
1.3.10 静态构造函数
结构的静态构造函数与类的静态构造函数所遵循的规则大体相同。应用程序域中第一次发生以下事件时将触发结构类型的静态构造函数的执行:
- 引用结构类型的静态成员。
- 结构类型的显式声明的构造函数被调用。
创建结构类型的默认值(第 11.3.4 节)不会触发静态构造函数。(一个示例是数组中元素的初始值。)
1.4 结构示例
下面的内容展示了关于应用 struct 类型的两个重要示例,它们各自创建一个类型,这些类型使用起来就像 C# 语言的预定义类型,但具有修改了的语义。
1.4.1 数据库整数类型
下面的 DBInt 结构实现了一个整数类型,它可以表示 int 类型的值的完整集合,再加上一个用于表示未知值的附加状态。具有这些特征的类型常用在数据库中。
using System;
public struct
DBInt
{
// The Null member represents an unknown
DBInt value.
public static readonly DBInt Null = new DBInt();
// When the defined field is true, this DBInt
represents a known value
// which is stored in the value field.
When the defined field is false,
// this DBInt represents an unknown
value, and the value field is 0.
int value;
bool defined;
// Private instance constructor. Creates a DBInt with a known value.
DBInt(int value) {
this.value = value;
this.defined = true;
}
// The IsNull property is true if this DBInt represents an unknown value.
public bool IsNull { get { return !defined; } }
// The Value property is the known value of
this DBInt, or 0 if this
// DBInt represents an unknown value.
public int Value { get { return value; } }
// Implicit conversion from int to DBInt.
public static implicit operator DBInt(int x) {
return new DBInt(x);
}
// Explicit conversion from DBInt to int.
Throws an exception if the
// given DBInt represents an unknown
value.
public static explicit operator int(DBInt x) {
if (!x.defined) throw new
InvalidOperationException();
return x.value;
}
public static DBInt operator +(DBInt x) {
return x;
}
public static DBInt operator -(DBInt x) {
return x.defined ? -x.value : Null;
}
public static DBInt operator +(DBInt x, DBInt
y) {
return x.defined &&
y.defined? x.value + y.value: Null;
}
public static DBInt operator -(DBInt x, DBInt
y) {
return x.defined &&
y.defined? x.value - y.value: Null;
}
public static DBInt operator *(DBInt x, DBInt
y) {
return x.defined &&
y.defined? x.value * y.value: Null;
}
public static DBInt operator /(DBInt x, DBInt
y) {
return x.defined &&
y.defined? x.value / y.value: Null;
}
public static DBInt operator %(DBInt x, DBInt
y) {
return x.defined &&
y.defined? x.value % y.value: Null;
}
public static DBBool operator ==(DBInt x, DBInt
y) {
return x.defined &&
y.defined? x.value == y.value: DBBool.Null;
}
public static DBBool operator !=(DBInt x, DBInt
y) {
return x.defined &&
y.defined? x.value != y.value: DBBool.Null;
}
public static DBBool operator >(DBInt x,
DBInt y) {
return x.defined &&
y.defined? x.value > y.value: DBBool.Null;
}
public static DBBool operator <(DBInt x,
DBInt y) {
return x.defined &&
y.defined? x.value < y.value: DBBool.Null;
}
public static DBBool operator >=(DBInt x,
DBInt y) {
return x.defined &&
y.defined? x.value >= y.value: DBBool.Null;
}
public static DBBool operator <=(DBInt x,
DBInt y) {
return x.defined &&
y.defined? x.value <= y.value: DBBool.Null;
}
public override bool Equals(object obj) {
if (!(obj is DBInt)) return false;
DBInt x = (DBInt)obj;
return value == x.value &&
defined == x.defined;
}
public override int GetHashCode() {
return value;
}
public override string ToString() {
return defined? value.ToString(): “DBInt.Null”;
}
}
1.4.2 数据库布尔类型
下面的 DBBool 结构实现了一个三值逻辑类型。此类型的可能值有 DBBool.True、DBBool.False 和 DBBool.Null,其中 Null 成员用于表示未知值。这样的三值逻辑类型经常用在数据库中。
using System;
public struct
DBBool
{
// The three possible DBBool values.
public static readonly DBBool Null = new
DBBool(0);
public static readonly DBBool False = new
DBBool(-1);
public static readonly DBBool True = new
DBBool(1);
// Private field that stores –1, 0, 1 for False, Null, True.
sbyte value;
// Private instance constructor. The value parameter must be –1, 0, or 1.
DBBool(int value) {
this.value = (sbyte)value;
}
// Properties to examine the value of a DBBool.
Return true if this
// DBBool has the given value, false
otherwise.
public bool IsNull { get { return value == 0; } }
public bool IsFalse { get { return value < 0; } }
public bool IsTrue { get { return value > 0; } }
// Implicit conversion from bool to DBBool.
Maps true to DBBool.True and
// false to DBBool.False.
public static implicit operator DBBool(bool x)
{
return x? True: False;
}
// Explicit conversion from DBBool to bool.
Throws an exception if the
// given DBBool is Null, otherwise
returns true or false.
public static explicit operator bool(DBBool x)
{
if (x.value == 0) throw new
InvalidOperationException();
return x.value > 0;
}
// Equality operator. Returns Null if either
operand is Null, otherwise
// returns True or False.
public static DBBool operator ==(DBBool x,
DBBool y) {
if (x.value == 0 || y.value == 0)
return Null;
return x.value == y.value? True:
False;
}
// Inequality operator. Returns Null if either
operand is Null, otherwise
// returns True or False.
public static DBBool operator !=(DBBool x,
DBBool y) {
if (x.value == 0 || y.value == 0)
return Null;
return x.value != y.value? True:
False;
}
// Logical negation operator. Returns True if
the operand is False, Null
// if the operand is Null, or False if
the operand is True.
public static DBBool operator !(DBBool x) {
return new DBBool(-x.value);
}
// Logical AND operator. Returns False if
either operand is False,
// otherwise Null if either operand is
Null, otherwise True.
public static DBBool operator &(DBBool x,
DBBool y) {
return new DBBool(x.value <
y.value? x.value: y.value);
}
// Logical OR operator. Returns True if either
operand is True, otherwise
// Null if either operand is Null,
otherwise False.
public static DBBool operator |(DBBool x,
DBBool y) {
return new DBBool(x.value >
y.value? x.value: y.value);
}
// Definitely true operator. Returns true if
the operand is True, false
// otherwise.
public static bool operator true(DBBool x) {
return x.value > 0;
}
// Definitely false operator. Returns true if
the operand is False, false
// otherwise.
public static bool operator false(DBBool x) {
return x.value < 0;
}
public override bool Equals(object obj) {
if (!(obj is DBBool)) return false;
return value == ((DBBool)obj).value;
}
public override int GetHashCode() {
return value;
}
public override string ToString() {
if (value > 0) return
"DBBool.True";
if (value < 0) return
"DBBool.False";
return "DBBool.Null";
}
}