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.,
intholds whole numbers −2,147,483,648 to 2,147,483,647).Operation: The operations permitted on those values (e.g., arithmetic on numbers,
.Lengthon 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, andstructandenum.Despite their keyword status, each is simply an alias for a .NET struct or class in the
Systemnamespace. For example,intaliasesSystem.Int32andstringaliasesSystem.String. [1]
Reference types:
Variables of reference types store references to their data (objects).
Reference types include
class,interface,delegate, andrecord.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, anddynamic.
Note that:
structvsclassis a common C# interview question because they look similar butstructis a value type (copied) andclassis a reference type (shared).stringis 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 |
|
|
Built-in examples |
|
|
User-defined examples |
|
|
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 |
|
1 byte/8 bits |
Integral range 0 to 255 (no negative values) |
|
2 |
|
16 bits |
Integral range -32,768 to 32,767 |
||
3 |
* |
4 bytes/32 bits |
Integral range -2,147,483,648 to 2,147,483,647 |
||
4 |
|
8 bytes/64 bits |
Integral range -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 |
||
5 |
Floating Point |
* |
4 bytes/32 bits |
~6-9 digits |
Fractional numbers |
6 |
|
8 bytes/64 bits |
~15-17 digits |
Fractional numbers, general math |
|
7 |
|
16 bytes/128 bits |
28-29 digits |
Fractional numbers, high precision (use for money) |
|
8 |
Boolean |
* |
1 bit |
True or false values |
|
9 |
Text |
|
2 bytes |
Single character, single quotes |
|
10 |
* |
2 bytes per character |
Sequence of characters, double quotes |
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 |
|---|---|---|---|---|
|
32-bit |
~6–9 digits |
|
Graphics, sensors |
|
64-bit |
~15–17 digits |
|
General math |
|
128-bit |
28–29 digits |
|
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 |
|---|---|
|
Newline |
|
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 |
|---|---|
|
|
|
|
|
|
|
new line |
|
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