迄今为止所有类型分为两个类别,引用类型和值类型,区别在于拷贝策略。
值类型的变量直接包含数据,变量名称直接和值的存储位置关联。引用类型变量的值是对一个对象实例的引用。
引用(reference)是地址,解引用是从地址获取资源。(取自299页脚注)
除了属性和字段,结构还可以包含方法和构造函数,但不可以包含用户自定义的默认(无参)构造函数。C#编译器自动生成默认构造函数将所有字段初始化为默认值。为了确保局部值类型变量被构造函数完整初始化,结构中的每个构造函数都必须初始化结构的所有字段,否则将发生编译时错误。C#不允许在结构声明中初始化字段。
为引用类型使用new操作符,“运行时”会在托管堆上创建对象的新实例,将所有字段初始化为默认值,再调用构造函数,将对实例的引用以this的形式传递。
值类型使用new操作符,“运行时”会在临时存储池中创建对象的新实例,将所有字段初始化为默认值,再调用构造函数,将临时存储位置作为ref变量以this的形式传递。
C++中struct和class区别在于默认的访问性是公共还是私有,两者在C#中的区别则大得多,在于类型的实例时以值还是引用的形式拷贝。
所有值类型都隐式密封。此外,除枚举之外的所有值类型都派生自System.ValueType。
装箱(boxing),从值类型的变量转换为引用类型会涉及以下几个步骤:
1.首先在堆上分配内存。它将用于存放值类型的数据以及少许额外开销。
2.接着发生一次内存拷贝,当前存储位置的值类型数据拷贝到堆上分配好的位置。
3.最后,转换结果是对堆上的新存储位置的引用。
相反的过程称为拆箱(unboxing)
必须先拆箱为基础类型
// ...
int number;
object thing;
double bigNumber;
number = 42;
thing = number;
// ERROR: InvalidCastException
// bigNumber = (double)thing;
bigNumber = (double)(int)thing;
// ...
容易忽视的装箱问题
interface IAngle
{
void MoveTo(int degrees, int minutes, int seconds);
}
struct Angle : IAngle
{
// ...
// NOTE: This makes Angle mutable, against the general
// guideline
public void MoveTo(int degrees, int minutes, int seconds)
{
_Degrees = degrees;
_Minutes = minutes;
_Seconds = seconds;
}
// ...
}
public class Program
{
public static void Main()
{
Angle angle = new(25, 58, 23);
// Example 1: Simple box operation
object objectAngle = angle; // Box
Console.Write(((Angle)objectAngle).Degrees);
// Example 2: Unbox, modify unboxed value,
// and discard value
((Angle)objectAngle).MoveTo(26, 58, 23);
Console.Write(", " + ((Angle)objectAngle).Degrees);
// Example 3: Box, modify boxed value,
// and discard reference to box
((IAngle)angle).MoveTo(26, 58, 23);
Console.Write(", " + ((Angle)angle).Degrees);
// Example 4: Modify boxed value directly
((IAngle)objectAngle).MoveTo(26, 58, 23);
Console.WriteLine(", " + ((Angle)objectAngle).Degrees);
}
}
一般不会这样写,所以很少遇到这样的问题,但还是应该避免可变值类型。
枚举总是具有一个基础类型,可以是除char之外的任意整型,默认是int,可用继承语法指定其他类型,但并没有真正建立继承关系,所有枚举的基类都是System.Enum,后者从System.ValueType派生。
值能转换成基础类型,就能转换成枚举类型。该设计的优点在于可在未来的API版本中为枚举添加新值,同时不破坏早期版本,允许在运行时分配未知的值。在枚举中部插入枚举值会使其后的所有枚举值发生顺移。
枚举的一个好处是ToString()方法会输出枚举值标识符。
很多时候不希望枚举值独一无二,还希望可以对其进行组合表示复合值。
如决定用位标志枚举,枚举的声明应该用FlagsAttribute来标记。
[Flags]
public enum FileAttributes
{
None = 0, // 000000000000000
ReadOnly = 1 << 0, // 000000000000001
Hidden = 1 << 1, // 000000000000010
System = 1 << 2, // 000000000000100
Directory = 1 << 4, // 000000000010000
Archive = 1 << 5, // 000000000100000
Device = 1 << 6, // 000000001000000
Normal = 1 << 7, // 000000010000000
Temporary = 1 << 8, // 000000100000000
SparseFile = 1 << 9, // 000001000000000
ReparsePoint = 1 << 10, // 000010000000000
Compressed = 1 << 11, // 000100000000000
Offline = 1 << 12, // 001000000000000
NotContentIndexed = 1 << 13, // 010000000000000
Encrypted = 1 << 14, // 100000000000000
}
public class Program
{
public static void Main()
{
string fileName = @"enumtest.txt";
System.IO.FileInfo file = new(fileName);
//按位与
file.Attributes = FileAttributes.Hidden |
FileAttributes.ReadOnly;
Console.WriteLine($"{file.Attributes} = {(int)file.Attributes}");
// Only the ReadOnly attribute works on Linux
// (The Hidden attribute does not work on Linux)
if (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Linux))
{
// Added in C# 4.0/Microsoft .NET Framework 4.0
if (!file.Attributes.HasFlag(FileAttributes.Hidden))
{
throw new Exception("File is not hidden.");
}
}
if ((file.Attributes & FileAttributes.ReadOnly) !=
FileAttributes.ReadOnly)
{
throw new Exception("File is not read-only.");
}
// ...
}
}
一个好习惯是在标志枚举中包含值为0的None成员,因为默认就是0。避免最后一个枚举值对应像Maximum这样的东西,有可能被解释成有效枚举值。检查枚举是否包含某个值,请使用System.Enum.IsDefined()方法。
[Flags]
enum DistributedChannel
{
None = 0,
Transacted = 1,
Queued = 2,
Encrypted = 4,
Persisted = 16,
FaultTolerant =
Transacted | Queued | Persisted
}