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》