C#: 8.0 和 9.0 常用新特性
在《帶你了解C#每個版本新特性》 一文中介紹了,C# 1.0 到 7.0 的不同特性,本文接著介紹在 8.0 和 9.0 中的一些常用新特性。
C# 8.0
在 dotNET Core 3.1 及以上版本中就可以使用 C# 8 的語法,下面是 C# 8 中我認為比較常用的一些新功能。
默認接口方法
接口是用來約束行為的,在 C# 8 以前,接口中只能進行方法的定義,下面的代碼在 C# 8 以前是會報編譯錯誤的:
public?interface?IUser {string?GetName()?=>??"oec2003"; }那么在 C# 8 中,可以正常使用上面的代碼,也就是說可以對接口中的方法提供默認實現。
接口默認方法最大的好處是,當在接口中進行方法擴展時,之前的實現類可以不受影響,而在 C# 8 之前,接口中如果要添加方法,所有的實現類需要進行新增接口方法的實現,否則編譯失敗。
C# 中不支持多重繼承,主要的原因是會導致菱形問題:
類 A ?是一個抽象類,定義有一個 方法 Test;
類 B 和 類 C 繼承自抽象類 A,并有各自的實現;
類 D 同時繼承類 B 和類 C;
當調用類 D 的 Test 方法時,就不知道應該使用 B 的 Test 還是 C 的 Test,這個就是菱形問題。
而接口是允許多繼承的,那么當接口支持默認方法時,是否也會導致菱形問題呢?看下面代碼:
public?interface?IA {void?Test()?=>?Console.WriteLine("Invoke?IA.Test"); } public?interface?IB:IA {void?Test()?=>?Console.WriteLine("Invoke?IB.Test"); } public?interface?IC:IA {?void?Test()?=>?Console.WriteLine("Invoke?IC.Test"); } public?class?D?:?IB,?IC?{?}static?void?Main(string[]?args) {D?d?=?new?D();d.Test(); }上面的代碼是無法通過編譯的,因為接口的默認方法不能被繼承,所以類 D 中沒有 Test 方法可以調用,如下圖:
所以,必須通過接口類型來進行相關方法的調用:
static?void?Main(string[]?args) {IA?d1?=?new?D();IB?d2?=?new?D();IC?d3?=?new?D();d1.Test();??//?Invoke?IA.Testd2.Test();??//?Invoke?IB.Testd3.Test();??//?Invoke?IC.Test }也正是因為必須通過接口類型來進行調用,所以也就不存在菱形問題。而當具體的類中有對接口方法實現的時候,就會調用類上實現的方法:
public?interface?IA {void?Test()?=>?Console.WriteLine("Invoke?IA.Test"); } public?interface?IB:IA {void?Test()?=>?Console.WriteLine("Invoke?IB.Test"); } public?interface?IC:IA {?void?Test()?=>?Console.WriteLine("Invoke?IC.Test"); } public?class?D?:?IB,?IC? {public?void?Test()?=>?Console.WriteLine("Invoke?D.Test"); } static?void?Main(string[]?args) {IA?d1?=?new?D();IB?d2?=?new?D();IC?d3?=?new?D();d1.Test();??//?Invoke?D.Testd2.Test();??//?Invoke?D.Testd3.Test();??//?Invoke?D.Test }類可能同時繼承類和接口,這時會優先調用類中的方法:
public?class?A {public?void?Test()?=>?Console.WriteLine("Invoke?A.Test"); } public?interface?IA {void?Test()?=>?Console.WriteLine("Invoke?IA.Test"); }public?class?D?:?A,?IA?{?} static?void?Main(string[]?args) {D?d?=?new?D();IA?d1?=?new?D();d.Test();??//?Invoke?A.Testd1.Test();??//?Invoke?A.Test }關于默認接口方法,總結如下:
默認接口方法可以讓我們在往底層接口中擴展方法的時候變得比較平滑;
默認方法,會優先調用類中的實現,如果類中沒有實現,才會去調用接口中的默認方法;
默認方法不能夠被繼承,當類中沒有自己實現的時候是不能從類上直接調用的。
using 變量聲明
我們都知道 using 關鍵字可以導入命名空間,也能定義別名,還能定義一個范圍,在范圍結束時銷毀對象,在 C# 8.0 中的 using 變量聲明可以讓代碼看起來更優雅。
在沒有 using 變量聲明的時候,我們是這樣使用的:
static?void?Main(string[]?args) {var?connString?=?"Host=221.234.36.41;Username=gpadmin;Password=123456;Database=postgres;Port=54320";using?(var?conn?=?new?NpgsqlConnection(connString)){conn.Open();using?(var?cmd?=?new?NpgsqlCommand("select?*?from?user_test",?conn)){using?(var?reader?=?cmd.ExecuteReader()){while?(reader.Read())Console.WriteLine(reader["user_name"]);}}}Console.ReadKey(); }當調用層級比較多時,會出現 using 的嵌套,對影響代碼的可讀性,當然,當兩個 using 語句中間沒有其他代碼時,可以這樣來優化:
static?void?Main(string[]?args) {var?connString?=?"Host=221.234.36.41;Username=gpadmin;Password=123456;Database=postgres;Port=54320";using?(var?conn?=?new?NpgsqlConnection(connString)){conn.Open();using?(var?cmd?=?new?NpgsqlCommand("select?*?from?user_test",?conn))using?(var?reader?=?cmd.ExecuteReader())while?(reader.Read())Console.WriteLine(reader["user_name"]);}Console.ReadKey(); }使用 using 變量聲明后的代碼如下:
static?void?Main(string[]?args) {var?connString?=?"Host=221.234.36.41;Username=gpadmin;Password=123456;Database=postgres;Port=54320";using?var?conn?=?new?NpgsqlConnection(connString);conn.Open();using?var?cmd?=?new?NpgsqlCommand("select?*?from?user_test",?conn);using?var?reader?=?cmd.ExecuteReader();while?(reader.Read())Console.WriteLine(reader["user_name"]);Console.ReadKey(); }Null 合并賦值
這是一個很有用的語法糖,在 C# 中如果調用一個為 Null 的引用類型上的方法,會出現經典的錯誤:”未將對應引用到對象的實例“,所以我們在返回引用類型時,需要做些判斷:
static?void?Main(string[]?args) {List<string>?list?=?GetUserNames();if(list==null){list?=?new?List<string>();}Console.WriteLine(list.Count); } public?static?List<string>?GetUserNames() {return?null; }在 C# 8 中可以使用 ??= 操作符更簡單地實現:
static?void?Main(string[]?args) {List<string>?list?=?GetUserNames();list???=?new?List<string>();Console.WriteLine(list.Count); }當 list 為 null 時,會將右邊的值分配給 list 。
C# 9.0
在 .NET 5 中可以使用 C# 9 ,下面是 ?C# 9 中幾個常用的新特性。
init
init 是屬性的一種修飾符,可以設置屬性為只讀,但在初始化的時候卻可以指定值:
public?class?UserInfo {public?string?Name?{?get;?init;?} } UserInfo?user?=?new?UserInfo?{?Name?=?"oec2003"?}; //當?user?初始化完了之后就不能再改變?Name?的值 user.Name?=?"oec2004";上面代碼中給 Name 屬性賦值會出現編譯錯誤:
record
在 C# 9 中新增了 record 修飾符,record 是一種引用類型的修飾符,使用 record 修飾的類型是一種特別的 class,一種不可變的引用類型。
我們創建一個名為 UserInfo 的 class ,不同的實例中即便屬性值完全相同,這兩個實例也是不相等的,看下面代碼:
public?class?UserInfo {public?string?Name?{?get;?set;?} } static?void?Main(string[]?args) {UserInfo?user1?=?new?UserInfo?{?Name?=?"oec2003"?};UserInfo?user2?=?new?UserInfo?{?Name?=?"oec2003"?};Console.WriteLine(user1==?user2);?//False }如果使用 record ,將會看到不一樣的結果,因為 record 中重寫了 ==、Equals 等 ,是按照屬性值的方式來進行比較的:
public?record?UserInfo {public?string?Name?{?get;?set;?} } static?void?Main(string[]?args) {UserInfo?user1?=?new?UserInfo?{?Name?=?"oec2003"?};UserInfo?user2?=?new?UserInfo?{?Name?=?"oec2003"?};Console.WriteLine(user1==?user2);?//True }在 class 中我們經常將一個對象的實例賦值給另一個值,對賦值后的對象實例進行屬性值的改變會影響到原對象實例:
public?class?UserInfo {public?string?Name?{?get;?set;?} } static?void?Main(string[]?args) {UserInfo?user?=?new?UserInfo?{?Name?=?"oec2003"?};UserInfo?user1?=?user;user1.Name?=?"oec2004";Console.WriteLine(user.Name);?//?oec2004 }如果想要不影響原對象實例,就需要使用到深拷貝,在 record 中,可以使用 with 語法簡單地達到目的:
public?record?UserInfo {public?string?Name?{?get;?set;?} } static?void?Main(string[]?args) {UserInfo?user?=?new?UserInfo?{?Name?=?"oec2003"?};UserInfo?user1?=?user?with?{?Name="eoc2004"};Console.WriteLine(user.Name);?//?oec2003Console.WriteLine(user1.Name);?//?oec2004 }模式匹配增強
模式匹配中我覺得最有用的就是對 Null 類型的判斷,在 9.0 中支持這樣的寫法了:
public?static?string?GetUserName(UserInfo?user) {if(user?is?not?null){return?user.Name;}return?string.Empty; }頂級語句
這個不知道有啥用?但挺好玩的,創建一個控制臺程序,將 Program.cs 中的內容替換為下面這一行,程序也能正常運行:
System.Console.WriteLine("Hello?World!");除此之外,在 C# 8.0 和 9.0 中還有一些其他的新功能,我目前沒有用到或者我覺得不太常用,就沒有寫在本文中了。
希望本文對您有所幫助。?
總結
以上是生活随笔為你收集整理的C#: 8.0 和 9.0 常用新特性的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 轻量级NuGet—BaGet
- 下一篇: 小米 华为都要造车?.NET高薪潮来了!