Variables, Types and Operators
How C# stores data: declaring variables, the built-in types, value types and reference types, var and const, conversions and casts, and the arithmetic, comparison and logical operators.
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,
stringandobjectare 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
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.