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

安木夕發表於2024-05-23

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 設定資料。

參考資料

  • .NET型別系統①基礎
  • C# 文件
  • 日期、時間和時區
  • 《C#8.0 In a Nutshell》

相關文章