C#9.0:Init


背景

在以前的C#版本里面,如果需要定義一個不可修改的的類型的做法一般是:聲明為readonly,並設置為只包含get訪問器,不包含set訪問器。如下:

 1  public class PersonInfo
 2     {
 3         /// <summary>
 4         /// 身份編號
 5         /// </summary>
 6         public string UserCode { get; }
 7 
 8         /// <summary>
 9         /// 姓名
10         /// </summary>
11         public string UserName { get; }
12 
13         /// <summary>
14         /// 初始化賦值
15         /// </summary>
16         /// <param name="_userCode"></param>
17         /// <param name="_userName"></param>
18         public PersonInfo(string _userCode,string _userName)
19         {
20             UserCode = _userCode;
21             UserName = _userName;
22         }
23

這種方式是可行的,也達到我們的目的,但是代碼量多,需要增加額外的構造方法來實現初始化賦值,並且如果字段越多,帶參構造函數也會越大,開發工作量也越大,更不好維護。

為了改變這種狀態,C#9.0提供了一種解決方案:在對象初始換的時候就配置為只讀的方式。

特別對一口氣創建含有嵌套結構的樹狀對象來說更有用。下面是一個用戶信息初始化的案例:

1 PersonInfo pi = new PersonInfo() { UserCode="1234567890", UserName="Brand" }; 

從這個例子說明了,要進行對象初始化,我們必須先要在需要初始化的屬性中添加set訪問器,然后才能在對象初始化器中通過給屬性或者索引器賦值來實現。如下:

 1   public class PersonInfo
 2     {
 3         /// <summary>
 4         /// 身份編號
 5         /// </summary>
 6         public string UserCode { get; set; }
 7 
 8         /// <summary>
 9         /// 姓名
10         /// </summary>
11         public string UserName { get; set; }
12

所以對於初始化來說,屬性必須是可變的,set訪問器就必須存在。這就是問題所在,很多情況下為了避免屬性初始化之后再被改變,就需要不可變對象類型,因此setter訪問器在這里明顯不適用。

基於這種有這種常見的需要和局限性,C#9.0引入了只用來初始化的init設置訪問器。這時,上面的PersonInfo類就可以定義成下面的樣子:

 1   public class PersonInfo
 2     {
 3         /// <summary>
 4         /// 身份編號
 5         /// </summary>
 6         public string? UserCode { get; init; }
 7 
 8         /// <summary>
 9         /// 姓名
10         /// </summary>
11         public string? UserName { get; init; }
12

這邊通過采用init訪問器,代碼變得簡潔易懂了,滿足了上面的只讀需求,而且更易編碼和維護。

定義和使用

init(只初始化屬性或索引器訪問器):只在對象構造階段進行初始化時可以用來賦值,算是set訪問器的變體,set訪問器的位置使用init來替換。init有着如下限制:

1、init訪問器只能用在實例屬性或索引器中,靜態屬性或索引器中不可用。

2、屬性或索引器不能同時包含init和set兩個訪問器

3、如果基類的屬性有init,那么屬性或索引器的所有相關重寫,都必須有init。接口也一樣。

什么時候設置init訪問器

除過在局部方法和lambda表達式中,帶有init訪問器的屬性和索引器可以在下面幾種情況中可設置的。這幾個設置的時機都是在對象的構造階段。過了構造階段,后續賦值操作就不允許了。

1、在對象初始化器工作期間

2、在with表達式初始化器工作期間

3、在所處或者派生的類型的實例構造函數中,在this或者base使用上

4、在任意屬性init訪問器里面,在this或者base使用上

5、在帶有命名參數的attribute使用中

在這些限制條件下,意味着我們上面定義的PersonInfo只能在對象初始化的時候使用,第二次賦值就不被允許了。

即:一旦初始化完成之后,只初始化屬性或索引就保護着對象的狀態免於改變。

1  var person = new PersonInfo() { UserCode="12345678", UserName="Brand" };
2 //提示錯誤:只能在對象初始器或實例構造函數中分配 init-only
3  person.UserName = "Brand1"

init屬性訪問器和只讀字段

因為init訪問器只能在初始化時被調用,所以在init屬性訪問器中可以改變封閉類的只讀字段。

需要注意的是,從init訪問器中來給readonly字段賦值僅限於跟init訪問器處於同一類型中定義的字段,通過它是不能給父類中定義的readonly字段賦值的,關於這繼承有關的示例,我們會在2.4類型間的層級傳遞中看到。

 1     public class PersonInfo
 2     {
 3         private readonly string userCode = "<unknown>";
 4         private readonly string userName = "<unknown>";
 5 
 6         public string UserCode
 7         {
 8             get => userCode;
 9             init => userCode = (value ?? throw new ArgumentNullException(nameof(UserCode)));
10         }
11         public string UserName
12         {
13             get => userName;
14             init => userName = (value ?? throw new ArgumentNullException(nameof(UserName)));
15         }      
16     }

類型層級間的傳遞

我們知道只包含get訪問器的屬性或索引器只能在所處類的自身構造函數中被初始化,但init訪問器可以進行設置的規則是可以跨類型層級傳遞的。

帶有init訪問器的成員只要是可訪問的,對象實例並能在構造階段被知曉,那這個成員就是可設置的。

1、在對象初始化中使用,是允許的

 1     public class PersonInfo
 2     {
 3         /// <summary>
 4         /// 身份編號
 5         /// </summary>
 6         public string UserCode { get; init; }
 7 
 8         /// <summary>
 9         /// 姓名
10         /// </summary>
11         public string UserName { get; init; }
12 
13         public PersonInfo()
14         {
15             UserCode = "1234567890";
16             UserName = "Brand";
17         }
18

2、在派生類的實例構造函數中,也是允許的,如下面這兩個例子:

1     public class PersonInfoExt : PersonInfo
2     {
3         public PersonInfoExt()
4         {
5             UserCode = "1234567890_0";
6             UserName = "Brand1";
7         }
8     }
1 var personext = new PersonInfoExt() { UserCode="1234567890_2", UserName="Brand2" };

從init訪問器能被調用這一方面來看,對象實例在開放的構造階段就可以被知曉。因此除過正常set可以做之外,init訪問器的下列行為也是被允許的。

1、通過this或者base調用其他可用的init訪問器

2、在同一類型中定義的readonly字段,是可以通過this給賦值的

init中是不能更改父類中的readonly字段的,只能更改本類中readonly字段。示例代碼如下:

 1 class PersonInfo1
 2     {
 3         protected readonly string UserCode_R;
 4         public String UserCode
 5         {
 6             get => UserCode_R;
 7             init => UserCode_R = value; // 正確:在同一類中定義的readonly屬性,可以直接通過this給賦值的
 8         }
 9         internal String UserName { get; init; }
10     }
11 
12     class PersonInfo1Ext : PersonInfo1
13     {
14         protected readonly int NewField;
15         internal int NewProp
16         {
17             get => NewField;
18             init
19             {
20                 NewField = 100;    // 正確
21                 UserCode = "123456";       // 正確
22                 UserCode_R = "1234567";     // 出錯,試圖修改基類中的readonly字段UserCode_R
23             }
24         }
25 
26         public PersonInfo1Ext()
27         {
28             UserCode = "123456";    // 正確 
29             UserCode_R = "1234567";  // 出錯,試圖修改基類中的readonly字段UserCode_R
30         }
31     }

如果init被用於virtual修飾的屬性或者索引器,那么所有的覆蓋重寫都必須被標記為init,是不能用set的。同樣地,我們不可能用init來覆蓋重寫一個set的。

 1 public class PersonInfo
 2     {
 3         /// <summary>
 4         /// 身份編號
 5         /// </summary>
 6         public virtual string UserCode { get; init; }
 7 
 8         /// <summary>
 9         /// 姓名
10         /// </summary>
11         public virtual string UserName { get; set; }
12     }
13 
14     public class PersonInfoExt1 : PersonInfo
15     {
16         public override string UserCode { get; init; }
17         public override string UserName { get; set; }
18     }
19 
20     public class PersonInfoExt2 : PersonInfo
21     {
22         // 錯誤: 基類的init屬性必須由init來重寫PersonInfo.UserCode
23         public override int UserCode { get; set; }
24         // 錯誤: 基類的init屬性必須由set來重寫PersonInfo.UserName
25         public override string UserName { get; init; }
26

init在接口接口中應用

一個接口中的默認實現,也是可以采用init進行初始化,下面就是一個應用模式示例。

 1  interface IPersonInfo
 2     {
 3         string Usercode { get; init; }
 4         string UserName { get; init; }
 5     }
 6 
 7     class PersonInfo
 8     {
 9         void NewPersonInfo<T>() where T : IPersonInfo, new()
10         {
11             var person = new T()
12             {
13                 Usercode = "1234567890",
14                 UserName = "Jerry"
15             };
16             person.Usercode = "111"; // 錯誤
17         }
18

init訪問器是允許在readonly struct中的屬性中使用的,init和readonly的目標都是一致的,就是只讀。示例代碼如下:

 1    readonly struct PersonInfo
 2     {
 3         /// <summary>
 4         /// 身份編號
 5         /// </summary>
 6         public string UserCode { get; init; }
 7 
 8         /// <summary>
 9         /// 姓名
10         /// </summary>
11         public string UserName { get; set; }
12

但是要注意的是:

1、不管是readonly結構還是非readonly結構,不管是手工定義屬性還是自動生成屬性,init都是可以使用的。

2、init訪問器本身是不能標記為readonly的。但是所在屬性或索引器可以被標記為readonly

 1     struct PersonInfo
 2     {
 3         /// <summary>
 4         /// 身份編號
 5         /// </summary>
 6         public readonly string UserCode { get; init; }
 7 
 8         /// <summary>
 9         /// 姓名
10         /// </summary>
11         public string UserName { get; readonly init; }
12


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM