Declaring variables

A variable is a named place that holds a value. In C# you declare it with its type, then its name, and usually an initial value:

string customer = "Lucía Romero";
int meterReadings = 12;
double averageTemperature = 11.5;
bool hasSmartMeter = true;

meterReadings = meterReadings + 1;   // a new value of the same type
Console.WriteLine($"{customer}: {meterReadings} readings, {averageTemperature} °C, smart meter: {hasSmartMeter}");
Lucía Romero: 13 readings, 11.5 °C, smart meter: True

The type is fixed for the life of the variable. You can give meterReadings a new number, but never a string; the compiler refuses before the program runs. This is static typing:

int meterReadings = 12;
meterReadings = "thirteen";
example.cs(2,17): error CS0029: Cannot implicitly convert type 'string' to 'int'

A local variable must also have a value before you read it. C# does not fill in a default for you, so using a variable that may never have been assigned is a compiler error, not a hidden bug:

decimal tariff;
Console.WriteLine(tariff);
example.cs(2,19): error CS0165: Use of unassigned local variable 'tariff'

Names follow a few rules: letters, digits and underscores, not starting with a digit, and not a keyword such as int or class. Names are case-sensitive. By convention local variables use camelCase (meterReadings), while types, methods and properties use PascalCase (MeterReading, WriteLine).

The var keyword

Writing the type twice is often redundant. With the var keyword the compiler takes the type from the value on the right-hand side. This is type inference: the variable is still statically typed, you just do not write the type.

var region = "North";        // string
var customers = 1250;        // int
var outageHours = 2.5;       // double
var tariff = 0.27m;          // decimal (the m suffix)

Console.WriteLine(region.GetType().Name);
Console.WriteLine(customers.GetType().Name);
Console.WriteLine(outageHours.GetType().Name);
Console.WriteLine(tariff.GetType().Name);
String
Int32
Double
Decimal

Because the type is fixed at the declaration, var needs a value to infer it from, and the same rules apply afterward:

var customers = 1250;
customers = 1250.5;
example.cs(2,13): error CS0266: Cannot implicitly convert type 'double' to 'int'. An explicit conversion exists (are you missing a cast?)

Constants

A constant declared with const gets its value at compile time and can never change. Only the built-in types (numbers, bool, char, string) and enums can be const. Constants are usually written in PascalCase.

const decimal VatRate = 0.21m;
VatRate = 0.09m;
example.cs(2,1): error CS0131: The left-hand side of an assignment must be a variable, property or indexer

For values that are only known when the program runs but should not change afterward, C# has readonly fields, covered in Classes and Objects.

Built-in types

C# has keywords for the most used types. Each keyword is a short name for a type in the .NET base class library, the same type a PowerShell user writes as [int] or [System.Int32].

Keyword .NET type Holds Example literal
int System.Int32 Whole numbers, 32 bits (about ±2.1 billion) 1250
long System.Int64 Whole numbers, 64 bits 8_100_000_000L
byte System.Byte 0 to 255 255
double System.Double Floating-point, about 15–17 significant digits 21.5
float System.Single Floating-point, about 6–9 digits 21.5f
decimal System.Decimal Exact decimal fractions, 28–29 digits 0.27m
bool System.Boolean true or false true
char System.Char One UTF-16 code unit 'N'
string System.String Text (immutable) "North"
object System.Object Any value: the base of every type —

There are also short (16 bits), sbyte, and unsigned versions such as uint and ulong; you rarely need them outside file formats and network protocols. Every numeric type knows its limits:

Console.WriteLine($"int:  {int.MinValue} to {int.MaxValue}");
Console.WriteLine($"long: {long.MinValue} to {long.MaxValue}");
Console.WriteLine($"byte: {byte.MinValue} to {byte.MaxValue}");
Console.WriteLine($"Size of a long in bytes: {sizeof(long)}");
int:  -2147483648 to 2147483647
long: -9223372036854775808 to 9223372036854775807
byte: 0 to 255
Size of a long in bytes: 8

Numeric literals

A number written in code is an int if it has no decimal point and fits, and a double if it has one. A suffix picks another type: L for long, f for float, m for decimal. Underscores make long numbers readable, and 0x and 0b start hexadecimal and binary numbers.

long householdsServed = 2_400_000L;
int statusMask = 0b0000_0101;
int errorCode = 0x1F;
decimal standingCharge = 7.95m;
double voltage = 2.3e2;   // 2.3 × 10², scientific notation

Console.WriteLine(householdsServed);
Console.WriteLine(statusMask);
Console.WriteLine(errorCode);
Console.WriteLine(standingCharge);
Console.WriteLine(voltage);
2400000
5
31
7.95
230

decimal for money

double is a binary floating-point number: fast and fine for measurements, but it cannot store most decimal fractions exactly, so tiny errors appear. The decimal type stores numbers in base 10, so amounts such as 0.10 and 0.27 are exact. It is slower and has a smaller range, which does not matter for money. Use decimal for prices, tariffs and balances.

double d = 0.1 + 0.2;
decimal m = 0.1m + 0.2m;

Console.WriteLine(d);
Console.WriteLine(d == 0.3);
Console.WriteLine(m);
Console.WriteLine(m == 0.3m);

// Ten payments of EUR 0.10
double totalDouble = 0;
decimal totalDecimal = 0;
for (int i = 0; i < 10; i++)
{
    totalDouble += 0.10;
    totalDecimal += 0.10m;
}
Console.WriteLine($"double: {totalDouble}, decimal: {totalDecimal}");
0.30000000000000004
False
0.3
True
double: 0.9999999999999999, decimal: 1.00

Without the m suffix, 0.27 is a double, and C# refuses to mix the two types silently. decimal tariff = 0.27; is a compiler error (CS0664) that tells you to add the suffix.

bool and char

A bool is true or false and nothing else; numbers and strings are never treated as true or false. A char is a single character in single quotes, while a string uses double quotes. Each char also has a number, its UTF-16 code (see Character Encoding).

char tariffCode = 'N';
string region = "N";
bool isPeakHour = 18 >= 17 && 18 < 21;

Console.WriteLine(tariffCode);
Console.WriteLine((int)tariffCode);
Console.WriteLine(region.Length);
Console.WriteLine(isPeakHour);
N
78
1
True

Value types and reference types

Every C# type belongs to one of two families, and the difference decides what happens when you assign or pass a value.

  • A value type holds its data directly. Assigning it to another variable copies the data, so the two variables are independent. All numeric types, bool, char, enums and structs are value types.
  • A reference type lives as an object on the heap; the variable holds only a reference to it. Assigning copies the reference, so both variables point to the same object, and a change through one is visible through the other. Classes, arrays, string and object are reference types.

The example below defines a struct (a value type) and a class (a reference type) with the same field, then copies each and changes the copy:

ReadingValue a = new ReadingValue { Kwh = 342 };
ReadingValue b = a;          // copies the data
b.Kwh = 999;
Console.WriteLine($"struct: a = {a.Kwh}, b = {b.Kwh}");

ReadingObject c = new ReadingObject { Kwh = 342 };
ReadingObject d = c;         // copies the reference: same object
d.Kwh = 999;
Console.WriteLine($"class:  c = {c.Kwh}, d = {d.Kwh}");

int[] week = [11, 9, 12];
int[] sameWeek = week;       // arrays are reference types too
sameWeek[0] = 0;
Console.WriteLine($"array:  week[0] = {week[0]}");

struct ReadingValue { public int Kwh; }
class ReadingObject { public int Kwh; }
struct: a = 342, b = 999
class:  c = 999, d = 999
array:  week[0] = 0
Value types and reference types in memory. On the left, the local variables a and b each hold their own copy of a struct: a holds Kwh 342 and b holds Kwh 999. On the right, the local variables c and d each hold a reference, and both references point to the same object on the heap, which holds Kwh 999. Local variables Heap a Kwh = 342 b Kwh = 999 struct: two copies c ref d ref ReadingObject Kwh = 999 class: one object, two references
Changing b leaves a alone; changing the object through d is visible through c.

The usual description is that value types live on the stack and reference types on the heap. That is a useful first picture (see values and references), but the real rule is about copying: a value type stored inside an object, for example, lives on the heap together with that object. Structs and classes are covered in Records, Structs and Enums and Classes and Objects.

Default values and null

Every type has a default value: 0 for numbers, false for bool, and null (no object) for reference types. Fields and array elements start with it; default(T) gives it for any type T.

int[] hourlyReadings = new int[3];
string?[] names = new string?[2];

Console.WriteLine(string.Join(", ", hourlyReadings));
Console.WriteLine(default(decimal));
Console.WriteLine(default(bool));
Console.WriteLine(names[0] is null);

int? missingReading = null;   // a value type that can also be null
Console.WriteLine(missingReading.HasValue);
0, 0, 0
0
False
True
False

A value type cannot normally be null; adding ? (int?) makes it nullable, which is handy for "no reading yet". For reference types, the ? in string? tells the compiler that null is expected. Both are explained in Nullable Reference Types and Null Safety.

Conversions and casts

C# converts between numeric types automatically only when no information can be lost: an int fits in a long or a double, so these implicit conversions just happen. The other way, from double to int, could lose the decimals, so the compiler demands an explicit type cast:

double average = 11.4;
int rounded = average;
example.cs(2,15): error CS0266: Cannot implicitly convert type 'double' to 'int'. An explicit conversion exists (are you missing a cast?)

A cast is the target type in parentheses. Casting a floating-point number to an integer truncates: it drops the decimals without rounding. To round, use Math.Round, and note that its default is banker's rounding: a value exactly halfway goes to the nearest even number.

int customers = 1250;
long asLong = customers;       // implicit: always safe
double asDouble = customers;   // implicit: always safe

Console.WriteLine((int)3.99);
Console.WriteLine((int)-3.99);
Console.WriteLine(Math.Round(2.5));
Console.WriteLine(Math.Round(3.5));
Console.WriteLine(Math.Round(2.5, MidpointRounding.AwayFromZero));
Console.WriteLine(Convert.ToInt32(3.5));
Console.WriteLine(asLong + asDouble);
3
-3
2
4
3
4
2500

Text is never converted to a number by a cast: (int)"42" does not compile. Use int.Parse("42") or int.TryParse, as shown in converting input to numbers. The other way round, every value has a ToString() method.

Arithmetic operators

The arithmetic operators are +, -, *, / and % (remainder). The type of the result follows from the types of the operands; when they differ, the "smaller" one is converted first, so int times double gives a double.

Integer division

When both operands of / are integers, the result is an integer too. This is integer division: the fractional part is dropped, truncating toward zero. To get the exact quotient, make at least one operand a double or decimal.

int minutes = 135;
Console.WriteLine(minutes / 60);       // whole hours
Console.WriteLine(minutes % 60);       // remaining minutes
Console.WriteLine(minutes / 60.0);     // one double operand: exact

Console.WriteLine(7 / 2);
Console.WriteLine(-7 / 2);
Console.WriteLine(-7 % 2);

int readings = 7, days = 2;
Console.WriteLine((double)readings / days);
2
15
2.25
3
-3
-1
3.5

Dividing an integer by zero throws a DivideByZeroException, and so does dividing a decimal by zero. A double follows the floating-point rules instead and gives ∞ (printed as Infinity) or NaN, "not a number":

double kwh = 12.5;
double zero = 0;
Console.WriteLine(kwh / zero);
Console.WriteLine(zero / zero);
Console.WriteLine(double.IsNaN(zero / zero));
Infinity
NaN
True
int totalKwh = 342;
int households = 0;
Console.WriteLine("Average per household:");
Console.WriteLine(totalKwh / households);
Average per household:
Unhandled exception. System.DivideByZeroException: Attempted to divide by zero.

Increment and compound assignment

count++ adds one and count-- subtracts one. Compound operators such as +=, -=, *= and /= combine an operation with assignment. Prefix (++count) and postfix (count++) differ only in the value of the expression itself: before or after the change.

int openTickets = 4;
openTickets++;
openTickets += 3;
openTickets -= 2;
Console.WriteLine(openTickets);

int count = 10;
Console.WriteLine(count++);   // prints 10, then count becomes 11
Console.WriteLine(++count);   // count becomes 12, then prints 12

decimal balance = 100m;
balance *= 1.05m;
Console.WriteLine(balance);
6
10
12
105.00

Overflow and checked

Integer types have a fixed size. When a result is too large, it overflows: by default C# keeps only the lowest bits, so the value wraps around to a large negative number, without any error. In a checked context, created with the checked keyword (or for a whole project with a build setting), overflow throws an OverflowException instead.

int counter = int.MaxValue;
counter = counter + 1;
Console.WriteLine(counter);

long big = int.MaxValue;
big = big + 1;
Console.WriteLine(big);
-2147483648
2147483648
int counter = int.MaxValue;
Console.WriteLine("Adding one in a checked block...");
checked
{
    counter = counter + 1;
}
Console.WriteLine(counter);
Adding one in a checked block...
Unhandled exception. System.OverflowException: Arithmetic operation resulted in an overflow.

Silent wrap-around is fast, but a counter of energy units or money that suddenly turns negative is a nasty bug. Use long when values can grow large, and checked where an overflow must never pass unnoticed. The compiler already checks constant expressions: int x = int.MaxValue + 1; written with constants is a compiler error (CS0220). decimal arithmetic always throws on overflow.

Comparison and logical operators

Comparison operators (==, !=, <, >, <=, >=) produce a bool. The logical operators combine bool values: && (and), || (or) and ! (not). && and || use short-circuit evaluation: the right side is skipped when the left side already decides the result.

double temperature = -3.5;
int customersAffected = 1250;
bool isWeekend = false;

bool frostWarning = temperature < 0;
bool majorIncident = customersAffected >= 1000 || isWeekend;
bool sendSms = frostWarning && !isWeekend;

Console.WriteLine(frostWarning);
Console.WriteLine(majorIncident);
Console.WriteLine(sendSms);
Console.WriteLine(10 == 10.0);
Console.WriteLine("North" == "north");

string priority = customersAffected >= 1000 ? "P1" : "P3";
Console.WriteLine(priority);
True
True
True
True
False
P1

condition ? a : b is the conditional operator: it gives a when the condition is true and b otherwise. == on strings compares the text, case-sensitively; case-insensitive comparison is shown in Strings and String Interpolation.

Conditions must be real bool values. A number, a string or an object is never "truthy" in C#, so the familiar Python and JavaScript shortcut does not compile:

int openTickets = 3;
if (openTickets)
{
    Console.WriteLine("There is work to do.");
}
example.cs(2,5): error CS0029: Cannot implicitly convert type 'int' to 'bool'

Write the comparison you mean: if (openTickets > 0). Conditions and branching are covered in Control Flow and Pattern Matching.

Operator precedence

When an expression has several operators, higher-precedence operators are applied first, as in arithmetic at school. From high to low, the ones on this page:

Operators Meaning
x++ x-- Postfix increment and decrement
! -x ++x --x (T)x Not, negation, prefix increment, cast
* / % Multiplication, division, remainder
+ - Addition, subtraction
< > <= >= Relational comparison
== != Equality
&& Logical and
|| Logical or
?? then ?: Null-coalescing, then the conditional operator
= += -= *= /= Assignment
decimal tariff = 0.27m;
int kwh = 342;
decimal standingCharge = 7.95m;

Console.WriteLine(standingCharge + kwh * tariff);     // * first
Console.WriteLine((standingCharge + kwh) * tariff);   // parentheses first
Console.WriteLine(10 - 4 - 3);                         // left to right
Console.WriteLine((int)7.9 / 2);                       // cast before /
100.29
94.4865
3
3

Most binary operators work left to right; assignment works right to left, so a = b = 0 sets both. When in doubt, add parentheses: they cost nothing and make the intent clear to the next reader.