Types and Variables
Definition
C# has a unified type system in which all types ultimately derive from System.Object. The language is strongly-typed: every variable, constant, and expression has a type that is known at compile time, and the compiler enforces type compatibility. C# distinguishes between value types (stored inline) and reference types (stored by reference).
Core Concepts
Value Types vs Reference Types
| Aspect | Value Types | Reference Types |
|---|---|---|
| Stored in | Stack (usually) | Heap |
| Contains | Direct data | Reference (pointer) to data |
| Assignment | Copies the value | Copies the reference |
| Default | Cannot be null (unless Nullable<T>) | Can be null |
| Examples | int, double, bool, char, struct, enum | string, object, class, interface, delegate, array |
| Base type | System.ValueType | System.Object |
Assigning a value type copies the data. Assigning a reference type copies the pointer — both variables point to the same object on the heap. Modifying one affects the other.
Built-in Types Table
| C# Keyword | .NET Type | Size | Range / Description |
|---|---|---|---|
int | System.Int32 | 4 bytes | -2,147,483,648 to 2,147,483,647 |
long | System.Int64 | 8 bytes | Very large integers |
short | System.Int16 | 2 bytes | -32,768 to 32,767 |
byte | System.Byte | 1 byte | 0 to 255 |
float | System.Single | 4 bytes | ~7 digits precision (suffix f) |
double | System.Double | 8 bytes | ~15-16 digits precision |
decimal | System.Decimal | 16 bytes | 28-29 digits precision (suffix m) |
bool | System.Boolean | 1 byte | true or false |
char | System.Char | 2 bytes | Unicode character |
string | System.String | Variable | Unicode text (reference type!) |
object | System.Object | N/A | Base of all types (reference type!) |
Variables
Declaration and initialization:
int age = 30;
string name = "Alice";
bool isActive = true;
var score = 95.5; // Compiler infers double
var productName = "Widget"; // Compiler infers string
The var keyword lets the compiler infer the type from the initializer. It is not a variant or dynamic type — the variable is still strongly typed at compile time.
Naming conventions:
| Style | Used For | Example |
|---|---|---|
camelCase | Local variables, parameters, private fields | orderCount, firstName |
PascalCase | Classes, methods, properties, public fields | OrderService, TotalPrice |
_camelCase | Private fields (alternative convention) | _orderCount, _repository |
IPascalCase | Interfaces | IDisposable, IRepository |
Constants: const vs readonly vs static readonly
| Keyword | When set | Where | Notes |
|---|---|---|---|
const | Compile time | Fields or locals | Value is embedded in calling code; only primitive/types |
readonly | Runtime (constructor) | Instance fields | Per-instance value set in constructor |
static readonly | Runtime (static constructor or initializer) | Static fields | Shared across all instances; can hold complex objects |
public class Configuration
{
public const int MaxRetries = 3; // Compile-time constant
public readonly string _instanceName; // Set in constructor
public static readonly DateTime LaunchDate // Set once at runtime
= DateTime.UtcNow;
}
const values are inlined at compile time. If you change a const in a library, callers compiled against the old value will still use the old value until they recompile. Use static readonly for values that may change across versions.
Operators
Arithmetic: +, -, *, /, %, ++, --
Comparison: ==, !=, <, >, <=, >=
Logical: &&, ||, !
Null-handling operators:
// Null-coalescing — returns left if non-null, otherwise right
string name = input ?? "default";
// Null-conditional — returns null if operand is null (instead of throwing)
int? length = name?.Length;
// Null-coalescing assignment — assigns only if variable is null
name ??= "unnamed";
Type Casting
| Approach | Description | Example |
|---|---|---|
| Implicit | Safe, no data loss; auto by compiler | int → long, float → double |
| Explicit (cast) | Possible data loss; requires (Type) syntax | (int)3.14 → 3 |
Convert class | Converts between base types | Convert.ToInt32("42") |
Parse | Parses string; throws on failure | int.Parse("42") |
TryParse | Parses string; returns bool | int.TryParse("42", out int result) |
// Implicit — safe widening conversion
int x = 100;
long big = x; // int → long, no cast needed
// Explicit — narrowing, possible data loss
double pi = 3.14159;
int truncated = (int)pi; // 3
// TryParse — safe parsing
if (int.TryParse(userInput, out int result))
{
Console.WriteLine($"Parsed: {result}");
}
Boxing and Unboxing
Boxing is converting a value type to a reference type (object or interface). The value is wrapped in a heap-allocated object.
Unboxing is extracting the value type from the boxed object with an explicit cast.
int number = 42;
object boxed = number; // Boxing — value copied to heap
int unboxed = (int)boxed; // Unboxing — value copied back to stack
Boxing allocates memory on the heap and causes GC pressure. Avoid it in hot paths. Use generics (List<int> instead of ArrayList) and avoid object parameters when possible.
Code Examples
Value vs Reference Type Behavior
// Value type — assignment copies the value
int a = 10;
int b = a;
b = 20;
Console.WriteLine(a); // 10 — unchanged
// Reference type — assignment copies the reference
var list1 = new List<int> { 1, 2, 3 };
var list2 = list1;
list2.Add(4);
Console.WriteLine(list1.Count); // 4 — affected!
Custom Value Type (struct)
public struct Point
{
public double X { get; }
public double Y { get; }
public Point(double x, double y) => (X, Y) = (x, y);
public double DistanceTo(Point other) =>
Math.Sqrt(Math.Pow(X - other.X, 2) + Math.Pow(Y - other.Y, 2));
}
Safe Parsing with TryParse
public int? ParseAge(string input)
{
if (int.TryParse(input, out int age) && age is >= 0 and <= 150)
{
return age;
}
return null;
}
When to Use
| Situation | Recommendation |
|---|---|
| Financial calculations | Use decimal — avoids floating-point rounding errors |
| Scientific / graphics | Use double — performance and sufficient precision |
| Parsing user input | Always prefer TryParse over Parse |
| Immutable configuration values | Use const for true constants; static readonly otherwise |
| Small data structures (16 bytes or less) | Consider struct for stack allocation and no GC overhead |
Common Pitfalls
- Mutable structs — structs are value types; modifying a property through a method returning a struct modifies a copy. Make structs immutable.
==vsEqualsfor strings —==compares values for strings (overloaded operator), but for other reference types it compares references. Usestring.Equals(a, b, StringComparison.Ordinal)for explicit, culture-safe comparison.decimalvsdoublefor money —doublehas rounding errors (0.1 + 0.2 != 0.3). Always usedecimalfor financial values.varand readability — usevarwhen the type is obvious from the right side (var stream = new FileStream(...)). Avoid it when the type is unclear (var result = GetData()).- Boxing in non-generic collections —
ArrayList,Hashtablestoreobject, causing boxing for every value type. UseList<T>,Dictionary<TKey, TValue>.
Key Takeaways
- C# has a unified type system — everything derives from
System.Object. - Value types live on the stack (typically); reference types live on the heap with a stack reference.
- Assignment semantics differ: value types copy data; reference types copy references.
- Use
decimalfor money,TryParsefor user input, and generics to avoid boxing. constis compile-time;readonlyis runtime — choose based on versioning needs.
Interview Questions
Q: What is boxing?
Boxing is the process of converting a value type to a reference type by wrapping it in a
System.Objecton the heap. This causes a heap allocation and GC overhead. Unboxing extracts the value with an explicit cast. Use generics to avoid boxing.
Q: What is the difference between value types and reference types?
Value types store data directly (usually on the stack) and are copied on assignment. Reference types store a pointer to data on the heap — assignment copies the reference, not the data. Value types derive from
System.ValueType; reference types derive fromSystem.Objectdirectly.
Q: What is the difference between const and readonly?
constvalues are set at compile time and inlined into calling code — they cannot hold complex objects and changes require recompilation of callers.readonlyvalues are set at runtime (in the constructor) and can hold any type. Usestatic readonlyfor shared runtime constants.
Q: What is the var keyword?
varenables implicit typing — the compiler infers the actual type from the initializer expression. The variable is still strongly typed at compile time;varis not dynamic. It improves readability when the type is obvious but can reduce clarity when the right-hand side is ambiguous.