Dotnet 6.0,你值得擁有
本文轉載自微信公眾號「老王Plus」,作者老王Plus的老王。轉載本文請聯系老王Plus公眾號。
最近在研究 Dotnet 6.0 & C# 10,一個字 - 爽!
下面,分享一下新的一些特性給大家。
一、編程語言方面
編程語言升到 C# 10,新東西不少。
1. 屬性的 required
看一個簡單的例子:
- public class User
- {
- public string name { get; set; }
- public DateTime dateOfBirth { get; set; }
- }
假設,我們希望 dateOfBirth 字段必須輸入。在 C# 9.0 之前,其實我們沒有更好的辦法。比方:
- var myUser = new User()
- {
- name = "WangPlus",
- }
這樣寫,編譯器是不會給出任何提醒或警告的。因此,我們需要在各個使用的地方,加上字段賦值的檢查。
而在最新的語言中,對于這樣的需求,增加了一個 required 屬性。看代碼:
- public class User
- {
- public string name { get; set; }
- public required DateTime dateOfBirth { get; set; }
- }
這時候,當你實例化 User,而沒有給 dateOfBirth 賦值時,編譯器會直接扔出異常。
在我寫這個文章的同時,剛剛發現這個特性從最新的 preview 里給移除了,似乎微軟想把這個放到 C# 11中。目前論壇上吵翻了。我們靜待一下結果。
2. 屬性的 field
在大多數情況下,我們定義一個類,會采用這種方式:
- public class User
- {
- public string name { get; set; }
- public DateTime dateOfBirth { get; set; }
- }
但有時候,因為一些需要,需要提前實例化,或者實例化時需要進行某些處理。比方上面的類,我們只想要 dateOfBirth 的日期部分,我們會把類做成這個樣子:
- public class User
- {
- public string name { get; set; }
- private DateTime _dateOfBirth;
- public DateTime dateOfBirth
- {
- get
- {
- return _dateOfBirth;
- }
- set
- {
- _dateOfBirth = value.Date;
- }
- }
- }
當然,習慣了也沒什么麻煩的。不過我們知道,_dateOfBirth 這個私有屬性其實是一個中間數據,對外沒有用處,但會占用實例資源。
現在,有了一個中間屬性,叫 field。代碼會變成這樣:
- public class User
- {
- public string name { get; set; }
- public DateTime dateOfBirth { get; set => field = value.Date; }
- }
嗯嗯,可讀性就高了不少。
3. 匿名對象的 with
匿名對象的出現,給我們帶來了相當多的方便。在類中,不需要對外輸出的結構化數據,都可以做成匿名對象,而不需要預先定義一個數據對象。
看代碼:
- var myUser = new { name = "WangPlus", gender = "Male" };
嗯。真的很方便。
不過,也有不方便的地方,就是匿名對象的傳遞。比方,我們想創建另一個對象 myUser1,屬性還是這些,僅僅需要改變幾個屬性的值,怎么辦?在以前,沒有別的辦法,只能重寫一個:
- var myUser1 = new { name = "WangPlus1", gender = "Male" };
- /** 或者 **/
- var myUser1 = new { name = "WangPlus1", gender = myUser.gender };
可以想象,如果這個匿名對象字段很多的話,就會麻煩的不要不要的。
現在有了 with,這個事情就簡單了:
- var myUser1 = myUser with { name = "WangPlus1" };
注意,這個寫法,不是把 myUser 里的屬性改了,而是新生成了一個實例,并傳遞了 myUser 的全部屬性和值到新實例 myUser1,然后才是把一些屬性的值改成新的值。
4. 非空參數檢查
在我們寫一個方法時,成熟的程序員,都會做參數的非空檢查:
- public string FormatName( string name )
- {
- if( string.isNullOrEmpty( name ) )
- return "ERROR";
- /** ... **/
- }
- public string FormatUser( User user )
- {
- if( user == null )
- return "ERROR";
- /** ... **/
- }
做法很正確,但很麻煩,一個套路性的東西,卻要不停的寫。
現在,有了一個神參數:!,沒錯,就是嘆號。
寫法是這樣:
- public string FormatName( string name! )
- {
- /** ... **/
- }
- public string FormatUser( User user! )
- {
- /** ... **/
- }
加上 ! 后,執行中,程序會自動檢查參數的非空狀態,如果出現 null,會拋出 ArgumentNullExceptions。
5. global using
這是最爽的一個特性。
以前我們寫代碼,每個文件前邊,都有無數個 using,而且很多 using 都是重復的。
現在,C# 10 提供了一個 global 關鍵字。從此,using 變成了:
- global using System;
- global using System.Collections.Generic;
- global using System.Threading.Tasks;
系統會識別 global using 后邊的內容會應用于整個項目。因此,在其它文件中,如果需要使用時,可以不寫對應的 using ,直接寫代碼即可。
再因此,可以把所有的 global using 放到一個單獨的文件中,而在其它文件中,不需要再做 using 引用。
同時,如果已經存在 global using,而你的文件中又寫了同樣庫的 using,系統會扔出一個警告。
6. 文件級的命名空間 namespace
這個特性好像沒有省了多少事。不過,也算是一個變化。
以前我們做代碼時,是這樣:
- namespace MyNamespace
- {
- public class User
- {
- public void User()
- {
- //...Method implementation
- }
- }
- }
外部調用時,就這么寫:
- var obj = new MyNamespace.User();
- /** 或者 **/
- using MyNamespace;
- var obj = new User();
現在,命令空間的定義改成了:
- namespace MyNamespace;
- public class User
- {
- public void User()
- {
- //...Method implementation
- }
- }
這樣寫,清爽了一些,縮進的層次也少了一層。當然,調用還是一樣的。
二、API方面
API 方面就更多了。在社區里,不停的會有新的 API 爆出來。我就選一些自己感覺有用的來說。
1. 非流式讀寫文件
流式讀寫,經常會涉及到中間流,資源浪費不說,寫起來也麻煩。
現在可以直接用底層 IO 來讀寫。方法加到了 File 類中。
- var handler = File.OpenHandle("abc.txt");
- var length = RandomAccess.GetLength(handler);
2. 強隨機數
我們知道,以前的隨機數 Random 類是弱隨機數,來自于一個算法,并不能做到真正的隨機。生成的隨機數序列取決于種子,相同的種子會產生相同的隨機數序列。
所以,為了取到不同的隨機數序列,我們一般這么寫:
- var rand = new Random( (int)DateTime.Now.Ticks );
當然,一般這樣也就夠了。但總有特殊的,需要真正的隨機數,即強隨機數。Dotnet Core 6.0 里,提供了一個 RandomNumberGenerator 的類。
- byte[] bytes = RandomNumberGenerator.GetBytes(200);
- int randomInt = RandomNumberGenerator.GetInt32(0, 10000);
另外需要注意一下,這個類不在 System 空間下,而在 System.Security.Cryptography 里。
3. 多任務的異步 Parallel.ForEachAsync
在多任務中,以前只有一個 Parallel.ForEach 的方法,用來同步執行。這回終于把異步方法 Parallel.ForEachAsync 加進來了,足以可見微軟在異步方面的深化決心。
寫法還是我們很熟悉的方式,這個切換很容易:
- var urls = new []
- {
- "https://test1.com",
- "https://test2.com"
- };
- var client = new HttpClient();
- await Parallel.ForEachAsync(urls, async (url, token) =>
- {
- HttpResponseMessage response = await client.GetAsync(url);
- });
4. 定時中止異步
這也是個不錯的 API。
以前當 await 異步進程時,如果這個進程長時間結束不了,我們只能通過 CancellationToken 來結束。現在,我們有了另一個方式,可以設置一個時間,以 Timeout 的方式結束這個異步進程。
- Task someTask = SomeLongRunningTaskAsync();
- await someTask.WaitAsync(TimeSpan.FromSeconds(10));
如果你寫過 CancellationToken 結束異步的代碼,就知道這個 WaitAsync 有多好。
5. ThrowIfNull
這個東西,其實跟上面判斷參數是否為空是一件事。當我們在參數據后面加 !來進行為空判斷時,實際就是執行的這一句:
- public string FormatUser( User user )
- {
- ArgumentNullException.ThrowIfNull( user );
- }
如果對象為空,就拋出一個 ArgumentNullException。
6. 使用直接內存
在以前,使用 unsafe 內存 malloc 時,都是在堆上分配空間。現在有了一個在直接內存分配空間的方法:
- using System.Runtime.InteropServices;
- unsafe
- {
- byte* buffer = (byte*)NativeMemory.Alloc(128);
- NativeMemory.Free(buffer);
- }
做嵌入式開發,有福了。
另外,通常使用非托管內容,需要進行大小對齊。所謂對齊就是分配的空間的大小需要是 2 的整指數。通常大家就是算好直接硬寫,現在也有了更靈活的方式:
- using System.Numerics;
- uint bufferSize = 211;
- if (!BitOperations.IsPow2(bufferSize))
- {
- bufferSize = BitOperations.RoundUpToPowerOf2(bufferSize);
- }
給一個空間,如果空間大小不是 2 的整指數,就找比這個數大的 2 的整指數。又省事了。
7. 新的計時器
好多文章都把這個計時器稱為 Modern Timer,足以可見它的好。
好在哪?這是一個異步的計時器。
以前的計時器 Timer,不管是 System.Timers 下的,還是 System.Threading 下的,或是 System.Windows.Forms 下的,都是同步的計時器,需要用 Tick 的事件綁定來實現回調。這個方式讓這個計時器十分依賴上級對象的生命周期,以至于在 UI 編程中,需要用 Invoke 來引入回調響應。
現在這個就簡單很多了:
- var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
- while (await timer.WaitForNextTickAsync())
- {
- /** ... **/
- }
這個寫法,看著就舒服。
寫了很多,但實際上,也只是冰山的一小角。