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

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.

image

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.

image

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

in parameter is a great feature that extends the C# language, it is easy to use and it is binary compatible (no CLR change required.) Read-only ref parameters help to avoid mistakes by giving a compile time error when modifying the read only parameter. This feature can be used with other ref features such as ref returns and ref structs.