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
switchfor,foreach,while,do whilebreakandcontinue- Declaring and calling methods
- Parameters, return values, defaults
- Overloading
refandout
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");
| Loop | Right when |
|---|---|
for | The count is known, or you need the index |
foreach | You are visiting every item and do not need the index |
while | The condition may be false from the start |
do while | The 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
| Part | In the example |
|---|---|
| Access modifier | public |
static | Called on the class, no object needed |
| Return type | decimal |
| Name | CalculateBalance |
| Parameters | totalFees, paidAmount, discountAmount |
| Body | Between 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.");
}
| Keyword | Meaning |
|---|---|
out | The method must assign it before returning; the caller need not initialise it |
ref | The 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
| Message | Cause | Fix |
|---|---|---|
CS0161: not all code paths return a value | A branch has no return | Add a final return after the if chain |
CS0165: Use of unassigned local variable | Declared but never given a value on some path | Assign it where you declare it |
CS0136: A local named 'x' cannot be declared in this scope | Reused a name inside a nested block | Rename one of them |
CS0111: already defines a member called 'FindStudent' with the same parameter types | Two overloads differing only by return type | Overloads must differ in parameters |
System.InvalidOperationException: Collection was modified | Removed an item inside foreach | Loop backwards with for, or build a second list |
An absent student shows as Fail | The absent check runs after the marks check | Put 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
ifbodies - The absent check placed after the marks comparison
- A general condition before a specific one
- No
defaultin aswitch - Modifying a collection inside
foreach forwith<=onCount, running one past the end- Methods that do several things
- More than four parameters
- Expression-bodied methods while still learning
- Returning
nullwithout documenting that the caller must check
Practice
- Write
GetGrade(int marks)with the full grade chain, using explicit braces. - Write the grading chain with the absent check last. Test it with an absent student and record the result.
- Fix the order and test again.
- Write a braceless
ifwith two indented statements and observe that the second always runs. - Convert a five-branch
ifchain onStaffDesignationinto aswitchwith a grouped case and adefault. - Print a numbered student list with
for, then the same list withforeach. - Write a menu loop with
do whilethat exits on3. - Remove transferred students inside a
foreachand read the exception. Fix it by looping backwards. - Use
continueto skip absent students while totalling marks, andbreakto stop on invalid data. - Write
CalculateBalanceand call it withTotalFees = 15000,PaidAmount = 12000,DiscountAmount = 500. - Add an optional
includeClassparameter to a formatting method and call it with named arguments. - Overload
FindStudentforintid andstringroll number. - Try overloading by return type alone and read the compiler error.
- Write
TryFindStudentwith anoutparameter, following theTryconvention. - 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,whileanddo while - Avoid modifying a collection while looping over it
- Write a method that does one thing, with a name that says what
- Use
outto return a value and a success flag together
Review questions
- Why does an absent student get marked Fail when the absent check comes second?
- When is
foreachthe wrong loop? - What happens if you remove an item from a list inside a
foreachover it? - What must differ between two overloads, and what is not enough?