2.2. Types#

In C#, everything is a type. Every value you store, every variable you declare, and every expression you write has a type. A type defines three things:

  • Value: The set of valid values it can hold (e.g., int holds whole numbers −2,147,483,648 to 2,147,483,647).

  • Operation: The operations permitted on those values (e.g., arithmetic on numbers, .Length on strings).

  • Memory: The amount of memory reserved when a variable of that type is created.

C# types can be grouped into two groups: value types and reference types.

Value types:

  • Variables of value types directly contain their data.

  • Value types include primitive types int, double, bool, char, string, and struct and enum.

  • Despite their keyword status, each is simply an alias for a .NET struct or class in the System namespace. For example, int aliases System.Int32 and string aliases System.String. [1]

Reference types:

  • Variables of reference types store references to their data (objects).

  • Reference types include class, interface,delegate, and record.

  • You can define your own types; their fields and properties are themselves typed with built-ins or other custom types.

  • There are three built-in reference types: object, string, delegate, and dynamic.

Note that:

  • struct vs class is a common C# interview question because they look similar but struct is a value type (copied) and class is a reference type (shared).

  • string is the famous exception: it’s technically a reference type, but behaves like a value type because it’s immutable. Reassigning a string always creates a new one rather than modifying the original.

Types split into two fundamental categories:

Value Types

Reference Types

Storage

Stored directly in the variable

Variable holds a reference to heap memory

Assignment

Copies the value

Copies the reference (both point to the same object)

Default value

0 / false / '\0'

null

Built-in examples

int, long, double, float, decimal, bool, char

string, object

User-defined examples

struct, enum, record struct

class, interface, delegate, record

2.2.1. Common Data Types in C##

C# is a “strongly typed” language, meaning that every variable and constant has a type. In addition to variables, types are also required for expressions that evaluate to a value, method declarations with names, input parameters, and return values. Examples of common value types (declared and initiated with value assignment) are as follows.

int myNum = 5;                  // Integer (whole number)
long myNum = 15000000000L;      // Integer
double myDoubleNum = 5.99D;     // Floating point number
char myLetter = 'D';            // Character
bool myBool = true;             // Boolean
string myText = "Hello";        // String

From the list above you may notice that C# requires number suffixes for numeric types. The purpose of using suffixes is to help the compiler unambiguously identify the data type of the value/literal. The basic rules for integral literal number suffixes are:

  • If an integral literal has no suffix, its type is the first of the following types in which its value can be represented: int, uint, long, ulong.

  • l or L for long

  • U or u for unsigned integer

  • UL or ul for unsigned long

For floating-point numeric types:

  • The number literal without suffix or with the d or D suffix is of type double

  • f or F suffix is of type float

  • m or M suffix is of type decimal

A purpose of defining data types is for memory allocation. Examples of some defined types and their memory size can be seen in the table below.

No.

Group

Type

Size

Precision

Description

1

Integer

byte

1 byte/8 bits

Integral range 0 to 255 (no negative values)

2

short

16 bits

Integral range -32,768 to 32,767

3

* int

4 bytes/32 bits

Integral range -2,147,483,648 to 2,147,483,647

4

long

8 bytes/64 bits

Integral range -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807

5

Floating Point

* float

4 bytes/32 bits

~6-9 digits

Fractional numbers

6

double

8 bytes/64 bits

~15-17 digits

Fractional numbers, general math

7

decimal

16 bytes/128 bits

28-29 digits

Fractional numbers, high precision (use for money)

8

Boolean

* bool

1 bit

True or false values

9

Text

char

2 bytes

Single character, single quotes 'A'

10

* string

2 bytes per character

Sequence of characters, double quotes "Hello"

Choosing types is a design decision to meet the needs of different use scenarios. For example, for financial unit, the decimal type may be a better choice because it is designed for the purpose of holding a larger range of digits with higher precision.

One attribute of the integral value data types is that they can be either signed or unsigned. A signed type uses its bytes to represent an equal number of positive and negative numbers; whereas an unsigned type (such as ushort, uint, and ulong) uses its bytes to represent only positive numbers. The difference between singed and unsigned numbers can be seen from the example below:

Console.WriteLine("Signed integral types:");
Console.WriteLine($"sbyte   : {sbyte.MinValue} to {sbyte.MaxValue} \t\t\t\t\t (max: 2^8-1)");
Console.WriteLine($"short  : {short.MinValue} to {short.MaxValue} \t\t\t\t (max: 2^15-1)");
Console.WriteLine($"int    : {int.MinValue} to {int.MaxValue}    \t\t\t (max: 2^31-1)");
Console.WriteLine($"long   : {long.MinValue} to {long.MaxValue} \t (max: 2^63-1)");

Console.WriteLine("");

Console.WriteLine("Unsigned integral types:");
Console.WriteLine($"byte   : {byte.MinValue} to {byte.MaxValue}     \t\t\t\t\t (2^8)");
Console.WriteLine($"ushort : {ushort.MinValue} to {ushort.MaxValue} \t\t\t\t\t (2^16)");
Console.WriteLine($"uint   : {uint.MinValue} to {uint.MaxValue} \t\t\t\t (2^32)");
Console.WriteLine($"ulong  : {ulong.MinValue} to {ulong.MaxValue} \t\t\t (2^64)");
Signed integral types:
sbyte   : -128 to 127 					 (max: 2^8-1)
short  : -32768 to 32767 				 (max: 2^15-1)
int    : -2147483648 to 2147483647    			 (max: 2^31-1)
long   : -9223372036854775808 to 9223372036854775807 	 (max: 2^63-1)

Unsigned integral types:
byte   : 0 to 255     					 (2^8)
ushort : 0 to 65535 					 (2^16)
uint   : 0 to 4294967295 				 (2^32)
ulong  : 0 to 18446744073709551615 			 (2^64)

You should see the output as below:

Signed integral types:
sbyte  : -128 to 127
short  : -32768 to 32767
int    : -2147483648 to 2147483647
long   : -9223372036854775808 to 9223372036854775807

Unsigned integral types:
byte   : 0 to 255
ushort : 0 to 65535
uint   : 0 to 4294967295
ulong  : 0 to 18446744073709551615

2.2.1.1. C# Built-in Type System#

C# is a statically typed language — every value has a type known at compile time. Types fall into two fundamental categories:

C# Types
├── Value Types          (stored directly on the stack)
│   ├── Simple Types
│   │   ├── Integral     sbyte  byte  short  ushort  int  uint  long  ulong  char
│   │   ├── Floating     float  double
│   │   ├── Decimal      decimal
│   │   └── Boolean      bool
│   ├── Enum Types       (user-defined named constants)
│   └── Struct Types     (user-defined value types)
└── Reference Types      (stored on the heap; variable holds a reference)
    ├── Class Types      string  object  and user-defined classes
    ├── Interface Types
    ├── Array Types
    └── Delegate Types

Value types hold their data directly. Assigning one value-type variable to another copies the value. Reference types hold a reference (memory address) to the data; assigning one reference-type variable to another copies the reference, not the data itself.

Each C# primitive type is an alias for a .NET struct in the System namespace — for example, int is System.Int32, bool is System.Boolean, and string is System.String.

2.2.2. Data Types in Action#

The following subsections explore each built-in type through examples you can run in csharprepl.

2.2.2.1. Integer Types#

Integer types store whole numbers with no fractional part. The most common choice is int (32-bit signed), which covers roughly ±2.1 billion. Use long (64-bit) when you need a larger range, or byte for small non-negative values (0–255).

Unsigned variants (uint, ulong, ushort, byte) hold only non-negative values, which doubles the positive range but disallows negatives.

Numeric literals default to int. Use suffixes to specify other types: L/l for long, U/u for uint, UL/ul for ulong.

int population = 2_147_483_647;        // int max (~2.1 billion); _ is a digit separator
long worldPopulation = 8_000_000_000L; // needs L suffix — too large for int
byte age = 25;                         // 0–255, no negative values
sbyte temperature = -10;               // -128–127
uint score = 1_000_000U;               // unsigned: 0–4.29 billion

// Signed vs unsigned: same bits, different interpretation
int  signed   = -1;
uint unsigned = (uint)signed;           // becomes 4294967295 (all bits set)
signed
unsigned

2.2.2.2. Floating-Point Types#

C# has three types for fractional numbers:

Type

Size

Precision

Suffix

Use for

float

32-bit

~6–9 digits

f or F

Graphics, sensors

double

64-bit

~15–17 digits

d or D (default)

General math

decimal

128-bit

28–29 digits

m or M

Money, finance

double is the default type for decimal literals. Use decimal whenever rounding errors are unacceptable (e.g., currency); never use float/double for money.

Important

Floating-point arithmetic is approximate — 0.1 + 0.2 does not equal 0.3 exactly in binary representation.

This is not a C# bug; it is a consequence of how computers store fractional numbers. Memory stores bits (0s and 1s), so values are encoded in base 2 (binary). Just as 1/3 cannot be written exactly in base 10 (0.3333…), many decimal fractions — including 0.1 and 0.2 — cannot be represented exactly in binary. The closest representable binary value is used instead, and when you add two of these approximations the tiny errors accumulate, giving a result like 0.30000000000000004 instead of 0.3.

For money or anything where exact decimal rounding matters, use decimal, which stores numbers in base 10 internally and avoids this problem entirely.

float  f = 3.14F;
double d = 3.141592653589793;    // default decimal literal type
decimal price = 19.99M;          // use M suffix for decimal

// Precision difference
0.1 + 0.2                        // 0.30000000000000004 — binary rounding
0.1M + 0.2M                      // 0.3 — decimal is exact
f.GetType()
d.GetType()
price.GetType()

2.2.2.3. Boolean (bool)#

A bool holds exactly one of two values: true or false. It is the result of any comparison or logical expression, and is the required type for if conditions and loop guards.

Unlike C/C++, C# does not allow integers to be used as booleans — if (1) is a compile-time error.

bool isOnline   = true;
bool hasExpired = false;

int x = 7;
bool inRange = x > 0 && x < 10;   // true
bool isEven  = x % 2 == 0;        // false

isOnline
inRange
isEven
isEven.GetType()

2.2.2.4. Character (char)#

char represents a single Unicode UTF-16 character, written in single quotes. Internally it is stored as a 16-bit unsigned integer, so it can be cast to and from int. Common escape sequences:

Sequence

Meaning

'\n'

Newline

'\t'

Tab

'\\'

Backslash

'\''

Single quote

Note that 'A' (char) is different from "A" (string of length 1).

char letter  = 'A';
char digit   = '7';
char newline = '\n';
char unicode = '\u00E9';       // é — U+00E9 LATIN SMALL LETTER E WITH ACUTE

// char ↔ int
int  code = letter;            // 65 — implicit cast to int
char back = (char)(code + 1);  // 'B' — explicit cast back

letter
code
back
unicode

2.2.2.5. String (string)#

string is a sequence of char values, written in double quotes. Strings are immutable — operations like concatenation return a new string.

Key features:

  • Interpolation: embed expressions with $"...{expr}..."

  • Verbatim literals: prefix with @ to treat backslashes literally (useful for file paths)

  • Common members: .Length, .ToUpper(), .ToLower(), .Contains(), .Substring(), .Replace(), indexing with [i]

string name    = "Alice";
string greeting = $"Hello, {name}!";     // string interpolation
string path    = @"C:\Users\Alice\docs"; // verbatim: backslashes are literal

// Common operations
name.Length                              // 5
name.ToUpper()                           // "ALICE"
name[0]                                  // 'A' — char at index 0
name.Contains("lic")                     // true
name.Replace("Alice", "Bob")             // "Bob"

greeting
path

A string is a sequence of characters — each character has a numeric index starting at 0. For example, in "formosa", 'f' is at index 0 and 'a' is at index 6. The last character is always at index length - 1.

Index

0

1

2

3

4

5

6

Character

f

o

r

m

o

s

a

When represented in code, indexing works like the example below.

string txt = "ilha formosa";
Console.WriteLine(txt[3]);
a

Note that, in the example above, the output is a character since strings consist of characters. Note that the char type literals are created by delimiting the characters by single quotation marks.

2.2.2.6. Creating a String#

There are many ways to declare and initialize a variable of type string, most of them involve using an assignment statement to assign the variable with a value.

As an example, to create a string variable in C# REPL:

string dayOfWeek = "Monday"; 
dayOfWeek

For the different ways of declaring and initializing a string variable, see below:

#pragma warning disable CS8632

using System.Runtime.InteropServices;

string message1;                // Declare without initializing.
string? message2 = null;        // Initialize to null.
string message3 = System.String.Empty;  // Initialize as an empty string use the Empty constant instead of the literal "".
string oldPath = "c:\\Program Files\\Microsoft Visual Studio 8.0";  // Initialize with a regular string literal.
string newPath = @"c:\Program Files\Microsoft Visual Studio 9.0";   // Initialize with a verbatim string literal.
System.String greeting = "Hello World!";    // Use System.String if you prefer.
var temp = "I'm still a strongly-typed System.String!"; // In local variables (i.e. within a method body) you can use implicit typing.
const string message4 = "You can't get rid of me!"; // Use a const string to prevent 'message4' from being used to store another string value.
char[] letters = { 'A', 'B', 'C' }; // Use the String constructor only when creating a string from a char*, char[], or sbyte*. See System.String documentation for details.
string alphabet = new string(letters);

Console.WriteLine("1. " + message1);
Console.WriteLine("2. " + message2);
Console.WriteLine("3. " + message3);
Console.WriteLine("4. " + oldPath);
Console.WriteLine("5. " + newPath);
Console.WriteLine("6. " + greeting);
Console.WriteLine("7. " + temp);
Console.WriteLine("8. " + message4);
Console.WriteLine("9. " + letters);
Console.WriteLine("10. " + alphabet);

#pragma warning restore CS8632
1. 
2. 
3. 
4. c:\Program Files\Microsoft Visual Studio 8.0
5. c:\Program Files\Microsoft Visual Studio 9.0
6. Hello World!
7. I'm still a strongly-typed System.String!
8. You can't get rid of me!
9. System.Char[]
10. ABC

Since this is a console app, we use the TERMINAL in VS Code to do dotnet run to see the project outcome. Make sure you are in the right project directory, though.

2.2.2.7. String Concatenation#

String concatenation is to join stings together. In C#, the + operator is used to perform concatenation. Note C# uses the + operator for both arithmetic addition and concatenation. To concatenate two strings together:

string firstName = "Tsangyao";    // assign value to variable
string lastName = " Chen";

firstName + lastName              // concatenation
Tsangyao Chen

C# can also concatenate values of different data types. The example below shows you how to concatenate data of string type and int type:

int aLargeAmountOf = 400;
"I spent " + aLargeAmountOf + " dollars on coffee this week."

2.2.2.8. Escape Special Characters#

Since C# requires double quotation marks as delimiters for creating strings, when we need to show quotation marks as part of a string, the situation becomes tricky. Consider the following string toBe1. We see that there is a syntax error at (1,18) (line# 1, character# 18) when trying to put a quotation inside the string:

string toBe1 = ""To be, or not to be" is a speech given by Prince Hamlet.";
(1,18): error CS1002: ; expected

(1,25): error CS1044: Cannot use more than one type in a for, using, fixed, or declaration statement

(1,28): error CS1002: ; expected

(1,35): error CS1002: ; expected

(1,37): error CS1002: ; expected

To make the quotation work, we need to use the special character backslash \ as escape character, meaning that the character following it should be treated specially: They turns special characters into string characters.

string toBe2 = “”To be, or not to be” is a speech given by Prince Hamlet.”;

Console.WriteLine(toBe2);

In our example above, the " in \"To be and to be\" are escaped and therefore special character " can be treated as string and shown as intended.

Another example would look like the following.

Console.WriteLine("Goog morning!");
Console.WriteLine("He said, \"Goog morning!\".");
Goog morning!
He said, "Goog morning!".

Common special cases to be escaped include:

Escape character

Result

\"

" (quote)

\'

' ( single quote in char literal)

\\

\ (backslash)

\n

new line

\t

new tab

The newline character (\n) inserts a new line and move the cursor to the beginning of the new line. This is useful because C# string literals are characters delimited by double quotation marks " in one line. [2] To print to multiple lines, we use \n like:

Console.WriteLine("Good morning. Good afternoon. Good evening.");
Console.WriteLine("Good morning. \nGood afternoon. \nGood evening.");
Good morning. Good afternoon. Good evening.
Good morning. 
Good afternoon. 
Good evening.

2.2.2.9. String Properties and Methods#

Although we use string literals, strings are objects. In object-oriented-programming, objects have instance properties and instance methods. Some examples of C# string properties and methods are:

The length of a string can be found using the Length property:

string txt = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
Console.WriteLine("The length of the txt string is: " + txt.Length);
The length of the txt string is: 26

There are many string methods available [3]. As examples, ToUpper() and ToLower() return a copy of the string converted to uppercase or lowercase:

string txt = "Hello World";
Console.WriteLine(txt.ToUpper());   // Outputs "HELLO WORLD"
Console.WriteLine(txt.ToLower());   // Outputs "hello world"
HELLO WORLD
hello world

2.2.3. Type Conversion#

C# variables have specific types, but values sometimes need to move between types. Type conversions are either implicit (automatic) or explicit (manual cast required).

Implicit casting happens automatically when converting a smaller type to a larger one — no data is lost:

char → int → long → float → double

Explicit casting is required when converting a larger type to a smaller one, because precision or range may be lost:

double → float → long → int → char

For example:

int a = 123;
long b = a;        // implicit: int fits in long, no cast needed
int c = (int) b;   // explicit: long → int, cast required

Use GetType() to inspect a variable’s type at runtime. When types are not cast properly, C# raises a compile-time error. For example, assigning a double directly to an int fails:

double d = 2.0;
int i = d;

Note that if you choose to agree with the message and perform a type casting, you lose the precision of double over an int.

double d = 2.5;       // create a double type variable d
d
int i;                // declare an int without value assignment
i                     // get the (default) value of an int
i = (int)d;           // explicitly telling the compiler you intend the conversion
i                     // get the value of i; the value .5 is lost

Rounding is similar to casting a floating type to possible as it gives us an int type. The function Math.Round will round to a mathematical integer, but leaves the type unchanged. So we need to perform a type casting after rounding:

d
d.GetType()
d = Math.Round(d);        // rounding and re-assignment
d
d.GetType()               // the type remains
i = (int)Math.Round(d);   // casting to int
i
i.GetType()               // type correct

Casting from int to double is usually not necessary but cause of implicit conversion. A use case for this would be when doing divisions, where double would work better than int. As an example, using csharprepl, we see that:

int denominator = 3;
int numerator = 14;
numerator / denominator               // an integer division
(double) numerator / denominator      // intended operation; casting required

Footnotes