2.1. Data Types
A data type specifies the allowed size and kind of variable values. Data types
are used when assigning values to variables. In programming, a variable is a name
that refers to a value of a specific type at a memory location. A number
10
, for example, is stored in the memory at certain location as 1010
.
As values can get extremely large or small or complex, or different in its composition,
there is a need to decide how much memory to reserve for different kinds of values
so that computer memories can be effectively used, hence the various data types.
There are two catagories of data types in C#: reference types and value types. Variables of value types contain the data directly (memory address allocated and value assigned) while variables of reference types store reference to the data known as objects. Value types are predefined types and are available in C# to be used as keywords. These keywords are aliases of types defined in the .NET Class Library. For example, the C# keyword int is an alias to a value type defined in the .NET Class Library: System.Int32. [1]
2.1.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.
Type |
Size |
Precision |
Description |
---|---|---|---|
|
1 byte/8 bits |
Integral range 0 to 255 (no negative values) |
|
|
16 bits |
Integral range -32,768 to 32,767 |
|
|
4 bytes/32 bits |
Integral range -2,147,483,648 to 2,147,483,647 |
|
|
8 bytes/64 bits |
Integral range from -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 |
|
|
4 bytes/32 bits |
~6-9 digits |
Fractional numbers. |
|
8 bytes/64 bits |
~15-17 digits |
Fractional numbers ±1.5 x 10−45 to ±3.4 x 1038. |
|
16 bytes/128 bits |
28-29 digits |
Fractional numbers ±1.0 x 10-28 to ±7.9228 x 1028 |
|
1 bit |
True or false values |
|
|
2 bytes |
A single character/letter corresponding to Unicode character set, surrounded by single quotes |
|
|
2 bytes per character |
A sequence of characters, surrounded by 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:
1Console.WriteLine("Signed integral types:");
2
3Console.WriteLine($"short : {short.MinValue} to {short.MaxValue}");
4Console.WriteLine($"int : {int.MinValue} to {int.MaxValue}");
5Console.WriteLine($"long : {long.MinValue} to {long.MaxValue}");
6
7Console.WriteLine("");
8Console.WriteLine("Unsigned integral types:");
9
10Console.WriteLine($"byte : {byte.MinValue} to {byte.MaxValue}");
11Console.WriteLine($"ushort : {ushort.MinValue} to {ushort.MaxValue}");
12Console.WriteLine($"uint : {uint.MinValue} to {uint.MaxValue}");
13Console.WriteLine($"ulong : {ulong.MinValue} to {ulong.MaxValue}");
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.1.2. C# Built-in Types System
C# has a type system with types defined that can be briefly described as follows.
- Reference types:
There are 4 reference types: class type, interface type, array type, and delegate type. Under class type, types such as string and array are defined.
For value types, C# defines a type system as follows.
- simple_type
: numeric_type | ‘bool’ ;
- numeric_type
: integral_type | floating_point_type | ‘decimal’ ;
- integral_type
: ‘sbyte’ | ‘byte’ | ‘short’ | ‘ushort’ | ‘int’ | ‘uint’ | ‘long’ | ‘ulong’ | ‘char’ ;
- floating_point_type
: ‘float’ | ‘double’ ;
2.1.3. Type Conversion
C# variables have specific types but from time to time we may need our data to switch between the types. For example, when your program takes a user input for age, the input is of string type by default while you are looking for numeric type. You therefore need to cast string type to a numeric type. This switch may be implicit or explicit:
- Implicit Casting (automatically)
converting a smaller type to a larger type size char -> int -> long -> float -> double
- Explicit Casting (manually)
converting a larger type to a smaller size type double -> float -> long -> int -> char
For instance, the conversion from type int to type long is implicit, so type int
can implicitly be treated as type long. On the other hand, to convert from type
long to type int, an explicit cast is required.
Observe the example below to see that we put the desired result type name in
parentheses as a cast. Also, we can use the GetType()
method to get the type of an variable. You can test the examples below using csharprepl
.
> int a = 123; // variable a is assigned a value 123
> long b = a; // implicit conversion from int to long by reassignment
> int c = (int) b; // explicit conversion from long to int
> a.GetType() // use the GetType() function to get the type of the variable
int
> b.GetType()
long
> c.GetType()
int
When the types are not cast properly, C# will give error messages. For example:
> double d = 2.0;
> int i = d;
┌─────────────────────────────────────────CompilationErrorException─────────────────────────────────────────┐
│ (1,9): error CS0266: Cannot implicitly convert type 'double' to 'int'. An explicit conversion exists (are │
│ you missing a cast?) │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────┘
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
2.5
> int i; // declare an int without value assignment
> i // get the (default) value of an int
0
> i = (int)d; // explicitly telling the compiler you intend the conversion
> i // get the value of i; the value .5 is lost
2
>
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
2.7
> d.GetType()
double
> d = Math.Round(d); // rounding and re-assignment
> d
3
> d.GetType() // the type remains
double
> i = (int)Math.Round(d); // casting to int
> i
3
> i.GetType() // type correct
int
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
4
> (double) numerator / denominator // intended operation; casting required
4.666666666666667
>
Footnotes