Three parameter modifiers change how an argument is passed, all working by passing a reference to the caller's storage location rather than a copy:
// ref — caller must initialize the variable first; method can read and write it
void Double(ref int x) { x *= 2; }
int a = 5;
Double(ref a);
Console.WriteLine(a); // 10
// out — caller does NOT need to initialize it first; method MUST assign it before returning
bool TryParse(string input, out int result)
{
if (int.TryParse(input, out result)) return true;
result = 0;
return false;
}
if (TryParse("42", out int value)) Console.WriteLine(value); // 42
// in — like ref, but read-only inside the method; used to avoid copying a large struct
// argument without allowing the callee to mutate the caller's variable
void PrintLength(in Vector2 v) { Console.WriteLine(v.Length()); /* v.X = 1; would be a compile error */ }
out is the standard pattern for a method that needs to return more than one logical value (TryParse-style APIs throughout the .NET base class library follow this exact convention: a bool success result plus an out parameter for the actual value, avoiding an exception for the extremely common "this input might be invalid" case). in exists purely as a performance tool for large-struct parameters — it gets the "no copy" benefit of ref while still preventing the callee from mutating the caller's data, since the compiler enforces read-only access inside the method body.