As we learned in the previous post, C# supports a type system. Part of that system is a group of "basic" types. These types, also called primitive types, form the foundation of many C# programs.

A set of straw huts clustered together in a jungle.
Sign me up for an off-the-grid vacation! Photo by fran hogan / Unsplash

The Sample Solution

exceptionnotfound/CSharpInSimpleTerms
Contribute to exceptionnotfound/CSharpInSimpleTerms development by creating an account on GitHub.
Project for this post: 2PrimitiveTypes

Number Types

The most basic of the primitive types are the number types. These include integral numeric types (which represent whole numbers, like 1, 67, 1957321, 8, and so on) and floating-point numeric types (which represent non-whole numbers such as 1.2, 6.99, 8234.66, and so on).

Int

Of the integral numeric types, the type int is the default and most common. int represents a 32-bit integer, with a positive or negative value.

int five = 5;
int thirteenHundred = 1300;
int negativeForty = -40;
int intMaxValue = int.MaxValue; //(2^31 - 1)

The type int is used for many kinds of variables, including math, counters, and iterators.

Short, Long, and Byte

short, long, and byte are all integral numeric types, like int. However, they represent different ranges of values.

A short represents a 16-bit integer:

short three = 3;
short negativeOneHundred = -100;
short shortMaxValue = short.MaxValue; //(2^15 - 1)

A long represents a 64-bit integer:

long fifty = 50;
long longMaxValue = long.MaxValue; //(2^63 - 1)

Finally, a byte is a 8-bit integer that only represents positive values.

byte four = 4;
byte byteMaxValue = byte.MaxValue; //(2^7 - 1)

Signed and Unsigned

Integral types in C# are normally signed, meaning they can represent positive or negative values (the exception to this is byte, which is unsigned and therefore can only represent positive values).

We can use the types ushort, uint, and ulong to represent unsigned integers, and sbyte to represent signed bytes.

ushort unsignedShortMax = 65535;
uint unsignedIntMax = 4294967295;
ulong unsignedLongMax = 18446744073709551615;
sbyte signedByteMin = -127;

Floating-Point Numeric Types

In C#, floating-point numeric types represents non-whole or partial numbers. They are used to do more complex math calculations, currency representations, and other places where we need more than simple integers.

Double and Float

A double is an 8-byte number used when we need quick calculations but don't care about precision (see "A Note About Precision" below).

double fortytwo = 42.0;
double pi = 3.14159;

A float is used in the same situation as a double, but it has less range, since it is a 4-byte number. Consequently it can perform calculations within its range even more quickly than double.

float negativeThirty = -30.0F;
float eighteenHundredAndAHalf = 1800.5F;
Note that we need the F literal here; more about literals below.

A Note About Precision

When using double or float, precision is lost when doing complex calculations. For example, in C# we can do this:

double sum = 0.1 + 0.2;

But we will get a strange result: 0.30000000000000004

When doing arithmetic with double or float, precision (which is the accuracy of numbers on the right side of the decimal point) is sacrificed to gain speed. Calculations involving floating-point numbers are computationally expensive, meaning it takes a long time (comparatively) to get a result.

Programming language compilers, including C#'s compiler, take shortcuts when doing these kinds of calculations; these shortcuts dramatically speed up the calculations while not reducing precision too much, except in certain circumstances. Most applications will not care that 0.1 + 0.2 = 0.30000000000000004, because precision is not an absolute requirement for this calculation.

For the vast majority of applications that do not deal with money or currency, we as developers probably don't care about the loss of precision that comes from doing arithmetic using double or float; it is most likely small enough to be negligible.

However, there are times when precision cannot be lost, and for those times, we use the decimal type.

Decimal

A decimal type is used when we need to keep precision, but don't mind that calculations are more computationally expensive to do. The type decimal is primarily used for currency or money calculations, since loss of precision would be harmful there.

decimal dollars = 1.45M;
decimal billionaire = 1000000000.01M;
Note that we need the M literal.

Mixing Number Types

It is possible to use decimal, double, and float in calculations with the integral types, though there are some rules.

For example, using any integral type and a double in a calculation results in a value of type double.

int five = 5;
double fivePointFive = 5.5;

double sum = five + fivePointFive; //Result is type double, value 10.5

This works similarly for float, though if the resulting value is too large, the type of the result is automatically converted to double.

As you might have guessed, mixing integral types and decimal gives a result of type decimal:

short three = 3;
decimal sixPointSevenTwo = 6.72;

decimal sum = three + sixPointSevenTwo; //Result is type decimal, value 9.72

In general, mixing integral numeric types and floating-point numeric types in math calculations will result in objects which have the floating-point numeric type.

Non-Number Types

Besides the integral numeric types and the floating-point numeric types, there are also several non-number types that C# provides.

Bool

C# includes a type bool to deal with boolean values (values that must be either true or false).

bool isTrue = true;
bool isFalse = false;

Boolean values are often utilized in boolean logic, which we will demonstrate more of in Part 4 (Operators) of this series.

Char

C# also has a char type to represent a single text character.

char a = 'a';
char ampersand = '&';
char x = 'x';
char comma = ',';
char semicolon = ';';

String

C# has a string type that represents a collection of characters.

string sentence = "This is a sentence.";
string otherSentence = "The quick brown fox jumped over the lazy dog.";

Please note that by Microsoft's own definition of a "primitive" type, string is NOT considered a primitive.

The term "string" comes from the idea of this type being a "string" of characters. In fact, the type string is implemented as collection of characters, and can be used as though it is an array. We will discuss arrays in Part 12 of this series.

Also, unlike all the other primitive types in this article, string is a reference type, not a value type, meaning its default value is null. In a later article in this series, we will see many ways of manipulating strings using a variety of C# operators.

DateTime

The type DateTime, like type string, is not considered a "primitive" type but it is so commonly used in C# applications that I felt it was worthy of inclusion in this post.

An instance of DateTime represents a point in time. Typically, this is expressed as a date and a time.

DateTime date1 = new DateTime();
DateTime date2 = new DateTime(2020, 3, 15); //15 March 2020
DateTime date3 = new DateTime(2020, 3, 15, 10, 30, 00); //15 March 2020, 10:30

There are many ways to create instances of this object; the above is just a few of them. We will see many other ways to manipulate DateTime objects during a later post.

Literal Values

The C# compiler makes assumptions as to what type a variable has if we do not directly tell it what that type should be. In C#, if we write this code:

var myValue = 7.8;

The type of myValue will be double, because double is the default type for any number with a decimal point.

If we want to create myValue as type decimal, we need to declare it with the literal marker M.

var myValue = 7.8M;

There are many types of these literal markers, including:

var myDouble = 5.6D; //double
var myFloat = 2.88F; //float
var myLong = 568373L; //Type long, will be type ulong if the value is too large
var myUnsignedInt = 98765U; //Type uint, will be type ulong if the value is too large

Nullable Types

C# allows for the use of nullable types, where a primitive value type can be either one of its "normal" values or null.  Nullable types are identified with the operator ?.

Nullable types get the value null as their default value.

char? a = null;
double? myDouble; //Value will be null
decimal? myMoney = 45.61M;
bool? trueFalseOrNotFound = false;
DateTime? myDate = null;
int? myNumber = null;
float? myFloat = 6.3F;

There are two special constrictions on nullable types:

  1. We cannot use var and make the type nullable AND
  2. Type string cannot be nullable, since it is a reference type.

When types are made nullable, the C# compiler gives them two special properties: HasValue and Value. HasValue can be used to check if the variable has a value at all, and Value gives the non-null value but will throw an exception if the value is indeed null.

int? myValue = 5;

if(myValue.HasValue)
{
    Console.WriteLine(myValue.Value); //Output: 5
}

int? myValue2 = null;

if(myValue2.HasValue)
{
    Console.WriteLine(myValue.Value); //Line does not execute, 
                                      //since myValue2 is null
}

Glossary

  • Primitive types - "Basic" C# types, including int, char, bool, string and others.
  • Integral numeric types - C# types which represent whole numbers.
  • Floating-point numeric types - C# types which represent partial numbers (e.g. decimals or fractions).
  • Signed - A value which can be positive or negative.
  • Unsigned - A value which can only be positive.
  • Precision - The degree to which values of "numbers after the decimal point" are correct. Types float and double sacrifice precision for speed, whereas type decimal keeps precision but is more computationally expensive.
  • Boolean - A value which must be either true or false.
  • Literal marker - In C#, a single character which identifies the type of the specified value. For example, 5.67M identifies this value as type decimal.
  • Nullable types - A value type in C# that allows an object to be null, in addition to its normal value range.

Summary

Basic types in C# include integral numeric types int, long, short, and byte; floating-point numeric types double, float and decimal; and non-numeric types bool, char, and string, plus others; each has a distinct purpose.

Combining objects of different numeric types in math calculations generally results in an object having the more-general type (e.g. combining an int and a double will result in a double, combining a short and a decimal results in a decimal, etc.)

To specify which type a given value should be, we can use literal markers, such as M for decimal or F for float. We can also make some objects nullable to allow them to have the value null in addition to the normal values those types can have.

In the next part of this series, we expand on C#'s type system to show how we can take objects of one type and change them to another type via casting, conversion, parsing, the is and as keywords, and more. Check it out!

C# in Simple Terms - Casting, Conversion, and Parsing
Let’s change some types!

Happy Coding!