C#.Net筑基-类型系统②常见类型

C#.Net筑基-类型系统②常见类型 类型系统 常见类型 C#. Net 筑基 C#

image.png

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.SpanReadOnlySpan 都是 readonly ref struct结构体。

public ref struct Point
{
	public int X,Y;
}

02、枚举Enum

枚举类型 是由基础值类型(byte、int、long等)组成的一组命名常量的值类型,用enum来申明定义。常用于一些有固定值的类别申明,如性别、方向、数据类型等。

  • 枚举成员默认是int,可以修改为其他整数类型,如byteshortuintlong等。
  • 枚举项可设置值,也可省略,或者部分设置值。值默认是从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 表示一段时间的时间长度(间隔),或一天内的时间(类似时钟,无日期) DateOnlyTimeOnly .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标准了。

image.png

我们日常使用的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

🔸静态成员 说明 NowUtcNow 当前本地时间、当前UTC时间,还有一个Today 只有日期部分的值 MinValueMaxValue 最小、最大值 UnixEpoch Unix 0点的时间,值就是 1970 年 1 月 1 日的 00:00:00.0000000 UTC ParseParseExact 解析字符串转换为DateTime值,转换失败会抛出异常 TryParseTryParseExact 作用同上,安全版本的,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值

image.png

用一个示例来理解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 看做是一个快速定义类(结构体)的语法糖,编译器会构建完整的类型。

  • 构造函数中的参数会生成公共的只读属性,其他自动生成的内容还包括EqualsToString、解构赋值等。
  • 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 类型是引用类型,成员是只读属性。

image.png

📢 优先推荐使用 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 代替,性能更好,也不需要额外的类型了。

image.png


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\\nOSVersion 获取操作系统信息 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比较少。

image.png

AppDomain成员 说明 CurrentDomain 静态属性,获取当前应AppDomain BaseDirectory ⭐ 获取程序跟目录 Load(AssemblyName) 加载程序集Assembly UnhandledException ⭐ 全局未处理异常 事件,可用来捕获处理全局异常 AppContext成员 说明 BaseDirectory 获取程序跟目录⭐ TargetFrameworkName 获取当前.Net框架版本 GetData(String) 获取指定名称的对象数据,SetData 设置数据。 TryGetSwitch(String, Boolean) 获取指定名称的bool值数据,SetSwitch 设置数据。

参考资料


评论