C#, like most programming languages, has a plethora of features and functionality that would take a long time to fully encompass. I’ve been using C# professionally for over 15 years now and am still learning new things about the language (including features that have been around for a while). A recent example would be the subject of this very blog post: Using the in
keyword as a parameter modifier.
The most I’ve done with parameter modifiers has been decorating TryParse()
’s second parameter with the out
keyword, to set a strongly-typed variable if the parsing succeeds.
// Integer variable "id" initialized with the value of 5
bool parsed = int.TryParse("5", out int id);
if (parsed)
{
_someRepository.GetById(id);
}
Beyond that I’ve not created anything with the out
keyword, nor have I ever used other parameter modifiers such as ref
or in
.
After discovering some of what in
can allow, however, I plan to fold it into my toolbox of useful features C# has to offer. Let’s explore what it does, how you can use it, and when one should probably avoid using it!
A Primer on Using the in
Parameter Modifier
To use the in
parameter modifier, simply decorate any parameters with the keyword in your method declaration:
public interface ILogger
{
// Interface and implementation must match all parameter modifiers.
public void LogMessage(in string message);
}
public class ConsoleLogger : ILogger
{
public void LogMessage(in string message) => Console.WriteLine(message);
}
You don’t have to explicitly declare your passed arguments as in
, but you can if you want:
public class Example
{
private readonly ILogger _logger = new ConsoleLogger();
public void DoStuff()
{
string myMessage = "Hello, World!";
_logger.LogMessage(myMessage);
_logger.LogMessage(in myMessage);
_logger.LogMessage("Some Inline String");
// Compiler error: "An expression cannot be used in this context
// because it may not be passed or returned by reference"
// _logger.LogMessage(in "Some Inline String");
}
}
There is an occasion where you must supply the in
keyword, however: When you overload your method to have a version with in
and a version without it:
public interface ILogger
{
public void LogMessage(in string message);
public void LogMessage(string message);
}
public class Example
{
private readonly ILogger _logger = new ConsoleLogger();
public void DoStuff()
{
string myMessage = "Hello, World!";
// Calls version with "in"
_logger.LogMessage(in myMessage);
// Inline strings won't be able to call the version with "in"
// _logger.LogMessage(in "Some Inline String");
// Calls version without "in"
_logger.LogMessage(myMessage);
_logger.LogMessage("Some Inline String");
}
}
Off the top of my head I can’t think of a reason to overload methods in this manner (but I’m sure there are use cases). But if you ever need to, the capability exists!
Now that we know the syntax, let’s dive into why we’d want to use the in
parameter modifying keyword.
Pass By Readonly Reference
When a method modifies a parameter with the in
keyword, the variable is passed in by readonly reference. Normally, value types (such as int
) have their value copied over to the parameter (“pass by value”), and reference types (such as string
) are passed by not-readonly reference. Using the in
modifier changes the behavior.
- Value types are now passed by reference (and are readonly), instead of “pass by value”
- Reference types are now passed by readonly reference
The “readonly” aspect is the game-changer when using in
. When using this modifier, attempting to change the input parameter in any way results in a compiler error!
public void LogMessage(in string message)
{
// Compiler error: "Cannot assign to variable 'message' or use it as the
// right hand side of a ref assignment because it is a readonly variable"
message = "Changing!";
}
For primitive value types, this doesn’t add much. Since value types are copied, it doesn’t matter what you do with the copied value within the method. The calling code will never know or care that the copied value was modified. We’ll get to a use case for using in
on value types later, but one takeaway here is that you don’t really ever need to use in
on primitive value types (such as int
), since there’s no real benefit to it.
Reference types, though? That’s another story! How many times have you worked in an application that had some method passing in a string
or some other reference type, only for the method to unintentionally modify the parameter by side effect? I know I’ve run into it on several occasions. Wouldn’t it be nice to guarantee that when you call a method, you know that it cannot modify your reference types?
This is what in
lets you do!
public void LogMessage(in string message) => Console.WriteLine(message);
// When using the above method, we know that our precious string cannot be changed!
string myPreciousString = GetPreciousString();
_logger.LogMessage(myPreciousString);
// Safely rely on the fact that myPreciousString is not changed.
We Can Fix Primary Constructors Now, Right?
You now have the power to declare parameters immutable. Perfect! One of my biggest gripes about C# 12’s new Primary Constructors feature is that the primary constructor values are not readonly. Lame! This means that this class
public class SomeClass
{
// _logger cannot be modified after construction
private readonly ILogger _logger;
public SomeClass(ILogger logger) => _logger = logger;
}
is not the same as this class (despite what IDEs may lead you to believe by suggesting this as a “code fix”):
public class SomeClass(ILogger logger)
{
// I can freely modify "logger" any time I want!
}
But we can solve that problem now, right? Just use our shiny new toy, in
! …Right?
public class SomeClass(in ILogger logger)
{
// Unfortunately, this doesn't work!
}
While you can declare your class with a primary constructor like this, you cannot then use the in
-decorated parameter. Attempting to do so results in a compiler error:
public class SomeClass(in ILogger logger)
{
public void DoStuff()
{
// Error: "Cannot use ref, out, or in primary constructor
// parameter 'logger' inside an instance member"
logger.LogMessage("Foo");
}
}
There does appear to be some buzz within Microsoft about allowing users to declare these primary constructor parameters readonly in the future. Unfortunately, we cannot use in
to solve this big issue with primary constructors presently.
But enough about what we can’t do. Let’s take a look at a use case for the in
modifier for value types: Efficiency!
Passing Large Value Objects By Reference
Earlier I noted that it’s not too useful to pass primitive value types (such as int
) using in
: There’s no benefit to your code, and it adds complexity for no real gain.
However, what if you have a value object that has a large footprint? A struct
, after all, is passed by value. What if you have code that is called rapidly, or otherwise must be super performant? It’d be convenient if we had a way to not copy around large value instances, while still maintaining the benefit of “pass by value” immutability.
in
lets us do just that!
public struct SomeStruct
{
public int Id { get; set; }
// Imagine a lot more things in here.
}
public interface ISomeProcess
{
void Process(in SomeStruct structToProcess);
}
public class Example(ISomeProcess someProcess)
{
public void DoSomethingEfficiently(SomeStruct myStruct)
{
// struct is passed by readonly reference, preventing
// us from making expensive copies in memory.
someProcess.Process(myStruct);
}
}
The struct is passed to Process()
by readonly reference. The implementation of Process()
cannot modify the given struct, but it also didn’t have to copy the entire struct in order to accomplish this. Using in
is saving us a lot of memory usage!
Summary
- The
in
parameter modifier keyword allows us to change how method parameters are passed. Value types and reference types alike both change from “pass by value” and “pass by reference”, respectively, into “pass by readonly reference.” - Use the
in
parameter modifier when you want to guarantee that a reference type will not be modified by the method being called. - Use the
in
parameter modifier when you have large value types that would be expensive to copy in memory, to improve efficiency. - Do not use the
in
parameter on primitive value types (such asint
). - We cannot use the
in
parameter to fix the “not readonly” issue present with primary constructors.