Skip to main content

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

AspectValue TypesReference Types
Stored inStack (usually)Heap
ContainsDirect dataReference (pointer) to data
AssignmentCopies the valueCopies the reference
DefaultCannot be null (unless Nullable<T>)Can be null
Examplesint, double, bool, char, struct, enumstring, object, class, interface, delegate, array
Base typeSystem.ValueTypeSystem.Object
Key distinction

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 TypeSizeRange / Description
intSystem.Int324 bytes-2,147,483,648 to 2,147,483,647
longSystem.Int648 bytesVery large integers
shortSystem.Int162 bytes-32,768 to 32,767
byteSystem.Byte1 byte0 to 255
floatSystem.Single4 bytes~7 digits precision (suffix f)
doubleSystem.Double8 bytes~15-16 digits precision
decimalSystem.Decimal16 bytes28-29 digits precision (suffix m)
boolSystem.Boolean1 bytetrue or false
charSystem.Char2 bytesUnicode character
stringSystem.StringVariableUnicode text (reference type!)
objectSystem.ObjectN/ABase 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:

StyleUsed ForExample
camelCaseLocal variables, parameters, private fieldsorderCount, firstName
PascalCaseClasses, methods, properties, public fieldsOrderService, TotalPrice
_camelCasePrivate fields (alternative convention)_orderCount, _repository
IPascalCaseInterfacesIDisposable, IRepository

Constants: const vs readonly vs static readonly

KeywordWhen setWhereNotes
constCompile timeFields or localsValue is embedded in calling code; only primitive/types
readonlyRuntime (constructor)Instance fieldsPer-instance value set in constructor
static readonlyRuntime (static constructor or initializer)Static fieldsShared 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 and versioning

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

ApproachDescriptionExample
ImplicitSafe, no data loss; auto by compilerint → long, float → double
Explicit (cast)Possible data loss; requires (Type) syntax(int)3.143
Convert classConverts between base typesConvert.ToInt32("42")
ParseParses string; throws on failureint.Parse("42")
TryParseParses string; returns boolint.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 performance

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

SituationRecommendation
Financial calculationsUse decimal — avoids floating-point rounding errors
Scientific / graphicsUse double — performance and sufficient precision
Parsing user inputAlways prefer TryParse over Parse
Immutable configuration valuesUse 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.
  • == vs Equals for strings== compares values for strings (overloaded operator), but for other reference types it compares references. Use string.Equals(a, b, StringComparison.Ordinal) for explicit, culture-safe comparison.
  • decimal vs double for moneydouble has rounding errors (0.1 + 0.2 != 0.3). Always use decimal for financial values.
  • var and readability — use var when 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 collectionsArrayList, Hashtable store object, causing boxing for every value type. Use List<T>, Dictionary<TKey, TValue>.

Key Takeaways

  1. C# has a unified type system — everything derives from System.Object.
  2. Value types live on the stack (typically); reference types live on the heap with a stack reference.
  3. Assignment semantics differ: value types copy data; reference types copy references.
  4. Use decimal for money, TryParse for user input, and generics to avoid boxing.
  5. const is compile-time; readonly is 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.Object on 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 from System.Object directly.

Q: What is the difference between const and readonly?

const values are set at compile time and inlined into calling code — they cannot hold complex objects and changes require recompilation of callers. readonly values are set at runtime (in the constructor) and can hold any type. Use static readonly for shared runtime constants.

Q: What is the var keyword?

var enables implicit typing — the compiler infers the actual type from the initializer expression. The variable is still strongly typed at compile time; var is not dynamic. It improves readability when the type is obvious but can reduce clarity when the right-hand side is ambiguous.

References