原文:https://blogs.msdn.microsoft.com/mazhou/2018/01/08/c-7-series-part-8-in-parameters/ 背景 預設情況下,方法參數是通過值傳遞的。也就是說,參數被覆制並傳遞到方法中。因此,修改方法體中的參數不會影響原始值。在大多數 ...
原文:https://blogs.msdn.microsoft.com/mazhou/2018/01/08/c-7-series-part-8-in-parameters/
背景
預設情況下,方法參數是通過值傳遞的。也就是說,參數被覆制並傳遞到方法中。因此,修改方法體中的參數不會影響原始值。在大多數情況下,修改是不必要的。
其他編程語言,如C++,有一個const參數或類似的概念:這表明方法體中的參數是一個不能被重新賦值的常量。它有助於避免在方法體內無意中重新賦值一個方法參數的錯誤,並通過不允許不必要的賦值來提高性能。
C# 7.2引入了in參數(又名,只讀的引用參數。) 帶有in修飾符的方法參數意味著該參數是引用且在方法體中只讀。
in參數
讓我們以下麵的方法定義為例。
public int Increment(int value) { //可以重新賦值,變數value是按值傳遞進來的。 value = value + 1; return value; }
若要創建只讀引用參數,請在參數前增加in修飾符。
public int Increment(in int value) { //不能重新賦值,變數value是只讀的。 int returnValue = value + 1; return returnValue; }
如果重新賦值,編譯器將生成一個錯誤。
可以使用常規方法來調用這個方法。
int v = 1; Console.WriteLine(Increment(v));
因為value變數是只讀的,所以不能將value變數放在等式左邊(即LValue)。執行賦值的一元運算符也是不允許的,比如++或--。但是,你仍然可以獲取值的地址並使用指針操作進行修改。
解決重載
in是一個方法參數的修飾符,它表明此參數是引用類型,它被視為方法簽名的一部分。這意味著你可以有兩個方法重載,只是in修飾符不同。(譯註:一個有in,一個沒有in)
下麵的代碼示例定義了兩個方法重載,只是引用類型不同。
public class C { public void A(int a) { Console.WriteLine("int a"); } public void A(in int a) { Console.WriteLine("in int a"); } }
預設情況下,方法調用將解析為值簽名的那個重載。為了清除歧義並顯式地調用引用簽名的重載,在顯式地調用A(in int)方法重載時,在實際的參數之前加上in修飾符。
private static void Main(string[] args) { C c = new C(); c.A(1); // A(int) int x = 1; c.A(in x); // A(in int) c.A(x); // A(int) }
程式輸出如下:
限制
因為in參數是只讀的引用參數,所以所有引用參數的限制都適用於in。
- 不能用於迭代器方法(即具有yield語句的方法)。
- 不能用於async非同步方法。
- 如果你用in修飾Main方法的args參數,則入口點的方法簽名會無效。
in參數和CLR
在.NET的CLR中已經有了一個類似的概念,所以in參數特性不需要改變CLR。
任何in參數在被編譯成MSIL時,都會在定義中附加一個[in]指令。為了觀察編譯行為,我使用ILDAsm.exe獲得上面示例反編譯的MSIL。
下麵的MSIL代碼是方法C.A(int):
.method public hidebysig instance void A(int32 a) cil managed { // Code size 13 (0xd) .maxstack 8 IL_0000: nop IL_0001: ldstr "int a" IL_0006: call void [System.Console]System.Console::WriteLine(string) IL_000b: nop IL_000c: ret } // end of method C::A
下麵的MSIL代碼是方法C.A(in int):
.method public hidebysig instance void A([in] int32& a) cil managed { .param [1] .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = ( 01 00 00 00 ) // Code size 13 (0xd) .maxstack 8 IL_0000: nop IL_0001: ldstr "in int a" IL_0006: call void [System.Console]System.Console::WriteLine(string) IL_000b: nop IL_000c: ret } // end of method C::A
你看到區別了嗎?int32&顯示它是一個引用參數;[in]是一個指示CLR如何處理此參數的附加元數據。
下麵的代碼是上面例子中Main方法的MSIL,它展示瞭如何調用這兩個C.A()方法的重載。
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint // Code size 35 (0x23) .maxstack 2 .locals init (class Demo.C V_0, int32 V_1) IL_0000: nop IL_0001: newobj instance void Demo.C::.ctor() IL_0006: stloc.0 IL_0007: ldloc.0 IL_0008: ldc.i4.1 IL_0009: callvirt instance void Demo.C::A(int32) IL_000e: nop IL_000f: ldc.i4.1 IL_0010: stloc.1 IL_0011: ldloc.0 IL_0012: ldloca.s V_1 IL_0014: callvirt instance void Demo.C::A(int32&) IL_0019: nop IL_001a: ldloc.0 IL_001b: ldloc.1 IL_001c: callvirt instance void Demo.C::A(int32) IL_0021: nop IL_0022: ret } // end of method Program::Main
在調用點,沒有其他元數據指示去調用C.A(in int)。
in參數和互操作
在許多地方,[In]特性被用於與本機方法簽名匹配以實現互操作性。讓我們以下麵的Windows API為例。
[DllImport("shell32")] public static extern int ShellAbout( [In] IntPtr handle, [In] string title, [In] string text, [In] IntPtr icon);
此方法對應的MSIL如下所示。
.method public hidebysig static pinvokeimpl("shell32" winapi) int32 ShellAbout([in] native int handle, [in] string title, [in] string text, [in] native int icon) cil managed preservesig
如果我們使用in修飾符來改變ShellAbout方法的簽名:
[DllImport("shell32")] public static extern int ShellAbout( in IntPtr handle, in string title, in string text, in IntPtr icon);
該方法生成的MSIL為:
.method public hidebysig static pinvokeimpl("shell32" winapi) int32 ShellAbout([in] native int& handle, [in] string& title, [in] string& cext, [in] native int& icon) cil managed preservesig { .param [1] .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = ( 01 00 00 00 ) .param [2] .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = ( 01 00 00 00 ) .param [3] .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = ( 01 00 00 00 ) .param [4] .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = ( 01 00 00 00 ) }
正如你所看到的,編譯器為每個in參數產生了使用[in]指令、引用數據類型和[IsReadOnly]特性的代碼。由於參數已從傳值更改為傳引用, P/Invoke可能會由於原始簽名不匹配而失敗。
結論
in參數是擴展C#語言的一個很棒的特性,它易於使用,並且是二進位相容的(不需要對CLR進行更改)。只讀引用參數在修改只讀參數時給出編譯時錯誤,有助於避免錯誤。這個特性可以與其他ref特性一起使用,比如引用返回和引用結構。
系列文章:
- [譯]C# 7系列,Part 1: Value Tuples 值元組
- [譯]C# 7系列,Part 2: Async Main 非同步Main方法
- [譯]C# 7系列,Part 3: Default Literals 預設文本表達式
- [譯]C# 7系列,Part 4: Discards 棄元
- [譯]C# 7系列,Part 5: private protected 訪問修飾符
- [譯]C# 7系列,Part 6: Read-only structs 只讀結構
- [譯]C# 7系列,Part 7: ref Returns ref返回結果