Beginner

Variables and Types in C#

Learn about variables, data types, type inference, and type conversions in C# with practical Docker-ready examples

C# is a statically, strongly typed language — every variable has a declared type known at compile time, and the compiler enforces type safety throughout your code. This is one of C#’s core strengths: mistakes that would cause runtime crashes in dynamic languages are caught before your program ever runs.

Despite being statically typed, C# is far from verbose. The var keyword lets the compiler infer the type from the assigned value, giving you the safety of static typing with much of the brevity of dynamic languages. Modern C# also has nullable reference types, record types, and pattern matching that make working with data concise and expressive.

In this tutorial you’ll learn C#’s built-in primitive types, how to declare and assign variables, how to use var for type inference, how to work with constants, and how to perform type conversions — both implicit and explicit.

The Complete Example

Create a file named Variables.cs:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// ── Primitive Types ──────────────────────────────────────────────────────────
// Integers
int age = 30;
long population = 8_100_000_000L;   // underscores improve readability
short smallNum = 32000;
byte byteVal = 255;

// Floating-point
double pi = 3.14159265358979;
float approxPi = 3.14159f;          // f suffix required for float literals
decimal price = 19.99m;             // m suffix for decimal; use for money

// Boolean
bool isActive = true;

// Characters and strings
char grade = 'A';                   // single quotes for char
string name = "C# Developer";       // double quotes for string

Console.WriteLine("── Primitive Types ──");
Console.WriteLine($"int:     {age}");
Console.WriteLine($"long:    {population}");
Console.WriteLine($"short:   {smallNum}");
Console.WriteLine($"byte:    {byteVal}");
Console.WriteLine($"double:  {pi}");
Console.WriteLine($"float:   {approxPi}");
Console.WriteLine($"decimal: {price}");
Console.WriteLine($"bool:    {isActive}");
Console.WriteLine($"char:    {grade}");
Console.WriteLine($"string:  {name}");

// ── Type Inference with var ───────────────────────────────────────────────────
// var lets the compiler deduce the type; the variable is still statically typed
var count = 42;                    // inferred as int
var message = "Hello, C#!";       // inferred as string
var ratio = 3.14;                  // inferred as double
var flag = true;                   // inferred as bool

Console.WriteLine("\n── Type Inference with var ──");
Console.WriteLine($"count   is {count.GetType().Name}: {count}");
Console.WriteLine($"message is {message.GetType().Name}: {message}");
Console.WriteLine($"ratio   is {ratio.GetType().Name}: {ratio}");
Console.WriteLine($"flag    is {flag.GetType().Name}: {flag}");

// var works great with complex types to reduce verbosity
var numbers = new List<int> { 1, 2, 3, 4, 5 };
Console.WriteLine($"numbers is {numbers.GetType().Name} with {numbers.Count} elements");

// ── Constants and Nullable Types ─────────────────────────────────────────────
// const: value must be known at compile time
const double Pi = 3.14159265358979;
const int DaysInWeek = 7;
const string AppName = "CodeArchaeology";

double circumference = 2 * Pi * 5.0;
Console.WriteLine("\n── Constants ──");
Console.WriteLine($"Pi          = {Pi}");
Console.WriteLine($"DaysInWeek  = {DaysInWeek}");
Console.WriteLine($"AppName     = {AppName}");
Console.WriteLine($"Circumference of r=5 circle: {circumference:F4}");

// Nullable value types: add ? to allow null for value types
int? optionalAge = null;
Console.WriteLine($"\noptionalAge is null: {optionalAge == null}");
optionalAge = 25;
Console.WriteLine($"optionalAge after assignment: {optionalAge}");

// Null-coalescing operator: return left if not null, else right
int definiteAge = optionalAge ?? 0;
Console.WriteLine($"definiteAge (using ??): {definiteAge}");

// ── Type Conversions ─────────────────────────────────────────────────────────
// Implicit conversions (widening)
int intVal = 100;
long longVal = intVal;           // int → long: always safe
double doubleVal = intVal;       // int → double: always safe

Console.WriteLine("\n── Type Conversions ──");
Console.WriteLine("Implicit conversions:");
Console.WriteLine($"  int    → long:   {longVal}");
Console.WriteLine($"  int    → double: {doubleVal}");

// Explicit casting (narrowing)
double piApprox = 3.99;
int truncated = (int)piApprox;   // fractional part is discarded, not rounded

Console.WriteLine("\nExplicit cast (truncates, does not round):");
Console.WriteLine($"  (int)3.99 = {truncated}");

// Convert class: parses strings and converts between types
string numberText = "42";
int parsed = Convert.ToInt32(numberText);
double parsedDouble = Convert.ToDouble("3.14");
bool parsedBool = Convert.ToBoolean(1);   // 0 = false, non-zero = true

Console.WriteLine("\nConvert class:");
Console.WriteLine($"  Convert.ToInt32(\"42\")      = {parsed}");
Console.WriteLine($"  Convert.ToDouble(\"3.14\")   = {parsedDouble}");
Console.WriteLine($"  Convert.ToBoolean(1)       = {parsedBool}");

// int.Parse / int.TryParse
bool success = int.TryParse("123abc", out int result);
Console.WriteLine($"\nint.TryParse(\"123abc\") succeeded: {success}");

success = int.TryParse("999", out result);
Console.WriteLine($"int.TryParse(\"999\")    succeeded: {success}, value: {result}");

// String representations
int x = 255;
Console.WriteLine($"\nString representations of 255:");
Console.WriteLine($"  Decimal: {x.ToString()}");
Console.WriteLine($"  Hex:     {x:X}");
Console.WriteLine($"  Binary:  {Convert.ToString(x, 2)}");

Walkthrough

Primitive Types

C# has a rich set of built-in value types that map directly to .NET runtime types. Each has a C# keyword alias (like int) and a full .NET name (like System.Int32) — they are identical.

Alias.NET TypeSizeRange
intSystem.Int3232-bit−2.1B to 2.1B
longSystem.Int6464-bitVery large integers
doubleSystem.Double64-bit float±1.7×10³⁰⁸
floatSystem.Single32-bit float±3.4×10³⁸
decimalSystem.Decimal128-bitHigh-precision decimals
boolSystem.Booleantrue or false
charSystem.Char16-bitSingle Unicode character
stringSystem.Stringref typeImmutable Unicode text

Underscores in numeric literals (8_100_000_000L) are purely cosmetic — the compiler ignores them but they greatly improve readability for large numbers.

Type Inference with var

The var keyword lets the compiler deduce the type from the right-hand side of the assignment. The variable is still statically typedvar is just syntax sugar, not dynamic typing.

1
2
3
var count = 42;        // compile-time type: int
var name  = "Alice";   // compile-time type: string
// count = "hello";    // ❌ compile error — type is fixed as int

var is especially useful with verbose generic types:

1
2
3
4
// Without var:
Dictionary<string, List<int>> map = new Dictionary<string, List<int>>();
// With var:
var map = new Dictionary<string, List<int>>();

Constants and Nullable Types

const declares a compile-time constant — the value is inlined by the compiler and cannot change. Use readonly (not shown here) for values that are set once at runtime, typically in a constructor.

Nullable value types (int?, double?, etc.) wrap a value type in Nullable<T>, enabling null assignment. The ?? (null-coalescing) operator provides a concise fallback:

1
2
int? score = null;
int display = score ?? -1;   // display = -1 when score is null

Type Conversions

C# distinguishes between:

  • Implicit (widening) — safe, no data loss, done automatically by the compiler (intlong, intdouble).
  • Explicit casting — may lose precision or overflow; written as (TargetType)value. Fractional parts are truncated, not rounded.
  • Convert class — parses strings and converts between many types; throws on failure.
  • TryParse — safe parsing that returns a bool instead of throwing; preferred for user input.

Running with Docker

1
2
3
4
5
# Pull the official .NET SDK image
docker pull mcr.microsoft.com/dotnet/sdk:9.0

# Run the variables example
docker run --rm -v $(pwd):/app -w /app mcr.microsoft.com/dotnet/sdk:9.0 sh -c "cp Variables.cs Program.cs.bak && dotnet new console -o . --force >/dev/null 2>&1 && cp Program.cs.bak Program.cs && rm Program.cs.bak && dotnet run"

Note: The Docker command bootstraps a temporary .NET console project in the current directory, replaces Program.cs with your file, then compiles and runs it. Your original file is preserved.

Expected Output

── Primitive Types ──
int:     30
long:    8100000000
short:   32000
byte:    255
double:  3.14159265358979
float:   3.14159
decimal: 19.99
bool:    True
char:    A
string:  C# Developer

── Type Inference with var ──
count   is Int32: 42
message is String: Hello, C#!
ratio   is Double: 3.14
flag    is Boolean: True
numbers is List`1 with 5 elements

── Constants ──
Pi          = 3.14159265358979
DaysInWeek  = 7
AppName     = CodeArchaeology
Circumference of r=5 circle: 31.4159

optionalAge is null: True
optionalAge after assignment: 25
definiteAge (using ??): 25

── Type Conversions ──
Implicit conversions:
  int    → long:   100
  int    → double: 100

Explicit cast (truncates, does not round):
  (int)3.99 = 3

Convert class:
  Convert.ToInt32("42")      = 42
  Convert.ToDouble("3.14")   = 3.14
  Convert.ToBoolean(1)       = True

int.TryParse("123abc") succeeded: False
int.TryParse("999")    succeeded: True, value: 999

String representations of 255:
  Decimal: 255
  Hex:     FF
  Binary:  11111111

Key Concepts

  • Static, strong typing — every variable’s type is fixed at compile time; the compiler rejects type mismatches before the program runs.
  • var is not dynamic — the compiler infers the type from the initializer; the variable is still statically typed and cannot change type later.
  • Value types vs reference types — primitives (int, double, bool, char, struct) are value types stored on the stack; string, arrays, and class instances are reference types on the heap.
  • string is special — despite being a reference type, strings behave like value types: they are immutable, and equality (==) compares content, not reference identity.
  • decimal for money — always use decimal (not double or float) for financial calculations; it avoids binary floating-point rounding errors.
  • Nullable value types (int?) — value types normally cannot be null; appending ? wraps them in Nullable<T>, enabling null assignment and the ?? null-coalescing operator.
  • Prefer TryParse over Parseint.Parse throws on invalid input; int.TryParse returns a bool and is safer for user-supplied strings.
  • Digit separators — underscores in numeric literals (8_100_000_000L) are purely cosmetic and improve readability of large numbers; the compiler ignores them.

Running Today

All examples can be run using Docker:

docker pull mcr.microsoft.com/dotnet/sdk:9.0
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining