only for RuBoard - do not distribute or recompile Previous Section Next Section

2.2 Types

A C# program is written by building new types and leveraging existing types, either those defined in the C# language itself or imported from other libraries. Each type contains a set of data and function members, which combine to form the modular units that are the key building blocks of a C# program.

2.2.1 Type Instances

Generally, you must create instances of a type to use that type. Those data members and function members that require a type to be instantiated are called instance members. Data members and function members that can be used on the type itself are called static members.

2.2.2 Example: Building and Using Types

In this program, we build our own type called Counter and another type called Test that uses instances of the Counter. The Counter type uses the predefined type int, and the Test type uses the static function member WriteLine of the Console class defined in the System namespace:

// Imports types from System namespace, such as Console
using System;
class Counter { // New types are typically classes or structs
  // --- Data members ---
  int value; // field of type int
  int scaleFactor; // field of type int

  // Constructor, used to initialize a type instance
  public Counter(int scaleFactor) { 
    this.scaleFactor = scaleFactor;  
  }
  // Method
  public void Inc(  ) {
    value+=scaleFactor;
  }
  // Property
  public int Count {
    get {return value; }
  }
}
class Test {
  // Execution begins here
  static void Main(  ) {

    // Create an instance of counter type
    Counter c = new Counter(5);
    c.Inc(  );
    c.Inc(  );
    Console.WriteLine(c.Count); // prints "10";

    // Create another instance of counter type
    Counter d = new Counter(7);
    d.Inc(  );
    Console.WriteLine(d.Count); // prints "7";
   }
}

2.2.3 Implicit and Explicit Conversions

Each type has its own set of rules defining how it can be converted to and from other types. Conversions between types may be implicit or explicit. Implicit conversions can be performed automatically, while explicit conversions require a cast usingthe C cast operator, ( ):

int x = 123456; // int is a 4-byte integer
long y = x; // implicit conversion to 8-byte integer
short z =(short)x; // explicit conversion to 2-byte integer

The rationale behind implicit conversions is that they are guaranteed to succeed and not lose information. Conversely, an explicitconversion is required when runtime circumstances determine whether the conversion succeeds or when information might be lost during the conversion.

Most conversion rules are supplied by the language, such as the previous numeric conversions. Occasionally it is useful for developers to define their own implicit and explicit conversions (see Section 2.4 later in this chapter).

2.2.4 Categories of Types

All C# types, including both predefined types and user-defined types, fall into one of three categories: value, reference, and pointer.

2.2.4.1 Value types

Value types typically represent basic types. Simple types, such as basic numeric types (int, long, bool, etc.) are structs, which are value types. You can expand the set of simple types by defining your own structs. In addition, C# allows you to define enums.

2.2.4.2 Reference types

Reference types typically represent more complex types with rich functionality. The most fundamental C# reference type is the class, but special functionality is provided by the array, delegate, and interface types.

2.2.4.3 Pointer types

Pointer types fall outside mainstream C# usage and are used only for explicit memory manipulation in unsafe blocks (see Section 2.17 later in this chapter).

2.2.5 Predefined Types

C# has two categories of predefined types:

Aliases for all these types can be found in the System namespace. For example, the following two statements are semantically identical:

int i = 5;
System.Int32 i = 5;
2.2.5.1 Integral types

The following table describes integral types:

C# type

System type

Size (bytes)

Signed?

sbyte
System.Sbyte

1

Yes

short
System.Int16

2

Yes

int
System.Int32

4

Yes

long
System.Int64

8

Yes

byte
System.Byte

1

No

ushort
System.UInt16

2

No

uint
System.UInt32

4

No

ulong
System.UInt64

8

No

sbyte, short, int, and long are signed integers; byte, ushort, uint, and ulong are unsigned integers.

For unsigned integers n bits wide, their possible values range from to 2n-1. For signed integers n bits wide, their possible values range from -2n-1 to 2n-1-1. Integer literals can use either decimal or hexadecimal notation:

int x = 5;
ulong y = 0x1234AF; // prefix with 0x for hexadecimal

When an integral literal is valid for several possible integral types, the chosen default type goes in this order:

int 
uint 
long 
ulong.  

These suffixes may be appended to a value to explicitly specify its type:

U

uint or ulong

L

long or ulong

UL

ulong

2.2.5.1.1 Integral conversions

An implicit conversion between integral types is permitted when the resulting type contains every possible value of the type it is converted from. Otherwise, an explicit conversion is required. For instance, you can implicitly convert an int to a long, but must explicitly convert an int to a short:

int x = 123456;
long y = x; // implicit, no information lost
short z = (short)x; // explicit, truncates x
2.2.5.2 Floating-point types

C# type

System type

Size (bytes)

float
System.Single

4

double
System.Double

8

A float can hold values from approximately ±1.5 x 10-45 to approximately ±3.4 x 1038 with seven significant figures.

A double can hold values from approximately ±5.0 x 10-324 to approximately ±1.7 x 10308 with 15 to 16 significant figures.

Floating-point types can hold the special values +0, -0, +figs/U221E.gif, -figs/U221E.gif, or NaN (not a number) that represent the outcome of mathematical operations such as division by zero. float and double implement the specification of the IEEE 754 format types, supported by almost all processors, defined at http://www.ieee.org.

Floating-point literals can use decimal or exponential notation. A float literal requires the suffix "f" or "F". A double literal may choose to add the suffix "d" or "D".

float x = 9.81f;
double y = 7E-02; // 0.07
double z = 1e2;   // 100
2.2.5.2.1 Floating-point conversions

An implicit conversion from a float to a double loses no information and is permitted but not vice versa. An implicit conversion from an sbyte, short, int, or long (or one of their unsigned counterparts) to a float or double is allowed for readability:

short strength = 2;
int offset = 3;
float x = 9.53f * strength - offset;

If this example used larger values, precision might be lost. However, the possible range of values isn't truncated because the lowest and highest possible values of a float or double exceed those of any integral type. All other conversions between integral and floating-point types must be explicit:

float x = 3.53f;
int offset = (int)x;
2.2.5.3 decimal type

C# type

System type

Size (bytes)

decimal
System.Decimal

16

The decimal type can hold values from ±1.0 x 10-28 to approximately ±7.9 x 1028 with 28 to 29 significant figures.

The decimal type holds 28 digits and the position of the decimal point on those digits. Unlike a floating-point value, it has more precision but a smaller range. It is typically useful in financial calculations, in which the combination of its high precision and the ability to store a base10 number without rounding errors is valuable. The number 0.1, for instance, is represented exactly with a decimal, but as a recurring binary number with a floating-point type. There is no concept of +0, -0, +figs/U221E.gif, -figs/U221E.gif, and NaN for a decimal.

A decimal literal requires the suffix "m" or "M":

decimal x = 80603.454327m; // holds exact value
2.2.5.3.1 decimal conversions

An implicit conversion from all integral types to a decimal type is permitted because a decimal type can represent every possible integer value. A conversion from a decimal to floating-point type or vice versa requires an explicit conversion, since floating-point types have a bigger range than a decimal, and a decimal has more precision than a floating-point type.

2.2.5.4 char type

C# type

System type

Size (bytes)

char
System.Char

2

The char type represents a Unicode character.

A char literal consists of either a character, Unicode format, or escape character enclosed in single quote marks:

'A' // simple character
'\u0041' // Unicode
'\x0041' // unsigned short hexadecimal
'\n' // escape sequence character

Table 2-1 summarizes the escape characters recognized by C#.

Table 2-1. Escape sequence characters

char

Meaning

Value

\'

Single quote

0x0027
\"

Double quote

0x0022
\\

Backslash

0x005C
\0

Null

0x0000
\a

Alert

0x0007
\b

Backspace

0x0008
\f

Form feed

0x000C
\n

New line

0x000A
\r

Carriage return

0x000D
\t

Horizontal tab

0x0009
\v

Vertical tab

0x000B

2.2.5.4.1 char conversions

An implicit conversion from a char to most numeric types works; it's simply dependent on whether the numeric type can accommodate an unsigned short. If it can't, an explicit conversion is required.

2.2.5.5 bool type

C# type

System type

Size (bytes)

bool
System.Boolean

1/2

The bool type is a logical value, which can be assigned the literal true or false.

Although Boolean values require only 1 bit (0 or 1), they occupy 1 byte of storage since this is the minimum chunk that most processor architectures can allocate. Each element of an array requires 2 bytes of memory.

2.2.5.5.1 bool conversions

No conversions can be made from Booleans to numeric types or vice versa.

2.2.5.6 object type

C# type

System type

Size (bytes)

object
System.Object

0/ 8 overhead

The object type is the ultimate base type for both value types and reference types. Value types have no storage overhead from object. Reference types, which are stored on the heap, intrinsically require an overhead. In the .NET runtime, a reference-type instance has an 8-byte overhead, which stores the object's type and temporary information such as its synchronization lock state or whether it has been fixed from movement by the garbage collector. Note that each reference to a reference-type instance uses 4 bytes of storage.

For more information on the System.Object type, see Section 3.1 in Chapter 3.

2.2.5.7 string type

C# type

System type

Size (bytes)

string
System.String

20 minimum

The C# string represents an immutable sequence of Unicode characters and aliases the System.String class (see Section 3.3 in Chapter 3).

Although string is a class, its use is so ubiquitous in programming that it is given special privileges by both the C# compiler and .NET runtime.

Unlike other classes, a new instance can be created with a string literal:

string a = "Heat";

Strings can also be created with verbatim string literals. Verbatim string literals start with a @, which indicates the string should be used verbatim, even if it spans multiple lines or includes escape characters, i.e., "\" (see Table 2-1). In this example the pairs a1 and a2 represent the same string, and the pairs b1 and b2 represent the same string:

public void StringDemo(  ) {
  string a1 = "\\\\server\\fileshare\\helloworld.cs";
  string a2 = @"\\server\fileshare\helloworld.cs";
  Console.WriteLine(a1==a2); // Prints "True"
  
  string b1 = "First Line\r\nSecond Line";
  string b2 = @"First Line
Second Line";
  Console.WriteLine(b1==b2); // Prints "True"
}

2.2.6 Types and Memory

The fundamental difference between value and reference types is how they are stored in memory.

2.2.6.1 Memory for value types

The memory of a value-type instance simply holds a raw value, like a number or character. Value types are stored either on the stack or inline. A stack is a block of memory that grows each time a method is entered (because its local variables need storage space) and shrinks each time a method exits (because its local variables are no longer needed). Inline just means that the value type is declared as part of a larger object, such as when it is a field in a class or a member of an array.

2.2.6.2 Memory for reference types

The memory of a reference-type instance holds the address of an object on the heap. A reference type may be null, which means no object is referenced. During a program's execution, references are assigned to existing or new objects on the heap. An object on the heap remains in memory until the runtime's garbage collector determines it is no longer referenced, at which time the garbage collector discards the object and releases its memory.

2.2.6.3 Value types and reference types side by side

To create a value-type or reference-type instance, the constructor for the type may be called with the new keyword. A value-type constructor simply initializes an object. A reference-type constructor creates a new object on the heap and then initializes the object:

// Reference-type declaration
class PointR {
  public int x, y;
}
// Value type declaration
struct PointV {
  public int x, y;
}
class Test {
  public static void Main(  ) {
    PointR a; // Local reference-type variable, uses 4 bytes of
              // memory on the stack to hold address
    PointV b; // Local value-type variable, uses 8 bytes of
              // memory on the stack for x and y
    a = new PointR(  ); // Assigns the reference to address of new
                      // instance of PointR allocated on the
                      // heap. The object on the heap uses 8
                      // bytes of memory for x and y and an
                      // additional 8 bytes for core object
                      // requirements, such as storing the 
                      // object's type & synchronization state
    b = new PointV(  ); // Calls the value type's default
                      // constructor.  The default constructor 
                      // for both PointR and PointV will set 
                      // each field to its default value, which 
                      // will be 0 for both x and y.
    a.x = 7;
    b.x = 7;
  }
}
// At the end of the method the local variables a and b go out of
// scope, but the new instance of a PointR remains in memory until
// the garbage collector determines it is no longer referenced

Assignment to a reference type copies an object reference; assignment to a value type copies an object value:

    ...
    PointR c = a;
    PointV d = b;
    c.x = 9;
    d.x = 9;
    Console.WriteLine(a.x); // Prints 9
    Console.WriteLine(b.x); // Prints 7
  }
}

As with this example, an object on the heap can be pointed at by many variables, whereas an object on the stack or inline can be accessed only via the variable it was declared with.

2.2.7 Unified Type System

C# provides a unified type system, whereby the object class is the ultimate base type for both reference and value types. This means that all types, apart from the occasionally used pointer types, share a basic set of characteristics.

2.2.7.1 Simple types are value types

In most languages, there is a strict distinction between simple types (int, float, etc.) and user-defined types (Rectangle, Button, etc.). In C#, simple types actually alias structs found in the System namespace. For instance, the int type aliases the System.Int32 struct, and the long type aliases the System.Int64struct, etc. This means simple types have the same features you expect any user-defined type to have. For instance, you can call a method on an int:

int i = 3;
string s = i.ToString(  );

This is equivalent to:

// This is an explanatory version of System.Int32
namespace System {
  struct Int32 {
    ...
    public string ToString(  ) {
      return ...;
    }
  }
}
// This is valid code, but we recommend you use the int alias
System.Int32 i = 5;
string s = i.ToString(  );
2.2.7.2 Value types expand the set of simple types

Creating an array of 1,000 ints is highly efficient. This line allocates 1,000 ints in one contiguous block of memory:

int[] iarr = new int [1000];

Similarly, creating an array of a value type PointV is also very efficient:

struct PointV {
  public int x, y;
}
PointV[] pvarr = new PointV[1000];

Had you used a reference type PointR, you would have needed to instantiate 1,000 individual points after instantiating the array:

class PointR {
   public int x, y;
}
PointR[] prarr = new PointR[1000];
for( int i=0; i<prarr.Length; i++ )
   prarr[i] = new PointR(  );

In Java, only the simple types (int, float, etc.) can be treated with this efficiency, while in C# you can expand the set of simple types by declaring a struct.

Furthermore, C#'s operators may be overloaded so that operations such as + and -, which are typically applicable to simple types, can also be applied to any class or struct (see Section 2.4 later in this chapter).

2.2.7.3 Boxing and unboxing value types

In C#, multiple reference types can share the characteristics of a common base type or interface, which allows reference types to be treated generically. This is very powerful. For instance, a method that takes a reference type R for a parameter works for any type that derives from R or implements R (see Section 2.7.2 later in this chapter).

To perform common operations on both reference and value types, each value type has a corresponding hidden reference type, which is automatically created when it is cast to a reference type. This process is called boxing.

In the following example, the Queue class can enqueue and dequeue any object (object is a reference type that is the base type of all types). You can put an int (a value type) in a queue, because an int can be boxed and unboxed to and from its corresponding reference type:

class Queue {
  ...
  void Enqueue(object o) {...}
  object Dequeue(  ) {return ...}
}

Queue q = new Queue(  );
q.Enqueue(9); // box the int
int i = (int)q.Dequeue(  ); // unbox the int

When a value type is boxed, a new reference type is created to hold a copy of the value type. Unboxing copies the value from the reference type back into a value type. Unboxing requires an explicit cast, and a check is made to ensure the specified value type matches the type contained in the reference type. An InvalidCastException is thrown if the check fails.

Function members of a value type don't actually override virtual function members of the class object or an implemented interface. However, a boxed value type overrides virtual function members.

only for RuBoard - do not distribute or recompile Previous Section Next Section