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》