C# is a statically typed language built around classes. That means the compiler checks your types before the program runs, catching a large class of mistakes early. This page covers the syntax you need to read and write ordinary C# — types, control flow, methods, collections and a first class — with the compiler errors each section tends to produce.
A complete program#
using System;
class Program
{
static void Main()
{
Console.WriteLine("Hello");
}
}
Modern C# lets you skip most of that with top-level statements:
Console.WriteLine("Hello");
Both compile to the same thing. The short form is fine for small programs and examples; the full form is what you will see inside real projects.
Variables and types#
int count = 42;
double price = 12.99;
decimal money = 19.99m; // use decimal for currency
bool isReady = true;
char letter = 'A'; // single quotes
string name = "Ana"; // double quotes
var total = count * 2; // compiler infers int
const int MaxUsers = 100; // cannot change
var does not mean “any type” — the type is still fixed at compile time, just inferred from the right-hand side. Once inferred it cannot change.
Use decimal for money. double is binary floating point and cannot represent 0.1 exactly, which produces rounding errors in totals.
Nullable types#
int? maybeCount = null; // int cannot be null; int? can
string? maybeName = null;
int actual = maybeCount ?? 0; // default if null
int length = maybeName?.Length ?? 0; // safe navigation
Value types such as int and bool cannot be null without the question mark. With nullable reference types enabled, the same applies to string and other classes, and the compiler warns when you might dereference a null.
Strings#
string name = "Ana";
int age = 34;
string message = $"Hello {name}, you are {age}"; // interpolation
string joined = string.Join(", ", new[] { "a", "b" });
Console.WriteLine(name.Length); // 3
Console.WriteLine(name.ToUpper()); // ANA
Console.WriteLine(name.Contains("n")); // True
Console.WriteLine(" x ".Trim()); // "x"
Console.WriteLine(string.IsNullOrWhiteSpace("")); // True
The $ prefix enables interpolation. Prefer it to concatenation — it reads better and avoids creating intermediate strings.
Conditions#
if (score >= 80)
{
grade = "A";
}
else if (score >= 70)
{
grade = "B";
}
else
{
grade = "F";
}
string status = age >= 18 ? "adult" : "minor";
string label = command switch
{
"start" => "Running",
"stop" => "Stopped",
_ => "Unknown",
};
The switch expression returns a value, so it can be assigned directly. The underscore is the catch-all, and the compiler will warn if you leave it out and a case is unhandled.
Loops#
for (int i = 0; i < 5; i++)
{
Console.WriteLine(i);
}
foreach (string fruit in fruits)
{
Console.WriteLine(fruit);
}
int n = 0;
while (n < 3)
{
n++;
}
do
{
n--;
} while (n > 0);
foreach is the one you will use most. Use for when you need the index or are modifying the collection by position.
Methods#
static int Add(int a, int b)
{
return a + b;
}
static double Average(params int[] numbers)
{
if (numbers.Length == 0) return 0;
return numbers.Sum() / (double)numbers.Length;
}
static string Greet(string name, string greeting = "Hello")
{
return $"{greeting}, {name}!";
}
// Expression-bodied, for one-liners
static int Square(int n) => n * n;
The cast to double in Average matters. Dividing two integers in C# gives an integer, so 7 / 2 is 3, not 3.5. Casting one side forces floating-point division.
Collections#
// Array - fixed size
int[] scores = { 88, 72, 95 };
Console.WriteLine(scores[0]);
Console.WriteLine(scores.Length);
// List - grows
var names = new List<string> { "Ana", "Sam" };
names.Add("Kim");
names.Remove("Sam");
Console.WriteLine(names.Count);
Console.WriteLine(names.Contains("Ana"));
// Dictionary - key and value
var ages = new Dictionary<string, int> { ["Ana"] = 34 };
ages["Sam"] = 28;
if (ages.TryGetValue("Ana", out int age))
{
Console.WriteLine(age);
}
TryGetValue is the idiomatic lookup. Using ages["Kim"] on a missing key throws KeyNotFoundException.
Note Length for arrays, Count for lists and dictionaries. That inconsistency catches everyone at least once.
LINQ#
using System.Linq;
var passing = scores.Where(s => s >= 70).ToList();
var doubled = scores.Select(s => s * 2).ToList();
var best = scores.Max();
var total = scores.Sum();
var sorted = scores.OrderByDescending(s => s).ToList();
var anyLow = scores.Any(s => s < 50);
These replace a great deal of loop-writing. They are lazy — nothing runs until you call ToList() or enumerate the result.
A first class#
public class Person
{
public string Name { get; set; }
public int Age { get; private set; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
public bool IsAdult => Age >= 18;
public void HaveBirthday()
{
Age++;
}
public override string ToString() => $"{Name} ({Age})";
}
var ana = new Person("Ana", 34);
ana.HaveBirthday();
Console.WriteLine(ana); // Ana (35)
Console.WriteLine(ana.IsAdult); // True
{ get; set; } is an auto-property — the compiler creates the hidden field for you. private set means the value can only be changed from inside the class, which is how HaveBirthday can increment it while outside code cannot.
Errors#
try
{
int value = int.Parse(input);
}
catch (FormatException)
{
Console.WriteLine("That was not a number.");
}
finally
{
Console.WriteLine("Done either way.");
}
// Often better: no exception at all
if (int.TryParse(input, out int parsed))
{
Console.WriteLine(parsed);
}
The TryParse pattern appears all over C#. Prefer it when failure is expected rather than exceptional — user input, for instance.
Questions people ask#
What is the difference between C# and .NET?
C# is the language. .NET is the runtime and library it runs on. You write C#, the compiler produces intermediate code, and .NET executes it — on Windows, macOS or Linux.
Should I use var everywhere?
Use it when the type is obvious from the right-hand side, and write the type out when it is not. Consistency within a codebase matters more than which convention you pick.
Why does 7 / 2 give 3?
Both operands are integers, so integer division is used and the remainder is discarded. Cast one to double, or write 7.0 / 2.
What is the difference between a class and a struct?
Classes are reference types — assigning one copies a reference. Structs are value types — assigning one copies the data. Use classes for almost everything; structs for small, immutable values.
Where to go next#
- C# advanced language features — LINQ, generics, async and records.
- What is object-oriented programming? — the model C# is built on.
- Which language should I learn? — where C# fits.