Variables and Data Types

A variable is a named storage location with a type fixed at compile time.

Concept

A variable is a named storage location with a type fixed at compile time. It must be definitely assigned before it is read.

Value types hold their data directly. Assigning one copies the value.

Range / precision true / false byte / sbyte Raw bytes, protocol data short / ushort Compact integers, CAN signals int / uint ±2.1 billion / 0..4.29 billion long / ulong Large counts, timestamps nint / nuint Native pointer size Interop, low-level code One UTF-16 code unit ~6–9 significant digits Graphics, sensors ~15–17 significant digits Default floating point, science 28–29 significant decimal digits Money, exact decimal values

structs and enums you declare are value types too.

Reference-type variables hold a reference to an object on the managed heap. Assigning copies the reference, so two variables can refer to the same object: string, arrays, classes, interfaces, delegates, object, records (record classes).

string is a reference type but immutable, so it behaves like a value in practice.

var asks the compiler to infer the type from the initializer. The type is still static: var count = 10; is exactly int count = 10;. var requires an initializer and cannot be null alone.

dynamic postpones member lookup to run time (the dynamic language runtime binder). It is useful for COM interop and dynamic languages, but mistakes become RuntimeBinderExceptions instead of compiler errors.

Constants and Readonly Fields When the value is fixed Compile time; inlined into callers Primitives, string, enum, null Changing it requires recompiling users readonly field In the declaration or constructor static readonly field Once, when the type is initialized Shared; good for "constant" objects

int age = 25;
double temperature = 27.5;
string name = "Alex";

Why It Matters

Choosing the right type prevents overflow, precision loss and performance problems — decimal for money, double for measurements, int for counts. Understanding value vs reference semantics prevents the classic "why did my other variable change?" bug.

Syntax

Type name = value;           // explicit type
var name = value;            // inferred type
const Type Name = constant;  // compile-time constant
readonly Type field;         // assign once (declaration or constructor)
dynamic name = value;        // late-bound

Example

// ---- Value types: sizes and ranges ----
Console.WriteLine($"{"type",-8}{"bytes",6}  {"min",-32}{"max"}");
Row("sbyte", sizeof(sbyte), sbyte.MinValue, sbyte.MaxValue);
Row("byte", sizeof(byte), byte.MinValue, byte.MaxValue);
Row("short", sizeof(short), short.MinValue, short.MaxValue);
Row("ushort", sizeof(ushort), ushort.MinValue, ushort.MaxValue);
Row("int", sizeof(int), int.MinValue, int.MaxValue);
Row("uint", sizeof(uint), uint.MinValue, uint.MaxValue);
Row("long", sizeof(long), long.MinValue, long.MaxValue);
Row("ulong", sizeof(ulong), ulong.MinValue, ulong.MaxValue);
Row("nint", nint.Size, nint.MinValue, nint.MaxValue);
Row("char", sizeof(char), (int)char.MinValue, (int)char.MaxValue);
Row("float", sizeof(float), float.MinValue, float.MaxValue);
Row("double", sizeof(double), double.MinValue, double.MaxValue);
Row("decimal", sizeof(decimal), decimal.MinValue, decimal.MaxValue);
Console.WriteLine($"bool is {sizeof(bool)} byte; true && false = {true && false}");

// ---- Floating point vs decimal ----
double d = 0.1 + 0.2;
decimal m = 0.1m + 0.2m;
Console.WriteLine($"double 0.1 + 0.2 = {d:R}  (== 0.3? {d == 0.3})");
Console.WriteLine($"decimal 0.1 + 0.2 = {m}  (== 0.3? {m == 0.
…

Expected Result

Key observations: double cannot represent 0.1 exactly, so 0.1 + 0.2 != 0.3, while decimal is exact for decimal fractions. Changing the struct copy did not affect the original, but changing through c2 changed the one shared object.

type     bytes  min                             max
sbyte        1  -128                            127
byte         1  0                               255
short        2  -32768                          32767
ushort       2  0                               65535
int          4  -2147483648                     2147483647
uint         4  0                               4294967295
long         8  -9223372036854775808            9223372036854775807
ulong        8  0                               18446744073709551615
nint         8  -9223372036854775808            9223372036854775807
char         2  0                               65535
float        4  -3.4028235E+38                  3.4028235E+38
double       8  -1.7976931348623157E+308        1.7976931348623157E+308
decimal     16  -79228162514264337593543950335  79228162514264337593543950335
bool is 1 byte; true && false = False
double 0.1 + 0.2 = 0.30000000000000004  (== 0.3? False)
decimal 0.1 + 0.2 = 0.3  (== 0.3? True)
struct: s1.X=1, s2.X=99   class: c1.X=99, c2.X=99
var types: Int32, String, Single
dynamic holds Int32: 15
dynamic holds String: HELLO
RuntimeBinderException: 'string' does not contain a definition for 'NoSuchMeth
…

Common Mistakes

Real compiler errors:

Using float/double for money — use decimal.

Comparing floating-point numbers with == — compare with a tolerance: Math.Abs(a - b) < 1e-9.

Assuming assigning a class instance copies it.

Overusing dynamic, losing compile-time checking.

Changing a public const in a library without recompiling its users (they keep the old inlined value).

int limit = 10;
const int Max = limit * 2;        // const needs a compile-time constant
var total;                        // var needs an initializer
Console.WriteLine(Max);
m05_err.cs(2,17): error CS0133: The expression being assigned to 'Max' must be constant
m05_err.cs(3,5): error CS0818: Implicitly-typed variables must be initialized
var sensor = new Sensor(3);
sensor.Id = 4;                    // readonly field assigned outside the constructor
Console.WriteLine(sensor.Id);

class Sensor
{
    public readonly int Id;
    public Sensor(int id) => Id = id;
}
m05_err2.cs(2,1): error CS0191: A readonly field cannot be assigned to (except in a constructor or init-only setter of the type in which the field is defined or a variable initializer)

Best Practices

Default to int, double, string, bool; use decimal for currency and long when values may exceed 2 billion.

Use var when the type is obvious from the right-hand side (var list = new List<int>();); write the type when it is not.

Prefer static readonly over const for values that might change between versions.

Keep variables in the smallest scope and give them intention-revealing names.

Interview Relevance

Very high: "value type vs reference type", "var vs dynamic", "const vs readonly", "why is 0.1 + 0.2 != 0.3?", "where are value types stored?" (answer: inline in their container — on the stack for locals, inside the object for fields; the stack/heap wording is a simplification).

Practice Questions

Which type would you use for: a person's age, a bank balance, a sensor voltage, a 32-bit CAN identifier, a Boolean relay state?

Show with code that arrays are reference types.

What happens when you assign null to var x? Try it.

Write a static readonly field holding the program start time and print it.

Compute 1.0 / 3.0 * 3.0 == 1.0 for double and 1m / 3m * 3m == 1m for decimal. Explain the results.

Quick Quiz

Q1. (Multiple choice) Which type is best for an invoice total? A) float B) double C) decimal D) int

Answer: C. decimal stores decimal fractions exactly.

Q2. (True/False) var makes a variable dynamically typed.

Answer: False. var is inferred at compile time and is fully static; dynamic is late-bound.

Q3. (Predict the output) PointStruct s1 = new(1, 2); var s2 = s1; s2.X = 99; Console.WriteLine(s1.X);

Answer: 1 — structs are copied on assignment (verified in the example).

Q4. (Find the bug) const DateTime Start = DateTime.Now;

Answer: const requires a compile-time constant of a primitive/string type; use static readonly DateTime Start = DateTime.Now;.

Coding Challenge

Write a program that stores a CAN frame's identifier (uint), data length (byte), timestamp in milliseconds (long) and a voltage reading (double), then prints each value with its .NET type name.

uint canId = 0x18FEF100;
byte dlc = 8;
long timestampMs = 1_725_000_000_123;
double voltage = 12.61;
foreach (object v in new object[] { canId, dlc, timestampMs, voltage })
    Console.WriteLine($"{v} : {v.GetType().Name}");