C#.Net筑基-类型系统②常见类型
01、结构体类型Struct
结构体 struct 是一种用户自定义的值类型,常用于定义一些简单(轻量)的数据结构。对于一些局部使用的数据结构,优先使用结构体,效率要高很多。
- 可以有构造函数,也可以没有。因此初始化时可以
new
,也可以用默认default
。但当给字段设置了初始值时,则必须有显示的构造函数。 - 结构体中可以定义字段、属性、方法,不能使用终结器。
- 结构体可继承接口,并实现接口,但不能继承其他类、结构体。
- 结构体是值类型,被分配在栈上面,因此在参数传递时为值传递。
⁉️结构体始终都是分配在栈上吗?—— 不一定,当结构体是类的成员时,则会随对象一起分配在堆上。同时当结构体上有引用类型字段时,该字段只存储引用对象的地址,引用对象还是分配在堆上。
void Main()
{
Point p1 = default;
//Point p1 = default(Point);
Point p2 = new Point(1, 2);
p1.X = 100;
p2.X = 100;
}
public struct Point
{
public int X;
public int Y;
public Point(int x, int y)
{
X = x;
Y = y;
}
}
1.1、只读结构体与只读函数
readonly struct
申明一个只读的结构体,其所有字段、属性都必须是只读的。
public readonly struct Point
{
public readonly int X,Y;
}
用在方法上,该方法中不可修改任何字段值。这只能用在结构体中,结构体不能继承,不知道这个特性有什么用?
public struct Point
{
public int X;
public int Y;
public readonly int GetValue()
{
X--; //Error:不可修改
return X + Y;
}
}
1.2、Ref 结构体
ref 结构类型 用ref struct
申明,该结构体只能存储在栈上,因此任何会导致其分配到堆上的行为都不支持,如装箱、拆箱,作为类的成员等都不支持。
Ref 结构体 可用于一些高性能场景,System.Span、ReadOnlySpan 都是 readonly ref struct
结构体。
public ref struct Point
{
public int X,Y;
}
02、枚举Enum
枚举类型 是由基础值类型(byte、int、long等)组成的一组命名常量的值类型,用enum
来申明定义。常用于一些有固定值的类别申明,如性别、方向、数据类型等。
- 枚举成员默认是
int
,可以修改为其他整数类型,如byte
、short
、uint
、long
等。 - 枚举项可设置值,也可省略,或者部分设置值。值默认是从
0
开始,并按顺序依次递增。 - 枚举变量的默认值始终是
0
。 - 枚举本质上就是命名常量,因此可以与值类型进行相互转换(强制转换)。
- 特性
Description
常用来定义枚项在UI上的显示内容,使用反射获取。
public enum UserType : int //常量类型,可以修改为其他整数类型
{
[Description("普通会员")]
Default,
VIP = 10,
SupperVIP, //继续前一个,值为11
}
void Main()
{
var t1 = UserType.Default;
Console.WriteLine(t1.ToString()); //输出名称:Default
Console.WriteLine((int)t1); //输出值:0
Console.WriteLine($"{t1:F}"); //输出名称:Default
Console.WriteLine($"{t1:D}"); //输出值:0
var t2 = (UserType)0;
int t3 = (int)UserType.Default;
Console.WriteLine(t1 == t2); //True
}
2.1、Enum 类API
System.Enum 类型是所有枚举类型的抽象基类,提供了一些API方法用于枚举的操作,基本都是静态方法。Enum 类型还可以作为泛型约束使用。
🔸静态成员 | 说明 |
---|---|
HasFlag(Enum) | 判断(位域)枚举是否包含一个枚举值,返回bool |
🔸静态成员 | 说明 |
GetName<TEnum>(TEnum) |
获取枚举值的(常数)名称 |
GetNames<TEnum>() |
获取枚举定义的所有(常数)名称数组 |
GetValues<TEnum>() |
获取枚举定义的所有成员数组 |
IsDefined(Type, Object) | 判断给定的值(数值或名称)是否在枚举中定义 |
Parse<TEnum>(String) |
解析数值、名称为枚举,转换失败抛出异常 |
TryParse<TEnum>(String, TEnum) |
安全的转换,同上,转换结果通过out参数输出,返回bool 表示是否转换成功 |
🔸其他 | 说明 |
Type.IsEnum | Type 的属性,用于判断一个类型是否枚举类型 |
2.2、位域Flags
枚举位域用[Flags]
特性标记,从而可以使用枚举的位操作,实现多个枚举值合并的的能力。在有些多选值的场景很有用,用一个数值可表示多个内容,如QQ的各种钻(绿钻、红钻、黄钻...)用一个值就可以表示,参考下面代码示例。
- 枚举定义时加上特性
[Flags]
。 - 要求枚举值必须是
2的n次方
,主要是各个成员的二进制值的对应位都不能一样,才能保障按位与、按位或运算的正确。 - 合并值用按位或
|
,判断是否包含可以用按位与&
,或者方法HasFlag(e)
。 - 枚举类型命名一般建议用复数名词。
void Main()
{
var t1 = QQDiamond.Green|QQDiamond.Red; //按位或运算,合并多个成员值
Console.WriteLine((int)t1); //3,同时为绿钻、红钻
//判断是否绿钻
Console.WriteLine(t1.HasFlag(QQDiamond.Green)); //True
//判断是否红钻,效果同上
Console.WriteLine((t1 & QQDiamond.Red) == QQDiamond.Red); //True
}
[Flags]
public enum QQDiamond : sbyte
{
None=0b0000, //或者0
[Description("绿钻")]
Green=0b0001, //或者1
Red=0b0010, //或者2、1<<1
Blue=0b0100, //或者4、1<<2
Yellow=0b1000,//或者8、1<<3
}
2.3、枚举值转换
枚举值为整形,枚举名称为string,因此常与int、string进行转换。
🔸转换为枚举 | 说明 |
---|---|
Enum.Parse()/TryParse() | 转换枚举值(字符串形式)、枚举名称为枚举对象,支持位域Flgas |
TEnum(int) | 强制转换整形值为枚举,如果没有不会报错,支持位域Flgas |
/Parse/TryParse方法解析
var t1 = Enum.Parse<QQDiamond>("3"); //Green
var t2 = Enum.Parse<QQDiamond>("Green"); //Green
//强转
QQDiamond t3 =(QQDiamond)56;
🔸枚举转换为string、int | 说明 |
---|---|
ToString() | 获取枚举名称,支持位域Flgas |
Enum.GetName(e) | 获取枚举名称,不支持位域Flgas |
字符格式:G(或F) | 获取枚举名称,其中F主要用于Flgas枚举 |
强制类型转换:(int)TEnum |
获取枚举值 |
字符格式:D(或X) | 格式化中获取枚举值,D为十进制整形,X为16进制 |
//string
var s1 = qd.ToString(); //Green
var s2 = Enum.GetName(qd); //Green 不支持位于Flgas
var s3 = $"{qd:G}"; //Green
//int
var n1 = (int)qd; //1
var n2 = $"{qd:D}"; //1
03、日期和时间的故事
在System命名空间中有 下面几个表示日期时间的类型:都是不可变的、结构体(Struct)。
类型 | 说明 |
---|---|
DateTime | 常用的日期时间类型,默认使用的是本地时间(本地时区) |
DateTimeOffset | 支持时区偏移量的的 DateTime,适合跨时区的场景。 |
TimeSpan | 表示一段时间的时间长度(间隔),或一天内的时间(类似时钟,无日期) |
DateOnly 、 TimeOnly | .NET 6 引入的只表示日期、时间,结构更简单轻量,适合特点场景 |
TimeZoneInfo | 时区,可表示世界上的任何时区 |
📢Ticks: 上面几个时间对象中都有一个
Ticks
值,其值为从公元0001/01/01开始的计数周期。1 Tick (一个周期)为100纳秒(ns),0.1微秒(us),千万分之一秒,可以看做是C#中的最小时间单位。
Console.WriteLine(DateTime.Now.Ticks); //638504277997063647
Console.WriteLine(DateTimeOffset.Now.Ticks); //638504277997063874
Console.WriteLine(TimeSpan.FromSeconds(1).Ticks); //10000000
3.1、什么是UTC、GMT?
UTC(Coordinated Universal Time)世界标准时间(协调时间时间),简单理解就是 0时区的时间,是国际通用时间。它与0度经线的平太阳时相差不超过1秒,接近格林尼治标准时间(GMT)。
格林尼治标准时间(Greenwich Mean Time,GMT)是指位于伦敦郊区的皇家格林尼治天文台的标准时间,因为本初子午线被定义在通过那里的经线。 理论上来说,格林尼治标准时间的正午是指当太阳横穿格林尼治子午线时的时间。
📢 由于地球在它的椭圆轨道里的运动速度不均匀,因此GMT是不稳定的。而UTC时间是由原子钟提供的,更为精确可靠,基本上已经取代GMT标准了。
我们日常使用的DateTime.Now
获取的时间其实是带了本地时区的(TimeZone),北京时区(+8小时),就是相比UTC时间,多了8个小时的偏差(时差)。DateTime 的Kind属性为DateTimeKind枚举,指定了时区类型:
- Unspecified:不确定的,大部分场景会被认为是
Local
的。 - Utc:UTC标准时区,偏移量为0。
- Local(默认值):本地时区的时间,偏移量根据本地时区计算,如北京时间的偏移量为
+8小时
。
public enum DateTimeKind
{
Unspecified,
Utc,
Local
}
3.2、DateTime
🔸静态成员 | 说明 |
---|---|
Now、UtcNow | 当前本地时间、当前UTC时间,还有一个Today 只有日期部分的值 |
MinValue、MaxValue | 最小、最大值 |
UnixEpoch | Unix 0点的时间,值就是 1970 年 1 月 1 日的 00:00:00.0000000 UTC |
Parse、ParseExact | 解析字符串转换为DateTime值,转换失败会抛出异常 |
TryParse、TryParseExact | 作用同上,安全版本的,Exact版本的方法可配置时间字符格式 |
🔸实例成员 | 说明 |
Date | 只有日期部分的DateTime值 |
Kind | DateTimeKind 类型,默认Local,构造函数中可以指定 |
Ticks | 计时周期总数,单位为100ns(纳秒) |
Year、Month、Day... | 当前时间的年、月、日、星期等等 |
🔸方法 | |
Add*** | 添加值后返回一个新的 DateTime,可以为负数 |
ToString(String) | 转换为字符串,指定日期时间格式,详细格式参考《String字符串全面了解》 |
ToUniversalTime() | 转换为UTC时间 |
3.3、DateTimeOffset
DateTimeOffset 和 DataTime 很像,使用、构造方式、API都差不多。主要的区别就是多了时区(偏移Offset),构造函数中可以用 TimeSpan 指定偏移量。DateTimeOffset 内部有两个比较重要的字段:
- 用一个短整型
short _offsetMinutes
来存储时区偏移量(基于UTC),单位为分钟。 - 用一个DateTime 存储始终为UTC的日期时间。
🔸静态成员 | 说明 |
---|---|
UtcTicks | (UTC) 日期和时间的计时周期数 |
Offset | 时区偏移量,如北京时间:DateTimeOffset.Now.Offset //08:00:00 |
UtcDateTime | 返回本地UTC的DateTime |
LocalDateTime | 返回本地时区的DateTime |
DateTime | 返回Kind类型为Unspecified的DateTime,忽略了时区的DateTime值 |
用一个示例来理解DataTime、DataTimeOffset的区别: 比如你在一个跨国(跨时区)团队,你要发布一个通知:
- “本周五下午5点前提交周报”,不同时区都是周五下午5点前提交报告,虽然他们不是同一时刻,此时可用
DateTime
。- “明天下午5点开视频会”,此时则需要大家都在同一时刻上线远程会议,可能有些地方的是白天,有些则在黑夜,此时可用DateTimeOffset。
3.4、TimeSpan
TimeSpan 用来表示一段时间长度,最大值为1000W天,最小值为100纳秒。常用TimeSpan.From***()
、构造函数、或DateTime的 差值结果 来构造。
TimeSpan t1 =TimeSpan.FromSeconds(12); //00:00:12 //12秒
TimeSpan t2= new TimeSpan(12,0,0) - t1; //11:59:48 //11小时59分48秒
TimeSpan t3 = DateTime.Now.AddSeconds(12) - DateTime.Now; ////00:00:12
var t4 = new TimeSpan(15,1,0,0); //15.01:00:00 //15天1小时
var t5= DateTime.Now.TimeOfDay; //当天的时间
04、record是什么类型?
record 记录类型用来定义一个简单的、不可变(只读) 的数据结构,定义比较方便,常用于一些简单的数据传输场景。record 本质上就是定义一个class
类型(也可申明为record struct
结构体),因此语法上就是 类型申明+主构造函数的形式。
🚩 可以把 Record 看做是一个快速定义类(结构体)的语法糖,编译器会构建完整的类型。
- 构造函数中的参数会生成公共的只读属性,其他自动生成的内容还包括
Equals
、ToString
、解构赋值等。 - record 默认为
class
(可缺省),用record struct
则可申明为一个结构体的。 - record 类型可以继承另一个record类型,或接口,但不能继承其他普通
class
。 - 支持使用
with
语句创建非破坏性副本。
public record Car(string Width); //class
public record struct User(string Name, int Age);//struct
public record class Person(DateTime Birthday); //class
void Main()
{
var u1 = new User("sam",122);
var u2 = new User("sam",122);
u1.Age = 1; //只读,不可修改
Console.WriteLine(u1 ==u2); //True
Console.WriteLine(Object.ReferenceEquals(u1,u2)); //False
var (name,_) = u1; //解构赋值
Console.WriteLine(name); //sam
}
public record Person2 //创建一个可更改的recored类型
{
public string FirstName { get; set; }
public string LastName { get; set; }
};
通过查看编译后的代码来了解recored
的本质,下面是代码public record User(string Name, int Age)
编译后生成的代码(简化后),完整代码可查看在线 sharplab代码。
- 主构造函数中的参数都生成了只读属性,如果是
struct
结构体则属性是可读、可写的。 - 生成了
ToString()
方法,用stringBuilder 打印了所有字段名、字段值。 - 生成了相等比较的方法、相等运算符重载,及
GetHashCode()
,相等比较会比较字段值。 - 还生成了
Deconstruct
方法,用来支持解构赋值,var (name,age) = new User("sam",19);
。
public class User : IEquatable<User>
{
public string Name{get;init;}
public int Age{get;init;}
public User(string Name, int Age)
{
this.Name = Name;
this.Age = Age;
}
public override string ToString()
{
StringBuilder stringBuilder = new StringBuilder();
//把所有字段名、值输出
return stringBuilder.ToString();
}
public static bool operator !=(User left, User right)
{
return !(left == right);
}
public static bool operator ==(User left, User right)
{...}
public override int GetHashCode()
{...}
public virtual bool Equals(User other)
{...}
//支持解构赋值Deconstruct
public void Deconstruct(out string Name, out int Age)
{
Name = this.Name;
Age = this.Age;
}
}
record 申明可以用简化的语法(只有主构造函数,没有“身体”),也可以和class
一样自定义一些内部成员。如下面示例中,自定义实现了ToString
方法,则编译器就不会再生成该方法了,同时这里加了密封sealed
标记,子类也就不能重写了。
void Main()
{
var u = new User("John", 25);
Console.WriteLine(u.ToString());
u.SayHi();
}
public record User(string Name, int Age)
{
public sealed override string ToString() => $"{Name} {Age}";
public void SayHi() => Console.WriteLine($"Hi {Name}");
}
05、元祖Tuple
元祖 Tuple 其实就微软内置的一组包含若干个属性的泛型类型,包括结构体类型的 System.ValueTuple、引用类型的 System.Tuple,包含1到8个只读属性。
- System.ValueTuple,是值类型,结构体,成员是字段,可修改。
- System.Tuple 类型是引用类型,成员是只读属性。
📢 优先推荐使用 ValueTuple,这也是微软深度支持的,性能更好,默认类型推断用的都是ValueTuple。Tuple 作为历史的产物,在语言级别没有任何特殊支持。
下面代码为Tuple<T1>
的源代码,就是这么朴实无华,其他就是相等比较、ToString
、索引器。
public struct ValueTuple<T1, T2>
{
public T1 Item1;
public T2 Item2;
public ValueTuple(T1 item1, T2 item2)
{
Item1 = item1;
Item2 = item2;
}
}
🚩C#在语法层面对
ValueTuple
的操作提供了很多便捷支持,让元祖的使用非常简单、优雅,基本可以替代匿名类型。
- 简化
Tuplec
申明:用括号的简化语法,(Type,Type,...)
,(string,int)
等效于ValueTuple<string,int>
,编译器会进行类型推断。 - 值相等:元祖内部实现了相等比较操作符重载,比较的是字段值。
- 元素命名:元祖可以显示指定字段名称,比原来的无意义Item1、Item2好用多了。不过命名是开发态支持,编译后还是Item1、Item2,因此在运行时(反射)不可用。
- 解构赋值,元祖对解构的支持是编译器行为。
ValueTuple<double,double> p1 = new (1,5);
//简化语法
(double, double) p2 = (3, 5.5);
var p3 = (3, 5.5); //类型推断,进一步简化
var dis = p2.Item1 * p2.Item2; //Item1、Item2 成员
//值比较
Console.WriteLine(p2 == p3); //True
//命名,有名字的元祖
var p4 = (Name:"sam",Age:22);
Console.WriteLine(p4.Name); //sam
//解构赋值
var (n,age) = p4;
Console.WriteLine(n); //sam
元祖的一个比较适用场景就是方法返回多个值,虽然本质上还是一个“值”。
void Main()
{
var u = FindUser(1);
var (nn,ss) = FindUser(2);
Console.WriteLine(u.name+u.score);
Console.WriteLine(nn+ss);
}
public (string name,int score) FindUser(int id) //返回一个元祖
{
return ("sam",1000);
}
06、匿名类型(Class)
匿名类型就是无需事先申明,可直接创建任意实例的一种类型。使用 new {}
语法创建,创建时申明字段并赋值。
- 由编译器进行推断创建出一个完整类型。
- 匿名类型属性都是只读的,同时实现了相等比较、
ToString()
方法。
var u = new { Name = "same", Age = 10, Birthday = DateTime.Now };
Console.WriteLine(u.Name);
//u.Age=120; //只读不可修改
因此,匿名类型也是一种语法糖,由编译器来生成完整的类型。大多数场景都可以由 ValueTuple 代替,性能更好,也不需要额外的类型了。
07、其他内置类型
7.1、Console
Console 静态类,控制台输入、输出。
成员 | 说明 |
---|---|
BackgroundColor | 获取、设置控制台背景色 |
ForegroundColor | 获取、设置控制台前景色 |
WriteLine(String) | 输出内容到控制台 |
ReadLine() | 接受控制台输入 |
Beep() | 播放一个提示音,参数还可以设置播放时长 |
Clear() | 清空控制台 |
7.2、Environment
Environment 静态类,提供全局环境的一些参数和方法,算是比较常用了。
成员 | 说明 |
---|---|
CurrentDirectory | 当前程序的工作目录,是运行态可变的,不一定是exe目录 |
ProcessPath | 当前程序exe的地址,.NET 5 支持 |
CurrentManagedThreadId | 当前托管现线程的ID |
Is64BitOperatingSystem | 获取操作系统是否64位,Is64BitProcess 获取当前进程是否64位进程。 |
NewLine | 换行符(\\r\\n ) |
OSVersion | 获取操作系统信息 |
ProcessId | 获取当前进程ID |
ProcessorCount | 获取CPU处理器核心数 |
UserName | 获取当前操作系统的用户名 |
WorkingSet | 获取当前进程的物理内存量 |
Exit(Int32) | 退出进程 |
GetFolderPath(SpecialFolder) | 获取系统特定文件夹目录,如临时目录、桌面等 |
SetEnvironmentVariable | 设置环境变量 |
7.2、AppDomain、AppContext
- AppDomain 是.Net Framework时代的产物,用来表示一个应用程序域,进程中可以创建多个引用程序域,拥有独立的程序集、隔离环境。在.Net Core 中 其功能大大削弱了,不再支持创建AppDomain,就只有一个CurrentDomain了。
- AppContext 表示全局应用上下文对象,是一个静态类。.NET Core引入的新类,可用来存放一些全局的数据、开关,API比较少。
AppDomain成员 | 说明 |
---|---|
CurrentDomain | 静态属性,获取当前应AppDomain |
BaseDirectory ⭐ | 获取程序跟目录 |
Load(AssemblyName) | 加载程序集Assembly |
UnhandledException ⭐ | 全局未处理异常 事件,可用来捕获处理全局异常 |
AppContext成员 | 说明 |
---|---|
BaseDirectory | 获取程序跟目录⭐ |
TargetFrameworkName | 获取当前.Net框架版本 |
GetData(String) | 获取指定名称的对象数据,SetData 设置数据。 |
TryGetSwitch(String, Boolean) | 获取指定名称的bool值数据,SetSwitch 设置数据。 |
参考资料
- .NET类型系统①基础
- C# 文档
- 日期、时间和时区
- 《C#8.0 In a Nutshell》