Guide to C# Data Types, Variables and Object Casting

C# supports all the major data types and a whole load more. This list has the most common types and ranges/lengths of values they can hold.

2,595 words, estimated reading time 10 minutes.
Introduction to Programming with C#

This article is part of a series of articles. Please use the links below to navigate between the articles.

  1. Learn to Program in C# - Full Introduction to Programming Course
  2. Introdution to Programming - C# Programming Fundamentals
  3. Guide to C# Data Types, Variables and Object Casting
  4. C# Operators: Arithmetic, Comparison, Logical and more
  5. Application Flow Control and Control Structures in C#
  6. Introduction to Object Oriented Programming for Beginners
  7. Introduction to C# Object-Oriented Programming Part 2
  8. C# Collection Types (Array,List,Dictionary,HashTable and More)
  9. Error and Exception Handling in C#
  10. Events, Delegates and Extension Methods
  11. Complete Guide to File Handling in C# - Reading and Writing Files
  12. Introduction to XML and XmlDocument with C#
  13. What is LINQ? The .NET Language Integrated Query
  14. Introduction to Asynchronous Programming in C#
  15. Working with Databases using Entity Framework
  16. All About Reflection in C# To Read Metadata and Find Assemblies
  17. Debugging and Testing in C#
  18. Introduction to ASP.Net MVC Web Applications and C#
  19. Windows Application Development Using .Net and Windows Forms
  20. Assemblies and the Global Assembly Cache in C#
  21. Working with Resources Files, Culture & Regions in .Net
  22. The Ultimate Guide to Regular Expressions: Everything You Need to Know
Guide to C# Data Types, Variables and Object Casting

Like most modern programming languages, C# supports all the major data types and a whole load more. This page lists the most common types and the ranges/lengths of values they can hold.

All minimum and maximum values can be found using (data type).MinValue and (data type).MaxValue (e.g. int.MinValue).

C# Common Data Types and Ranges

Type Bytes Description Minimum Maximum Example
bool 1 Named literal false true
sbyte 1 Signed byte -128 127
byte 1 Unsigned byte 0 255
short 2 Signed short integer -32768 32767
ushort 2 Unsigned short 0 65535
int 4 Signed integer -2147483648 2147483647
uint 4 Unsigned integer 0 4294967295
long 8 Signed long int -9.2233E+18 9.2233E+18
ulong 8 Unsigned long int 0 18446E+19
char 2 Unicode character, contained within single quotes. 0 128 a,b,4
float 4 floating point -3.402823E+38 3.402823E+38 3.14159
double 8 Floating point -1.7976931E+308 1.7976931E+308 3.14159
decimal 16 Floating point, accurate to the 28th decimal place. -7.9228E+24 7.9228E+24
object 8+ Base type for all other types n/a n/a n/a
string 20+ Immutable character array n/a n/a "Hello World"
DateTime 8 Represents an instant in time, typically expressed as a date and time of day. 00:00:00 01/01/0001 23:59:59 31/12/9999 14:289:35 08/05/2010

Working with Dates & Time

The first step in using the DateTime object when working with dates and times in C# is to create an instance of the DateTime class. This is done using the new keyword passing in the year, month and day values.

C#
DateTime christmas = new DateTime(25, 12, 2018);

When we get the value of the DateTime, we get the following:

C#
string dateAsString = christmas.ToString();

The value of dateAsString is now "25/12/2018 12:00:00 AM". This will depend on your system locale and date settings.

We can also specify the time when creating a DateTime. This is done by adding hours, minutes and seconds to the constructor.

C#
DateTime christmas = new DateTime(25, 12, 2018, 15, 30, 55);

Now, this will have a value of "25/12/2018 3:30:55 PM".

You can access the current date and time using DateTime.Now.

Adding or Subtracting Time

Let's say you wanted to get the date 30 days from now. You can use the AddDays method for this.

C#
DateTime date30DaysTime = DateTime.Now.AddDays(30);

You can do the same to get the date 30 days in the past; pass a negative number to subtract days.

C#
DateTime date30DaysTime = DateTime.Now.AddDays(-30);

You can do the same for years, months, days, hours, minutes and seconds.

Difference Between Two DateTimes

You can calculate the distance between two DateTimes by doing the following.

C#
TimeSpan duration = dateTime1 - dateTime2;

The TimeSpan class has many properties you can access to get the hours, minutes and seconds between the two DateTimes.

C#
duration.TotalSeconds
duration.TotalMinutes
duration.TotalHours

Date/Time Format Strings

Date/Time formats depend on the user's locale so that the output may differ. These can be passed into the ToString method as well as string.Format method.

C#
string date = DateTime.Now.ToString("d");
string date = string.Format("{0:d}", DateTime.Now);
Character Description Usage Example Output
d Short date {0:d} 08/12/2007
D Long date {0:D} 08 December 2007
t Short time {0:t} 15:27
T Long time {0:T} 15:27:40
f Long date time {0:f} 08 December 2007 15:27
F Long date time {0:F} 08 December 2007 15:27:40
g Short date time {0:g} 08/12/2007 15:27
G Short date time {0:G} 08/12/2007 15:27:40
M Short date {0:M} 08 December
r RFC1123 Date time string {0:r} Sat, 08 Dec 2007 15:27:40 GMT
s Sortable date/time {0:s} 2007-12-08T15:27:40
u Universal sortable date {0:u} 2007-12-08 15:27:40
U Universal full date {0:U} 08 December 2007 15:27:40
Y Year month pattern {0:Y} December 2007

Enums and Flags

An enum or enumerate is a set consisting only of defined constants. They are useful for limiting the values that a type can contain.

C#
enum daysInWeek {Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday};

Each item in the set is assigned a numerical value; in this example, Monday equals 1 and Sunday = 7. You can specify your values using the equal's symbol followed by a numeric.

C#
enum daysInWeek {Monday=0, Tuesday=2, Wednesday=4, Thursday, Friday, Saturday, Sunday};

Values that have not been explicitly assigned a value take the value of the previous item plus one; Thursday will equal 5.

Enums can be used as constants to allow only the specified values.

C#
int day = daysInWeek.Wednesday;

if (day == daysInWeek.Thursday)
{
  Console.WriteLine("Today is Thursday");
}

Enums can also define a binary set, a value that holds various options about something using the [Flags] attribute. These are useful where there are many boolean options that an object can have and cut down on the number of properties that a call will have.

C#
bool hasButtons = true;
bool hasZip = false;
bool hasPockets = false;
bool hasEmbrodery = true;

Rewritten using enum bit flags gives:

C#
[Flags]
public enum garmentOptions
{
  hasButtons = 0x01,   // 00000001  1
  hasZip = 0x02,       // 00000010  2
  hasPockets = 0x04,   // 00000100  4
  hasEmbrodery = 0x08; // 00001000  8
}

garmentOptions myGarment = garmentOptions.hasButtons | garmentOptions.hasEmbrodery;

// result of myGarment is 9 or:
//   00000001  1 - hasButtons 
// + 00001000  8 - hasEmbrodery
// = 00001001  9

if ((myGarment & garmentOptions.hasEmbrodery) > 0)
{
  // Embrodery specific code here
}

The logical bitwise & is used to test if an enum bit flag is part of the set in myGarment.

C# Nullable Types

Nullable types can represent all the values of an underlying type T and an additional null value.

Nullable types are used when you need to represent the undefined value of an underlying type. For example, a boolean variable can only have true and false values. There is no "undefined" value. A variable value can be undefined or missing in many programming applications, most commonly database interactions. For example, a field in a database may contain the values true or false, or it may contain no value at all. You use a Nullable type in that case.

Nullable can be defined in one of two ways. The second is the shorthand version of the first.

C#
Nullable<int> i = null;
int? i = null;

To check if a nullable has a value, you can use the HasValue property. If you try to access the value without checking if it has a value, you will get an exception thrown.

C#
static void Main()
{
  int? i = null;

  if (i.HasValue)
    Console.WriteLine(i.Value);
  else
     Console.WriteLine("Null");
}

You can also use the GetValueOrDefault() method to get an actual value if it is not null and the default value if it is null. For example:

C#
static void Main()
{
    int? i = null;

    Console.WriteLine(i.GetValueOrDefault()); 
}

C# Constants and Read-Only Variables

C# Constants and Read Only variables perform the same task; the only difference is that a constant is given a value at compile time and can never be changed. Only variables can be assigned once at runtime, but they cannot be changed after that.

Constants

Constants are declared using the const keyword and can be used as class members or method values and marked with any access modifier apart from static. Constants should be used anywhere you would hard code a value, such as several results to return, literal strings, loop control, mathematical constants, fixed costs, etc.

C#
public const decimal pi = 3.14M;
private const int numRows = 10;
private const decimal vatRate = 17.5M;

This will define a value for pi, which will never change once the program is compiled; similarly, numRows will be initialised to 10 and never change. If you try to assign to a const value, the compiler will throw an error.

Read Only

Read-only values allow one assignment at runtime and can hold information that needs to be read or processed once a read-only value has been assigned. However, it cannot be assigned again until the application is restarted.

C#
public class SomeTestClass
{
  private readonly string filePath;

  public SomeTestClass(string filename)
  {
    filePath = Path.GetDirectoryName(filename);
  }
}

This will initialise the value filePath to the directory of the specified filename. Once the constructor has been assigned to filePath, it can no longer be assigned.

Using Consts and Read Only Values

Let's assume you are creating a class for calculations involving circles. You create the class using hard-coded values for pi.

C#
public class Circles
{
  public double Circumference(double radius)
  {
    return circumference= 3.14 * (radius * 2);
  }

  public double Area(double radius)
  {
    return area = 3.14 * Math.Pow(radius, 2);
  }

  public double AnotherTest(double radius)
  {
    if (radius <= 3.14)
      return radius * 3.14 * Math.Pow(3.14, 3);
    else 
      return radius * Math.Pow(3.14, 4);   
  }
}

This class could contain many more methods and constants. During testing, it was found that the calculations performed were not as accurate as required. It is necessary to improve accuracy by changing the value of pi from 3.14 to 3.1415926. How do you accomplish this?

You could do a search and replace, but how do you know that 3.14 is always the value of pi? The AnotherTest method does not use pi but another fixed value with the same value. Changing this value could result in undesired results.

It would have been much better to have created the class using constants, which would have the following benefits:

  1. The value of pi needs only to be changed once.
  2. You can be sure you haven't missed any values.
  3. The code looks neater and is easier to understand.
  4. pi value 3.14 is not confused with another value of 3.14
C#
public class Circles
{
  private const double pi = 3.1415926;
  private const double test = 3.14;

  public double Circumference(double radius)
  {
    return circumference= pi * (radius * 2);
  }

  public double Area(double radius)
  {
    return area = pi * Math.Pow(radius, 2);
  }

  public double AnotherTest(double radius)
  {
    if (radius <= test)
      return radius + test * Math.Pow(pi, 3);
    else 
      return radius * Math.Pow(pi, 4);   
  }
}

From this, it is easy to see that the values for test and pi are different in the AnotherTest method. Also, to change the value of pi, change one line of code at the top of the class.

Of course, in the real world, you would use Math.PI for the value of pi as this figure is an even more accurate predefined constant.

Implicitly Typed Variables

Local variables can be declared without giving an explicit type. The var keyword instructs the compiler to infer the variable type from the expression on the right side of the initialisation statement. This is only possible inside a method, not at the class level.

C#
int age = 37 // Explicitly typed variable

var name = "Tim Trott"; // Implicitly typed variable

There are a few important points to consider when using an implicitly typed variable. Firstly, Assigning an initial value to the implicitly typed variable is mandatory. Secondly, implicit variables mean the compiler determines the variable type at compile time.

Generic Types

C# Generics make it possible to design classes and methods that do not specify data types until the class or method is declared and instantiated by client code.

Using a generic type parameter, you can write a single class that other client codes can use without incurring the cost or risk of casts or boxing operations.

This first example shows how a method can accept an array of various types and output the values. Without generics, you would need to override the method for each data type, causing problems with scalability and custom types.

C#
using System;
using System.Collections.Generic;

class Generics
{
    static void Main(string[] args)
    {
        // create arrays of various types
        int[] intArray = { 1, 2, 3, 4, 5, 6 };
        double[] doubleArray = { 1.0, 2.0, 3.0, 4.0, 5.0 };
        char[] charArray = { 'A', 'B', 'C', 'D', 'E' };

        DisplayArray(intArray);
        DisplayArray(doubleArray);
        DisplayArray(charArray);

        Console.ReadLine();
    }

    // generic method displays array of any type
    static void DisplayArray<E>(E[] array)
    {
        Console.WriteLine("Display array of type " + array.GetType() + ":");
        foreach (E element in array)
            Console.Write(element + " ");
    }
}

You can see how the one method will handle requests for an array of ints, doubles and chars. We can also use reflection and the GetType method to determine the type of array passed as the generic parameter.

Anonymous Types

Anonymous types provide a convenient way to encapsulate a set of read-only properties into a single object without explicitly defining a type first. The compiler generates the type name, which is unavailable at the source code level. The compiler infers the type of each property.

The following example shows an anonymous type that is initialised with two properties named Name and Age.

C#
var result = new { Name = "Tim Trott", Age = 37 };

Anonymous types are typically used in the select clause of a query expression to return a subset of the properties from each object in the source sequence. For more information about queries, see the tutorial on LINQ Query Expressions.

Dynamic Types

Dynamic type is the same as static type, but an object of dynamic type bypasses static type checking. At compile time, an element typed as dynamic is assumed to support any operation.

A dynamic type changes its type at runtime based on the value of the expression to the right of the "=" operator.

A dynamic type can be defined using the dynamic keyword.

C#
dynamic dynamicVariable = 1;

The following example shows how a dynamic variable changes its type based on its value:

C#
static void Main(string[] args)
{
    dynamic dynamicVariable = 100;
    Console.WriteLine("Dynamic variable value: {0}, Type: {1}",dynamicVariable, dynamicVariable.GetType().ToString());

    dynamicVariable = "Hello World!!";
    Console.WriteLine("Dynamic variable value: {0}, Type: {1}", dynamicVariable, dynamicVariable.GetType().ToString());

    dynamicVariable = true;
    Console.WriteLine("Dynamic variable value: {0}, Type: {1}", dynamicVariable, dynamicVariable.GetType().ToString());

    dynamicVariable = DateTime.Now;
    Console.WriteLine("Dynamic variable value: {0}, Type: {1}", dynamicVariable, dynamicVariable.GetType().ToString());
}

Type Casting

Some data types can be assigned to a different type if they can be implicitly cast. This means a small number can be assigned to a large number, not vice versa.

Let's look at two data types: the byte and the long. As you can see from the data types guide, a byte can hold whole numbers between 0 and 255, and a long can hold whole numbers between 0 and 9,223,372,036,854,775,807. Think of these as a shot glass and a pint glass.

C#
  byte shotGlass;
 ulong pintGlass;

In these analogies, please imagine that when we "pour" one glass into another, the original glass does not lose any of its contents. Rather, an equal amount of fluid is added to the other glass while the original retains its contents.

Implicit Casting

It makes sense that you can pour the contents of the shot glass into the pint glass; there is no danger of it overflowing, so the compiler will allow this to happen without any warnings. This is called an implicit typecast and is done by the compiler automatically each time you assign a variable to another variable.

C#
  pintGlass = shotGlass;

The pintGlass now contains the contents of the shotGlass.

Explicit Casting

So what happens if you want to pour the contents of the pint glass into the shot glass? It may be possible; it depends on how much is in the pint glass. The compiler will see this as dangerous as there is a good chance that the shot glass will overflow, so it will flag an error and prevent the program from compiling.

C#
 shotGlass = pintGlass;

Cannot implicitly convert type 'ulong' to 'byte'. An explicit conversion exists (are you missing a cast?)

It is possible, however, to say to the compiler, "I know what I am doing; please let me pour the contents of my pint glass into my shot glass." This is called an explicit typecast and is performed using the data type you convert to in brackets just before the variable.

C#
  shotGlass = (byte) pintGlass;

In this case, the compiler assumes we know what we are doing and allows it.

If the shot glass does overflow, the value of the shot glass will roll over and start from 0, so if you try and put a value of 256 into the shot glass, the value would be 0, a value of 257 would be 1, 258 would be two and so on.

You should only explicitly typecast when you are certain that the values will fit, and it is sensible to test for this condition before performing the conversion.

Floating Point

Floating-point numbers can store values with a decimal fraction and a value, for example, 2.5632. You can implicitly cast an int to a float or decimal type, but you cannot implicitly cast a decimal or float to an int, as an int cannot hold the data after the decimal point. You can explicitly cast; however, you will lose any decimal data, i.e. 2.5632 cast as an int will become just 2.

About the Author

Tim Trott is a senior software engineer with over 20 years of experience in designing, building, and maintaining software systems across a range of industries. Passionate about clean code, scalable architecture, and continuous learning, he specialises in creating robust solutions that solve real-world problems. He is currently based in Edinburgh, where he develops innovative software and collaborates with teams around the globe.

Related ArticlesThese articles may also be of interest to you

CommentsShare your thoughts in the comments below

My website and its content are free to use without the clutter of adverts, popups, marketing messages or anything else like that. If you enjoyed reading this article, or it helped you in some way, all I ask in return is you leave a comment below or share this page with your friends. Thank you.

This post has only 1 comment. Why not join the discussion!

New comments for this post are currently closed.