C# Data Types Tutorial

In this tutorial we learn about the C# common type system and what value types, reference types and object types are.

We also cover the primitive value types, type overflowing and how to check for it.

What are data types

A data type is simply the type of data that can be stored in our variables, constants, arrays etc. We’ve already seen some data types being stored such as integers, floats and strings.

C# uses a Common Type System (CTS), which is categorized into the following types:

  • Value Types
  • Reference Types
  • Object Type
  • Dynamic Type
  • String Type

We’ll only cover value, reference and object types in this tutorial lesson.

Value types

Value types live on the stack in memory, and can be directly stored and accessed. Because of this, it’s not possible for an operation on one variable to affect another. Although, in the case of ref, in, and out parameter variables it is possible.

Because value types are on the stack, in most cases they are more performant than reference types.

The following table shows the value types in C#:


Value types are not nullable, which means they cannot store a non-value state.

 int shoes = null; // compiler error

In the example above, we try to give a null value to an integer. The compiler will raise an error because an int is a value data type and is not nullable.

The char value type

A char is a single ASCII character such as A or a and uses only 1 byte of memory.

char character = 'A';

We can’t assign multiple characters to a char type. If we want multiple characters, we have to use a string type.

A string type is a reference type and has its own tutorial lesson, but here is a simple example.

string message = "Hello World";

A computer can only understand numbers. The compiler will convert characters into their corresponding ASCII numbers. For example, the capital letter A converts to number 65, and the lowercase letter a converts to number 97.

When writing chars it’s good practice to use the letter instead of its ASCII number. It’s easier to read and no one has to go and look up which character maps to which number.

// Good practice
char letter = 'A';

// Bad practice
char letter = 97;

You can see a complete ASCII lookup table at: www.asciitable.com . The Dec column shows the number it’s converted into.

When working with characters and strings, we have to remember that characters are enclosed in single quotes, and strings are enclosed in double quotes.

// single character
char letter = 'A';

// string
string message = "Hello World";

The bool value type

A bool stands for boolean type and can only ever hold one of two values, true or false.

bool legal = true;

bool auth = false;

A bool is most often used in conditional statement checks.

The true and false values of a bool is representative of a 1 or 0. The keywords true and false only serve to make our code more readable.

bool legal = 1; // true

bool auth = 0; // false

We are allowed to use 0 and 1 instead of true or false. However, it’s considered bad practice because the code will be less readable.

The byte value type

A byte is a whole number ranging from 0 to 255.

byte num = 255;

A byte cannot have a value greater than 255 as its number.

// will throw a compiler error
byte num = 256;

The short value type

A short is a whole number ranging from -32,768 to 32,767.

short num = 5000;

The int value type

An int is a whole number ranging from -2,147,483,648 to 2,147,483,647 (-2.1 billion to 2.1 billion).

int num = 1000000;

The long value type

A long is a whole number ranging from -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 (-9.2 quintillion to 9.2 quintillion).

long num = 786860323568;

The float value type

A float is a floating point number, that’s to say, a number with decimal points like 3.14.

float pi = 3.14f;

A float has a decimal precision of 7.

float num = 4.999995f;

When we assign a value to a float we must add an f at the end. If a float doesn’t have the f suffix it would be treated as a double.

The double value type

A double is the same as a float but with 15 decimal digits of precision instead of only 7.

double num = 4.999999999999995;

A double doesn’t need to have a suffix like float does.

The decimal value type

A decimal is similar to the other floating-points types. Decimal has more precision and a smaller range which makes it appropriate for financial and monetary calculations.

decimal money = 300.5m;

A decimal needs the m suffix.

Type overflowing

Overflowing is when you give a type a value that’s more than it was designed to handle.

Let’s use a byte as an example. We know that its value cannot be greater than 255 or the compiler will raise an error.

byte num = 255;

// will overflow
num = num + 1;

The result of the overflow is that num now has a value of “0”. The result can also be a negative number.

This may never be a problem in your whole programming career, because you can simply change the type to accommodate bigger numbers. There are, however, situations where it needs to be dealt with. In such cases we use checked.

Online games with virtual currency and resources often have this issue where a bad actor would use int overflowing to give themselves more of a currency or resource.

How to use a checked block to check for type overflowing

A checked block is used to check for overflow.

    byte num = 255;

    num = num + 1;

The code to be checked is between open and close curly braces (a code block).

The result is that the number will not overflow when the application is running. Instead, an exception (error) will be thrown and the program will crash unless you handle the exception.

Unsigned types

An unsigned type is essentially a type that cannot be negative. The range shifts from negative to positive, starting at 0.

byte num = 255; 		 // 0 to 255

ushort num = 2897; 		 // 0 to 65535

uint num = 8798798; 	 // 0 to 4,294,967,295

ulong num = 5446876239;  // 0 to 18,446,744,073,709,551,615

A byte does not have the u prefix. A byte is already unsigned by default, if we wanted to use a signed byte that supports negative values, we would have to use an sbyte.

The sbyte signed value type

An sbyte is the signed variation of byte ranging from -128 to 127.

sbyte num = -65;

A byte is the only type that is unsigned by default. If we want a signed byte, we have to use sbyte.

Reference types

Reference types live on the heap in memory, and does not contain the actual data stored in a variable. Rather, it contains a reference to such a variable.

With reference types, two variables can reference the same object. Operations on one variable can affect the object referenced by the other variable. In other words, it means that they refer to a memory location.

The following table lists the available reference types in C#.


Reference types are nullable by default

A nullable type is used when you want an undefined state. A boolean, for example, has only two states: true and false. There is no undefined value for it.

bool auth = null; // compiler error

However, reference types are nullable by default because they don’t hold the actual value, only a reference (or pointer) to it.

string message = null;

Object type

The object type is the base class for all data types in C#. Object types can be assigned values of any other type:

  • Value types
  • Reference types
  • Predefined types
  • User-defined types

Before assigning a value to it, an object type needs type conversion.

When a value type is converted to an object type, it’s called boxing. When an object type is converted to a value type, it’s called unboxing.

object obj;

// boxing
obj = 100;

100 is an integer, assigning it to obj needs type conversion and causes boxing.

Boxing and unboxing is expensive. It uses more of your computer’s resources if the application has to box and unbox constantly.

Summary: Points to remember

  • A data type is simply the type of data that we can store in our variables, constants, arrays, collections etc.
  • C#’s common type system is categorized into 5 types, namely value, reference, object, dynamic and string types.
  • When a type receives more data than it can handle, it may overflow and raise an exception.
    • We use a checked block to check if a type overflows.
  • Value types live on the stack and are more performant than reference types.
    • Numerical primitive value types have signed and unsigned versions.
    • In most instances, value types aren’t nullable.
  • Reference types live on the heap and are references to locations in memory.
    • Reference types are nullable by default
  • The object type is the base class for all data types, and before a value is assigned to it, the value requires conversion.
    • Boxing and unboxing, when done constantly, is expensive.