Skip to main content
Published / updated

Control Flow and Methods

Before you start

You need: variables, types and operators (Article 02).

Time: about 50 minutes, plus the practice.

Learning objective

Write conditions and loops that handle every case correctly, and organise logic into methods that do one thing.

Topics

  • if, else if, else
  • Condition order, and why it decides correctness
  • switch
  • for, foreach, while, do while
  • break and continue
  • Declaring and calling methods
  • Parameters, return values, defaults
  • Overloading
  • ref and out

Conditions

public static string GetGrade(int marks)
{
if (marks >= 90)
{
return "A+";
}

if (marks >= 80)
{
return "A";
}

if (marks >= 70)
{
return "B";
}

if (marks >= 35)
{
return "Pass";
}

return "Fail";
}

Always use full braces. C# allows a braceless single statement, and it is the source of a classic defect:

// Do not write this
if (marks >= 35)
Console.WriteLine("Pass");
Console.WriteLine("Certificate issued"); // runs always — not part of the if

The indentation says one thing and the compiler does another. Braces cost two characters and remove the entire class of problem.

Condition order decides correctness

The order of conditions is not a style choice. It changes the answer.

// WRONG — an absent student is reported as Fail
public static string GetResult(ExamResult result, Subject subject)
{
if (result.MarksObtained >= subject.PassingMarks)
{
return "Pass";
}

if (result.IsAbsent)
{
return "Absent";
}

return "Fail";
}

An absent student has MarksObtained of null. Any comparison with null is false, so the first check fails, execution falls through to return "Fail", and the IsAbsent check is never reached.

Nothing throws. Nothing is logged. The report card says Fail for a student who was ill.

// CORRECT — the absent check comes first
public static string GetResult(ExamResult result, Subject subject)
{
if (result.IsAbsent)
{
return "Absent";
}

if (result.MarksObtained >= subject.PassingMarks)
{
return "Pass";
}

return "Fail";
}

Rule for the whole track: in any grading chain, the absent check comes first. This exact bug reappears in the SQL, API and AI tracks, in different languages, with the same silence.

More generally: put the most specific condition first. A general condition placed earlier swallows the cases the later ones were meant to catch.

switch

public static string GetDesignationDescription(StaffDesignation designation)
{
switch (designation)
{
case StaffDesignation.Clerk:
return "Office clerk";

case StaffDesignation.Librarian:
return "Library in-charge";

case StaffDesignation.LabAssistant:
return "Laboratory assistant";

case StaffDesignation.Accountant:
return "Accounts department";

case StaffDesignation.Peon:
case StaffDesignation.Security:
return "Support staff";

default:
return "Unknown designation";
}
}

switch is clearer than a long if chain when you are comparing one value against a fixed set. Grouped cases — Peon and Security above — express "these two behave the same" directly.

Always include a default. Without it, a new enum member added later returns nothing and the compiler may not warn you.

Each case must end with return, break or throw — C# does not fall through from one case to the next the way C does.

Loops

// for — you need the index or a known count
for (int i = 0; i < students.Count; i++)
{
Console.WriteLine($"{i + 1}. {students[i].Name}");
}

// foreach — you need each item and not its position
foreach (Student student in students)
{
Console.WriteLine(student.Name);
}

// while — repeat until a condition changes
int attempts = 0;

while (attempts < 3)
{
Console.Write("Enter roll number: ");
string input = Console.ReadLine();

if (!string.IsNullOrWhiteSpace(input))
{
break;
}

attempts = attempts + 1;
}

// do while — body must run at least once
string choice;

do
{
Console.WriteLine("1. List students 2. Add student 3. Exit");
choice = Console.ReadLine();
}
while (choice != "3");
LoopRight when
forThe count is known, or you need the index
foreachYou are visiting every item and do not need the index
whileThe condition may be false from the start
do whileThe body must run at least once — a menu

Prefer foreach when you do not need the index. It cannot run off the end of the collection, which removes the most common loop bug.

Never modify a collection while iterating it with foreach:

// Throws InvalidOperationException
foreach (Student student in students)
{
if (student.Status == StudentStatus.Transferred)
{
students.Remove(student);
}
}

Build a second list, or loop backwards with for:

for (int i = students.Count - 1; i >= 0; i--)
{
if (students[i].Status == StudentStatus.Transferred)
{
students.RemoveAt(i);
}
}

Backwards works because removing an item does not shift anything you have yet to visit.

foreach (ExamResult result in results)
{
if (result.IsAbsent)
{
continue; // skip this one, carry on
}

if (result.MarksObtained > 100)
{
Console.WriteLine("Invalid data found. Stopping.");
break; // leave the loop entirely
}

total = total + result.MarksObtained.Value;
}

continue skips the rest of this iteration. break exits the loop.

Methods

public static decimal CalculateBalance(decimal totalFees, decimal paidAmount, decimal discountAmount)
{
decimal payable = totalFees - discountAmount;
decimal balance = payable - paidAmount;

return balance;
}
decimal balance = CalculateBalance(15000.00m, 12000.00m, 500.00m);
Console.WriteLine($"Balance: {balance:N2}"); // Balance: 2,500.00
PartIn the example
Access modifierpublic
staticCalled on the class, no object needed
Return typedecimal
NameCalculateBalance
ParameterstotalFees, paidAmount, discountAmount
BodyBetween the braces

A method should do one thing and its name should say what. CalculateBalance calculates a balance. It does not also print, save, or send an email — because the moment it does, it cannot be reused or tested.

void means the method returns nothing:

public static void PrintStudent(Student student)
{
Console.WriteLine($"{student.RollNumber}{student.Name} ({student.ClassName}-{student.Section})");
}

Never use expression-bodied methods in code you are learning from:

// Compact, and harder to read while learning
public static decimal GetPayable(decimal fees, decimal discount) => fees - discount;

// Write this instead
public static decimal GetPayable(decimal fees, decimal discount)
{
return fees - discount;
}

The => form is common in real codebases and you will read it. Write the explicit form until the structure is second nature.

Parameters and defaults

public static string FormatStudent(string name, string rollNumber, bool includeClass = false, string className = "")
{
if (includeClass)
{
return $"{name} ({rollNumber}) — {className}";
}

return $"{name} ({rollNumber})";
}
string basic = FormatStudent("Ravi Kumar", "NCA-2024-0012");
string full = FormatStudent("Ravi Kumar", "NCA-2024-0012", true, "10th");
string named = FormatStudent("Priya Sharma", "NCA-2024-0013", includeClass: true, className: "9th");

Optional parameters must come last, and named arguments make a call with several parameters readable.

Three or four parameters is usually the limit. Beyond that, the parameters probably belong together in a class — which is Article 04.

Overloading

public static Student FindStudent(List<Student> students, int studentId)
{
foreach (Student student in students)
{
if (student.Id == studentId)
{
return student;
}
}

return null;
}

public static Student FindStudent(List<Student> students, string rollNumber)
{
foreach (Student student in students)
{
if (student.RollNumber == rollNumber)
{
return student;
}
}

return null;
}

Same name, different parameter types. The compiler picks the match. Overloads must differ in parameter types or count — a different return type alone is not enough.

Use overloading when the methods genuinely do the same thing to different inputs. FindStudent by id and by roll number qualifies; Save that sometimes deletes does not.

ref and out

public static bool TryFindStudent(List<Student> students, string rollNumber, out Student found)
{
found = null;

foreach (Student student in students)
{
if (student.RollNumber == rollNumber)
{
found = student;
return true;
}
}

return false;
}
Student student;

if (TryFindStudent(students, "NCA-2024-0012", out student))
{
Console.WriteLine(student.Name);
}
else
{
Console.WriteLine("Student not found.");
}
KeywordMeaning
outThe method must assign it before returning; the caller need not initialise it
refThe caller must initialise it; the method may read and change it

out is the pattern behind int.TryParse — a bool for success, the value through out. Follow the Try naming convention when you write one.

Use them sparingly. A method needing several ref parameters is usually a method that should return an object instead.

Errors you will hit

MessageCauseFix
CS0161: not all code paths return a valueA branch has no returnAdd a final return after the if chain
CS0165: Use of unassigned local variableDeclared but never given a value on some pathAssign it where you declare it
CS0136: A local named 'x' cannot be declared in this scopeReused a name inside a nested blockRename one of them
CS0111: already defines a member called 'FindStudent' with the same parameter typesTwo overloads differing only by return typeOverloads must differ in parameters
System.InvalidOperationException: Collection was modifiedRemoved an item inside foreachLoop backwards with for, or build a second list
An absent student shows as FailThe absent check runs after the marks checkPut the absent check first

The last row produces no error at all. It compiles, runs, and prints the wrong result on a report card.

Common mistakes

  • Braceless if bodies
  • The absent check placed after the marks comparison
  • A general condition before a specific one
  • No default in a switch
  • Modifying a collection inside foreach
  • for with <= on Count, running one past the end
  • Methods that do several things
  • More than four parameters
  • Expression-bodied methods while still learning
  • Returning null without documenting that the caller must check

Practice

  1. Write GetGrade(int marks) with the full grade chain, using explicit braces.
  2. Write the grading chain with the absent check last. Test it with an absent student and record the result.
  3. Fix the order and test again.
  4. Write a braceless if with two indented statements and observe that the second always runs.
  5. Convert a five-branch if chain on StaffDesignation into a switch with a grouped case and a default.
  6. Print a numbered student list with for, then the same list with foreach.
  7. Write a menu loop with do while that exits on 3.
  8. Remove transferred students inside a foreach and read the exception. Fix it by looping backwards.
  9. Use continue to skip absent students while totalling marks, and break to stop on invalid data.
  10. Write CalculateBalance and call it with TotalFees = 15000, PaidAmount = 12000, DiscountAmount = 500.
  11. Add an optional includeClass parameter to a formatting method and call it with named arguments.
  12. Overload FindStudent for int id and string roll number.
  13. Try overloading by return type alone and read the compiler error.
  14. Write TryFindStudent with an out parameter, following the Try convention.
  15. Take a 40-line method that reads input, validates, calculates and prints, and split it into four methods.

Exercises 2 and 3 together are the most important pair in this article.

You can now

  • Write conditions in an order that is correct, not just plausible
  • Explain why the absent check must come first
  • Choose between if, switch, for, foreach, while and do while
  • Avoid modifying a collection while looping over it
  • Write a method that does one thing, with a name that says what
  • Use out to return a value and a success flag together

Review questions

  1. Why does an absent student get marked Fail when the absent check comes second?
  2. When is foreach the wrong loop?
  3. What happens if you remove an item from a list inside a foreach over it?
  4. What must differ between two overloads, and what is not enough?

Next: Classes and practical OOP