C#6.0语言规范(十一) 结构
结构与类类似,因为它们表示可以包含数据成员和函数成员的数据结构。但是,与类不同,结构是值类型,不需要堆分配。结构类型的变量直接包含结构的数据,而类类型的变量包含对数据的引用,后者称为对象。
结构对于具有值语义的小型数据结构特别有用。复数,坐标系中的点或字典中的键值对都是结构的好例子。这些数据结构的关键是它们具有很少的数据成员,它们不需要使用继承或引用标识,并且可以使用值语义方便地实现它们,其中赋值复制值而不是引用。
如在描述的简单类型,由C#提供的简单的类型,如int
,double
,和bool
,实际上所有结构类型。正如这些预定义类型是结构一样,也可以使用结构和运算符重载来实现C#语言中的新“原始”类型。本章末尾给出了这类类型的两个例子(结构示例)。
结构声明
一个struct_declaration是type_declaration(类型声明)声明一个新的结构:
1 struct_declaration 2 : attributes? struct_modifier* 'partial'? 'struct' identifier type_parameter_list? 3 struct_interfaces? type_parameter_constraints_clause* struct_body ';'? 4 ;
struct_declaration包括一组可选的属性(属性),随后一组可选的struct_modifier S(STRUCT改性剂),随后任选的partial
改性剂,接着由关键字struct
和标识符名称的结构,接着任选的type_parameter_list规范(类型参数),后跟可选的struct_interfaces规范(Partial modifier)),后跟可选的type_parameter_constraints_clause规范(Type参数约束)),后跟一个struct_body(结构体),后面跟一个分号。
结构修饰符
struct_declaration可任选地包括结构改性剂的一个序列:
1 struct_modifier 2 : 'new' 3 | 'public' 4 | 'protected' 5 | 'internal' 6 | 'private' 7 | struct_modifier_unsafe 8 ;
同一修饰符在结构声明中多次出现是编译时错误。
结构声明的修饰符与类声明(类声明)的修饰符具有相同的含义。
部分编辑
所述partial
改性剂表明此struct_declaration是部分类型声明。在封闭命名空间或类型声明中具有相同名称的多个部分结构声明组合在一起形成一个结构声明,遵循在部分类型中指定的规则。
结构接口
结构声明可以包括struct_interfaces规范,在这种情况下,结构被称为直接实现给定的接口类型。
1 struct_interfaces 2 : ':' interface_type_list 3 ;
接口实现在接口实现中进一步讨论。
结构体
struct 的struct_body定义了struct的成员。
1 struct_body 2 : '{' struct_member_declaration* '}' 3 ;
结构成员
结构的成员由其struct_member_declaration引入的成员和从该类型继承的成员组成System.ValueType
。
1 struct_member_declaration 2 : constant_declaration 3 | field_declaration 4 | method_declaration 5 | property_declaration 6 | event_declaration 7 | indexer_declaration 8 | operator_declaration 9 | constructor_declaration 10 | static_constructor_declaration 11 | type_declaration 12 | struct_member_declaration_unsafe 13 ;
除了在类和结构差异中指出的差异之外,通过迭代器在类成员中提供的类成员的描述也适用于结构成员。
类和结构的差异
结构在几个重要方面与类不同:
- 结构是值类型(值语义)。
- 所有结构类型都隐式继承自类
System.ValueType
(继承)。 - 分配给结构类型的变量会创建所分配值的副本(赋值)。
- struct的默认值是通过将所有值类型字段设置为其默认值并将所有引用类型字段设置为
null
(默认值)而生成的值。 - 装箱和拆箱操作用于在结构类型和
object
(装箱和拆箱)之间进行转换。 - 结构的含义
this
是不同的(此访问权限)。 - 不允许结构的实例字段声明包含变量初始值设定项(字段初始值设定项)。
- 不允许结构体声明无参数实例构造函数(Constructors)。
- 不允许结构体声明析构函数(Destructors)。
值语义
结构是值类型(值类型),据说具有值语义。另一方面,类是引用类型(引用类型),据说具有引用语义。
结构类型的变量直接包含结构的数据,而类类型的变量包含对数据的引用,后者称为对象。当struct B
包含一个类型的实例字段A
并且A
是一个结构类型时,它A
依赖于编译时错误B
或从中构造的类型B
。如果结构包含类型的实例字段,则结构X
直接依赖于结构。给定这个定义,结构所依赖的完整结构集是直接依赖于关系的传递闭包。例如Y
X
Y
1 struct Node 2 { 3 int data; 4 Node next; // error, Node directly depends on itself 5 }
是一个错误,因为Node
包含自己类型的实例字段。另一个例子
1 struct A { B b; } 2 3 struct B { C c; } 4 5 struct C { A a; }
是错误的,因为每种类型的A
,B
以及C
彼此依赖。
对于类,两个变量可以引用同一个对象,因此对一个变量的操作可能会影响另一个变量引用的对象。对于结构体,每个变量都有自己的数据副本(除了ref
和out
参数变量的情况除外),并且对一个变量的操作不可能影响另一个。此外,因为结构不是引用类型,所以结构类型的值不可能null
。
结构声明
1 struct Point 2 { 3 public int x, y; 4 5 public Point(int x, int y) { 6 this.x = x; 7 this.y = y; 8 } 9 }
代码片段
1 Point a = new Point(10, 10); 2 Point b = a; 3 a.x = 100; 4 System.Console.WriteLine(b.x);
输出值10
。赋值为a
to b
创建值的副本,b
因此不受赋值的影响a.x
。已经Point
代替被声明为类,输出将是100
因为a
和b
将引用相同的对象。
继承
所有结构类型都隐式继承自类System.ValueType
,而类继承自类object
。结构声明可以指定已实现接口的列表,但结构声明不可能指定基类。
结构类型永远不是抽象的,并且总是隐式密封。因此,在结构声明中不允许使用abstract
和sealed
修饰符。
由于结构不支持继承,因此结构成员的声明可访问性不能为protected
或protected internal
。
结构中的函数成员不能是abstract
或者virtual
,并且override
仅允许修饰符覆盖从中继承的方法System.ValueType
。
分配
对结构类型变量的赋值会创建要分配的值的副本。这与赋值给类类型的变量不同,后者复制引用但不复制引用标识的对象。
与赋值类似,当结构作为值参数传递或作为函数成员的结果返回时,将创建结构的副本。可以使用ref
or out
参数通过引用函数成员来传递结构。
当结构的属性或索引器是赋值的目标时,与属性或索引器访问关联的实例表达式必须归类为变量。如果实例表达式被分类为值,则会发生编译时错误。这在简单分配中有更详细的描述。
默认值
如默认值中所述,几种变量在创建时会自动初始化为其默认值。对于类类型和其他引用类型的变量,此默认值为null
。但是,由于结构是不可能的值类型,结构null
的默认值是通过将所有值类型字段设置为其默认值并将所有引用类型字段设置为而生成的值null
。
参考Point
上面声明的结构,示例
Point[] a = new Point[100];
Point
将数组中的每个值初始化为通过将x
和y
字段设置为零而生成的值。
struct的默认值对应于struct的默认构造函数返回的值(Default constructors)。与类不同,不允许结构声明无参数实例构造函数。相反,每个结构都隐式地具有无参数实例构造函数,该构造函数始终返回将所有值类型字段设置为其默认值并将所有引用类型字段设置为的结果null
。
应该将结构设计为将默认初始化状态视为有效状态。在这个例子中
1 using System; 2 3 struct KeyValuePair 4 { 5 string key; 6 string value; 7 8 public KeyValuePair(string key, string value) { 9 if (key == null || value == null) throw new ArgumentException(); 10 this.key = key; 11 this.value = value; 12 } 13 }
用户定义的实例构造函数仅在显式调用它时保护null值。如果KeyValuePair
变量受默认值初始化的影响,则key
和value
字段将为null,并且必须准备结构以处理此状态。
装箱和拆箱
类类型的值可以转换为类型object
或由类实现的接口类型,只需在编译时将引用视为另一种类型即可。同样,object
可以在不更改引用的情况下将类型的值或接口类型的值转换回类类型(但在这种情况下,当然需要运行时类型检查)。
由于结构不是引用类型,因此对于结构类型,这些操作的实现方式不同。当结构类型的值转换为类型object
或由结构实现的接口类型时,将发生装箱操作。同样,当类型object
的值或接口类型的值转换回结构类型时,会发生拆箱操作。与类类型相同操作的主要区别在于装箱和拆箱将结构值复制到盒装实例中或从盒装实例中复制出来。因此,在装箱或取消装箱操作之后,对未装箱的结构所做的更改不会反映在装箱的结构中。
当结构类型重写从继承虚拟方法System.Object
(如Equals
,GetHashCode
或ToString
),通过结构类型的实例的虚拟方法的调用不会导致发生装箱。即使将结构体用作类型参数并且通过类型参数类型的实例进行调用,也是如此。例如:
1 using System; 2 3 struct Counter 4 { 5 int value; 6 7 public override string ToString() { 8 value++; 9 return value.ToString(); 10 } 11 } 12 13 class Program 14 { 15 static void Test<T>() where T: new() { 16 T x = new T(); 17 Console.WriteLine(x.ToString()); 18 Console.WriteLine(x.ToString()); 19 Console.WriteLine(x.ToString()); 20 } 21 22 static void Main() { 23 Test<Counter>(); 24 } 25 }
该计划的输出是:
1 1 2 2 3 3
虽然ToString
有副作用是不好的风格,但是这个例子证明了三次调用没有发生装箱x.ToString()
。
类似地,在访问约束类型参数上的成员时,从不隐式发生装箱。例如,假设接口ICounter
包含Increment
可用于修改值的方法。如果ICounter
用作约束,Increment
则调用方法的实现时引用调用的变量,Increment
而不是盒装副本。
1 using System; 2 3 interface ICounter 4 { 5 void Increment(); 6 } 7 8 struct Counter: ICounter 9 { 10 int value; 11 12 public override string ToString() { 13 return value.ToString(); 14 } 15 16 void ICounter.Increment() { 17 value++; 18 } 19 } 20 21 class Program 22 { 23 static void Test<T>() where T: ICounter, new() { 24 T x = new T(); 25 Console.WriteLine(x); 26 x.Increment(); // Modify x 27 Console.WriteLine(x); 28 ((ICounter)x).Increment(); // Modify boxed copy of x 29 Console.WriteLine(x); 30 } 31 32 static void Main() { 33 Test<Counter>(); 34 } 35 }
第一次调用Increment
修改变量中的值x
。这不等于第二次调用Increment
,它修改了盒装副本中的值x
。因此,该程序的输出是:
1 0 2 1 3 1
有关装箱和拆箱的更多详细信息,请参阅装箱和拆箱。
this
在类的实例构造函数或实例函数成员中,this
被分类为值。因此,虽然this
可以用于引用调用函数成员的实例,但是不可能this
在类的函数成员中分配。
在struct的实例构造函数中,this
对应于out
struct类型的参数,并且在struct的实例函数成员中,this
对应于ref
struct类型的参数。在两种情况下,this
被分类为一个变量,它可以修改的量,功能部件是由分配给调用的整个结构this
或通过将其作为ref
或out
参数。
字段初始化程序
如在所描述的默认值中,结构体的默认值是由来自所有值类型字段设置为它们的缺省值,所有引用类型字段,以所得的值的null
。因此,struct不允许实例字段声明包含变量初始值设定项。此限制仅适用于实例字段。允许结构的静态字段包括变量初始值设定项。
这个例子
1 struct Point 2 { 3 public int x = 1; // Error, initializer not permitted 4 public int y = 1; // Error, initializer not permitted 5 }
是错误的,因为实例字段声明包括变量初始值设定项。
构造函数
与类不同,不允许结构声明无参数实例构造函数。相反,每个结构都隐式地具有无参数实例构造函数,该构造函数始终返回将所有值类型字段设置为其默认值并将所有引用类型字段设置为null(默认构造函数)所产生的值。struct可以声明具有参数的实例构造函数。例如
1 struct Point 2 { 3 int x, y; 4 5 public Point(int x, int y) { 6 this.x = x; 7 this.y = y; 8 } 9 }
鉴于上述声明,声明
1 Point p1 = new Point(); 2 Point p2 = new Point(0, 0);
都创建一个Point
with x
并y
初始化为零。
不允许结构实例构造函数包含表单的构造函数初始值设定项base(...)
。
如果struct实例构造函数未指定构造函数初始值设定项,则this
变量对应于out
struct类型的out
参数,并且类似于参数,this
必须在构造函数返回的每个位置明确赋值(Definite assignment)。如果struct实例构造函数指定了构造函数初始值设定项,则该this
变量对应于ref
struct类型的ref
参数,并且类似于参数,this
在进入构造函数体时被认为是明确赋值的。考虑下面的实例构造函数实现:
1 struct Point 2 { 3 int x, y; 4 5 public int X { 6 set { x = value; } 7 } 8 9 public int Y { 10 set { y = value; } 11 } 12 13 public Point(int x, int y) { 14 X = x; // error, this is not yet definitely assigned 15 Y = y; // error, this is not yet definitely assigned 16 } 17 }
在构造的结构的所有字段都已明确赋值之前,不能调用实例成员函数(包括属性的集合访问器X
和Y
)。唯一的例外是自动实现的属性(自动实现的属性)。明确的赋值规则(简单赋值表达式)特别免除了对该结构类型的实例构造函数中结构类型的自动属性的赋值:这样的赋值被认为是对auto-property的隐藏后备字段的明确赋值。因此,允许以下内容:
1 struct Point 2 { 3 public int X { get; set; } 4 public int Y { get; set; } 5 6 public Point(int x, int y) { 7 X = x; // allowed, definitely assigns backing field 8 Y = y; // allowed, definitely assigns backing field 9 }
析构函数
不允许结构体声明析构函数。
静态构造函数
结构的静态构造函数遵循与类相同的大多数规则。结构类型的静态构造函数的执行由应用程序域中发生的以下第一个事件触发:
- 引用了struct类型的静态成员。
- 调用显式声明的struct类型的构造函数。
结构类型的默认值(默认值)的创建不会触发静态构造函数。(这方面的一个例子是数组中元素的初始值。)
结构示例
下面显示了使用struct
类型创建类型的两个重要示例,这些类型可以类似于预定义的语言类型使用,但具有修改的语义。
数据库整数类型
DBInt
下面的结构实现了一个整数类型,它可以表示该类型的完整值集int
,以及一个指示未知值的附加状态。具有这些特征的类型通常用于数据库中。
1 using System; 2 3 public struct DBInt 4 { 5 // The Null member represents an unknown DBInt value. 6 7 public static readonly DBInt Null = new DBInt(); 8 9 // When the defined field is true, this DBInt represents a known value 10 // which is stored in the value field. When the defined field is false, 11 // this DBInt represents an unknown value, and the value field is 0. 12 13 int value; 14 bool defined; 15 16 // Private instance constructor. Creates a DBInt with a known value. 17 18 DBInt(int value) { 19 this.value = value; 20 this.defined = true; 21 } 22 23 // The IsNull property is true if this DBInt represents an unknown value. 24 25 public bool IsNull { get { return !defined; } } 26 27 // The Value property is the known value of this DBInt, or 0 if this 28 // DBInt represents an unknown value. 29 30 public int Value { get { return value; } } 31 32 // Implicit conversion from int to DBInt. 33 34 public static implicit operator DBInt(int x) { 35 return new DBInt(x); 36 } 37 38 // Explicit conversion from DBInt to int. Throws an exception if the 39 // given DBInt represents an unknown value. 40 41 public static explicit operator int(DBInt x) { 42 if (!x.defined) throw new InvalidOperationException(); 43 return x.value; 44 } 45 46 public static DBInt operator +(DBInt x) { 47 return x; 48 } 49 50 public static DBInt operator -(DBInt x) { 51 return x.defined ? -x.value : Null; 52 } 53 54 public static DBInt operator +(DBInt x, DBInt y) { 55 return x.defined && y.defined? x.value + y.value: Null; 56 } 57 58 public static DBInt operator -(DBInt x, DBInt y) { 59 return x.defined && y.defined? x.value - y.value: Null; 60 } 61 62 public static DBInt operator *(DBInt x, DBInt y) { 63 return x.defined && y.defined? x.value * y.value: Null; 64 } 65 66 public static DBInt operator /(DBInt x, DBInt y) { 67 return x.defined && y.defined? x.value / y.value: Null; 68 } 69 70 public static DBInt operator %(DBInt x, DBInt y) { 71 return x.defined && y.defined? x.value % y.value: Null; 72 } 73 74 public static DBBool operator ==(DBInt x, DBInt y) { 75 return x.defined && y.defined? x.value == y.value: DBBool.Null; 76 } 77 78 public static DBBool operator !=(DBInt x, DBInt y) { 79 return x.defined && y.defined? x.value != y.value: DBBool.Null; 80 } 81 82 public static DBBool operator >(DBInt x, DBInt y) { 83 return x.defined && y.defined? x.value > y.value: DBBool.Null; 84 } 85 86 public static DBBool operator <(DBInt x, DBInt y) { 87 return x.defined && y.defined? x.value < y.value: DBBool.Null; 88 } 89 90 public static DBBool operator >=(DBInt x, DBInt y) { 91 return x.defined && y.defined? x.value >= y.value: DBBool.Null; 92 } 93 94 public static DBBool operator <=(DBInt x, DBInt y) { 95 return x.defined && y.defined? x.value <= y.value: DBBool.Null; 96 } 97 98 public override bool Equals(object obj) { 99 if (!(obj is DBInt)) return false; 100 DBInt x = (DBInt)obj; 101 return value == x.value && defined == x.defined; 102 } 103 104 public override int GetHashCode() { 105 return value; 106 } 107 108 public override string ToString() { 109 return defined? value.ToString(): "DBInt.Null"; 110 } 111 }
数据库布尔类型
DBBool
下面的结构实现了三值逻辑类型。这种类型的可能的值是DBBool.True
,DBBool.False
,和DBBool.Null
,其中,所述Null
构件表示未知值。这种三值逻辑类型通常用于数据库中。
1 using System; 2 3 public struct DBBool 4 { 5 // The three possible DBBool values. 6 7 public static readonly DBBool Null = new DBBool(0); 8 public static readonly DBBool False = new DBBool(-1); 9 public static readonly DBBool True = new DBBool(1); 10 11 // Private field that stores -1, 0, 1 for False, Null, True. 12 13 sbyte value; 14 15 // Private instance constructor. The value parameter must be -1, 0, or 1. 16 17 DBBool(int value) { 18 this.value = (sbyte)value; 19 } 20 21 // Properties to examine the value of a DBBool. Return true if this 22 // DBBool has the given value, false otherwise. 23 24 public bool IsNull { get { return value == 0; } } 25 26 public bool IsFalse { get { return value < 0; } } 27 28 public bool IsTrue { get { return value > 0; } } 29 30 // Implicit conversion from bool to DBBool. Maps true to DBBool.True and 31 // false to DBBool.False. 32 33 public static implicit operator DBBool(bool x) { 34 return x? True: False; 35 } 36 37 // Explicit conversion from DBBool to bool. Throws an exception if the 38 // given DBBool is Null, otherwise returns true or false. 39 40 public static explicit operator bool(DBBool x) { 41 if (x.value == 0) throw new InvalidOperationException(); 42 return x.value > 0; 43 } 44 45 // Equality operator. Returns Null if either operand is Null, otherwise 46 // returns True or False. 47 48 public static DBBool operator ==(DBBool x, DBBool y) { 49 if (x.value == 0 || y.value == 0) return Null; 50 return x.value == y.value? True: False; 51 } 52 53 // Inequality operator. Returns Null if either operand is Null, otherwise 54 // returns True or False. 55 56 public static DBBool operator !=(DBBool x, DBBool y) { 57 if (x.value == 0 || y.value == 0) return Null; 58 return x.value != y.value? True: False; 59 } 60 61 // Logical negation operator. Returns True if the operand is False, Null 62 // if the operand is Null, or False if the operand is True. 63 64 public static DBBool operator !(DBBool x) { 65 return new DBBool(-x.value); 66 } 67 68 // Logical AND operator. Returns False if either operand is False, 69 // otherwise Null if either operand is Null, otherwise True. 70 71 public static DBBool operator &(DBBool x, DBBool y) { 72 return new DBBool(x.value < y.value? x.value: y.value); 73 } 74 75 // Logical OR operator. Returns True if either operand is True, otherwise 76 // Null if either operand is Null, otherwise False. 77 78 public static DBBool operator |(DBBool x, DBBool y) { 79 return new DBBool(x.value > y.value? x.value: y.value); 80 } 81 82 // Definitely true operator. Returns true if the operand is True, false 83 // otherwise. 84 85 public static bool operator true(DBBool x) { 86 return x.value > 0; 87 } 88 89 // Definitely false operator. Returns true if the operand is False, false 90 // otherwise. 91 92 public static bool operator false(DBBool x) { 93 return x.value < 0; 94 } 95 96 public override bool Equals(object obj) { 97 if (!(obj is DBBool)) return false; 98 return value == ((DBBool)obj).value; 99 } 100 101 public override int GetHashCode() { 102 return value; 103 } 104 105 public override string ToString() { 106 if (value > 0) return "DBBool.True"; 107 if (value < 0) return "DBBool.False"; 108 return "DBBool.Null"; 109 } 110 }