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.
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.) - Cannot apply with an async method
- If you mark the
args
of theMain
method asin
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.
Comments
- Anonymous
November 12, 2018
Hallo,i have write a simple example for a struct and a class to test in readonly parameter. with the struct i can call the setString methode with not error or warning and nothing is happens. with class i can call set String and the class variable is changed in the the methode testClass and later in the Main. is this correct?using System;namespace test9{ public struct mystruct { private string _str; public mystruct( string s = "") { _str = s; } public void setString (string s) { _str = s; } public string getString () { return _str; } } public class myclass { private string _str; public myclass( string s = "") { _str = s; } public void setString (string s) { _str = s; } public string getString () { return _str; } } class Program { static public void testStruct (in mystruct s) { Console.WriteLine("testStruct before {0}",s.getString()); s.setString("Car"); Console.WriteLine("testStruct after {0}",s.getString()); } static public void testClass (in myclass s) { Console.WriteLine("testClass before {0}",s.getString()); s.setString("House"); Console.WriteLine("testClass after {0}",s.getString()); } static void Main(string[] args) { Console.WriteLine("----------------------- test struct ------------------------"); mystruct s = new mystruct(); s.setString("Tree"); Console.WriteLine("Main before {0}",s.getString()); testStruct(in s); Console.WriteLine("Main after {0}",s.getString()); Console.WriteLine("----------------------- test class ------------------------"); myclass t = new myclass(); t.setString("Bus"); Console.WriteLine("Main before {0}",t.getString()); testClass(in t); Console.WriteLine("Main after {0}",t.getString()); } }}- Anonymous
February 27, 2019
The compiler hints are limited to property assignments related to structs. Remember nothing stops you for updating structs internally but they will rely on copy semantics and will be a perf hit for large structs. - Anonymous
February 27, 2019
Sorry the compiler hints happen if you mark the struct as readonly. Then you can better understand immutable without copy semantic.
- Anonymous