
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》
发表评论