C# 7 Series, Part 8: “in” Parameters

01/08/2018

5 minutes to read

In this article

C# 7 Series

Part 1: Value Tuples

Part 2: Async Main

Part 3: Default Literals

Part 4: Discards

Part 5: Private Protected

Part 6: Read-only structs

Part 7: Ref Returns

Part 8: (This post) “in” Parameters

Background

By default, method arguments are passed by value. That is, arguments are copied and passed into the method. Therefore, modification to the argument inside the method body does not affect the original value. In most of the cases, modifications are unnecessary.

Other programming languages, such as C++, has a const parameter or similar concept: This indicates that the parameter inside the method body is a constant that cannot be reassigned. It helps to avoid mistakes where you unintentionally reassign a method parameter in the body, and improves the performance by disallowing the unnecessary assignments.

C# 7.2 introduces the in parameter (aka. readonly ref parameter.) A method parameter with in modifier means that this parameter is by ref and read only within the method body.

in parameters

Let’s take the following method definition as an example.

public int Increment(int value) { // Reassignment is ok, "value" is passed by value. value = value + 1; return value; }

To make a readonly ref parameter, use the in modifier for a parameter.

public int Increment(in int value) { // Reassignment is not ok, "value" is passed by ref and read-only. int returnValue = value + 1; return returnValue; }

If you reassign value, the compiler will generate an error.

To call this method, use your normal way.

int v = 1; Console.WriteLine(Increment(v));

Because value is read-only, you cannot put value in the left side (I.e. LValue. ) Unary operators that does an assignment is also disallowed, such as ++ or – . However, you can still take the address of the value and modify using pointer operations.

Overload Resolutions

in is a modifier to a method parameter that indicates the ref kind of such parameter, it is considered as part of the method signature. That means you can have two method overloads that just differ by in modifier.

The following code example defines two method overloads, with just the ref kind different.

public class C { public void A(int a) { Console.WriteLine("int a"); } public void A(in int a) { Console.WriteLine("in int a"); } }

By default, the method call will resolve to use by value signature. To clear the ambiguity and explicitly call the by ref signature, put in before the actual argument when explicitly call the A(in int) method overload.

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) }

The program output is as following.

Restrictions

Since in parameters are read-only ref parameters, all ref parameter limitations apply.

Cannot apply with an iterator method (I.e. method that has yield statements.)

statements.) Cannot apply with an async method

If you mark the args of the Main method as in modifier, the method signature will become invalid for the entry point.

in parameter and the CLR

.NET already has a similar concept in CLR, so the in parameter feature does not require CLR changes.

Any in parameter will be compiled to MSIL with an additional [in] directive in the definition. To observe the compilation behavior, I use ILDAsm.exe to get the decompiled MSIL for the above example.

The following MSIL code is for method 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

The following MSIL code is for method 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

Do you see the difference? int32& shows it is a by ref parameter; [in] is an additional metadata that instructs the CLR how to deal with this parameter.

The below code is the MSIL for the Main method in the above example, it shows how to call these two C.A() method overloads.

.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

From the call site, there is no additional metadata to instruct to call C.A(in int) .

in parameter and the Interop

There are many places where the [In] attributes are used to match with the native method signature for the interoperability. Let’s take the following Windows API as an example.

[DllImport("shell32")] public static extern int ShellAbout( [In] IntPtr handle, [In] string title, [In] string text, [In] IntPtr icon);

The corresponding MSIL for this method is as below.

.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

If we change the ShellAbout signature to use in parameter:

[DllImport("shell32")] public static extern int ShellAbout( in IntPtr handle, in string title, in string text, in IntPtr icon);

The generated MSIL for this method is:

.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 ) }

As you can see, the compiler emits code for each in parameter with [in] directive, ref data type and also [IsReadOnly] attribute. Since the parameter has changed from by value to by ref, the P/Invoke may fail due to the mismatch of the original signature.

Conclusion