C#.Net築基-類型系統②常見類型

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\\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比較少。

image.png

AppDomain成員 說明
CurrentDomain 靜態屬性,獲取當前應AppDomain
BaseDirectory 獲取程序跟目錄
Load(AssemblyName) 加載程序集Assembly
UnhandledException 全局未處理異常 事件,可用來捕獲處理全局異常
AppContext成員 說明
BaseDirectory 獲取程序跟目錄⭐
TargetFrameworkName 獲取當前.Net框架版本
GetData(String) 獲取指定名稱的對象數據,SetData 設置數據。
TryGetSwitch(String, Boolean) 獲取指定名稱的bool值數據,SetSwitch 設置數據。

參考資料


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章