Skip to main content

37. Ref, Out, and In Keywords in C#

Level: Intermediate to Advanced
Goal: Pass variables by reference for efficiency and returning multiple values from methods.

Normally, when you pass a variable to a method, C# makes a copy:

int age = 20;
IncrementAge(age); // Method gets a copy
// age is still 20

With ref, the method can modify the original:

int age = 20;
IncrementAge(ref age); // Method modifies original
// age is now 21
ℹ️ What You'll Learn
  • Pass by value vs pass by reference
  • ref keyword - modify original variable
  • out keyword - return multiple values
  • in keyword - read-only reference (performance)
  • When to use each keyword
  • Common patterns (TryParse, Swap, Multiple returns)
  • Performance implications with large structs
  • Interview questions on parameter modifiers

Parameter Modifiers Reference

KeywordPassesMust InitializeMethod Must AssignCan ReadCan WriteUse CaseExample
(none)Value (copy)N/AN/A? Yes? NoSimple parametervoid Add(int x)
refReference? Yes? No? Yes? YesModify original valuevoid Swap(ref int a, ref int b)
outReference? No? Yes? Only after assign? YesReturn multiple valuesbool TryParse(string s, out int result)
inReference (read-only)? Yes? No? Yes? NoPerformance, large structsdouble Calculate(in LargeStruct s)

Quick Definitions

  • Pass by value - Copy value passed to method (original unchanged)
  • Pass by reference - Pass address, method can modify original
  • ref keyword - Pass by reference, read AND write
  • out keyword - Pass by reference, method MUST assign value
  • in keyword - Pass by reference, read-only (performance)
  • Parameter modifier - ref, out, in keywords
  • Return value - What method returns with return statement
  • Multiple returns - Use out to return multiple values
  • Struct - Value type (unlike class, which is reference type)
  • Performance - Avoid copying large objects with in modifier

Default: Pass by Value

// Without any modifier - value is COPIED
static void AddBonus(double salary)
{
salary += 5000; // changes COPY only
}

double teacherSalary = 50000;
AddBonus(teacherSalary);
Console.WriteLine(teacherSalary); // still 50000 - unchanged

ref - Pass by Reference (Read + Write)

ref passes the original variable - changes inside method affect the caller.

static void AddBonus(ref double salary)
{
salary += 5000; // changes ORIGINAL
}

double teacherSalary = 50000;
AddBonus(ref teacherSalary);
Console.WriteLine(teacherSalary); // 55000 ?

// Rules:
// ? Variable MUST be initialized before passing
// ? Caller must use ref keyword too
// ? Method can READ and WRITE the value

School Management - Swap Marks

static void SwapMarks(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}

int english = 85, maths = 91;
Console.WriteLine($"Before: English={english}, Maths={maths}");
SwapMarks(ref english, ref maths);
Console.WriteLine($"After : English={english}, Maths={maths}");
// After: English=91, Maths=85

// Real use - update student attendance count
static void MarkPresent(ref int presentCount, ref int totalDays)
{
presentCount++;
totalDays++;
}

int present = 20, total = 25;
MarkPresent(ref present, ref total);
Console.WriteLine($"Attendance: {present}/{total}");

out - Output Parameter (Write Only)

out passes a variable that the method MUST assign. Used to return multiple values.

static void CalculateResult(
int[] marks,
out double percentage,
out string grade,
out bool passed)
{
percentage = marks.Average();

if (percentage >= 90)
{
grade = "A+";
}
else if (percentage >= 80)
{
grade = "A";
}
else if (percentage >= 70)
{
grade = "B";
}
else if (percentage >= 60)
{
grade = "C";
}
else if (percentage >= 35)
{
grade = "D";
}
else
{
grade = "F";
}

passed = percentage >= 35;
// MUST assign all out params - compiler error otherwise
}

// Usage
int[] studentMarks = { 82, 91, 78, 85, 88 };

CalculateResult(studentMarks, out double pct, out string grade, out bool passed);
Console.WriteLine($"Percentage: {pct:F1}% | Grade: {grade} | Passed: {passed}");

// Inline out variable (C# 7+) - no pre-declaration needed
if (int.TryParse("85", out int marks))
Console.WriteLine($"Valid marks: {marks}");

// Discard out param you don't need with _
CalculateResult(studentMarks, out double percentage, out _, out bool isPass);

Rules: ref vs out

refout
Must initialize before callYesNo
Method must assignNot requiredYes, must
Can read inside methodYesOnly after assign
Use caseModify existing valueReturn multiple values

in - Read-Only Reference (C# 7.2+)

in passes by reference but is read-only inside the method. Used for performance with large structs.

// Large struct - copying is expensive
public struct ExamResult
{
public string StudentName;
public int EnglishMarks;
public int MathsMarks;
public int ScienceMarks;
public int SocialMarks;
public int HindiMarks;
// ... more fields
}

// Without in - struct COPIED on every call (expensive)
static double GetPercentage(ExamResult result)
{
return (result.EnglishMarks + result.MathsMarks +
result.ScienceMarks + result.SocialMarks +
result.HindiMarks) / 500.0 * 100;
}

// With in - passed by reference, no copy, read-only
static double GetPercentageFast(in ExamResult result)
{
// result.EnglishMarks = 100; <- compile error - read only
return (result.EnglishMarks + result.MathsMarks +
result.ScienceMarks + result.SocialMarks +
result.HindiMarks) / 500.0 * 100;
}

var exam = new ExamResult { StudentName="Sahasra", EnglishMarks=82,
MathsMarks=91, ScienceMarks=78, SocialMarks=85, HindiMarks=88 };

double pct = GetPercentageFast(in exam);
Console.WriteLine($"{exam.StudentName}: {pct:F1}%");

Summary Comparison

💻 Try It — Console App
💡 Paste into Program.cs and press F5⌥ GitHub
// ref - modify existing value
static void ApplyLateFeePenalty(ref decimal fees) => fees *= 1.05m;

// out - return multiple computed values
static bool TryEnroll(string name, out string rollNumber, out string message)
{
if (string.IsNullOrWhiteSpace(name))
{
rollNumber = "";
message = "Name is required.";
return false;
}
rollNumber = $"NCA-{DateTime.Now.Year}-{new Random().Next(1000, 9999)}";
message = $"{name} enrolled successfully.";
return true;
}

// in - read-only reference for large structs (performance)
static void PrintReport(in ExamResult result) { /* read only */ }

// Usage
decimal fee = 50000m;
ApplyLateFeePenalty(ref fee);
Console.WriteLine($"With penalty: {fee:C}");

if (TryEnroll("Sahasra Kumar", out string roll, out string msg))
Console.WriteLine($"Roll: {roll}");
else
Console.WriteLine($"Error: {msg}");

Common Mistakes

? Forgetting ref in caller when method expects it:

static void AddBonus(ref double salary) => salary += 5000;

double pay = 50000;
AddBonus(pay); // Error - forgot ref keyword

? Use ref both in method and caller:

AddBonus(ref pay); // ? Correct

? Using out for parameter that method doesn't initialize:

static void Process(out string message)
{
// Forgot to assign message
Console.WriteLine("Processing");
// Error - must assign all out params
}

? Always assign out parameters before return:

static void Process(out string message)
{
message = "Processing complete"; // ? Assigned
}

? Using ref when out more appropriate:

// Method returns two values - use out, not ref
static void Calculate(int a, int b, ref int sum, ref int product)
{
sum = a + b;
product = a * b;
}

? Use out for computed return values:

static void Calculate(int a, int b, out int sum, out int product)
{
sum = a + b;
product = a * b;
}

? Modifying in parameter (compiler catches this):

static void Process(in ExamResult result)
{
result.Marks = 100; // Error - in is read-only
}

? Treat in as read-only:

static void Process(in ExamResult result)
{
Console.WriteLine(result.Marks); // ? Reading OK
}

? Not using in for large structs (copies on each call):

// Large struct, called frequently - should use in
static bool Validate(LargeConfiguration config) // Copied each call!
{
return config.IsValid;
}

? Use in for large readonly structs:

static bool Validate(in LargeConfiguration config) // No copy, passed by ref
{
return config.IsValid;
}

When You'll Use This in SMS

SMS uses ref and out for specific scenarios:

// out - TryParse pattern (return + output)
public bool TryEnrollStudent(string rollNumber, out Student student)
{
student = null;

var found = _studentRepo.FindByRoll(rollNumber);
if (found == null)
return false; // student still null here

student = found; // Must assign before success
return true;
}

// Usage
if (TryEnrollStudent("NCA-2024-0001", out var student))
Console.WriteLine($"Enrolled: {student.Name}");

// ref - swap values efficiently
public static void SwapStudentMarks(ref double marks1, ref double marks2)
{
double temp = marks1;
marks1 = marks2;
marks2 = temp;
}

// in - pass large result struct without copying
public decimal CalculateFinalMarks(in StudentResult result)
{
return result.InternalMarks + result.ExternalMarks;
// result not copied (performance win on large struct)
}

Real impact: out reduces boilerplate, ref enables swaps/modifications, in avoids struct copying overhead.


Try This Now

  1. Create TryGetStudentByRoll using out pattern
  2. Implement Swap method using ref on two variables
  3. Use in modifier on large struct parameter
  4. Call each method and observe behavior
  5. Measure performance difference with/without in

ℹ️ Video Tutorial

Parameter modifiers explained: ref vs out vs in, pass by value vs reference, TryParse pattern, when to use each. Video coming soon. Subscribe to NexCoding YouTube for updates.


Best Practices

  1. Use value parameters by default - Simpler, safer, less confusion
  2. Use ref when method needs to modify original - Swap, increment counters
  3. Use out for returning multiple values - Alternative to returning tuple
  4. Use in for large readonly structs - Avoid copying overhead
  5. Always check for null with ref/out - Null reference still possible (for classes)
  6. Document what ref/out do - Caller needs to know value changes
  7. Prefer return value over out - Clearer intent, works with LINQ
  8. Use out with TryPattern - TryParse, TryGetValue standard approach
  9. Don't use ref for performance - Only use in for large structs
  10. Test parameter modifications carefully - Easy to forget ref in tests
  11. Avoid ref chains - Don't pass ref parameter to another ref parameter
  12. Consider tuples as alternative to multiple out - (bool, int, string) vs three out params

🎯 Q1: What's the difference between ref and out?

ref: Original variable must be initialized before call. Method can READ and WRITE. Used to modify existing value.

out: Variable doesn't need to be initialized. Method MUST assign value. Used to return multiple values.

static void ModifyValue(ref int x) => x *= 2; // ref - modify existing
static bool TryGetValue(out int x)
{
x = 100; // out - assign return value
return true;
}

int num = 50;
ModifyValue(ref num); // num initialized first

int result;
TryGetValue(out result); // result doesn't need to be initialized

Rule: ref for modification, out for returning.

🎯 Q2: When should I use ref instead of returning a value?

Use ref when: modifying mutable object, swapping values, updating collection element in-place.

Prefer return when: method computes new value, more readable, works with LINQ.

// ? Appropriate for ref - modifying in-place
static void SwapValues(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}

// ? Don't use ref - return is clearer
static int AddBonus(int salary)
{
return salary + 5000; // Better than ref for computation
}

// ? Appropriate for ref - updating counter
static void IncrementCount(ref int count) => count++;

Use ref sparingly - return is clearer for most cases.

🎯 Q3: Why does int.TryParse use out instead of returning null?

Value types (int, double) can't be null in C# < 8. TryParse needs way to return:

  1. Success/failure (bool)
  2. Parsed value (int)

Can't return null for int. So use out:

int.TryParse("123", out int result); // out for parsed value, return for success

If it returned int? nullable:

int? result = int.TryParse("123"); // Can't call - what about failure case?

Better: Return bool (success), use out for value.

🎯 Q4: What is the in keyword and when do I use it?

in = pass by reference, read-only. Used for performance with large structs.

Without in - struct COPIED each call (expensive). With in - passed by reference, no copy.

public struct LargeData { } // Lots of fields

// Without in - copies struct on every call
static void Process(LargeData data) { } // Expensive!

// With in - passes reference, no copy, read-only
static void Process(in LargeData data) { } // Fast!

Only use in when: struct is large (100+ bytes), method doesn't modify it, called frequently.

🎯 Q5: Can you pass null to a ref parameter?

Yes, if parameter type allows null (reference type or nullable value type).

static void Process(ref int? x) => x = 100; // int? allows null

int? value = null;
Process(ref value); // Passing null - OK

But with non-nullable reference types warning:

static void Process(ref string name) => name = "New";

string? s = null;
Process(ref s); // WARNING - passing nullable to non-nullable ref

Be careful: ref doesn't prevent null, only adds reference passing.

🎯 Q6: What's the performance impact of ref, out, and in?
  • ref/out: No performance difference from regular parameters (both pass reference). Used for semantics (modify existing vs return new).
  • in: Performance benefit with large structs - avoids copy overhead.
// Large struct - 1000+ bytes
public struct Config { /* many fields */ }

// Without in - copies 1000+ bytes on each call
static void Setup(Config cfg) { } // Copies Config

// With in - passes reference, no copy (8-16 bytes pointer)
static void Setup(in Config cfg) { } // Just passes pointer

Benchmark: in can be 10-100x faster for large structs. Don't use ref/out for performance - only in.


🤖Use AI to Learn Faster

Use ChatGPT, Claude, or Copilot to go deeper on C# ref, out, in parameter modifiers. Try these prompts:

  • "What is the difference between ref and out in C# with a real example?"
  • "When should I use ref vs returning a value from a method?"
  • "Why does int.TryParse use out instead of returning null?"
  • "Give me 3 practical scenarios where out parameters are better than returning a tuple"

💡 Tip: After reading this article, paste your own code into AI and ask "What could go wrong here and why?" — fastest way to find edge cases and deepen understanding.

Next Article

Static Keyword ->

nexcoding.in