结构类型(“structure type”或“struct type”)是一种可封装数据和相关功能的值类型 。 使用 struct 关键字定义结构类型:
public struct Coords
{
public Coords(double x, double y)
{
X = x;
Y = y;
}
public double X { get; }
public double Y { get; }
public override string ToString() => $"({X}, {Y})";
}
结构类型具有值语义 。 也就是说,结构类型的变量包含类型的实例。 默认情况下,在分配中,通过将参数传递给方法并返回方法结果来复制变量值。 对于结构类型变量,将复制该类型的实例。
使用readonly
关键字来保证结构体状态不可变。以此保证结构体内的成员不会修改结构体本身状态。正是由于它是值类型的,因此有可能会被修改,而我们又不希望它被修改。
这里也要点出为什么class往往优于struct,因为结构体是值类型的,一方面,结构体的赋值是通过复制整个结构体的值来实现的。这意味着当结构体的值较大时,赋值操作需要复制较多的数据,可能会消耗大量的内存和时间。
另一方面,结构体在作为参数传递给方法时,会进行值传递。这意味着传递的是结构体的一个副本,而不是原始的结构体实例。这会导致在方法内对结构体的修改不会影响到原始实例。
相比之下,使用类作为引用类型可以避免上述问题。类对象的赋值和传递只涉及引用的复制,而不是整个对象的复制。这样可以避免不必要的内存和时间消耗。而且类对象的传递是引用传递,这意味着方法内对对象的修改会影响到原始实例。
而一切的缺陷,本质根源于结构体是一个值类型,而class是引用类型。
元组功能提供了简洁的语法来将多个数据元素分组成一个轻型数据结构。 下面的示例演示了如何声明元组变量、对它进行初始化并访问其数据成员:
(double, int) t1 = (4.5, 3);
Console.WriteLine($"Tuple with elements {t1.Item1} and {t1.Item2}.");
// Output:
// Tuple with elements 4.5 and 3.
(double Sum, int Count) t2 = (4.5, 3);
Console.WriteLine($"Sum of {t2.Count} elements is {t2.Sum}.");
// Output:
// Sum of 3 elements is 4.5.
若要定义元组类型,需要指定其所有数据成员的类型,或者,可以指定字段名称。 虽然不能在元组类型中定义方法,但可以使用 .NET 提供的方法,如下面的示例所示:
(double, int) t = (4.5, 3);
Console.WriteLine(t.ToString());
Console.WriteLine($"Hash code of {t} is {t.GetHashCode()}.");
// Output:
// (4.5, 3)
// Hash code of (4.5, 3) is 718460086.
使用元组类型的情况通常用于接受函数多返回值。如果想要一个可变动的,带有方法的数据结构,类还是优于元组的。
在值类型的变量中,大部分值是不允许为空的,因此我们可以使用Nullable
或T?
定义可为空的值类型。但基础值类型 T
本身不能是可为空的值类型。
需要表示基础值类型的未定义值时,通常使用可为空的值类型。 例如,布尔值或 bool 变量只能为 true 或 false。 但是,在某些应用程序中,变量值可能未定义或缺失。 例如,某个数据库字段可能包含 true 或 false,或者它可能不包含任何值,即 NULL。 在这种情况下,可以使用 bool? 类型。
也就是说,当我们需要一个不可为空的值,而实际情况下可能会出现为空值的情况,我们就需要用到T?
由于值类型可隐式转换为相应的可为空的值类型,因此可以像向其基础值类型赋值一样,向可为空值类型的变量赋值。 还可分配 null 值。 例如:
double? pi = 3.14;
char? letter = 'a';
int m2 = 10;
int? m = m2;
bool? flag = null;
// An array of a nullable value type:
int?[] arr = new int?[10];
可为空值类型的默认值表示 null
,也就是说,它是其 Nullable
属性返回 false
的实例。
通常判断可为空值内是否为空有三种做法:
int? a = 42;
if (a is int valueOfA) // valueOfA代表A的ASCII码对应值
{
}
if (a is null)
{
}
或者
if (a.HasValue)
{
}
或者
if (a != null)
{
}
如果要将可为空值类型的值分配给不可以为 null 的值类型变量,则可能需要指定要分配的替代 null 的值。
int? a = 28;
-- 使用??操作符,使用方法是a = x ?? y 或x ??= y
-- a = x??y当x为空,则a=y ,x非空则a= x
-- x??= y当x为空则x=y,非空则不处理
int b = a ?? -1;
Console.WriteLine($"b is {b}"); // output: b is 28
int? c = null;
int d = c ?? -1;
Console.WriteLine($"d is {d}"); // output: d is -1
注意,实际上T
和T?
不是同一种值类型,所以同为值类型如果使用强制转换是可以的,但是如果把一个空值转换给一个非空类型是会报错的:
int? n = null;
//int m1 = n; // Doesn't compile
int n2 = (int)n; // Compiles, but throws an exception if n is null
任何T
类型本身所支持的运算符,如果在运算时带有了T?
类型,那么运算也是可以正常运行的。这些运算符将被提升,而运算结果将变为可为空值,但是类型还是需要符合T
运算时的类型转换(例如int+float=浮点型,所以int?+folat?=浮点型?
)。
int? a = 10;
float? b = null;
double? c = 0;
c = a + b; // a is null
print(c); --Null
而bool值的计算稍微特殊,总体上也是符合bool运算法则的(我在lua学习笔记中总结了Lua入门):
bool? a = true;
bool? b = null;
bool? c = true;
c = a & b;
Debug.Log(c); --null
c = a | b;
Debug.Log(c); --true
对于比较运算符<、>、<= 和 >=
,如果一个或全部两个操作数都为 null,则结果为 false
;否则,将比较操作数的包含值。而带有null
值时唯一可以进行比较运算的只有==
和!=
。
int? a = 10;
Console.WriteLine($"{a} >= null is {a >= null}");
Console.WriteLine($"{a} < null is {a < null}");
Console.WriteLine($"{a} == null is {a == null}");
// Output:
// 10 >= null is False
// 10 < null is False
// 10 == null is False
int? b = null;
int? c = null;
Console.WriteLine($"null >= null is {b >= c}");
Console.WriteLine($"null == null is {b == c}");
// Output:
// null >= null is False
// null == null is True
IsNullable(typeof(T?))
Console.WriteLine($"int? is {(IsNullable(typeof(int?)) ? "nullable" : "non nullable")} value type");
Console.WriteLine($"int is {(IsNullable(typeof(int)) ? "nullable" : "non-nullable")} value type");
bool IsNullable(Type type) => Nullable.GetUnderlyingType(type) != null;
// Output:
// int? is nullable value type
// int is non-nullable value type
在获取可为空的值类型的时候,注意只能使用typeof()
不能使用GetType()
,后者只能返回基类的类型:
int? a = 17;
Type typeOfA = a.GetType();
Console.WriteLine(typeOfA.FullName);
// Output:
// System.Int32
此外,is关键字无法判断 T
和T?
,默认它们是同类型
int? a = 42;
if (a is int valueOfA)
{
print(a); --结果打印 42
}
T?
T?
虽然可以避免值类型接受空值,但是我们应该尽量避免使用T?
,这是因为这个类型实际上是对T
类型的装箱和拆箱。当我们声明这个变量的时候,它会被编译器装箱为T?
,而当我们操作T?
的时候编译器又会对它拆箱,实际上它像是一个拥有T
和另一个变量Null
的类。为了避免装箱拆箱操作对内存的影响,能不用尽量不用。
由于 T?
已装箱,因此如果我们再对其装箱则会产生以下的情况判断:
HasValue
返回 false
,则生成空引用。HasValue
返回 true
,则基础值类型 T
的对应值将装箱,而不对 Nullable
的实例进行装箱。(也就是重新对T
类型的对应值装箱一次)可将值类型 T
的已装箱值取消装箱到相应的可为空值类型 T?
,如以下示例所示:
int a = 41;
object aBoxed = a;
int? aNullable = (int?)aBoxed; -- 把已装箱的a取消装箱并重新装箱为int?
Console.WriteLine($"Value of aNullable: {aNullable}");
object aNullableBoxed = aNullable; -- HasValue=true,则基础类型int将重新被装箱
if (aNullableBoxed is int valueOfA)
{
Console.WriteLine($"aNullableBoxed is boxed int: {valueOfA}");
}
int? b = null;
object aNullableBoxed = b; -- HasValue=false,则生成空引用
if (aNullableBoxed == null)
{
Console.WriteLine($"aNullableBoxed is boxed int: {valueOfA}");
}
// Output:
// Value of aNullable: 41
// aNullableBoxed is boxed int: 41
// aNullableBoxed is boxed int: 41